Merge branch 'master' of https://github.com/Regalis11/Barotrauma into develop

This commit is contained in:
EvilFactory
2025-06-17 15:45:16 -03:00
298 changed files with 7347 additions and 2422 deletions

View File

@@ -73,7 +73,7 @@ body:
label: Version label: Version
description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu. description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu.
options: options:
- v1.8.8.1 (Calm Before the Storm Hotfix 2) - v1.9.7.0 (Summer Update 2025)
- Other - Other
validations: validations:
required: true required: true

View File

@@ -435,6 +435,13 @@ namespace Barotrauma
{ {
cam.OffsetAmount = targetOffsetAmount = item.Prefab.OffsetOnSelected * item.OffsetOnSelectedMultiplier; cam.OffsetAmount = targetOffsetAmount = item.Prefab.OffsetOnSelected * item.OffsetOnSelectedMultiplier;
} }
else if (HeldItems.SelectMany(static item => item.GetComponents<Holdable>())
.Where(static holdable => holdable.Aimable)
.MaxOrNull(static holdable => holdable.CameraAimOffset) is float maxOffset
&& maxOffset > 0f && IsKeyDown(InputType.Aim))
{
cam.OffsetAmount = targetOffsetAmount = maxOffset;
}
else if (SelectedItem != null && ViewTarget == null && else if (SelectedItem != null && ViewTarget == null &&
SelectedItem.Components.Any(ic => ic?.GuiFrame != null && ic.ShouldDrawHUD(this))) SelectedItem.Components.Any(ic => ic?.GuiFrame != null && ic.ShouldDrawHUD(this)))
{ {

View File

@@ -653,7 +653,10 @@ namespace Barotrauma
(int)(HUDLayoutSettings.BottomRightInfoArea.Width / 2), (int)(HUDLayoutSettings.BottomRightInfoArea.Width / 2),
(int)(HUDLayoutSettings.BottomRightInfoArea.Height * 0.7f)), character.Info.IsDisguisedAsAnother); (int)(HUDLayoutSettings.BottomRightInfoArea.Height * 0.7f)), character.Info.IsDisguisedAsAnother);
float yOffset = (GameMain.GameSession?.Campaign is MultiPlayerCampaign ? -10 : 4) * GUI.Scale; float yOffset = (GameMain.GameSession?.Campaign is MultiPlayerCampaign ? -10 : 4) * GUI.Scale;
character.Info.DrawPortrait(spriteBatch, HUDLayoutSettings.PortraitArea.Location.ToVector2(), new Vector2(-12 * GUI.Scale, yOffset), targetWidth: HUDLayoutSettings.PortraitArea.Width, true, character.Info.IsDisguisedAsAnother);
character.Info?.DrawIcon(spriteBatch,
new Vector2(HUDLayoutSettings.PortraitArea.Center.X - 12 * GUI.Scale, HUDLayoutSettings.PortraitArea.Center.Y), HUDLayoutSettings.PortraitArea.Size.ToVector2(),
flip: true);
character.Info.DrawForeground(spriteBatch); character.Info.DrawForeground(spriteBatch);
} }
mouseOnPortrait = MouseOnCharacterPortrait() && !character.ShouldLockHud(); mouseOnPortrait = MouseOnCharacterPortrait() && !character.ShouldLockHud();
@@ -733,8 +736,9 @@ namespace Barotrauma
string focusName = character.FocusedCharacter.Info == null ? character.FocusedCharacter.DisplayName : character.FocusedCharacter.Info.DisplayName; string focusName = character.FocusedCharacter.Info == null ? character.FocusedCharacter.DisplayName : character.FocusedCharacter.Info.DisplayName;
Vector2 textPos = startPos; Vector2 textPos = startPos;
Vector2 textSize = GUIStyle.Font.MeasureString(focusName); //measure arbitrary one-line text instead of the potentially-multi-line name
Vector2 largeTextSize = GUIStyle.SubHeadingFont.MeasureString(focusName); Vector2 textSize = GUIStyle.Font.MeasureString("T");
Vector2 largeTextSize = GUIStyle.SubHeadingFont.MeasureString("T");
textPos -= new Vector2(textSize.X / 2, textSize.Y); textPos -= new Vector2(textSize.X / 2, textSize.Y);

View File

@@ -360,64 +360,6 @@ namespace Barotrauma
GUIStyle.Font.DrawString(spriteBatch, str, new Vector2(barRect.Right - iconXOffset - scaledTextSizeX - GUI.IntScale(4), barRect.Center.Y - scaledTextSizeY / 2), GUIStyle.TextColorNormal, 0f, Vector2.Zero, textScale, SpriteEffects.None, 0f); GUIStyle.Font.DrawString(spriteBatch, str, new Vector2(barRect.Right - iconXOffset - scaledTextSizeX - GUI.IntScale(4), barRect.Center.Y - scaledTextSizeY / 2), GUIStyle.TextColorNormal, 0f, Vector2.Zero, textScale, SpriteEffects.None, 0f);
} }
public void DrawPortrait(SpriteBatch spriteBatch, Vector2 screenPos, Vector2 offset, float targetWidth, bool flip = false, bool evaluateDisguise = false)
{
if (evaluateDisguise && IsDisguised) { return; }
Vector2? sheetIndex;
Sprite portraitToDraw;
List<WearableSprite> attachmentsToDraw;
Color hairColor;
Color facialHairColor;
Color skinColor;
if (!IsDisguisedAsAnother || !evaluateDisguise)
{
sheetIndex = Head.SheetIndex;
portraitToDraw = Portrait;
attachmentsToDraw = AttachmentSprites;
hairColor = Head.HairColor;
facialHairColor = Head.FacialHairColor;
skinColor = Head.SkinColor;
}
else
{
sheetIndex = disguisedSheetIndex;
portraitToDraw = disguisedPortrait;
attachmentsToDraw = disguisedAttachmentSprites;
hairColor = disguisedHairColor;
facialHairColor = disguisedFacialHairColor;
skinColor = disguisedSkinColor;
}
if (portraitToDraw != null)
{
var currEffect = spriteBatch.GetCurrentEffect();
// Scale down the head sprite 10%
float scale = targetWidth * 0.9f / Portrait.size.X;
if (sheetIndex.HasValue)
{
SetHeadEffect(spriteBatch);
portraitToDraw.SourceRect = new Rectangle(CalculateOffset(portraitToDraw, sheetIndex.Value.ToPoint()), portraitToDraw.SourceRect.Size);
}
portraitToDraw.Draw(spriteBatch, screenPos + offset, skinColor, portraitToDraw.Origin, scale: scale, spriteEffect: flip ? SpriteEffects.FlipHorizontally : SpriteEffects.None);
if (attachmentsToDraw != null)
{
float depthStep = 0.000001f;
foreach (var attachment in attachmentsToDraw)
{
SetAttachmentEffect(spriteBatch, attachment);
DrawAttachmentSprite(spriteBatch, attachment, portraitToDraw, sheetIndex, screenPos + offset, scale, depthStep, GetAttachmentColor(attachment, hairColor, facialHairColor), flip ? SpriteEffects.FlipHorizontally : SpriteEffects.None);
depthStep += depthStep;
}
}
spriteBatch.SwapEffect(currEffect);
}
}
//TODO: I hate this so much :( //TODO: I hate this so much :(
private SpriteBatch.EffectWithParams headEffectParameters; private SpriteBatch.EffectWithParams headEffectParameters;
private Dictionary<WearableType, SpriteBatch.EffectWithParams> attachmentEffectParameters private Dictionary<WearableType, SpriteBatch.EffectWithParams> attachmentEffectParameters
@@ -466,23 +408,26 @@ namespace Barotrauma
} }
} }
public void DrawIcon(SpriteBatch spriteBatch, Vector2 screenPos, Vector2 targetAreaSize) public void DrawIcon(SpriteBatch spriteBatch, Vector2 screenPos, Vector2 targetAreaSize, bool flip = false)
{ {
var headSprite = HeadSprite; var headSprite = HeadSprite;
if (headSprite != null) if (headSprite != null)
{ {
var spriteEffects = flip ? SpriteEffects.FlipHorizontally : SpriteEffects.None;
var currEffect = spriteBatch.GetCurrentEffect(); var currEffect = spriteBatch.GetCurrentEffect();
float scale = Math.Min(targetAreaSize.X / headSprite.size.X, targetAreaSize.Y / headSprite.size.Y); float scale = Math.Min(targetAreaSize.X / headSprite.size.X, targetAreaSize.Y / headSprite.size.Y);
headSprite.SourceRect = new Rectangle(CalculateOffset(headSprite, Head.SheetIndex.ToPoint()), headSprite.SourceRect.Size); headSprite.SourceRect = new Rectangle(CalculateOffset(headSprite, Head.SheetIndex.ToPoint()), headSprite.SourceRect.Size);
SetHeadEffect(spriteBatch); SetHeadEffect(spriteBatch);
headSprite.Draw(spriteBatch, screenPos, scale: scale, color: Head.SkinColor); headSprite.Draw(spriteBatch, screenPos, scale: scale, color: Head.SkinColor, spriteEffect: spriteEffects);
if (AttachmentSprites != null) if (AttachmentSprites != null)
{ {
float depthStep = 0.000001f; float depthStep = 0.000001f;
foreach (var attachment in AttachmentSprites) foreach (var attachment in AttachmentSprites)
{ {
SetAttachmentEffect(spriteBatch, attachment); SetAttachmentEffect(spriteBatch, attachment);
DrawAttachmentSprite(spriteBatch, attachment, headSprite, Head.SheetIndex, screenPos, scale, depthStep, GetAttachmentColor(attachment, Head.HairColor, Head.FacialHairColor)); DrawAttachmentSprite(spriteBatch, attachment, headSprite, Head.SheetIndex, screenPos, scale, depthStep, GetAttachmentColor(attachment, Head.HairColor, Head.FacialHairColor),
spriteEffects: spriteEffects);
depthStep += depthStep; depthStep += depthStep;
} }
} }
@@ -534,9 +479,6 @@ namespace Barotrauma
{ {
origin.Y = attachment.Sprite.size.Y - origin.Y; origin.Y = attachment.Sprite.size.Y - origin.Y;
} }
//the portrait's origin is forced to 0,0 (presumably for easier drawing on the UI?), see LoadHeadElement
//we need to take that into account here and draw the attachment at where the origin of the "actual" head sprite would be
drawPos += HeadSprite.Origin * scale;
} }
float depth = attachment.Sprite.Depth; float depth = attachment.Sprite.Depth;
if (attachment.InheritLimbDepth) if (attachment.InheritLimbDepth)
@@ -545,7 +487,6 @@ namespace Barotrauma
} }
attachment.Sprite.Draw(spriteBatch, drawPos, color ?? Color.White, origin, rotate: 0, scale: scale, depth: depth, spriteEffect: spriteEffects); attachment.Sprite.Draw(spriteBatch, drawPos, color ?? Color.White, origin, rotate: 0, scale: scale, depth: depth, spriteEffect: spriteEffects);
} }
public static CharacterInfo ClientRead(Identifier speciesName, IReadMessage inc, bool requireJobPrefabFound = true) public static CharacterInfo ClientRead(Identifier speciesName, IReadMessage inc, bool requireJobPrefabFound = true)
{ {
ushort infoID = inc.ReadUInt16(); ushort infoID = inc.ReadUInt16();
@@ -568,7 +509,6 @@ namespace Barotrauma
Color hairColor = inc.ReadColorR8G8B8(); Color hairColor = inc.ReadColorR8G8B8();
Color facialHairColor = inc.ReadColorR8G8B8(); Color facialHairColor = inc.ReadColorR8G8B8();
Identifier npcId = inc.ReadIdentifier(); Identifier npcId = inc.ReadIdentifier();
Identifier factionId = inc.ReadIdentifier(); Identifier factionId = inc.ReadIdentifier();

View File

@@ -833,6 +833,9 @@ namespace Barotrauma
causeOfDeathAffliction = afflictionPrefab; causeOfDeathAffliction = afflictionPrefab;
} }
} }
Character killer = FindEntityByID(msg.ReadUInt16()) as Character;
bool containsAfflictionData = msg.ReadBoolean(); bool containsAfflictionData = msg.ReadBoolean();
if (!IsDead) if (!IsDead)
{ {
@@ -842,7 +845,7 @@ namespace Barotrauma
} }
else else
{ {
Kill(causeOfDeathType, causeOfDeathAffliction?.Instantiate(1.0f), true); Kill(causeOfDeathType, causeOfDeathAffliction?.Instantiate(1.0f, killer), true);
} }
} }
if (containsAfflictionData) if (containsAfflictionData)

View File

@@ -221,7 +221,7 @@ namespace Barotrauma
new GUICustomComponent(new RectTransform(new Vector2(0.2f, 1.0f), nameContainer.RectTransform, Anchor.CenterLeft), new GUICustomComponent(new RectTransform(new Vector2(0.2f, 1.0f), nameContainer.RectTransform, Anchor.CenterLeft),
onDraw: (spriteBatch, component) => onDraw: (spriteBatch, component) =>
{ {
character.Info?.DrawPortrait(spriteBatch, new Vector2(component.Rect.X, component.Rect.Center.Y - component.Rect.Width / 2), Vector2.Zero, component.Rect.Width, false, character != Character.Controlled); character.Info?.DrawIcon(spriteBatch, component.Rect.Center.ToVector2(), component.Rect.Size.ToVector2());
}); });
characterName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), nameContainer.RectTransform), "", textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont) characterName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), nameContainer.RectTransform), "", textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont)
{ {
@@ -1041,8 +1041,29 @@ namespace Barotrauma
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions) foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{ {
var affliction = kvp.Key; var affliction = kvp.Key;
affliction.Prefab.AfflictionOverlay?.Draw(spriteBatch, Vector2.Zero, Color.White * affliction.GetAfflictionOverlayMultiplier(), Vector2.Zero, 0.0f, if (affliction.Prefab is AfflictionPrefab { AfflictionOverlay: not null } afflictionPrefab)
new Vector2(GameMain.GraphicsWidth / DamageOverlay.size.X, GameMain.GraphicsHeight / DamageOverlay.size.Y)); {
Vector2 screenSize = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
if (afflictionPrefab.AfflictionOverlay is SpriteSheet spriteSheet)
{
spriteSheet.Draw(spriteBatch,
spriteIndex: spriteSheet.GetAnimatedSpriteIndex(afflictionPrefab.AfflictionOverlayAnimSpeed),
pos: Vector2.Zero,
color: Color.White * affliction.GetAfflictionOverlayMultiplier(),
origin: Vector2.Zero,
rotate: 0,
scale: screenSize / spriteSheet.FrameSize.ToVector2());
}
else if (afflictionPrefab.AfflictionOverlay is Sprite sprite)
{
sprite.Draw(spriteBatch,
pos: Vector2.Zero,
color: Color.White * affliction.GetAfflictionOverlayMultiplier(),
origin: Vector2.Zero,
rotate: 0,
scale: screenSize / sprite.size);
}
}
var activeEffect = affliction.GetActiveEffect(); var activeEffect = affliction.GetActiveEffect();
if (activeEffect is { ThermalOverlayRange: > 0.0f }) if (activeEffect is { ThermalOverlayRange: > 0.0f })
@@ -1554,9 +1575,9 @@ namespace Barotrauma
}; };
var description = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), parent.RectTransform), var description = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), parent.RectTransform),
affliction.Prefab.GetDescription( RichString.Rich(affliction.Prefab.GetDescription(
affliction.Strength, affliction.Strength,
Character == Character.Controlled ? AfflictionPrefab.Description.TargetType.Self : AfflictionPrefab.Description.TargetType.OtherCharacter), Character == Character.Controlled ? AfflictionPrefab.Description.TargetType.Self : AfflictionPrefab.Description.TargetType.OtherCharacter)),
textAlignment: Alignment.TopLeft, wrap: true) textAlignment: Alignment.TopLeft, wrap: true)
{ {
CanBeFocused = false CanBeFocused = false

View File

@@ -912,6 +912,14 @@ namespace Barotrauma
DebugConsole.ThrowError("The command 'wikiimage_sub' failed.", e); DebugConsole.ThrowError("The command 'wikiimage_sub' failed.", e);
} }
})); }));
AssignOnExecute("loslightingfreecam", (string[] args) =>
{
ExecuteCommand("los");
ExecuteCommand("lighting");
ExecuteCommand("freecam");
});
AssignRelayToServer("loslightingfreecam", false);
AssignRelayToServer("kick", false); AssignRelayToServer("kick", false);
AssignRelayToServer("kickid", false); AssignRelayToServer("kickid", false);
@@ -1101,6 +1109,42 @@ namespace Barotrauma
(lightComponent.LightColor.A / 255.0f) * value.W); (lightComponent.LightColor.A / 255.0f) * value.W);
} }
}, isCheat: false)); }, isCheat: false));
commands.Add(new Command("steamtimelinetest", "steamtimelinetest: Test the Steamworks timeline feature.", (string[] args) =>
{
// Add an instantaneous event to the Steam timeline
var eventHandle = Steamworks.SteamTimeline.AddInstantaneousTimelineEvent(
"Barotrauma Test Event",
"This is a test event created from the debug console",
"steam_marker", // Important: icon must be specified, or it does nothing :D
1, // Priority
0.0f, // Current time (no offset)
Steamworks.TimelineEventClipPriority.Standard);
NewMessage($"Steamworks timeline test: Added instantaneous event with handle: {eventHandle}", Color.Yellow);
}));
commands.Add(new Command("setsteamtimelinegamemode", "setsteamtimelinegamemode [gamemode]: Sets the Steam timeline gamemode to the specified value.", args =>
{
if (args.Length == 0)
{
NewMessage("Please specify a gamemode. Available modes: " + string.Join(", ", Enum.GetNames(typeof(SteamTimelineManager.TimelineGameMode))), Color.Red);
return;
}
if (Enum.TryParse<SteamTimelineManager.TimelineGameMode>(args[0], ignoreCase: true, out var gameMode))
{
SteamTimelineManager.SetTimelineGameMode(gameMode);
NewMessage($"Timeline gamemode set to: {gameMode}", Color.Green);
}
else
{
NewMessage($"Invalid gamemode '{args[0]}'. Available modes: " + string.Join(", ", Enum.GetNames(typeof(SteamTimelineManager.TimelineGameMode))), Color.Red);
}
}, isCheat: true, getValidArgs: () =>
{
return new[] { Enum.GetNames(typeof(SteamTimelineManager.TimelineGameMode)) };
}));
commands.Add(new Command("color|colour", "Change color (as bytes from 0 to 255) of the selected item/structure instances. Applied only in the subeditor.", (string[] args) => commands.Add(new Command("color|colour", "Change color (as bytes from 0 to 255) of the selected item/structure instances. Applied only in the subeditor.", (string[] args) =>
{ {
@@ -2535,6 +2579,85 @@ namespace Barotrauma
#if DEBUG #if DEBUG
commands.Add(new Command(
"listachievements",
"listachievements: Lists all achievement identifiers, names, descriptions, and their current Steam status (Locked/Unlocked).",
(string[] args) =>
{
NewMessage("--- Achievement Status: Name - (Identifier) - [Status] - Description ---", Color.Cyan);
var supportedIds = AchievementManager.SupportedAchievements;
if (supportedIds == null || !supportedIds.Any())
{
NewMessage("No achievement identifiers found in AchievementManager.SupportedAchievements.", Color.Yellow);
NewMessage("-------------------------------------------------------------------", Color.Cyan);
return;
}
if (!SteamManager.IsInitialized || !Steamworks.SteamClient.IsValid)
{
NewMessage("Steam not initialized. Cannot fetch achievement status or texts.", Color.Red);
foreach (Identifier id in supportedIds.OrderBy(i => i.Value))
{
NewMessage($"- Name N/A - ({id.Value}) - [Status Unknown] - Description N/A", Color.Red);
}
}
else
{
try
{
var steamAchievements = Steamworks.SteamUserStats.Achievements
.ToDictionary(a => a.Identifier, StringComparer.OrdinalIgnoreCase);
foreach (Identifier id in supportedIds.OrderBy(i => i.Value))
{
string statusText;
string nameText = "Name N/A";
string descText = "Description N/A";
Color statusColor;
if (steamAchievements.TryGetValue(id.Value, out var steamAch))
{
nameText = steamAch.Name ?? "Name N/A";
descText = steamAch.Description ?? "Description N/A";
if (steamAch.State)
{
statusText = "[Unlocked]";
statusColor = Color.LimeGreen;
}
else
{
statusText = "[Locked]";
statusColor = Color.Orange;
}
}
else
{
statusText = "[Not Found on Steam]";
statusColor = Color.Red;
}
string output = $"- {nameText} - ({id.Value}) - {statusText} - {descText}";
NewMessage(output, statusColor);
}
}
catch (Exception e)
{
ThrowError("Error retrieving achievement statuses/texts from Steam.", e);
foreach (Identifier id in supportedIds.OrderBy(i => i.Value))
{
NewMessage($"- Name N/A - ({id.Value}) - [Status Error] - Description N/A", Color.Red);
}
}
}
NewMessage("-------------------------------------------------------------------", Color.Cyan);
},
isCheat: true
));
commands.Add(new Command("unlockachievement", "unlockachievement [identifier]: Unlocks the specified achievement.", (string[] args) => commands.Add(new Command("unlockachievement", "unlockachievement [identifier]: Unlocks the specified achievement.", (string[] args) =>
{ {
if (args.Length < 1) if (args.Length < 1)
@@ -2545,6 +2668,60 @@ namespace Barotrauma
NewMessage($"Unlocked \"{args[0]}\"."); NewMessage($"Unlocked \"{args[0]}\".");
AchievementManager.UnlockAchievement(args[0].ToIdentifier()); AchievementManager.UnlockAchievement(args[0].ToIdentifier());
}, isCheat: true)); }, isCheat: true));
commands.Add(new Command(
"resetachievement",
"resetachievement [identifier]: Clears (locks) the specified Steam achievement for testing.",
args =>
{
if (args.Length < 1)
{
ThrowError("Please specify the achievement identifier to reset.");
return;
}
if (!SteamManager.IsInitialized || !Steamworks.SteamClient.IsValid)
{
ThrowError("Steam not initialized.");
return;
}
string achievementId = args[0];
bool found = false;
bool success = false;
try
{
var achievement = Steamworks.SteamUserStats.Achievements
.FirstOrDefault(a => a.Identifier.Equals(achievementId, StringComparison.OrdinalIgnoreCase));
if (achievement.Identifier == null)
{
ThrowError($"Achievement with identifier \"{achievementId}\" not found.");
return;
}
found = true;
success = achievement.Clear();
if (success)
{
// IMPORTANT: Store the stats to make the change persistent
SteamManager.StoreStats();
NewMessage($"Reset achievement \"{achievementId}\".", Color.Yellow);
}
else
{
ThrowError($"Failed to clear achievement \"{achievementId}\" (Steamworks returned false).");
}
}
catch (Exception e)
{
ThrowError($"An error occurred while trying to reset achievement \"{achievementId}\". Found: {found}, Success: {success}", e);
}
},
isCheat: true
));
commands.Add(new Command("deathprompt", "Shows the death prompt for testing purposes.", (string[] args) => commands.Add(new Command("deathprompt", "Shows the death prompt for testing purposes.", (string[] args) =>
{ {
@@ -2706,7 +2883,14 @@ namespace Barotrauma
{ {
int amount = 1; int amount = 1;
if (args.Length > 0) { int.TryParse(args[0], out amount); } if (args.Length > 0) { int.TryParse(args[0], out amount); }
GameMain.LevelEditorScreen.TestLevelGenerationForErrors(amount); try
{
GameMain.LevelEditorScreen.TestLevelGenerationForErrors(amount);
}
catch (Exception e)
{
ThrowError("Failed to generate levels", e);
}
} }
else else
{ {

View File

@@ -139,6 +139,6 @@ namespace Barotrauma
} }
} }
public override IEnumerable<Entity> HudIconTargets => targets.Where(static t => !t.Retrieved && t.Item.GetRootInventoryOwner() is not Character { IsLocalPlayer: true }).Select(static t => t.Item); public override IEnumerable<Entity> HudIconTargets => targets.Where(static t => !t.Retrieved && t.Item?.GetRootInventoryOwner() is not Character { IsLocalPlayer: true }).Select(static t => t.Item);
} }
} }

View File

@@ -2236,6 +2236,29 @@ namespace Barotrauma
return textBox; return textBox;
} }
/// <summary>
/// Creates a pre-built filter box.
/// </summary>
/// <remarks>
/// The filter function must be set using <see cref="GUITextBox.OnTextChanged"/>.<see langword="add"/>.
/// </remarks>
public static GUITextBox CreateFilterBox(RectTransform rectT)
{
GUITextBox textBox = new(rectT, createClearButton: true)
{
OnEnterPressed = (tb, _) =>
{
tb.Deselect();
return true;
}
};
GUITextBlock label = new(new RectTransform(Vector2.One, textBox.TextBlock.RectTransform, Anchor.CenterLeft), TextManager.Get("serverlog.filter"), GUIStyle.TextColorNormal * 0.75f);
textBox.OnSelected += (_, _) => label.Visible = false;
textBox.OnDeselected += (tb, _) => label.Visible = tb.Text.IsNullOrEmpty();
textBox.OnTextChanged += (tb, text) => label.Visible = !tb.Selected && text.IsNullOrEmpty();
return textBox;
}
public static void NotifyPrompt(LocalizedString header, LocalizedString body) public static void NotifyPrompt(LocalizedString header, LocalizedString body)
{ {
GUIMessageBox msgBox = new GUIMessageBox(header, body, new[] { TextManager.Get("Ok") }, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); GUIMessageBox msgBox = new GUIMessageBox(header, body, new[] { TextManager.Get("Ok") }, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175));
@@ -2295,9 +2318,61 @@ namespace Barotrauma
return msgBox; return msgBox;
} }
#endregion #nullable enable
#region Item UI
/// <summary>
/// Creates a 7-segment display.
/// </summary>
/// <param name="leftLabel">Returns <see langword="null"/> if <paramref name="leftLabelText"/> is <see langword="null"/> or empty.</param>
/// <param name="rightLabelText">Defaults to <c>TextManager.Get("kilowatt")</c>.</param>
/// <param name="leftLabelFont">Defaults to <see cref="GUIStyle.LargeFont"/>.</param>
public static GUITextBlock CreateDigitalDisplay(RectTransform rect, out GUITextBlock? leftLabel, out GUITextBlock rightLabel, LocalizedString? leftLabelText = null, LocalizedString? rightLabelText = null, LocalizedString? tooltip = null, GUIFont? leftLabelFont = null)
{
GUILayoutGroup textArea = new(rect, isHorizontal: true, childAnchor: Anchor.CenterLeft)
{
Stretch = true,
CanBeFocused = true,
ToolTip = tooltip!,
AbsoluteSpacing = 5
};
#region Element positioning leftLabel = null;
if (!leftLabelText.IsNullOrEmpty())
{
leftLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), textArea.RectTransform), leftLabelText, textColor: GUIStyle.TextColorBright, font: leftLabelFont ?? GUIStyle.LargeFont, textAlignment: Alignment.CenterRight);
}
GUIFrame displayBackground = new(new RectTransform(new Vector2(0.55f, 0.8f), textArea.RectTransform), style: "DigitalFrameDark");
GUITextBlock displayText = new(new RectTransform(new Vector2(0.9f, 0.95f), displayBackground.RectTransform, Anchor.Center), "8888", font: GUIStyle.DigitalFont, textColor: GUIStyle.TextColorDark, textAlignment: Alignment.CenterRight);
displayText.TextScale = Math.Max((displayText.Rect.Height - 10) / GUIStyle.DigitalFont.LineHeight, 0.1f);
rightLabel = new GUITextBlock(new RectTransform(Vector2.Zero, textArea.RectTransform), rightLabelText ?? TextManager.Get("kilowatt"), textColor: GUIStyle.TextColorNormal, font: GUIStyle.Font, textAlignment: Alignment.CenterRight)
{
Padding = Vector4.Zero
};
rightLabel.RectTransform.MinSize = rightLabel.TextSize.ToPoint();
textArea.GetAllChildren().ForEach(child => child.CanBeFocused = false);
return displayText;
}
/// <param name="labelFont">Defaults to <see cref="GUIStyle.SubHeadingFont"/>.</param>
public static GUITickBox CreateIndicatorLight(RectTransform rect, string style = "", LocalizedString? label = null, LocalizedString? tooltip = null, GUIFont? labelFont = null)
{
GUITickBox indicator = new(rect, label, font: labelFont ?? GUIStyle.SubHeadingFont, style: style)
{
Enabled = false,
ToolTip = tooltip!
};
indicator.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal);
return indicator;
}
#endregion
#nullable restore
#endregion
#region Element positioning
private static List<T> CreateElements<T>(int count, RectTransform parent, Func<RectTransform, T> constructor, private static List<T> CreateElements<T>(int count, RectTransform parent, Func<RectTransform, T> constructor,
Vector2? relativeSize = null, Point? absoluteSize = null, Vector2? relativeSize = null, Point? absoluteSize = null,
Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, Point? minSize = null, Point? maxSize = null, Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, Point? minSize = null, Point? maxSize = null,

View File

@@ -211,7 +211,7 @@ namespace Barotrauma
} }
var selfStyle = Style; var selfStyle = Style;
textBlock = new GUITextBlock(new RectTransform(Vector2.One, rectT, Anchor.Center), text, textAlignment: textAlignment, style: null) textBlock = new GUITextBlock(new RectTransform(Vector2.One, rectT, Anchor.Center), RichString.Rich(text), textAlignment: textAlignment, style: null)
{ {
TextColor = selfStyle?.TextColor ?? Color.Black, TextColor = selfStyle?.TextColor ?? Color.Black,
HoverTextColor = selfStyle?.HoverTextColor ?? Color.Black, HoverTextColor = selfStyle?.HoverTextColor ?? Color.Black,

View File

@@ -446,19 +446,22 @@ namespace Barotrauma
UpdateScrollBarSize(); UpdateScrollBarSize();
} }
public void Select(object userData, Force force = Force.No, AutoScroll autoScroll = AutoScroll.Enabled) public bool Select(object userData, Force force = Force.No, AutoScroll autoScroll = AutoScroll.Enabled)
{ {
var children = Content.Children; var children = Content.Children;
int i = 0; int i = 0;
bool wasSelected = false;
foreach (GUIComponent child in children) foreach (GUIComponent child in children)
{ {
if (Equals(child.UserData, userData)) if (Equals(child.UserData, userData))
{ {
wasSelected = true;
Select(i, force, autoScroll); Select(i, force, autoScroll);
if (!SelectMultiple) { return; } if (!SelectMultiple) { return true; }
} }
i++; i++;
} }
return wasSelected;
} }
private Point CalculateFrameSize(bool isHorizontal, int scrollBarSize) private Point CalculateFrameSize(bool isHorizontal, int scrollBarSize)

View File

@@ -1021,6 +1021,14 @@ namespace Barotrauma
{ {
MaxTextLength = Client.MaxNameLength MaxTextLength = Client.MaxNameLength
}; };
nameBox.OnTextChanged += (GUITextBox textBox, string text) =>
{
if (text.Contains('\n') || text.Contains('\r'))
{
textBox.Text = text.Replace("\r\n", " ").Replace('\n', ' ').Replace('\r', ' ');
}
return true;
};
new GUIButton(new RectTransform(groupElementSize, layoutGroup.RectTransform), text: TextManager.Get("confirm")) new GUIButton(new RectTransform(groupElementSize, layoutGroup.RectTransform), text: TextManager.Get("confirm"))
{ {
OnClicked = (button, userData) => OnClicked = (button, userData) =>

View File

@@ -1,4 +1,4 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -749,7 +749,7 @@ namespace Barotrauma
{ {
new GUICustomComponent(new RectTransform(Vector2.One, parent.RectTransform, scaleBasis: ScaleBasis.BothHeight), (spriteBatch, component) => new GUICustomComponent(new RectTransform(Vector2.One, parent.RectTransform, scaleBasis: ScaleBasis.BothHeight), (spriteBatch, component) =>
{ {
info.DrawPortrait(spriteBatch, component.Rect.Location.ToVector2(), Vector2.Zero, component.Rect.Width); info.DrawIcon(spriteBatch, component.Rect.Center.ToVector2(), component.Rect.Size.ToVector2());
}); });
GUILayoutGroup textGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.8f), parent.RectTransform)); GUILayoutGroup textGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.8f), parent.RectTransform));

View File

@@ -273,7 +273,7 @@ namespace Barotrauma
new GUICustomComponent(new RectTransform(new Vector2(0.25f, 1f), topLayout.RectTransform), onDraw: (batch, component) => new GUICustomComponent(new RectTransform(new Vector2(0.25f, 1f), topLayout.RectTransform), onDraw: (batch, component) =>
{ {
info.DrawPortrait(batch, component.Rect.Location.ToVector2(), Vector2.Zero, component.Rect.Width, false, false); info.DrawIcon(batch, component.Rect.Center.ToVector2(), component.Rect.Size.ToVector2());
}); });
GUILayoutGroup nameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1f), topLayout.RectTransform)) GUILayoutGroup nameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1f), topLayout.RectTransform))
@@ -327,7 +327,7 @@ namespace Barotrauma
if (extraTalent.IsHiddenExtraTalent) { continue; } if (extraTalent.IsHiddenExtraTalent) { continue; }
GUIImage talentImg = new GUIImage(new RectTransform(Vector2.One, extraTalentList.Content.RectTransform, scaleBasis: ScaleBasis.BothHeight), sprite: extraTalent.Icon, scaleToFit: true) GUIImage talentImg = new GUIImage(new RectTransform(Vector2.One, extraTalentList.Content.RectTransform, scaleBasis: ScaleBasis.BothHeight), sprite: extraTalent.Icon, scaleToFit: true)
{ {
ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{extraTalent.DisplayName}‖color:end‖" + "\n\n" + ToolBox.ExtendColorToPercentageSigns(extraTalent.Description.Value)), ToolTip = GetTalentTooltip(extraTalent, characterInfo),
Color = GUIStyle.Green Color = GUIStyle.Green
}; };
} }
@@ -637,7 +637,7 @@ namespace Barotrauma
GUIFrame croppedTalentFrame = new GUIFrame(new RectTransform(Vector2.One, talentFrame.RectTransform, anchor: Anchor.Center, scaleBasis: ScaleBasis.BothHeight), style: null); GUIFrame croppedTalentFrame = new GUIFrame(new RectTransform(Vector2.One, talentFrame.RectTransform, anchor: Anchor.Center, scaleBasis: ScaleBasis.BothHeight), style: null);
GUIButton talentButton = new GUIButton(new RectTransform(Vector2.One, croppedTalentFrame.RectTransform, anchor: Anchor.Center), style: null) GUIButton talentButton = new GUIButton(new RectTransform(Vector2.One, croppedTalentFrame.RectTransform, anchor: Anchor.Center), style: null)
{ {
ToolTip = CreateTooltip(talent, characterInfo), ToolTip = GetTalentTooltip(talent, characterInfo),
UserData = talent.Identifier, UserData = talent.Identifier,
PressedColor = pressedColor, PressedColor = pressedColor,
Enabled = info.Character != null, Enabled = info.Character != null,
@@ -703,24 +703,6 @@ namespace Barotrauma
}, },
}; };
static RichString CreateTooltip(TalentPrefab talent, CharacterInfo? character)
{
LocalizedString progress = string.Empty;
if (character is not null && talent.TrackedStat.TryUnwrap(out var stat))
{
var statValue = character.GetSavedStatValue(StatTypes.None, stat.PermanentStatIdentifier);
var intValue = (int)MathF.Round(statValue);
progress = "\n\n";
progress += statValue < stat.Max
? TextManager.GetWithVariables("talentprogress", ("[amount]", intValue.ToString()), ("[max]", stat.Max.ToString()))
: TextManager.Get("talentprogresscompleted");
}
RichString tooltip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{talent.DisplayName}‖color:end‖\n\n{ToolBox.ExtendColorToPercentageSigns(talent.Description.Value)}{progress}");
return tooltip;
}
talentButton.Color = talentButton.HoverColor = talentButton.PressedColor = talentButton.SelectedColor = talentButton.DisabledColor = Color.Transparent; talentButton.Color = talentButton.HoverColor = talentButton.PressedColor = talentButton.SelectedColor = talentButton.DisabledColor = Color.Transparent;
GUIComponent iconImage; GUIComponent iconImage;
@@ -814,6 +796,24 @@ namespace Barotrauma
GUITextBlock.AutoScaleAndNormalize(talentResetButton.TextBlock, talentApplyButton.TextBlock); GUITextBlock.AutoScaleAndNormalize(talentResetButton.TextBlock, talentApplyButton.TextBlock);
} }
private static RichString GetTalentTooltip(TalentPrefab talent, CharacterInfo? character)
{
LocalizedString progress = string.Empty;
if (character is not null && talent.TrackedStat.TryUnwrap(out var stat))
{
var statValue = character.GetSavedStatValue(StatTypes.None, stat.PermanentStatIdentifier);
var intValue = (int)MathF.Round(statValue);
progress = "\n\n";
progress += statValue < stat.Max
? TextManager.GetWithVariables("talentprogress", ("[amount]", intValue.ToString()), ("[max]", stat.Max.ToString()))
: TextManager.Get("talentprogresscompleted");
}
RichString tooltip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{talent.DisplayName}‖color:end‖\n\n{ToolBox.ExtendColorToPercentageSigns(talent.Description.Value)}{progress}");
return tooltip;
}
private bool ResetTalentSelection(GUIButton guiButton, object userData) private bool ResetTalentSelection(GUIButton guiButton, object userData)
{ {
if (characterInfo is null) { return false; } if (characterInfo is null) { return false; }

View File

@@ -7,6 +7,7 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using Barotrauma.Extensions; using Barotrauma.Extensions;
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
using Barotrauma.Networking;
using FarseerPhysics; using FarseerPhysics;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
@@ -949,7 +950,7 @@ namespace Barotrauma
GUILayoutGroup buttonLayout = new GUILayoutGroup(rectT(1f, 1f, toggleButton.Frame), isHorizontal: true); GUILayoutGroup buttonLayout = new GUILayoutGroup(rectT(1f, 1f, toggleButton.Frame), isHorizontal: true);
LocalizedString slotText = ""; LocalizedString slotText = "";
if (linkedItems.Count() > 1) if (linkedItems.Count > 1)
{ {
slotText = TextManager.GetWithVariable("weaponslot", "[number]", string.Join(", ", linkedItems.Select(it => (swappableEntities.IndexOf(it) + 1).ToString()))); slotText = TextManager.GetWithVariable("weaponslot", "[number]", string.Join(", ", linkedItems.Select(it => (swappableEntities.IndexOf(it) + 1).ToString())));
} }
@@ -978,7 +979,7 @@ namespace Barotrauma
List<GUIFrame> frames = new List<GUIFrame>(); List<GUIFrame> frames = new List<GUIFrame>();
if (currentOrPending != null) if (currentOrPending != null)
{ {
bool canUninstall = item.PendingItemSwap != null || !(currentOrPending.SwappableItem?.ReplacementOnUninstall.IsEmpty ?? true); bool canUninstall = HasPermission && (item.PendingItemSwap != null || !(currentOrPending.SwappableItem?.ReplacementOnUninstall.IsEmpty ?? true));
bool isUninstallPending = item.Prefab.SwappableItem != null && item.PendingItemSwap?.Identifier == item.Prefab.SwappableItem.ReplacementOnUninstall; bool isUninstallPending = item.Prefab.SwappableItem != null && item.PendingItemSwap?.Identifier == item.Prefab.SwappableItem.ReplacementOnUninstall;
if (isUninstallPending) { canUninstall = false; } if (isUninstallPending) { canUninstall = false; }
@@ -1030,7 +1031,8 @@ namespace Barotrauma
buttonStyle: isPurchased ? "WeaponInstallButton" : "StoreAddToCrateButton").Frame); 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 (HasPermission && PlayerBalance >= price)
{ {
buyButton.Enabled = true; buyButton.Enabled = true;
buyButton.OnClicked += (button, o) => buyButton.OnClicked += (button, o) =>
@@ -1272,7 +1274,7 @@ namespace Barotrauma
if (!prefabFrame.BuyButton.TryUnwrap(out BuyButtonFrame buyButtonFrame)) { return; } if (!prefabFrame.BuyButton.TryUnwrap(out BuyButtonFrame buyButtonFrame)) { return; }
if (!HasPermission || !prefab.IsApplicable(submarine.Info) || (itemsOnSubmarine != null && !itemsOnSubmarine.Any(it => category.CanBeApplied(it, prefab)))) if (!prefab.IsApplicable(submarine.Info) || (itemsOnSubmarine != null && !itemsOnSubmarine.Any(it => category.CanBeApplied(it, prefab))))
{ {
prefabFrame.Frame.Enabled = false; prefabFrame.Frame.Enabled = false;
prefabFrame.Description.Enabled = false; prefabFrame.Description.Enabled = false;
@@ -1289,12 +1291,14 @@ namespace Barotrauma
("[amount]", prefab.Price.GetBuyPrice(prefab, Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation, characterList).ToString())); ("[amount]", prefab.Price.GetBuyPrice(prefab, Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation, characterList).ToString()));
currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), promptBody, () => currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), promptBody, () =>
{ {
if (GameMain.NetworkMember != null) if (Campaign.UpgradeManager.TryPurchaseUpgrade(prefab, category))
{ {
WaitForServerUpdate = true; if (GameMain.NetworkMember != null)
{
WaitForServerUpdate = true;
}
GameMain.Client?.SendCampaignState();
} }
Campaign.UpgradeManager.PurchaseUpgrade(prefab, category);
GameMain.Client?.SendCampaignState();
return true; return true;
}, overrideConfirmButtonSound: GUISoundType.ConfirmTransaction); }, overrideConfirmButtonSound: GUISoundType.ConfirmTransaction);
@@ -1722,7 +1726,7 @@ namespace Barotrauma
if (buttonParent.FindChild(UpgradeStoreUserData.BuyButton, recursive: true) is GUIButton button) if (buttonParent.FindChild(UpgradeStoreUserData.BuyButton, recursive: true) is GUIButton button)
{ {
bool canBuy = !WaitForServerUpdate && !isMax && campaign.GetBalance() >= price && prefab.HasResourcesToUpgrade(Character.Controlled, currentLevel + 1); bool canBuy = !WaitForServerUpdate && HasPermission && !isMax && campaign.GetBalance() >= price && prefab.HasResourcesToUpgrade(Character.Controlled, currentLevel + 1);
button.Enabled = canBuy; button.Enabled = canBuy;
} }
@@ -1935,7 +1939,7 @@ namespace Barotrauma
return frames.ToArray(); return frames.ToArray();
} }
private bool HasPermission => true; private static bool HasPermission => CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageCampaign);
// just a shortcut to create new RectTransforms since all the new RectTransform and new Vector2 confuses my IDE (and me) // just a shortcut to create new RectTransforms since all the new RectTransform and new Vector2 confuses my IDE (and me)
private static RectTransform rectT(float x, float y, GUIComponent parentComponent, Anchor anchor = Anchor.TopLeft, ScaleBasis scaleBasis = ScaleBasis.Normal) private static RectTransform rectT(float x, float y, GUIComponent parentComponent, Anchor anchor = Anchor.TopLeft, ScaleBasis scaleBasis = ScaleBasis.Normal)

View File

@@ -686,6 +686,7 @@ namespace Barotrauma
private void OnInvitedToSteamGame(string connectCommand) private void OnInvitedToSteamGame(string connectCommand)
{ {
DebugConsole.NewMessage($"Invited to Steam game, connect command: {connectCommand}", Color.Lime);
try try
{ {
ConnectCommand = Barotrauma.Networking.ConnectCommand.Parse(ToolBox.SplitCommand(connectCommand)); ConnectCommand = Barotrauma.Networking.ConnectCommand.Parse(ToolBox.SplitCommand(connectCommand));
@@ -825,6 +826,7 @@ namespace Barotrauma
{ {
if (ConnectCommand.TryUnwrap(out var connectCommand)) if (ConnectCommand.TryUnwrap(out var connectCommand))
{ {
DebugConsole.NewMessage($"Processing connect command: {connectCommand}...", Color.Lime);
if (Client != null) if (Client != null)
{ {
Client.Quit(); Client.Quit();
@@ -836,6 +838,7 @@ namespace Barotrauma
if (connectCommand.SteamLobbyIdOption.TryUnwrap(out var lobbyId)) if (connectCommand.SteamLobbyIdOption.TryUnwrap(out var lobbyId))
{ {
DebugConsole.NewMessage($"Connecting to lobby ID {lobbyId}...", Color.Lime);
SteamManager.JoinLobby(lobbyId.Value, joinServer: true); SteamManager.JoinLobby(lobbyId.Value, joinServer: true);
} }
else if ((connectCommand.NameAndP2PEndpointsOption.TryUnwrap(out var nameAndEndpoint) && nameAndEndpoint is { ServerName: var serverName, Endpoints: var endpoints })) else if ((connectCommand.NameAndP2PEndpointsOption.TryUnwrap(out var nameAndEndpoint) && nameAndEndpoint is { ServerName: var serverName, Endpoints: var endpoints }))
@@ -844,6 +847,7 @@ namespace Barotrauma
endpoints.Cast<Endpoint>().ToImmutableArray(), endpoints.Cast<Endpoint>().ToImmutableArray(),
string.IsNullOrWhiteSpace(serverName) ? endpoints.First().StringRepresentation : serverName, string.IsNullOrWhiteSpace(serverName) ? endpoints.First().StringRepresentation : serverName,
Option<int>.None()); Option<int>.None());
DebugConsole.NewMessage($"Connecting to endpoint {endpoints.First().StringRepresentation}...", Color.Lime);
} }
else if ((connectCommand.NameAndLidgrenEndpointOption.TryUnwrap(out var nameAndLidgrenEndpoint) && nameAndLidgrenEndpoint is { ServerName: var lidgrenServerName, Endpoint: var endpoint })) else if ((connectCommand.NameAndLidgrenEndpointOption.TryUnwrap(out var nameAndLidgrenEndpoint) && nameAndLidgrenEndpoint is { ServerName: var lidgrenServerName, Endpoint: var endpoint }))
{ {
@@ -853,6 +857,10 @@ namespace Barotrauma
string.IsNullOrWhiteSpace(lidgrenServerName) ? endpoint.StringRepresentation : lidgrenServerName, string.IsNullOrWhiteSpace(lidgrenServerName) ? endpoint.StringRepresentation : lidgrenServerName,
Option<int>.None()); Option<int>.None());
} }
else
{
DebugConsole.NewMessage($"Cannot connect: unrecognized connect command.", Color.Lime);
}
ConnectCommand = Option<ConnectCommand>.None(); ConnectCommand = Option<ConnectCommand>.None();
} }
@@ -1197,8 +1205,9 @@ namespace Barotrauma
CoroutineManager.StopCoroutines("EndCinematic"); CoroutineManager.StopCoroutines("EndCinematic");
if (GameSession != null) if (GameSession != null && GameSession.IsRunning)
{ {
AchievementManager.OnRoundEnded(GameSession, roundInterrupted: true);
GameAnalyticsManager.AddProgressionEvent(GameAnalyticsManager.ProgressionStatus.Fail, GameAnalyticsManager.AddProgressionEvent(GameAnalyticsManager.ProgressionStatus.Fail,
GameSession.GameMode?.Preset.Identifier.Value ?? "none", GameSession.GameMode?.Preset.Identifier.Value ?? "none",
GameSession.RoundDuration); GameSession.RoundDuration);

View File

@@ -592,8 +592,7 @@ namespace Barotrauma
public bool CharacterClicked(GUIComponent component, object selection) public bool CharacterClicked(GUIComponent component, object selection)
{ {
if (!AllowCharacterSwitch) { return false; } if (!AllowCharacterSwitch) { return false; }
if (selection is not Character character || character.IsDead || character.IsUnconscious) { return false; } if (selection is not Character character || !character.IsOnPlayerTeam) { return false; }
if (!character.IsOnPlayerTeam) { return false; }
if (GameMain.IsMultiplayer) if (GameMain.IsMultiplayer)
{ {
@@ -605,6 +604,8 @@ namespace Barotrauma
return true; return true;
} }
if (character.IsDead || character.IsUnconscious) { return false; }
SelectCharacter(character); SelectCharacter(character);
if (GUI.KeyboardDispatcher.Subscriber == crewList) { GUI.KeyboardDispatcher.Subscriber = null; } if (GUI.KeyboardDispatcher.Subscriber == crewList) { GUI.KeyboardDispatcher.Subscriber = null; }
return true; return true;
@@ -3703,7 +3704,8 @@ namespace Barotrauma
canIssueOrders = canIssueOrders =
ChatMessage.CanUseRadio(Character.Controlled) && ChatMessage.CanUseRadio(Character.Controlled) &&
Character.Controlled?.CurrentHull?.Submarine?.TeamID == Character.Controlled.TeamID && Character.Controlled?.CurrentHull?.Submarine?.TeamID == Character.Controlled.TeamID &&
!Character.Controlled.CurrentHull.Submarine.Info.IsWreck; !Character.Controlled.CurrentHull.Submarine.Info.IsWreck &&
GameMain.Client is not { IsBlockedBySpamFilter: true };
} }
if (canIssueOrders) if (canIssueOrders)

View File

@@ -133,56 +133,15 @@ namespace Barotrauma
case "map": case "map":
map = Map.Load(this, subElement); map = Map.Load(this, subElement);
break; break;
case "cargo":
CargoManager.LoadPurchasedItems(subElement);
break;
case "pendingupgrades": //backwards compatibility
case "upgrademanager":
UpgradeManager = new UpgradeManager(this, subElement, isSingleplayer: true);
break;
case "pets":
petsElement = subElement;
break;
case Wallet.LowerCaseSaveElementName:
Bank = new Wallet(Option<Character>.None(), subElement);
break;
case "stats":
LoadStats(subElement);
break;
case "eventmanager":
GameMain.GameSession.EventManager.Load(subElement);
break;
} }
} }
LoadSaveSharedSingleAndMultiplayer(element);
UpgradeManager ??= new UpgradeManager(this); UpgradeManager ??= new UpgradeManager(this);
InitUI(); InitUI();
//backwards compatibility for saves made prior to the addition of personal wallets
int oldMoney = element.GetAttributeInt("money", 0);
if (oldMoney > 0)
{
Bank = new Wallet(Option<Character>.None())
{
Balance = oldMoney
};
}
PurchasedLostShuttlesInLatestSave = element.GetAttributeBool("purchasedlostshuttles", false);
PurchasedHullRepairsInLatestSave = element.GetAttributeBool("purchasedhullrepairs", false);
PurchasedItemRepairsInLatestSave = element.GetAttributeBool("purchaseditemrepairs", false);
CheatsEnabled = element.GetAttributeBool("cheatsenabled", false);
if (CheatsEnabled)
{
DebugConsole.CheatsEnabled = true;
if (!AchievementManager.CheatsEnabled)
{
AchievementManager.CheatsEnabled = true;
new GUIMessageBox("Cheats enabled", "Cheat commands have been enabled on the campaign. You will not receive Steam Achievements until you restart the game.");
}
}
if (map == null) if (map == null)
{ {
throw new System.Exception("Failed to load the campaign save file (saved with an older, incompatible version of Barotrauma)."); throw new System.Exception("Failed to load the campaign save file (saved with an older, incompatible version of Barotrauma).");
@@ -687,6 +646,11 @@ namespace Barotrauma
modeElement.Add(GameMain.GameSession?.EventManager.Save()); modeElement.Add(GameMain.GameSession?.EventManager.Save());
} }
foreach (Identifier unlockedRecipe in GameMain.GameSession.UnlockedRecipes)
{
modeElement.Add(new XElement("unlockedrecipe", new XAttribute("identifier", unlockedRecipe)));
}
//save and remove all items that are in someone's inventory so they don't get included in the sub file as well //save and remove all items that are in someone's inventory so they don't get included in the sub file as well
foreach (Character c in Character.CharacterList) foreach (Character c in Character.CharacterList)
{ {

View File

@@ -346,7 +346,6 @@ namespace Barotrauma
/// </summary> /// </summary>
public void RefreshAnyOpenPlayerInfo() public void RefreshAnyOpenPlayerInfo()
{ {
DebugConsole.NewMessage($"Refreshing any open player info");
if (IsTabMenuOpen && TabMenu.SelectedTab == TabMenu.InfoFrameTab.Talents) if (IsTabMenuOpen && TabMenu.SelectedTab == TabMenu.InfoFrameTab.Talents)
{ {
TabMenuInstance.SelectInfoFrameTab(TabMenu.InfoFrameTab.Talents); TabMenuInstance.SelectInfoFrameTab(TabMenu.InfoFrameTab.Talents);

View File

@@ -907,7 +907,7 @@ namespace Barotrauma
void SetReputationText(GUITextBlock textBlock) void SetReputationText(GUITextBlock textBlock)
{ {
LocalizedString reputationText = Reputation.GetFormattedReputationText(reputation.NormalizedValue, reputation.Value, addColorTags: true); LocalizedString reputationText = Reputation.GetFormattedReputationText(reputation.NormalizedValue, reputation.Value, addColorTags: true);
int reputationChange = (int)Math.Round(reputation.Value - initialReputation); int reputationChange = (int)reputation.Value - (int)initialReputation;
if (Math.Abs(reputationChange) > 0) if (Math.Abs(reputationChange) > 0)
{ {
string changeText = $"{(reputationChange > 0 ? "+" : "") + reputationChange}"; string changeText = $"{(reputationChange > 0 ? "+" : "") + reputationChange}";

View File

@@ -771,6 +771,8 @@ namespace Barotrauma
private QuickUseAction GetQuickUseAction(Item item, bool allowEquip, bool allowInventorySwap, bool allowApplyTreatment) private QuickUseAction GetQuickUseAction(Item item, bool allowEquip, bool allowInventorySwap, bool allowApplyTreatment)
{ {
if (!item.IsInteractable(Character.Controlled)) { return QuickUseAction.None; }
if (allowApplyTreatment && CharacterHealth.OpenHealthWindow != null && if (allowApplyTreatment && CharacterHealth.OpenHealthWindow != null &&
//if the item can be equipped in the health interface slot, don't use it as a treatment but try to equip it //if the item can be equipped in the health interface slot, don't use it as a treatment but try to equip it
!item.AllowedSlots.Contains(InvSlotType.HealthInterface)) !item.AllowedSlots.Contains(InvSlotType.HealthInterface))

View File

@@ -158,7 +158,7 @@ namespace Barotrauma.Items.Components
public void ClientEventRead(IReadMessage msg, float sendingTime) public void ClientEventRead(IReadMessage msg, float sendingTime)
{ {
Health = msg.ReadRangedSingle(0, MaxHealth, 8); Health = msg.ReadRangedSingle(0, MaxWater, 8);
int startOffset = msg.ReadRangedInteger(-1, MaximumVines); int startOffset = msg.ReadRangedInteger(-1, MaximumVines);
if (startOffset > -1) if (startOffset > -1)
{ {

View File

@@ -1,8 +1,9 @@
using Barotrauma.Networking; using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using System; using System;
using System.Diagnostics.Tracing; using System.Collections.Generic;
namespace Barotrauma.Items.Components namespace Barotrauma.Items.Components
{ {
@@ -15,18 +16,33 @@ namespace Barotrauma.Items.Components
public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null)
{ {
if (!IsActive || picker == null || !CanBeAttached(picker) || !picker.IsKeyDown(InputType.Aim) || picker != Character.Controlled) if (!IsActive || picker == null || !picker.IsKeyDown(InputType.Aim) || picker != Character.Controlled || !attachable)
{ {
Drawable = false; Drawable = false;
return; return;
} }
Vector2 gridPos = picker.Position; Color indicatorColor = Color.White;
Vector2 roundedGridPos = new Vector2( if (!CanBeAttached(picker, out IEnumerable<Item> overlappingItems))
MathUtils.RoundTowardsClosest(picker.Position.X, Submarine.GridSize.X), {
MathUtils.RoundTowardsClosest(picker.Position.Y, Submarine.GridSize.Y)); foreach (var overlappingItem in overlappingItems)
{
overlappingItem.Draw(spriteBatch, editing: false, overrideColor: Color.Red * 0.7f, overrideDepth: 0.0f);
}
indicatorColor = Color.Red;
}
Vector2 attachPos = GetAttachPosition(picker); Vector2 attachPos = GetAttachPosition(picker);
Vector2 gridPos = picker.Position;
if (AttachesToFloor)
{
gridPos.Y = attachPos.Y - item.Rect.Height / 2;
}
Vector2 roundedGridPos = new Vector2(
MathUtils.RoundTowardsClosest(gridPos.X, Submarine.GridSize.X),
MathUtils.RoundTowardsClosest(gridPos.Y, Submarine.GridSize.Y));
if (item.Submarine == null) if (item.Submarine == null)
{ {
Structure attachTarget = Structure.GetAttachTarget(item.WorldPosition); Structure attachTarget = Structure.GetAttachTarget(item.WorldPosition);
@@ -35,20 +51,20 @@ namespace Barotrauma.Items.Components
if (attachTarget.Submarine != null) if (attachTarget.Submarine != null)
{ {
//set to submarine-relative position //set to submarine-relative position
gridPos += attachTarget.Submarine.Position; gridPos += attachTarget.Submarine.DrawPosition;
roundedGridPos += attachTarget.Submarine.Position; roundedGridPos += attachTarget.Submarine.DrawPosition;
attachPos += attachTarget.Submarine.Position; attachPos += attachTarget.Submarine.DrawPosition;
} }
} }
} }
else else
{ {
gridPos += item.Submarine.Position; gridPos += item.Submarine.DrawPosition;
roundedGridPos += item.Submarine.Position; roundedGridPos += item.Submarine.DrawPosition;
attachPos += item.Submarine.Position; attachPos += item.Submarine.DrawPosition;
} }
Submarine.DrawGrid(spriteBatch, 14, gridPos, roundedGridPos, alpha: 0.4f); Submarine.DrawGrid(spriteBatch, 14, gridPos, roundedGridPos, alpha: 0.4f, color: indicatorColor);
Sprite sprite = item.Sprite; Sprite sprite = item.Sprite;
foreach (ContainedItemSprite containedSprite in item.Prefab.ContainedSprites) foreach (ContainedItemSprite containedSprite in item.Prefab.ContainedSprites)
@@ -63,7 +79,7 @@ namespace Barotrauma.Items.Components
sprite.Draw( sprite.Draw(
spriteBatch, spriteBatch,
new Vector2(attachPos.X, -attachPos.Y), new Vector2(attachPos.X, -attachPos.Y),
item.SpriteColor * 0.5f, item.SpriteColor.Multiply(indicatorColor) * 0.5f,
item.RotationRad, item.RotationRad,
item.Scale, SpriteEffects.None, 0.0f); item.Scale, SpriteEffects.None, 0.0f);
@@ -75,7 +91,7 @@ namespace Barotrauma.Items.Components
public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null)
{ {
if (!attachable || body == null) { return; } if (!attachable || originalBody == null) { return; }
var eventData = ExtractEventData<AttachEventData>(extraData); var eventData = ExtractEventData<AttachEventData>(extraData);
@@ -115,9 +131,9 @@ namespace Barotrauma.Items.Components
if (attached) if (attached)
{ {
DropConnectedWires(null); DropConnectedWires(null);
if (body != null) if (originalBody != null)
{ {
item.body = body; item.body = originalBody;
item.body.Enabled = true; item.body.Enabled = true;
} }
IsActive = false; IsActive = false;

View File

@@ -76,7 +76,7 @@ namespace Barotrauma.Items.Components
particleEmitterCharges.Add(new ParticleEmitter(subElement)); particleEmitterCharges.Add(new ParticleEmitter(subElement));
break; break;
case "chargesound": case "chargesound":
chargeSound = RoundSound.Load(subElement, false); chargeSound = RoundSound.Load(subElement);
break; break;
} }
} }

View File

@@ -236,7 +236,11 @@ namespace Barotrauma.Items.Components
public ItemComponent GetReplacementOrThis() public ItemComponent GetReplacementOrThis()
{ {
return ReplacedBy?.GetReplacementOrThis() ?? this; if (ReplacedBy != null && ReplacedBy != this)
{
return ReplacedBy.GetReplacementOrThis();
}
return this;
} }
public bool NeedsSoundUpdate() public bool NeedsSoundUpdate()
@@ -511,7 +515,7 @@ namespace Barotrauma.Items.Components
if (HUDOverlay is SpriteSheet spriteSheet) if (HUDOverlay is SpriteSheet spriteSheet)
{ {
spriteSheet.Draw(spriteBatch, spriteSheet.Draw(spriteBatch,
spriteIndex: (int)(Math.Floor(Timing.TotalTimeUnpaused * HUDOverlayAnimSpeed) % spriteSheet.FrameCount), spriteIndex: spriteSheet.GetAnimatedSpriteIndex(HUDOverlayAnimSpeed),
pos: screenSize / 2, color: Color.White, origin: HUDOverlay.Origin, rotate: 0, scale: screenSize / spriteSheet.FrameSize.ToVector2()); pos: screenSize / 2, color: Color.White, origin: HUDOverlay.Origin, rotate: 0, scale: screenSize / spriteSheet.FrameSize.ToVector2());
} }
else else

View File

@@ -553,12 +553,12 @@ namespace Barotrauma.Items.Components
bool flipX = rootBody is { Dir: -1 } || flippedX; bool flipX = rootBody is { Dir: -1 } || flippedX;
if (flipX) if (flipX)
{ {
spriteEffects |= MathUtils.NearlyEqual(spriteRotation % 180, 90.0f) ? SpriteEffects.FlipVertically : SpriteEffects.FlipHorizontally; spriteEffects |= SpriteEffects.FlipHorizontally;
} }
bool flipY = flippedY; bool flipY = flippedY;
if (flipY) if (flipY)
{ {
spriteEffects |= MathUtils.NearlyEqual(spriteRotation % 180, 90.0f) ? SpriteEffects.FlipHorizontally : SpriteEffects.FlipVertically; spriteEffects |= SpriteEffects.FlipVertically;
} }
contained.Item.Sprite.Draw( contained.Item.Sprite.Draw(

View File

@@ -434,12 +434,19 @@ namespace Barotrauma.Items.Components
foreach (FabricationRecipe fi in fabricationRecipes.Values) foreach (FabricationRecipe fi in fabricationRecipes.Values)
{ {
RichString recipeTooltip = RichString.Rich(fi.TargetItem.Description);
if (fi.RequiresRecipe)
{
recipeTooltip += "\n\n" + $"‖color:{XMLExtensions.ToStringHex(GUIStyle.Red)}‖{TextManager.Get("fabricatorrequiresrecipe")}‖color:end‖";
}
recipeTooltip = RichString.Rich(recipeTooltip);
var frame = new GUIFrame(new RectTransform(new Point(itemList.Content.Rect.Width, (int)(40 * GUI.yScale)), itemList.Content.RectTransform), style: null) var frame = new GUIFrame(new RectTransform(new Point(itemList.Content.Rect.Width, (int)(40 * GUI.yScale)), itemList.Content.RectTransform), style: null)
{ {
UserData = fi, UserData = fi,
HoverColor = Color.Gold * 0.2f, HoverColor = Color.Gold * 0.2f,
SelectedColor = Color.Gold * 0.5f, SelectedColor = Color.Gold * 0.5f,
ToolTip = RichString.Rich(fi.TargetItem.Description) ToolTip = recipeTooltip
}; };
var container = new GUILayoutGroup(new RectTransform(Vector2.One, frame.RectTransform), var container = new GUILayoutGroup(new RectTransform(Vector2.One, frame.RectTransform),
@@ -451,8 +458,8 @@ namespace Barotrauma.Items.Components
new GUIImage(new RectTransform(new Point(frame.Rect.Height,frame.Rect.Height), container.RectTransform), new GUIImage(new RectTransform(new Point(frame.Rect.Height,frame.Rect.Height), container.RectTransform),
itemIcon, scaleToFit: true) itemIcon, scaleToFit: true)
{ {
Color = fi.TargetItem.InventoryIconColor, Color = itemIcon == fi.TargetItem.Sprite ? fi.TargetItem.SpriteColor : fi.TargetItem.InventoryIconColor,
ToolTip = RichString.Rich(fi.TargetItem.Description) ToolTip = recipeTooltip
}; };
} }
@@ -461,7 +468,7 @@ namespace Barotrauma.Items.Components
{ {
Padding = Vector4.Zero, Padding = Vector4.Zero,
AutoScaleVertical = true, AutoScaleVertical = true,
ToolTip = RichString.Rich(fi.TargetItem.Description) ToolTip = recipeTooltip
}; };
new GUITextBlock(new RectTransform(new Vector2(0.85f, 1f), frame.RectTransform, Anchor.BottomRight), new GUITextBlock(new RectTransform(new Vector2(0.85f, 1f), frame.RectTransform, Anchor.BottomRight),
@@ -513,7 +520,7 @@ namespace Barotrauma.Items.Components
var nonItems = itemList.Content.Children.Where(c => c.UserData is not FabricationRecipe).ToList(); var nonItems = itemList.Content.Children.Where(c => c.UserData is not FabricationRecipe).ToList();
nonItems.ForEach(i => i.Visible = false); nonItems.ForEach(i => i.Visible = false);
SortItems(character: null); SortItems(character);
FilterEntities(selectedItemCategory, itemFilterBox?.Text ?? string.Empty); FilterEntities(selectedItemCategory, itemFilterBox?.Text ?? string.Empty);
HideEmptyItemListCategories(); HideEmptyItemListCategories();
} }
@@ -1196,6 +1203,15 @@ namespace Barotrauma.Items.Components
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), TextManager.FormatCurrency(SelectedItem.RequiredMoney), new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), TextManager.FormatCurrency(SelectedItem.RequiredMoney),
font: GUIStyle.SmallFont); font: GUIStyle.SmallFont);
} }
if (selectedRecipe.RequiresRecipe && !AnyOneHasRecipeForItem(Character.Controlled, selectedRecipe.TargetItem))
{
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform),
TextManager.Get("fabricatorrequiresrecipe"), textColor: GUIStyle.Red, font: GUIStyle.SubHeadingFont)
{
AutoScaleHorizontal = true,
};
}
} }
public void HighlightRecipe(string identifier, Color color) public void HighlightRecipe(string identifier, Color color)

View File

@@ -92,10 +92,10 @@ namespace Barotrauma.Items.Components
switch (subElement.Name.ToString().ToLowerInvariant()) switch (subElement.Name.ToString().ToLowerInvariant())
{ {
case "temperatureboostsoundup": case "temperatureboostsoundup":
temperatureBoostSoundUp = RoundSound.Load(subElement, false); temperatureBoostSoundUp = RoundSound.Load(subElement);
break; break;
case "temperatureboostsounddown": case "temperatureboostsounddown":
temperatureBoostSoundDown = RoundSound.Load(subElement, false); temperatureBoostSoundDown = RoundSound.Load(subElement);
break; break;
} }
} }

View File

@@ -1060,6 +1060,7 @@ namespace Barotrauma.Items.Components
int missionIndex = 0; int missionIndex = 0;
foreach (Mission mission in GameMain.GameSession.Missions) foreach (Mission mission in GameMain.GameSession.Missions)
{ {
if (!mission.Prefab.ShowSonarLabels) { continue; }
int i = 0; int i = 0;
foreach ((LocalizedString label, Vector2 position) in mission.SonarLabels) foreach ((LocalizedString label, Vector2 position) in mission.SonarLabels)
{ {
@@ -1714,15 +1715,15 @@ namespace Barotrauma.Items.Components
foreach (Structure structure in Structure.WallList) foreach (Structure structure in Structure.WallList)
{ {
if (structure.Submarine != sub) { continue; } if (structure.Submarine != sub) { continue; }
CreateBlips(structure.IsHorizontal, structure.WorldPosition, structure.WorldRect); CreateBlips(structure.IsHorizontal, structure.WorldPosition, structure.WorldRect, -structure.RotationWithFlipping);
} }
foreach (var door in Door.DoorList) foreach (var door in Door.DoorList)
{ {
if (door.Item.Submarine != sub || door.IsOpen) { continue; } if (door.Item.Submarine != sub || door.IsOpen) { continue; }
CreateBlips(door.IsHorizontal, door.Item.WorldPosition, door.Item.WorldRect, BlipType.Door); CreateBlips(door.IsHorizontal, door.Item.WorldPosition, door.Item.WorldRect, rotation: 0.0f, BlipType.Door);
} }
void CreateBlips(bool isHorizontal, Vector2 worldPos, Rectangle worldRect, BlipType blipType = BlipType.Default) void CreateBlips(bool isHorizontal, Vector2 worldPos, Rectangle worldRect, float rotation, BlipType blipType = BlipType.Default)
{ {
Vector2 point1, point2; Vector2 point1, point2;
if (isHorizontal) if (isHorizontal)
@@ -1735,6 +1736,14 @@ namespace Barotrauma.Items.Components
point1 = new Vector2(worldPos.X, worldRect.Y); point1 = new Vector2(worldPos.X, worldRect.Y);
point2 = new Vector2(worldPos.X, worldRect.Y - worldRect.Height); point2 = new Vector2(worldPos.X, worldRect.Y - worldRect.Height);
} }
if (!MathUtils.NearlyEqual(rotation, 0.0f))
{
float rotationRad = MathHelper.ToRadians(rotation);
point1 = MathUtils.RotatePointAroundTarget(point1, worldPos, rotationRad);
point2 = MathUtils.RotatePointAroundTarget(point2, worldPos, rotationRad);
}
CreateBlipsForLine( CreateBlipsForLine(
point1, point1,
point2, point2,

View File

@@ -973,6 +973,7 @@ namespace Barotrauma.Items.Components
} }
PosToMaintain += nudgeAmount; PosToMaintain += nudgeAmount;
} }
unsentChanges = true;
return true; return true;
} }

View File

@@ -0,0 +1,180 @@
#nullable enable
using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma.Items.Components
{
internal partial class PowerDistributor : PowerTransfer, IServerSerializable, IClientSerializable
{
private partial class PowerGroup
{
private GUIFrame? frame;
private GUITextBox? nameBox;
private GUIScrollBar? ratioSlider;
private readonly List<GUITextBlock> powerUnitLabels = new List<GUITextBlock>();
private GUIFrame? divider;
public bool IsVisible { get; private set; } = true;
public void CreateGUI()
{
frame = new GUIFrame(new RectTransform(new Vector2(1f, 0.25f), distributor.groupList!.Content.RectTransform, minSize: (0, 130)), style: null);
GUIFrame groupContent = new(new RectTransform(frame.Rect.Size - new Point(10), frame.RectTransform, Anchor.Center), style: null);
GUILayoutGroup nameGroup = new(new RectTransform(new Vector2(0.65f, 0.33f), groupContent.RectTransform, Anchor.TopLeft), isHorizontal: true, childAnchor: Anchor.CenterLeft)
{
Stretch = true
};
GUIButton penIcon = new(new RectTransform(new Vector2(0.75f), nameGroup.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "TextBoxIcon")
{
HoverCursor = CursorState.IBeam,
OnClicked = (_, _) =>
{
nameBox!.Select();
return true;
}
};
nameBox = new GUITextBox(new RectTransform(Vector2.One, nameGroup.RectTransform), Name, font: GUIStyle.SubHeadingFont, style: "GUITextBoxNoStyle")
{
MaxTextLength = MaxNameLength,
OverflowClip = true,
TextBlock = { ForceUpperCase = ForceUpperCase.No },
OnEnterPressed = static (textBox, _) =>
{
textBox.Deselect();
return true;
}
};
nameBox.OnDeselected += (tb, _) =>
{
Name = tb.Text;
if (GameMain.Client == null) { return; }
distributor.item.CreateClientEvent(distributor, new EventData(this, EventType.NameChange));
};
GUITextBlock loadDisplay = GUI.CreateDigitalDisplay(new RectTransform(new Vector2(0.35f, 0.33f), groupContent.RectTransform, Anchor.TopRight) { AbsoluteOffset = (5, 0) },
out GUITextBlock? _, out GUITextBlock loadDisplayUnitLabel, TextManager.Get("PowerTransferLoadLabel"), tooltip: TextManager.Get("PowerTransferTipLoad"), leftLabelFont: GUIStyle.Font);
loadDisplay.TextGetter = () => MathUtils.RoundToInt(Load).ToString();
ratioSlider = new GUIScrollBar(new RectTransform(new Vector2(1f, 0.33f), groupContent.RectTransform, Anchor.Center), barSize: 0.15f, style: "DeviceSlider")
{
Step = SupplyRatioStep,
BarScroll = SupplyRatio,
OnMoved = (GUIScrollBar scrollBar, float barScroll) =>
{
if (MathUtils.NearlyEqual(barScroll, SupplyRatio)) { return false; }
SupplyRatio = barScroll;
if (GameMain.Client != null)
{
distributor.item.CreateClientEvent(distributor, new EventData(this, EventType.RatioChange));
distributor.correctionTimer = CorrectionDelay;
}
return true;
}
};
ratioSlider.Bar.RectTransform.MaxSize = new Point(ratioSlider.Bar.Rect.Height);
GUITextBlock ratioDisplay = GUI.CreateDigitalDisplay(new RectTransform(new Vector2(0.2f, 0.33f), groupContent.RectTransform, Anchor.BottomLeft),
out GUITextBlock? _, out GUITextBlock _,
rightLabelText: "%");
ratioDisplay.TextGetter = () => DisplayRatio.ToString();
GUITextBlock outputDisplay = GUI.CreateDigitalDisplay(new RectTransform(new Vector2(0.35f, 0.33f), groupContent.RectTransform, Anchor.BottomRight) { AbsoluteOffset = (5, 0) },
out GUITextBlock? _, out GUITextBlock outputDisplayUnitLabel,
TextManager.Get("powerdistributor.supplylabel"), tooltip: TextManager.Get("PowerTransferTipPower"), leftLabelFont: GUIStyle.Font);
outputDisplay.TextGetter = () => distributor.IsShortCircuited(PowerOut) ? "err" : MathUtils.RoundToInt(distributor.CalculatePowerOut(this)).ToString();
powerUnitLabels.Add(loadDisplayUnitLabel);
powerUnitLabels.Add(outputDisplayUnitLabel);
GUITextBlock.AutoScaleAndNormalize(powerUnitLabels);
divider = new GUIFrame(new RectTransform(Vector2.UnitX, distributor.groupList!.Content.RectTransform), style: "HorizontalLine");
}
private void UpdateNameBox()
{
if (nameBox == null || nameBox.Text == DisplayName) { return; }
nameBox.Text = DisplayName?.Value ?? string.Empty;
}
private void UpdateSlider()
{
if (ratioSlider == null || MathUtils.NearlyEqual(ratioSlider.BarScroll, supplyRatio)) { return; }
ratioSlider.BarScroll = supplyRatio;
}
public void UpdateGUI()
{
IsVisible = PowerOut.Wires.Count >= 1;
frame!.Visible = IsVisible;
divider!.Visible = IsVisible && distributor.powerGroups.Last(group => group.frame!.Visible) != this;
if (distributor.prevLanguage != GameSettings.CurrentConfig.Language) { GUITextBlock.AutoScaleAndNormalize(powerUnitLabels); }
}
}
private GUIListBox? groupList;
private GUITextBlock? noConnectionsText;
protected override void CreateGUI()
{
if (GuiFrame == null) { return; }
guiContent = new GUILayoutGroup(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset })
{
Stretch = true
};
GUIFrame defaultUIContainer = new(new RectTransform(Vector2.UnitX, guiContent.RectTransform, minSize: (0, 125)), style: null)
{
CanBeFocused = false
};
CreateDefaultPowerUI(defaultUIContainer);
groupList = new(new RectTransform(Vector2.One, guiContent.RectTransform)) { Enabled = false };
noConnectionsText = new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.0f), groupList.Content.RectTransform, Anchor.Center), TextManager.Get("powerdistributor.noconnections"), wrap: true)
{
Visible = false
};
powerGroups.ForEach(group => group.CreateGUI());
}
public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam)
{
if (GuiFrame == null) { return; }
powerGroups.ForEach(group => group.UpdateGUI());
noConnectionsText!.Visible = powerGroups.None(group => group.IsVisible);
base.UpdateHUDComponentSpecific(character, deltaTime, cam);
}
#region Networking
public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData? extraData = null) => SharedEventWrite(msg, extraData);
public void ClientEventRead(IReadMessage msg, float sendingTime)
{
int msgStartPos = msg.BitPosition;
SharedEventRead(msg, out EventType eventType, out PowerGroup powerGroup, out string newName, out float newRatio);
if (correctionTimer > 0f)
{
int msgBits = msg.BitPosition - msgStartPos;
msg.BitPosition -= msgBits;
StartDelayedCorrection(msg.ExtractBits(msgBits), sendingTime);
return;
}
switch (eventType)
{
case EventType.NameChange:
powerGroup.Name = newName;
break;
case EventType.RatioChange:
powerGroup.SupplyRatio = newRatio;
break;
}
}
#endregion
}
}

View File

@@ -6,125 +6,84 @@ namespace Barotrauma.Items.Components
{ {
partial class PowerTransfer : Powered partial class PowerTransfer : Powered
{ {
public override bool RecreateGUIOnResolutionChange => true;
protected GUIComponent guiContent;
private GUITickBox powerIndicator; private GUITickBox powerIndicator;
private GUITickBox highVoltageIndicator; private GUITickBox highVoltageIndicator;
private GUITickBox lowVoltageIndicator; private GUITickBox lowVoltageIndicator;
private GUITextBlock powerLabel, loadLabel; private GUITextBlock powerLabel, loadLabel;
protected GUITextBlock powerDisplay, loadDisplay;
private LanguageIdentifier prevLanguage; protected LanguageIdentifier prevLanguage;
partial void InitProjectSpecific(XElement element) partial void InitProjectSpecific(XElement element)
{ {
if (GuiFrame == null) { return; } if (GuiFrame == null) { return; }
CreateGUI();
prevLanguage = GameSettings.CurrentConfig.Language;
}
var paddedFrame = new GUIFrame(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }, protected override void CreateGUI()
style: null) {
if (GuiFrame == null) { return; }
guiContent = new GUIFrame(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }, style: null)
{ {
CanBeFocused = false CanBeFocused = false
}; };
CreateDefaultPowerUI(guiContent);
}
var lightsArea = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1), paddedFrame.RectTransform, Anchor.CenterLeft)) protected void CreateDefaultPowerUI(GUIComponent parent)
{
GUILayoutGroup lightsArea = new(new RectTransform(new Vector2(0.4f, 1f), parent.RectTransform, Anchor.CenterLeft))
{ {
Stretch = true Stretch = true
}; };
powerIndicator = new GUITickBox(new RectTransform(new Vector2(1, 0.33f), lightsArea.RectTransform), powerIndicator = GUI.CreateIndicatorLight(new RectTransform(new Vector2(1, 0.33f), lightsArea.RectTransform),
TextManager.Get("PowerTransferPowered"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightGreen") "IndicatorLightGreen", TextManager.Get("PowerTransferPowered"));
{ highVoltageIndicator = GUI.CreateIndicatorLight(new RectTransform(new Vector2(1, 0.33f), lightsArea.RectTransform),
CanBeFocused = false "IndicatorLightRed", TextManager.Get("PowerTransferHighVoltage"), TextManager.Get("PowerTransferTipOvervoltage"));
}; lowVoltageIndicator = GUI.CreateIndicatorLight(new RectTransform(new Vector2(1, 0.33f), lightsArea.RectTransform),
highVoltageIndicator = new GUITickBox(new RectTransform(new Vector2(1, 0.33f), lightsArea.RectTransform), "IndicatorLightRed", TextManager.Get("PowerTransferLowVoltage"), TextManager.Get("PowerTransferTipLowvoltage"));
TextManager.Get("PowerTransferHighVoltage"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRed")
{
ToolTip = TextManager.Get("PowerTransferTipOvervoltage"),
Enabled = false
};
lowVoltageIndicator = new GUITickBox(new RectTransform(new Vector2(1, 0.33f), lightsArea.RectTransform),
TextManager.Get("PowerTransferLowVoltage"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRed")
{
ToolTip = TextManager.Get("PowerTransferTipLowvoltage"),
Enabled = false
};
powerIndicator.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal);
highVoltageIndicator.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal);
lowVoltageIndicator.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal);
GUITextBlock.AutoScaleAndNormalize(powerIndicator.TextBlock, highVoltageIndicator.TextBlock, lowVoltageIndicator.TextBlock); GUITextBlock.AutoScaleAndNormalize(powerIndicator.TextBlock, highVoltageIndicator.TextBlock, lowVoltageIndicator.TextBlock);
var textContainer = new GUIFrame(new RectTransform(new Vector2(0.58f, 1.0f), paddedFrame.RectTransform, Anchor.CenterRight), style: null); GUIFrame textContainer = new(new RectTransform(new Vector2(0.58f, 1f), parent.RectTransform, Anchor.CenterRight), style: null);
var upperTextArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), textContainer.RectTransform, Anchor.TopLeft), isHorizontal: true, childAnchor: Anchor.CenterLeft)
{
Stretch = true
};
var lowerTextArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), textContainer.RectTransform, Anchor.BottomLeft), isHorizontal: true, childAnchor: Anchor.CenterLeft)
{
Stretch = true
};
powerLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), upperTextArea.RectTransform), powerDisplay = GUI.CreateDigitalDisplay(new RectTransform(new Vector2(1f, 0.5f), textContainer.RectTransform, Anchor.TopLeft),
TextManager.Get("PowerTransferPowerLabel"), textColor: GUIStyle.TextColorBright, font: GUIStyle.LargeFont, textAlignment: Alignment.CenterRight) out powerLabel, out GUITextBlock unitLabel1, TextManager.Get("PowerTransferPowerLabel"), TextManager.Get("kilowatt"), TextManager.Get("PowerTransferTipPower"));
{
ToolTip = TextManager.Get("PowerTransferTipPower")
};
loadLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), lowerTextArea.RectTransform),
TextManager.Get("PowerTransferLoadLabel"), textColor: GUIStyle.TextColorBright, font: GUIStyle.LargeFont, textAlignment: Alignment.CenterRight)
{
ToolTip = TextManager.Get("PowerTransferTipLoad")
};
var digitalBackground = new GUIFrame(new RectTransform(new Vector2(0.55f, 0.8f), upperTextArea.RectTransform), style: "DigitalFrameDark"); powerDisplay.TextGetter = () =>
var powerText = new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.95f), digitalBackground.RectTransform, Anchor.Center),
"", font: GUIStyle.DigitalFont, textColor: GUIStyle.TextColorDark)
{ {
TextAlignment = Alignment.CenterRight, float currPower = powerLoad < 0 ? -powerLoad : 0;
ToolTip = TextManager.Get("PowerTransferTipPower"), if (this is not RelayComponent && PowerConnections != null && PowerConnections.Count > 0 && PowerConnections[0].Grid != null)
TextGetter = () => {
float currPower = powerLoad < 0 ? -powerLoad: 0;
if (this is not RelayComponent && PowerConnections != null && PowerConnections.Count > 0 && PowerConnections[0].Grid != null)
{
currPower = PowerConnections[0].Grid.Power;
}
return ((int)Math.Round(currPower)).ToString();
}
};
var kw1 = new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.5f), upperTextArea.RectTransform),
TextManager.Get("kilowatt"), textColor: GUIStyle.TextColorNormal, font: GUIStyle.Font)
{
Padding = Vector4.Zero,
TextAlignment = Alignment.BottomCenter
};
digitalBackground = new GUIFrame(new RectTransform(new Vector2(0.55f, 0.8f), lowerTextArea.RectTransform), style: "DigitalFrameDark");
var loadText = new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.95f), digitalBackground.RectTransform, Anchor.Center),
"", font: GUIStyle.DigitalFont, textColor: GUIStyle.TextColorDark)
{
TextAlignment = Alignment.CenterRight,
ToolTip = TextManager.Get("PowerTransferTipLoad"),
TextGetter = () =>
{ {
float load = PowerLoad; currPower = PowerConnections[0].Grid.Power;
if (this is RelayComponent relay)
{
load = relay.DisplayLoad;
}
else if (load < 0)
{
load = 0;
}
return ((int)Math.Round(load)).ToString();
} }
return MathUtils.RoundToInt(currPower).ToString();
}; };
var kw2 = new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.5f), lowerTextArea.RectTransform),
TextManager.Get("kilowatt"), textColor: GUIStyle.TextColorNormal, font: GUIStyle.Font) loadDisplay = GUI.CreateDigitalDisplay(new RectTransform(new Vector2(1f, 0.5f), textContainer.RectTransform, Anchor.BottomLeft),
out loadLabel, out GUITextBlock unitLabel2, TextManager.Get("PowerTransferLoadLabel"), TextManager.Get("kilowatt"), TextManager.Get("PowerTransferTipLoad"));
loadDisplay.TextGetter = () =>
{ {
Padding = Vector4.Zero, float load = PowerLoad;
TextAlignment = Alignment.BottomCenter if (this is RelayComponent relay)
{
load = relay.DisplayLoad;
}
else if (load < 0)
{
load = 0;
}
return MathUtils.RoundToInt(load).ToString();
}; };
GUITextBlock.AutoScaleAndNormalize(powerLabel, loadLabel); GUITextBlock.AutoScaleAndNormalize(powerLabel, loadLabel);
GUITextBlock.AutoScaleAndNormalize(true, true, powerText, loadText); GUITextBlock.AutoScaleAndNormalize(true, true, powerDisplay, loadDisplay);
GUITextBlock.AutoScaleAndNormalize(kw1, kw2); GUITextBlock.AutoScaleAndNormalize(unitLabel1, unitLabel2);
prevLanguage = GameSettings.CurrentConfig.Language;
} }
public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam)

View File

@@ -12,7 +12,7 @@
switch (subElement.Name.ToString().ToLowerInvariant()) switch (subElement.Name.ToString().ToLowerInvariant())
{ {
case "poweronsound": case "poweronsound":
powerOnSound = RoundSound.Load(subElement, false); powerOnSound = RoundSound.Load(subElement);
break; break;
} }
} }

View File

@@ -4,10 +4,6 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace Barotrauma.Items.Components namespace Barotrauma.Items.Components
{ {
@@ -28,7 +24,7 @@ namespace Barotrauma.Items.Components
private readonly List<ParticleEmitter> particleEmitterHitCharacter = new List<ParticleEmitter>(); private readonly List<ParticleEmitter> particleEmitterHitCharacter = new List<ParticleEmitter>();
private readonly List<(RelatedItem relatedItem, ParticleEmitter emitter)> particleEmitterHitItem = new List<(RelatedItem relatedItem, ParticleEmitter emitter)>(); private readonly List<(RelatedItem relatedItem, ParticleEmitter emitter)> particleEmitterHitItem = new List<(RelatedItem relatedItem, ParticleEmitter emitter)>();
private float prevProgressBarState; private float prevProgressBarState = 1;
private Item prevProgressBarTarget = null; private Item prevProgressBarTarget = null;
partial void InitProjSpecific(ContentXElement element) partial void InitProjSpecific(ContentXElement element)

View File

@@ -1,4 +1,4 @@
using Barotrauma.Extensions; using Barotrauma.Extensions;
using Barotrauma.Networking; using Barotrauma.Networking;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;

View File

@@ -150,16 +150,16 @@ namespace Barotrauma.Items.Components
crosshairPointerSprite = new Sprite(subElement, path: textureDir); crosshairPointerSprite = new Sprite(subElement, path: textureDir);
break; break;
case "startmovesound": case "startmovesound":
startMoveSound = RoundSound.Load(subElement, false); startMoveSound = RoundSound.Load(subElement);
break; break;
case "endmovesound": case "endmovesound":
endMoveSound = RoundSound.Load(subElement, false); endMoveSound = RoundSound.Load(subElement);
break; break;
case "movesound": case "movesound":
moveSound = RoundSound.Load(subElement, false); moveSound = RoundSound.Load(subElement);
break; break;
case "chargesound": case "chargesound":
chargeSound = RoundSound.Load(subElement, false); chargeSound = RoundSound.Load(subElement);
break; break;
case "particleemitter": case "particleemitter":
particleEmitters.Add(new ParticleEmitter(subElement)); particleEmitters.Add(new ParticleEmitter(subElement));

View File

@@ -357,6 +357,18 @@ namespace Barotrauma
#if DEBUG #if DEBUG
toolTip += $" ({item.Prefab.Identifier})"; toolTip += $" ({item.Prefab.Identifier})";
#endif #endif
if (!item.Prefab.UnlockedRecipeInToolTip.IsEmpty && GameMain.GameSession is { } GameSession)
{
if (GameSession.UnlockedRecipes.Contains(item.Prefab.UnlockedRecipeInToolTip))
{
toolTip += TextManager.Get("unlockedrecipe.true");
}
else
{
toolTip += $"\n‖color:{XMLExtensions.ToStringHex(GUIStyle.Yellow)}‖{TextManager.Get("unlockedrecipe.false")}‖color:end‖";
}
}
if (PlayerInput.KeyDown(InputType.ContextualCommand)) if (PlayerInput.KeyDown(InputType.ContextualCommand))
{ {
toolTip += $"\n‖color:gui.blue‖{TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders"))}‖color:end‖"; toolTip += $"\n‖color:gui.blue‖{TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders"))}‖color:end‖";
@@ -365,7 +377,8 @@ namespace Barotrauma
{ {
var colorStr = XMLExtensions.ToStringHex(Color.LightGray * 0.7f); var colorStr = XMLExtensions.ToStringHex(Color.LightGray * 0.7f);
toolTip += $"\n‖color:{colorStr}‖{TextManager.Get("itemmsg.morreoptionsavailable")}‖color:end‖"; toolTip += $"\n‖color:{colorStr}‖{TextManager.Get("itemmsg.morreoptionsavailable")}‖color:end‖";
} }
return RichString.Rich(toolTip); return RichString.Rich(toolTip);
} }
@@ -425,9 +438,9 @@ namespace Barotrauma
} }
} }
public Inventory GetReplacementOrThiS() public Inventory GetReplacementOrThis()
{ {
return ReplacedBy?.GetReplacementOrThiS() ?? this; return ReplacedBy?.GetReplacementOrThis() ?? this;
} }
public virtual void CreateSlots() public virtual void CreateSlots()
@@ -1255,7 +1268,7 @@ namespace Barotrauma
container.AllowDragAndDrop && container.AllowDragAndDrop &&
inventory.CanBePut(DraggingItems.FirstOrDefault()); inventory.CanBePut(DraggingItems.FirstOrDefault());
bool isTargetingValidCharacter = IsValidTargetForDragDropGive(Character.Controlled, Character.Controlled.FocusedCharacter); bool isTargetingValidCharacter = IsValidTargetForDragDropGive(Character.Controlled, Character.Controlled.FocusedCharacter, DraggingItems);
if (DraggingItemToWorld && (isTargetingValidContainer || isTargetingValidCharacter)) if (DraggingItemToWorld && (isTargetingValidContainer || isTargetingValidCharacter))
{ {
@@ -1418,11 +1431,13 @@ namespace Barotrauma
} }
} }
private static bool IsValidTargetForDragDropGive(Character giver, Character receiver) private static bool IsValidTargetForDragDropGive(Character giver, Character receiver, IEnumerable<Item> draggedItems)
{ {
if (giver == null || receiver == null) { return false; } if (giver == null || receiver == null || draggedItems.None()) { return false; }
if (receiver == giver) { return false; } if (receiver == giver) { return false; }
return receiver.IsInventoryAccessibleTo(giver, IsDragAndDropGiveAllowed ? CharacterInventory.AccessLevel.Allowed : CharacterInventory.AccessLevel.Limited); return
receiver.IsInventoryAccessibleTo(giver, IsDragAndDropGiveAllowed ? CharacterInventory.AccessLevel.Allowed : CharacterInventory.AccessLevel.Limited) &&
receiver.Inventory.CanBePut(draggedItems.FirstOrDefault(), InvSlotType.Any);
} }
private static bool CanSelectSlot(SlotReference selectedSlot) private static bool CanSelectSlot(SlotReference selectedSlot)
@@ -1651,7 +1666,7 @@ namespace Barotrauma
(LocalizedString, Color) GetDragLabelTextAndColor(bool mouseOnHealthInterface) (LocalizedString, Color) GetDragLabelTextAndColor(bool mouseOnHealthInterface)
{ {
bool useDragDropGive = IsValidTargetForDragDropGive(Character.Controlled, Character.Controlled.FocusedCharacter); bool useDragDropGive = IsValidTargetForDragDropGive(Character.Controlled, Character.Controlled.FocusedCharacter, DraggingItems);
Color toolTipColor = Color.LightGreen; Color toolTipColor = Color.LightGreen;

View File

@@ -316,9 +316,6 @@ namespace Barotrauma
if (worldPosition.X + extents.X > worldView.Right || worldPosition.X + extents.Width < worldView.X) { return false; } if (worldPosition.X + extents.X > worldView.Right || worldPosition.X + extents.Width < worldView.X) { return false; }
if (worldPosition.Y + extents.Height < worldView.Y - worldView.Height || worldPosition.Y + extents.Y > worldView.Y) { return false; } if (worldPosition.Y + extents.Height < worldView.Y - worldView.Height || worldPosition.Y + extents.Y > worldView.Y) { return false; }
if (extents.Width * Screen.Selected.Cam.Zoom < 1.0f) { return false; }
if (extents.Height * Screen.Selected.Cam.Zoom < 1.0f) { return false; }
return true; return true;
} }
@@ -327,7 +324,7 @@ namespace Barotrauma
Draw(spriteBatch, editing, back, overrideColor: null); Draw(spriteBatch, editing, back, overrideColor: null);
} }
public void Draw(SpriteBatch spriteBatch, bool editing, bool back = true, Color? overrideColor = null) public void Draw(SpriteBatch spriteBatch, bool editing, bool back = true, Color? overrideColor = null, float? overrideDepth = null)
{ {
if (!Visible || (!editing && IsHidden) || !SubEditorScreen.IsLayerVisible(this)) { return; } if (!Visible || (!editing && IsHidden) || !SubEditorScreen.IsLayerVisible(this)) { return; }
@@ -395,7 +392,7 @@ namespace Barotrauma
} }
} }
float depth = GetDrawDepth(); float depth = overrideDepth ?? GetDrawDepth();
if (isWiringMode && isLogic && !PlayerInput.IsShiftDown()) { depth = 0.01f; } if (isWiringMode && isLogic && !PlayerInput.IsShiftDown()) { depth = 0.01f; }
if (activeSprite != null) if (activeSprite != null)
{ {
@@ -427,7 +424,7 @@ namespace Barotrauma
textureScale: Vector2.One * Scale, textureScale: Vector2.One * Scale,
depth: d); depth: d);
} }
DrawDecorativeSprites(spriteBatch, DrawPosition, flippedX && Prefab.CanSpriteFlipX, flippedY && Prefab.CanSpriteFlipY, rotation: 0, depth, overrideColor); DrawDecorativeSprites(spriteBatch, DrawPosition, FlippedX && Prefab.CanSpriteFlipX, FlippedY && Prefab.CanSpriteFlipY, rotation: 0, depth, overrideColor);
} }
} }
else else
@@ -448,7 +445,7 @@ namespace Barotrauma
Prefab.DamagedInfectedSprite?.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, Infector.HealthColor, Prefab.DamagedInfectedSprite.Origin, RotationRad, Scale, activeSprite.effects, depth - 0.002f); Prefab.DamagedInfectedSprite?.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, Infector.HealthColor, Prefab.DamagedInfectedSprite.Origin, RotationRad, Scale, activeSprite.effects, depth - 0.002f);
} }
DrawDecorativeSprites(spriteBatch, DrawPosition, flippedX && Prefab.CanSpriteFlipX, flippedY && Prefab.CanSpriteFlipY, -RotationRad, depth, overrideColor); DrawDecorativeSprites(spriteBatch, DrawPosition, FlippedX && Prefab.CanSpriteFlipX, FlippedY && Prefab.CanSpriteFlipY, -RotationRad, depth, overrideColor);
} }
} }
else if (body.Enabled) else if (body.Enabled)
@@ -524,8 +521,8 @@ namespace Barotrauma
if (!spriteAnimState[decorativeSprite].IsActive) { continue; } if (!spriteAnimState[decorativeSprite].IsActive) { continue; }
float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor);
Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, -RotationRad) * Scale; Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, -RotationRad) * Scale;
if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } if (FlippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; }
if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } if (FlippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; }
decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, decorativeSprite.Sprite.Origin, decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, decorativeSprite.Sprite.Origin,
rotation, decorativeSprite.GetScale(ref spriteAnimState[decorativeSprite].ScaleState, spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, rotation, decorativeSprite.GetScale(ref spriteAnimState[decorativeSprite].ScaleState, spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects,
depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth));
@@ -543,7 +540,7 @@ namespace Barotrauma
//causing them to be removed from the list //causing them to be removed from the list
for (int i = drawableComponents.Count - 1; i >= 0; i--) for (int i = drawableComponents.Count - 1; i >= 0; i--)
{ {
drawableComponents[i].Draw(spriteBatch, editing, depth, overrideColor); drawableComponents[i].Draw(spriteBatch, editing && !GameMain.SubEditorScreen.TransformWidgetSelected, depth, overrideColor);
} }
if (GameMain.DebugDraw) if (GameMain.DebugDraw)
@@ -813,6 +810,8 @@ namespace Barotrauma
} }
if (Screen.Selected != GameMain.SubEditorScreen) { return; } if (Screen.Selected != GameMain.SubEditorScreen) { return; }
if (Character.Controlled == null) { activeHUDs.Clear(); }
if (GameMain.SubEditorScreen.TransformWidgetSelected) { return; }
if (GetComponent<ElectricalDischarger>() is { } discharger) if (GetComponent<ElectricalDischarger>() is { } discharger)
{ {
@@ -826,8 +825,6 @@ namespace Barotrauma
} }
} }
if (Character.Controlled == null) { activeHUDs.Clear(); }
foreach (ItemComponent ic in components) foreach (ItemComponent ic in components)
{ {
ic.UpdateEditing(deltaTime); ic.UpdateEditing(deltaTime);
@@ -2341,6 +2338,7 @@ namespace Barotrauma
} }
} }
bool onInsertedEffectsAppliedOnPreviousRound = msg.ReadBoolean();
byte bodyType = msg.ReadByte(); byte bodyType = msg.ReadByte();
bool spawnedInOutpost = msg.ReadBoolean(); bool spawnedInOutpost = msg.ReadBoolean();
bool allowStealing = msg.ReadBoolean(); bool allowStealing = msg.ReadBoolean();
@@ -2453,6 +2451,10 @@ namespace Barotrauma
AllowStealing = allowStealing, AllowStealing = allowStealing,
Quality = quality Quality = quality
}; };
if (onInsertedEffectsAppliedOnPreviousRound)
{
item.OnInsertedEffectsApplied = item.OnInsertedEffectsAppliedOnPreviousRound = true;
}
} }
catch (Exception e) catch (Exception e)
{ {

View File

@@ -132,7 +132,7 @@ namespace Barotrauma
SoundTriggers = new LevelTrigger[Prefab.Sounds.Count]; SoundTriggers = new LevelTrigger[Prefab.Sounds.Count];
for (int i = 0; i < Prefab.Sounds.Count; i++) for (int i = 0; i < Prefab.Sounds.Count; i++)
{ {
Sounds[i] = RoundSound.Load(Prefab.Sounds[i].SoundElement, false); Sounds[i] = RoundSound.Load(Prefab.Sounds[i].SoundElement);
SoundTriggers[i] = Prefab.Sounds[i].TriggerIndex > -1 ? Triggers[Prefab.Sounds[i].TriggerIndex] : null; SoundTriggers[i] = Prefab.Sounds[i].TriggerIndex > -1 ? Triggers[Prefab.Sounds[i].TriggerIndex] : null;
} }

View File

@@ -714,21 +714,21 @@ namespace Barotrauma.Lights
const float MaxOffset = 256.0f; const float MaxOffset = 256.0f;
//the magic numbers here are just based on experimentation //the magic numbers here are just based on experimentation
float MinHorizontalScale = MathHelper.Lerp(3.5f, 1.5f, ObstructVisionAmount); float MinHorizontalScale = MathHelper.Lerp(3.5f, 1.5f, ObstructVisionAmount);
float MaxHorizontalScale = MinHorizontalScale * 1.25f; float MaxHorizontalScale = 10.0f;
float VerticalScale = MathHelper.Lerp(4.0f, 1.25f, ObstructVisionAmount); float VerticalScale = MathHelper.Lerp(4.0f, 1.25f, ObstructVisionAmount);
//Starting point and scale-based modifier that moves the point of origin closer to the edge of the texture if the player moves their mouse further away, or vice versa.
float relativeOriginStartPosition = 0.1f; //Increasing this value moves the origin further behind the character
float originStartPosition = visionCircle.Width * relativeOriginStartPosition * MinHorizontalScale;
float relativeOriginLookAtPosModifier = -0.055f; //Increase this value increases how much the vision changes by moving the mouse
float originLookAtPosModifier = visionCircle.Width * relativeOriginLookAtPosModifier;
Vector2 scale = new Vector2( Vector2 scale = new Vector2(
MathHelper.Clamp(losOffset.Length() / MaxOffset, MinHorizontalScale, MaxHorizontalScale), VerticalScale); MathHelper.Clamp(losOffset.Length() / MaxOffset, MinHorizontalScale, MaxHorizontalScale), VerticalScale);
//Increasing this value moves the origin further behind the character (current value chosen by experimentation)
float relativeOriginStartPosition = 0.2f;
//Divide by scale to move the origin closer to the edge of the texture, meaning the visible area moves forwards.
//Just stretching the texture without touching the origin would otherwise mean the blurry edge of visibility moves further behind the character (allowing you to see behind you better when looking far away)
float originStartPosition = visionCircle.Width * relativeOriginStartPosition / scale.X;
spriteBatch.Begin(SpriteSortMode.Deferred, transformMatrix: cam.Transform * Matrix.CreateScale(new Vector3(GameSettings.CurrentConfig.Graphics.LightMapScale, GameSettings.CurrentConfig.Graphics.LightMapScale, 1.0f))); spriteBatch.Begin(SpriteSortMode.Deferred, transformMatrix: cam.Transform * Matrix.CreateScale(new Vector3(GameSettings.CurrentConfig.Graphics.LightMapScale, GameSettings.CurrentConfig.Graphics.LightMapScale, 1.0f)));
spriteBatch.Draw(visionCircle, new Vector2(ViewTarget.WorldPosition.X, -ViewTarget.WorldPosition.Y), null, Color.White, rotation, spriteBatch.Draw(visionCircle, new Vector2(ViewTarget.WorldPosition.X, -ViewTarget.WorldPosition.Y), null, Color.White, rotation,
new Vector2(originStartPosition + (scale.X * originLookAtPosModifier), visionCircle.Height / 2), scale, SpriteEffects.None, 0.0f); new Vector2(originStartPosition, visionCircle.Height / 2), scale, SpriteEffects.None, 0.0f);
spriteBatch.End(); spriteBatch.End();
} }
else else
@@ -788,8 +788,8 @@ namespace Barotrauma.Lights
if (!convexHull.Intersects(camView)) { continue; } if (!convexHull.Intersects(camView)) { continue; }
Vector2 relativeViewPos = pos; Vector2 relativeViewPos = pos;
if (convexHull.ParentEntity?.Submarine != null) if (convexHull.ParentEntity?.Submarine != null)
{ {
relativeViewPos -= convexHull.ParentEntity.Submarine.DrawPosition; relativeViewPos -= convexHull.ParentEntity.Submarine.DrawPosition;
} }

View File

@@ -83,6 +83,8 @@ namespace Barotrauma
#if DEBUG #if DEBUG
private GUIComponent editor; private GUIComponent editor;
private bool editorEnabled;
private void CreateEditor() private void CreateEditor()
{ {
editor = new GUIFrame(new RectTransform(new Vector2(0.25f, 1.0f), GUI.Canvas, Anchor.TopRight, minSize: new Point(400, 0))); editor = new GUIFrame(new RectTransform(new Vector2(0.25f, 1.0f), GUI.Canvas, Anchor.TopRight, minSize: new Point(400, 0)));
@@ -534,8 +536,15 @@ namespace Barotrauma
#if DEBUG #if DEBUG
if (GameMain.DebugDraw) if (GameMain.DebugDraw)
{ {
if (editor == null) CreateEditor(); if (editor == null) { CreateEditor(); }
editor.AddToGUIUpdateList(order: 1); if (editorEnabled)
{
editor.AddToGUIUpdateList(order: 1);
}
if (PlayerInput.KeyHit(Keys.T))
{
editorEnabled = !editorEnabled;
}
} }
if (PlayerInput.KeyHit(Keys.Space)) if (PlayerInput.KeyHit(Keys.Space))
@@ -822,7 +831,7 @@ namespace Barotrauma
drawRect.X = (int)pos.X - drawRect.Width / 2; drawRect.X = (int)pos.X - drawRect.Width / 2;
drawRect.Y = (int)pos.Y - drawRect.Width / 2; drawRect.Y = (int)pos.Y - drawRect.Width / 2;
if (drawRect.X > rect.Right - GUI.IntScale(100) && generationParams.MissionIcon != null && location.AvailableMissions.Any()) if (drawRect.X > rect.Right - GUI.IntScale(100) && generationParams.MissionIcon != null && location.AvailableAndVisibleMissions.Any(m => m.Prefab.ShowInMenus))
{ {
Vector2 offScreenMissionIconPos = new Vector2(rect.Right - GUI.IntScale(50), drawRect.Center.Y); Vector2 offScreenMissionIconPos = new Vector2(rect.Right - GUI.IntScale(50), drawRect.Center.Y);
generationParams.MissionIcon.Draw(spriteBatch, generationParams.MissionIcon.Draw(spriteBatch,
@@ -934,18 +943,19 @@ namespace Barotrauma
} }
if (location != CurrentLocation && generationParams.MissionIcon != null) if (location != CurrentLocation && generationParams.MissionIcon != null)
{ {
if ((CurrentLocation == currentDisplayLocation && CurrentLocation.AvailableMissions.Any(m => m.Locations.Contains(location))) || var currentLocationVisibleMissions = CurrentLocation.AvailableAndVisibleMissions;
location.AvailableMissions.Any(m => m.Locations[0] == m.Locations[1])) if ((CurrentLocation == currentDisplayLocation && currentLocationVisibleMissions.Any(m => m.Locations.Contains(location))) ||
location.AvailableAndVisibleMissions.Any(m => m.Locations[0] == m.Locations[1]))
{ {
Vector2 missionIconPos = pos + new Vector2(1.35f, 0.35f) * generationParams.LocationIconSize * 0.5f * zoom; Vector2 missionIconPos = pos + new Vector2(1.35f, 0.35f) * generationParams.LocationIconSize * 0.5f * zoom;
generationParams.MissionIcon.Draw(spriteBatch, missionIconPos, generationParams.IndicatorColor, scale: missionIconScale * zoom); generationParams.MissionIcon.Draw(spriteBatch, missionIconPos, generationParams.IndicatorColor, scale: missionIconScale * zoom);
if (Vector2.Distance(PlayerInput.MousePosition, missionIconPos) < generationParams.MissionIcon.SourceRect.Width * zoom && IsPreferredTooltip(missionIconPos)) if (Vector2.Distance(PlayerInput.MousePosition, missionIconPos) < generationParams.MissionIcon.SourceRect.Width * zoom && IsPreferredTooltip(missionIconPos))
{ {
var availableMissions = CurrentLocation.AvailableMissions var allVisibleMissions = currentLocationVisibleMissions
.Where(m => m.Locations.Contains(location)) .Where(m => m.Locations.Contains(location))
.Concat(location.AvailableMissions.Where(m => m.Locations[0] == m.Locations[1])) .Concat(location.AvailableAndVisibleMissions.Where(m => m.Locations[0] == m.Locations[1]))
.Distinct(); .Distinct();
tooltip = (new Rectangle(missionIconPos.ToPoint(), new Point(30)), TextManager.Get("mission") + '\n'+ string.Join('\n', availableMissions.Select(m => "- " + m.Name))); tooltip = (new Rectangle(missionIconPos.ToPoint(), new Point(30)), TextManager.Get("mission") + '\n'+ string.Join('\n', allVisibleMissions.Select(m => "- " + m.Name)));
} }
} }
} }
@@ -992,6 +1002,12 @@ namespace Barotrauma
spriteBatch.End(); spriteBatch.End();
GameMain.Instance.GraphicsDevice.ScissorRectangle = prevScissorRect; GameMain.Instance.GraphicsDevice.ScissorRectangle = prevScissorRect;
spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable);
#if DEBUG
if (GameMain.DebugDraw)
{
GUI.DrawString(spriteBatch, new Vector2(mapContainer.Center.X, mapContainer.Rect.Y), "Press T to toggle editing map generation parameters.", Color.Magenta, font: GUIStyle.SmallFont);
}
#endif
} }
public static void DrawNoise(SpriteBatch spriteBatch, Rectangle rect, float strength) public static void DrawNoise(SpriteBatch spriteBatch, Rectangle rect, float strength)

View File

@@ -1159,15 +1159,6 @@ namespace Barotrauma
public virtual void DrawEditing(SpriteBatch spriteBatch, Camera cam) { } public virtual void DrawEditing(SpriteBatch spriteBatch, Camera cam) { }
private float RotationRad
=> MathHelper.ToRadians(
this switch
{
Structure s => s.Rotation,
Item it => it.Rotation,
_ => 0.0f
});
private Vector2 GetEditingHandlePos(int x, int y, Camera cam) private Vector2 GetEditingHandlePos(int x, int y, Camera cam)
{ {
Vector2 handleDiff = new Vector2(x * (rect.Width * 0.5f), y * (rect.Height * 0.5f)); Vector2 handleDiff = new Vector2(x * (rect.Width * 0.5f), y * (rect.Height * 0.5f));

View File

@@ -29,6 +29,15 @@ namespace Barotrauma
Volume = element.GetAttributeFloat("volume", 1.0f); Volume = element.GetAttributeFloat("volume", 1.0f);
IgnoreMuffling = element.GetAttributeBool("dontmuffle", false); IgnoreMuffling = element.GetAttributeBool("dontmuffle", false);
MuteBackgroundMusic = element.GetAttributeBool("MuteBackgroundMusic", false); MuteBackgroundMusic = element.GetAttributeBool("MuteBackgroundMusic", false);
if (!Stream && Sound.DurationSeconds > 60.0f)
{
DebugConsole.AddWarning(
$"Potential issue in content package: a large audio clip \"{System.IO.Path.GetFileName(Filename)}\" is set to be loaded into memory instead of streaming it from the disk. "+
"This can lead to excessive memory usage. Large clips should generally be streamed, while small and frequently played sounds should be loaded to memory to avoid the IO overhead of streaming. "+
"Consider adding stream=\"true\" to the sound's XML element.",
contentPackage: element.ContentPackage);
}
FrequencyMultiplierRange = new Vector2(1.0f); FrequencyMultiplierRange = new Vector2(1.0f);
string freqMultAttr = element.GetAttributeString("frequencymultiplier", element.GetAttributeString("frequency", "1.0")); string freqMultAttr = element.GetAttributeString("frequencymultiplier", element.GetAttributeString("frequency", "1.0"));
@@ -61,10 +70,11 @@ namespace Barotrauma
private static readonly List<RoundSound> roundSounds = new List<RoundSound>(); private static readonly List<RoundSound> roundSounds = new List<RoundSound>();
private static readonly Dictionary<string, RoundSound> roundSoundByPath = new Dictionary<string, RoundSound>(); private static readonly Dictionary<string, RoundSound> roundSoundByPath = new Dictionary<string, RoundSound>();
public static RoundSound? Load(ContentXElement element, bool stream = false) public static RoundSound? Load(ContentXElement element)
{ {
if (GameMain.SoundManager?.Disabled ?? true) { return null; } if (GameMain.SoundManager?.Disabled ?? true) { return null; }
bool stream = element.GetAttributeBool(nameof(Stream), false);
var filename = element.GetAttributeContentPath("file") ?? element.GetAttributeContentPath("sound"); var filename = element.GetAttributeContentPath("file") ?? element.GetAttributeContentPath("sound");
if (filename is null) if (filename is null)
{ {

View File

@@ -320,8 +320,8 @@ namespace Barotrauma
{ {
RectangleF worldRect = Quad2D.FromSubmarineRectangle(WorldRect).Rotated( RectangleF worldRect = Quad2D.FromSubmarineRectangle(WorldRect).Rotated(
FlippedX != FlippedY FlippedX != FlippedY
? rotationRad ? RotationRad
: -rotationRad).BoundingAxisAlignedRectangle; : -RotationRad).BoundingAxisAlignedRectangle;
Vector2 worldPos = WorldPosition; Vector2 worldPos = WorldPosition;
Vector2 min = new Vector2(worldRect.X, worldRect.Y); Vector2 min = new Vector2(worldRect.X, worldRect.Y);
@@ -451,7 +451,7 @@ namespace Barotrauma
MathUtils.PositiveModulo(-textureOffset.X, Prefab.BackgroundSprite.SourceRect.Width * TextureScale.X * Scale), MathUtils.PositiveModulo(-textureOffset.X, Prefab.BackgroundSprite.SourceRect.Width * TextureScale.X * Scale),
MathUtils.PositiveModulo(-textureOffset.Y, Prefab.BackgroundSprite.SourceRect.Height * TextureScale.Y * Scale)); MathUtils.PositiveModulo(-textureOffset.Y, Prefab.BackgroundSprite.SourceRect.Height * TextureScale.Y * Scale));
float rotationRad = GetRotationForSprite(this.rotationRad, Prefab.BackgroundSprite); float rotationRad = GetRotationForSprite(RotationRad, Prefab.BackgroundSprite);
Prefab.BackgroundSprite.DrawTiled( Prefab.BackgroundSprite.DrawTiled(
spriteBatch, spriteBatch,
@@ -484,7 +484,7 @@ namespace Barotrauma
if (back == GetRealDepth() > 0.5f) if (back == GetRealDepth() > 0.5f)
{ {
Vector2 advanceX = MathUtils.RotatedUnitXRadians(this.rotationRad).FlipY(); Vector2 advanceX = MathUtils.RotatedUnitXRadians(RotationRad).FlipY();
Vector2 advanceY = advanceX.YX().FlipX(); Vector2 advanceY = advanceX.YX().FlipX();
if (FlippedX != FlippedY) if (FlippedX != FlippedY)
{ {
@@ -492,7 +492,7 @@ namespace Barotrauma
advanceY = advanceY.FlipX(); advanceY = advanceY.FlipX();
} }
float sectionSpriteRotationRad = GetRotationForSprite(this.rotationRad, Prefab.Sprite); float sectionSpriteRotationRad = GetRotationForSprite(RotationRad, Prefab.Sprite);
for (int i = 0; i < Sections.Length; i++) for (int i = 0; i < Sections.Length; i++)
{ {
@@ -558,9 +558,11 @@ namespace Barotrauma
foreach (var decorativeSprite in Prefab.DecorativeSprites) foreach (var decorativeSprite in Prefab.DecorativeSprites)
{ {
if (!spriteAnimState[decorativeSprite].IsActive) { continue; } if (!spriteAnimState[decorativeSprite].IsActive) { continue; }
float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor) + this.rotationRad; float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor) + RotationRad;
Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier) * Scale; Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier) * Scale;
Vector2 drawPos = DrawPosition + MathUtils.RotatePoint(offset, -this.rotationRad); if (FlippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; }
if (FlippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; }
Vector2 drawPos = DrawPosition + MathUtils.RotatePoint(offset, -this.RotationRad);
decorativeSprite.Sprite.Draw( decorativeSprite.Sprite.Draw(
spriteBatch: spriteBatch, spriteBatch: spriteBatch,
pos: drawPos.FlipY(), pos: drawPos.FlipY(),

View File

@@ -230,30 +230,28 @@ namespace Barotrauma
} }
} }
public static void DrawGrid(SpriteBatch spriteBatch, int gridCells, Vector2 gridCenter, Vector2 roundedGridCenter, float alpha = 1.0f) public static void DrawGrid(SpriteBatch spriteBatch, int gridCells, Vector2 gridCenter, Vector2 roundedGridCenter, float alpha = 1.0f, Color? color = null)
{ {
Vector2 topLeft = roundedGridCenter - Vector2.One * GridSize * gridCells / 2; Vector2 topLeft = roundedGridCenter - Vector2.One * GridSize * gridCells / 2;
Vector2 bottomRight = roundedGridCenter + Vector2.One * GridSize * gridCells / 2; Vector2 bottomRight = roundedGridCenter + Vector2.One * GridSize * gridCells / 2;
for (int i = 0; i < gridCells; i++) for (int i = 0; i < gridCells; i++)
{ {
float distFromGridX = (MathUtils.RoundTowardsClosest(gridCenter.X, GridSize.X) - gridCenter.X) / GridSize.X; float middleIndex = (gridCells - 1) / 2.0f;
float distFromGridY = (MathUtils.RoundTowardsClosest(gridCenter.Y, GridSize.Y) - gridCenter.Y) / GridSize.Y; float normalizedPos = Math.Abs((i - middleIndex) / middleIndex);
float expandX = MathHelper.Lerp(30.0f, 0.0f, normalizedPos);
float expandY = expandX;
float normalizedDistX = Math.Abs(i + distFromGridX - gridCells / 2) / (gridCells / 2); Color lineColor = color ?? Color.White;
float normalizedDistY = Math.Abs(i - distFromGridY - gridCells / 2) / (gridCells / 2);
float expandX = MathHelper.Lerp(30.0f, 0.0f, normalizedDistY);
float expandY = MathHelper.Lerp(30.0f, 0.0f, normalizedDistX);
GUI.DrawLine(spriteBatch, GUI.DrawLine(spriteBatch,
new Vector2(topLeft.X - expandX, -bottomRight.Y + i * GridSize.Y), new Vector2(topLeft.X - expandX, -bottomRight.Y + i * GridSize.Y),
new Vector2(bottomRight.X + expandX, -bottomRight.Y + i * GridSize.Y), new Vector2(bottomRight.X + expandX, -bottomRight.Y + i * GridSize.Y),
Color.White * (1.0f - normalizedDistY) * alpha, depth: 0.6f, width: 3); lineColor * (1.0f - normalizedPos) * alpha, depth: 0.6f, width: 3);
GUI.DrawLine(spriteBatch, GUI.DrawLine(spriteBatch,
new Vector2(topLeft.X + i * GridSize.X, -topLeft.Y + expandY), new Vector2(topLeft.X + i * GridSize.X, -topLeft.Y + expandY),
new Vector2(topLeft.X + i * GridSize.X, -bottomRight.Y - expandY), new Vector2(topLeft.X + i * GridSize.X, -bottomRight.Y - expandY),
Color.White * (1.0f - normalizedDistX) * alpha, depth: 0.6f, width: 3); lineColor * (1.0f - normalizedPos) * alpha, depth: 0.6f, width: 3);
} }
} }

View File

@@ -150,6 +150,9 @@ namespace Barotrauma.Networking
styleSetting = msg.ReadString(); styleSetting = msg.ReadString();
txt = TextManager.GetServerMessage(txt).Value; txt = TextManager.GetServerMessage(txt).Value;
break; break;
case ChatMessageType.BlockedBySpamFilter:
GameMain.Client.BlockedBySpamFilterTimer = BlockedBySpamFilterTime;
break;
} }
if (NetIdUtils.IdMoreRecent(id, LastID)) if (NetIdUtils.IdMoreRecent(id, LastID))

View File

@@ -104,6 +104,10 @@ namespace Barotrauma.Networking
private UInt16 lastQueueChatMsgID = 0; //last message added to the queue private UInt16 lastQueueChatMsgID = 0; //last message added to the queue
private readonly List<ChatMessage> chatMsgQueue = new List<ChatMessage>(); private readonly List<ChatMessage> chatMsgQueue = new List<ChatMessage>();
public float BlockedBySpamFilterTimer;
public bool IsBlockedBySpamFilter => BlockedBySpamFilterTimer > 0.0f;
public UInt16 LastSentEntityEventID; public UInt16 LastSentEntityEventID;
#if DEBUG #if DEBUG
@@ -479,6 +483,8 @@ namespace Barotrauma.Networking
} }
#endif #endif
BlockedBySpamFilterTimer -= deltaTime;
foreach (Client c in ConnectedClients) foreach (Client c in ConnectedClients)
{ {
if (c.Character != null && c.Character.Removed) { c.Character = null; } if (c.Character != null && c.Character.Removed) { c.Character = null; }
@@ -868,6 +874,10 @@ namespace Barotrauma.Networking
case ServerPacketHeader.ACHIEVEMENT: case ServerPacketHeader.ACHIEVEMENT:
ReadAchievement(inc); ReadAchievement(inc);
break; break;
case ServerPacketHeader.UNLOCKRECIPE:
Identifier identifier = inc.ReadIdentifier();
GameMain.GameSession.UnlockRecipe(identifier, showNotifications: true);
break;
case ServerPacketHeader.ACHIEVEMENT_STAT: case ServerPacketHeader.ACHIEVEMENT_STAT:
ReadAchievementStat(inc); ReadAchievementStat(inc);
break; break;
@@ -1069,13 +1079,15 @@ namespace Barotrauma.Networking
CloseReconnectBox(); CloseReconnectBox();
GUI.ClearCursorWait(); GUI.ClearCursorWait();
string disconnectMessage = $"Client received a disconnect message. Reason: {disconnectPacket.DisconnectReason}";
SteamTimelineManager.OnClientDisconnect(disconnectMessage);
if (disconnectPacket.ShouldCreateAnalyticsEvent) if (disconnectPacket.ShouldCreateAnalyticsEvent)
{ {
GameAnalyticsManager.AddErrorEventOnce( GameAnalyticsManager.AddErrorEventOnce(
"GameClient.HandleDisconnectMessage", "GameClient.HandleDisconnectMessage",
GameAnalyticsManager.ErrorSeverity.Debug, GameAnalyticsManager.ErrorSeverity.Debug, disconnectMessage);
$"Client received a disconnect message. Reason: {disconnectPacket.DisconnectReason}");
} }
if (disconnectPacket.DisconnectReason == DisconnectReason.ServerFull) if (disconnectPacket.DisconnectReason == DisconnectReason.ServerFull)
@@ -1235,7 +1247,16 @@ namespace Barotrauma.Networking
private void OnConnectionInitializationComplete() private void OnConnectionInitializationComplete()
{ {
UpdatePresence($"-connect \"{ToolBox.EscapeCharacters(ServerName)}\" {string.Join(",", serverEndpoints.Select(e => e.StringRepresentation))}"); //don't allow connecting through the friend list if we're connected to localhost (others can't join to "localhost")
//we could potentially find the public IP of the server (assuming it's a public server) from the Steam API, but maybe not worth the trouble?
bool connectedToLocalHost = serverEndpoints.All(e => e is LidgrenEndpoint lidgrenEndpoint && lidgrenEndpoint.Address.IsLocalHost);
string escapedServerName = ServerName.IsNullOrWhiteSpace() ? "Server" : ToolBox.EscapeCharacters(ServerName);
string connectCommand =
connectedToLocalHost ?
string.Empty :
$"-connect \"{escapedServerName}\" {string.Join(",", serverEndpoints.Select(e => e.StringRepresentation))}";
UpdatePresence(connectCommand);
canStart = true; canStart = true;
connected = true; connected = true;
@@ -2150,11 +2171,14 @@ namespace Barotrauma.Networking
if (lobbyUpdated) if (lobbyUpdated)
{ {
//we don't want the client to create any network events
//when they modify the server lobby to match the server state as a result of this message
ServerSettings.SuppressNetworkMessages = true;
var prevDispatcher = GUI.KeyboardDispatcher.Subscriber; var prevDispatcher = GUI.KeyboardDispatcher.Subscriber;
UInt16 updateID = inc.ReadUInt16(); UInt16 updateID = inc.ReadUInt16();
UInt16 settingsLen = inc.ReadUInt16(); UInt16 settingsLen = inc.ReadUInt16();
byte[] settingsData = inc.ReadBytes(settingsLen); byte[] settingsData = inc.ReadBytes(settingsLen);
@@ -2299,6 +2323,9 @@ namespace Barotrauma.Networking
} }
lastSentChatMsgID = inc.ReadUInt16(); lastSentChatMsgID = inc.ReadUInt16();
ServerSettings.SuppressNetworkMessages = false;
break; break;
case ServerNetSegment.ClientList: case ServerNetSegment.ClientList:
ReadClientList(inc); ReadClientList(inc);
@@ -3091,6 +3118,7 @@ namespace Barotrauma.Networking
public void RequestSelectSub(SubmarineInfo sub, SelectedSubType type) public void RequestSelectSub(SubmarineInfo sub, SelectedSubType type)
{ {
if (!HasPermission(ClientPermissions.SelectSub) || sub == null) { return; } if (!HasPermission(ClientPermissions.SelectSub) || sub == null) { return; }
if (ServerSettings.SuppressNetworkMessages) { return; }
IWriteMessage msg = new WriteOnlyMessage(); IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND); msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND);
@@ -3392,6 +3420,10 @@ namespace Barotrauma.Networking
{ {
msgBox = GameMain.NetLobbyScreen.ChatInput; msgBox = GameMain.NetLobbyScreen.ChatInput;
} }
if (msgBox != null)
{
msgBox.Enabled = !IsBlockedBySpamFilter;
}
UpdateLogButtonVisibility(); UpdateLogButtonVisibility();

View File

@@ -12,6 +12,8 @@ namespace Barotrauma.Networking
private static readonly LocalizedString packetAmountTooltip = TextManager.Get("ServerSettingsMaxPacketAmountTooltip"); private static readonly LocalizedString packetAmountTooltip = TextManager.Get("ServerSettingsMaxPacketAmountTooltip");
private static readonly RichString packetAmountTooltipWarning = RichString.Rich($"{packetAmountTooltip}\n\n‖color:gui.red‖{TextManager.Get("PacketLimitWarning")}‖end‖"); private static readonly RichString packetAmountTooltipWarning = RichString.Rich($"{packetAmountTooltip}\n\n‖color:gui.red‖{TextManager.Get("PacketLimitWarning")}‖end‖");
public static bool SuppressNetworkMessages;
partial class NetPropertyData partial class NetPropertyData
{ {
public GUIComponent GUIComponent; public GUIComponent GUIComponent;
@@ -94,6 +96,14 @@ namespace Barotrauma.Networking
get get
{ {
if (GUIComponent == null) { return false; } if (GUIComponent == null) { return false; }
if (GUIComponent is GUIDropDown dropDown &&
dropDown.SelectedIndex == -1)
{
//nothing selected in the dropdown
//it's not possible to select nothing via the UI, which means the client cannot have selected anything locally
//(so this must mean that either nothing has been selected yet or that there's nothing in the dropdown)
return false;
}
return !PropEquals(TempValue, GUIComponentValue); return !PropEquals(TempValue, GUIComponentValue);
} }
} }
@@ -238,6 +248,7 @@ namespace Barotrauma.Networking
int traitorDangerLevel = 0) int traitorDangerLevel = 0)
{ {
if (!GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) { return; } if (!GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) { return; }
if (SuppressNetworkMessages) { return; }
IWriteMessage outMsg = new WriteOnlyMessage(); IWriteMessage outMsg = new WriteOnlyMessage();

View File

@@ -11,6 +11,14 @@ namespace Barotrauma
{ {
abstract class CampaignSetupUI abstract class CampaignSetupUI
{ {
protected enum SaveSortingType
{
LastPlayedDescending, LastPlayedAscending,
NameDescending, NameAscending
}
private const SaveSortingType DefaultSaveSortingType = SaveSortingType.LastPlayedDescending;
protected readonly GUIComponent newGameContainer, loadGameContainer; protected readonly GUIComponent newGameContainer, loadGameContainer;
protected GUIListBox saveList; protected GUIListBox saveList;
@@ -111,24 +119,54 @@ namespace Barotrauma
return saveFrame; return saveFrame;
} }
protected void SortSaveList() protected void SortSaveList(SaveSortingType sortingType = DefaultSaveSortingType) => saveList?.Content.RectTransform.SortChildren((rect1, rect2) =>
{ {
saveList.Content.RectTransform.SortChildren((c1, c2) => if (rect1.GUIComponent.UserData is not CampaignMode.SaveInfo file1 || rect2.GUIComponent.UserData is not CampaignMode.SaveInfo file2) { return 0; }
if (!file1.SaveTime.TryUnwrap(out SerializableDateTime file1WriteTime) || !file2.SaveTime.TryUnwrap(out SerializableDateTime file2WriteTime)) { return 0; }
return sortingType switch
{ {
if (c1.GUIComponent.UserData is not CampaignMode.SaveInfo file1 SaveSortingType.LastPlayedDescending => file2WriteTime.CompareTo(file1WriteTime),
|| c2.GUIComponent.UserData is not CampaignMode.SaveInfo file2) SaveSortingType.LastPlayedAscending => file1WriteTime.CompareTo(file2WriteTime),
{ SaveSortingType.NameDescending => string.Compare(Path.GetFileNameWithoutExtension(file1.FilePath), Path.GetFileNameWithoutExtension(file2.FilePath), StringComparison.OrdinalIgnoreCase),
return 0; SaveSortingType.NameAscending => string.Compare(Path.GetFileNameWithoutExtension(file2.FilePath), Path.GetFileNameWithoutExtension(file1.FilePath), StringComparison.OrdinalIgnoreCase),
} _ => 0
};
});
if (!file1.SaveTime.TryUnwrap(out var file1WriteTime) protected void CreateSaveFilteringHeader(GUIComponent parent)
|| !file2.SaveTime.TryUnwrap(out var file2WriteTime)) {
GUILayoutGroup container = new(new RectTransform(Vector2.UnitX, parent.RectTransform), true) { Stretch = true };
GUI.CreateFilterBox(new RectTransform(new Vector2(0.6f, 1f), container.RectTransform)).OnTextChanged += (_, filterText) =>
{
filterText = filterText.Trim();
foreach (GUIComponent saveElement in saveList.Content.Children)
{ {
return 0; if (saveElement.UserData is not CampaignMode.SaveInfo saveInfo) { continue; }
saveElement.Visible = filterText.IsNullOrEmpty()
|| Path.GetFileNameWithoutExtension(saveInfo.FilePath).Contains(filterText, StringComparison.OrdinalIgnoreCase);
} }
return true;
return file2WriteTime.CompareTo(file1WriteTime); };
});
SaveSortingType[] sortingTypes = Enum.GetValues<SaveSortingType>();
GUIDropDown dropDown = new(new RectTransform(new Vector2(0.4f, 1f), container.RectTransform), elementCount: sortingTypes.Length)
{
OnSelected = (_, data) =>
{
SortSaveList((SaveSortingType)data);
return true;
}
};
foreach (SaveSortingType sortingType in sortingTypes)
{
dropDown.AddItem(TextManager.Get($"SaveSortingType.{sortingType}"), sortingType);
}
dropDown.SelectItem(DefaultSaveSortingType);
container.RectTransform.MinSize = (0, container.Children.Max(child => child.Rect.Size.Y));
} }
public struct CampaignSettingElements public struct CampaignSettingElements

View File

@@ -217,6 +217,8 @@ namespace Barotrauma
RelativeSpacing = 0.03f RelativeSpacing = 0.03f
}; };
CreateSaveFilteringHeader(leftColumn);
saveList = new GUIListBox(new RectTransform(Vector2.One, leftColumn.RectTransform)) saveList = new GUIListBox(new RectTransform(Vector2.One, leftColumn.RectTransform))
{ {
PlaySoundOnSelect = true, PlaySoundOnSelect = true,

View File

@@ -320,14 +320,14 @@ namespace Barotrauma
if (string.IsNullOrWhiteSpace(sender.Text)) if (string.IsNullOrWhiteSpace(sender.Text))
{ {
characterInfo.Name = characterInfo.GetRandomName(Rand.RandSync.Unsynced); characterInfo.Name = characterInfo.GetRandomName(Rand.RandSync.Unsynced);
sender.Text = characterInfo.Name;
sender.UserData = "random"; sender.UserData = "random";
} }
else else
{ {
characterInfo.Name = sender.Text; characterInfo.Rename(sender.Text);
sender.UserData = "user"; sender.UserData = "user";
} }
sender.Text = characterInfo.Name;
}; };
characterName.OnEnterPressed += (sender, text) => characterName.OnEnterPressed += (sender, text) =>
{ {
@@ -594,6 +594,8 @@ namespace Barotrauma
RelativeSpacing = 0.03f RelativeSpacing = 0.03f
}; };
CreateSaveFilteringHeader(leftColumn);
saveList = new GUIListBox(new RectTransform(Vector2.One, leftColumn.RectTransform)) saveList = new GUIListBox(new RectTransform(Vector2.One, leftColumn.RectTransform))
{ {
PlaySoundOnSelect = true, PlaySoundOnSelect = true,

View File

@@ -352,7 +352,7 @@ namespace Barotrauma
Location currentDisplayLocation = Campaign.GetCurrentDisplayLocation(); Location currentDisplayLocation = Campaign.GetCurrentDisplayLocation();
if (connection != null && connection.Locations.Contains(currentDisplayLocation)) if (connection != null && connection.Locations.Contains(currentDisplayLocation))
{ {
List<Mission> availableMissions = currentDisplayLocation.GetMissionsInConnection(connection).ToList(); List<Mission> availableMissions = currentDisplayLocation.GetMissionsInConnection(connection).Where(m => m.Prefab.ShowInMenus || GameMain.DebugDraw).ToList();
if (!availableMissions.Any()) { availableMissions.Insert(0, null); } if (!availableMissions.Any()) { availableMissions.Insert(0, null); }
@@ -389,19 +389,26 @@ namespace Barotrauma
AbsoluteSpacing = GUI.IntScale(5) AbsoluteSpacing = GUI.IntScale(5)
}; };
var missionName = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), mission?.Name ?? TextManager.Get("NoMission"), font: GUIStyle.SubHeadingFont, wrap: true); LocalizedString missionName = mission?.Name ?? TextManager.Get("NoMission");
missionName.RectTransform.MinSize = new Point(0, GUI.IntScale(15)); if (GameMain.DebugDraw && mission != null)
{
if (!mission.Prefab.ShowInMenus) { missionName = $"[HIDDEN] {missionName}"; }
missionName += $" ({mission.Prefab.Identifier})";
}
var missionNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), missionName, font: GUIStyle.SubHeadingFont, wrap: true);
missionNameBlock.RectTransform.MinSize = new Point(0, GUI.IntScale(15));
if (mission == null) if (mission == null)
{ {
missionTextContent.RectTransform.MinSize = missionName.RectTransform.MinSize = new Point(0, GUI.IntScale(35)); missionTextContent.RectTransform.MinSize = missionNameBlock.RectTransform.MinSize = new Point(0, GUI.IntScale(35));
missionTextContent.ChildAnchor = Anchor.CenterLeft; missionTextContent.ChildAnchor = Anchor.CenterLeft;
} }
else else
{ {
GUITickBox tickBox = null; GUITickBox tickBox = null;
if (!isMissionInNextLocation) if (!isMissionInNextLocation && mission.Prefab.ShowInMenus)
{ {
tickBox = new GUITickBox(new RectTransform(Vector2.One * 0.9f, missionName.RectTransform, anchor: Anchor.CenterLeft, scaleBasis: ScaleBasis.Smallest) { AbsoluteOffset = new Point((int)missionName.Padding.X, 0) }, label: string.Empty) tickBox = new GUITickBox(new RectTransform(Vector2.One * 0.9f, missionNameBlock.RectTransform, anchor: Anchor.CenterLeft, scaleBasis: ScaleBasis.Smallest) { AbsoluteOffset = new Point((int)missionNameBlock.Padding.X, 0) }, label: string.Empty)
{ {
UserData = mission, UserData = mission,
Selected = Campaign.Map.CurrentLocation?.SelectedMissions.Contains(mission) ?? false Selected = Campaign.Map.CurrentLocation?.SelectedMissions.Contains(mission) ?? false
@@ -443,7 +450,7 @@ namespace Barotrauma
GUILayoutGroup difficultyIndicatorGroup = null; GUILayoutGroup difficultyIndicatorGroup = null;
if (mission.Difficulty.HasValue) if (mission.Difficulty.HasValue)
{ {
difficultyIndicatorGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.9f), missionName.RectTransform, anchor: Anchor.CenterRight) { AbsoluteOffset = new Point((int)missionName.Padding.Z, 0) }, difficultyIndicatorGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.9f), missionNameBlock.RectTransform, anchor: Anchor.CenterRight) { AbsoluteOffset = new Point((int)missionNameBlock.Padding.Z, 0) },
isHorizontal: true, childAnchor: Anchor.CenterRight) isHorizontal: true, childAnchor: Anchor.CenterRight)
{ {
AbsoluteSpacing = 1, AbsoluteSpacing = 1,
@@ -465,11 +472,11 @@ namespace Barotrauma
float extraPadding = 0;// 0.8f * tickBox.Rect.Width; float extraPadding = 0;// 0.8f * tickBox.Rect.Width;
float extraZPadding = difficultyIndicatorGroup != null ? mission.Difficulty.Value * (difficultyIndicatorGroup.Children.First().Rect.Width + difficultyIndicatorGroup.AbsoluteSpacing) : 0; float extraZPadding = difficultyIndicatorGroup != null ? mission.Difficulty.Value * (difficultyIndicatorGroup.Children.First().Rect.Width + difficultyIndicatorGroup.AbsoluteSpacing) : 0;
missionName.Padding = new Vector4(missionName.Padding.X + (tickBox?.Rect.Width ?? 0) * 1.2f + extraPadding, missionNameBlock.Padding = new Vector4(missionNameBlock.Padding.X + (tickBox?.Rect.Width ?? 0) * 1.2f + extraPadding,
missionName.Padding.Y, missionNameBlock.Padding.Y,
missionName.Padding.Z + extraZPadding + extraPadding, missionNameBlock.Padding.Z + extraZPadding + extraPadding,
missionName.Padding.W); missionNameBlock.Padding.W);
missionName.CalculateHeightFromText(); missionNameBlock.CalculateHeightFromText();
//spacing //spacing
new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform) { MinSize = new Point(0, GUI.IntScale(10)) }, style: null); new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform) { MinSize = new Point(0, GUI.IntScale(10)) }, style: null);
@@ -544,7 +551,7 @@ namespace Barotrauma
OnClicked = (GUIButton btn, object obj) => OnClicked = (GUIButton btn, object obj) =>
{ {
if (missionList.Content.FindChild(c => c is GUITickBox tickBox && tickBox.Selected, recursive: true) == null && if (missionList.Content.FindChild(c => c is GUITickBox tickBox && tickBox.Selected, recursive: true) == null &&
missionList.Content.Children.Any(c => c.UserData is Mission mission && mission.Locations.Contains(Campaign?.Map?.CurrentLocation))) missionList.Content.Children.Any(c => c.UserData is Mission { Prefab.ShowInMenus: true } mission && mission.Locations.Contains(Campaign?.Map?.CurrentLocation)))
{ {
var noMissionVerification = new GUIMessageBox(string.Empty, TextManager.Get("nomissionprompt"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); var noMissionVerification = new GUIMessageBox(string.Empty, TextManager.Get("nomissionprompt"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") });
noMissionVerification.Buttons[0].OnClicked = (btn, userdata) => noMissionVerification.Buttons[0].OnClicked = (btn, userdata) =>

View File

@@ -482,6 +482,11 @@ namespace Barotrauma
public void TestLevelGenerationForErrors(int amountOfLevelsToGenerate) public void TestLevelGenerationForErrors(int amountOfLevelsToGenerate)
{ {
if (selectedParams == null)
{
throw new InvalidOperationException("No level generation parameters selected in the level editor.");
}
CoroutineManager.StartCoroutine(GenerateLevels()); CoroutineManager.StartCoroutine(GenerateLevels());
IEnumerable<CoroutineStatus> GenerateLevels() IEnumerable<CoroutineStatus> GenerateLevels()
@@ -520,7 +525,7 @@ namespace Barotrauma
errorCatcher.Errors.ToList().ForEach(e => DebugConsole.ThrowError(e.Text)); errorCatcher.Errors.ToList().ForEach(e => DebugConsole.ThrowError(e.Text));
yield return CoroutineStatus.Success; yield return CoroutineStatus.Success;
} }
yield return CoroutineStatus.Running; yield return new WaitForSeconds(0.1f);
} }
} }
} }

View File

@@ -740,6 +740,30 @@ namespace Barotrauma
List<Identifier> missionTypes = MissionPrefab.GetAllMultiplayerSelectableMissionTypes().ToList(); List<Identifier> missionTypes = MissionPrefab.GetAllMultiplayerSelectableMissionTypes().ToList();
GUILayoutGroup buttonGroup = new(new RectTransform(Vector2.UnitX, missionTypeList.Content.RectTransform), true) { Stretch = true };
GUIButton selectAllMissionsButton = new(new RectTransform(new Vector2(0.5f, 1f), buttonGroup.RectTransform), TextManager.Get("selectall"))
{
OnClicked = (_, _) =>
{
IEnumerable<Identifier> validMissions = GetValidMissions();
validMissions.ForEach(missionType => GameMain.Client.ServerSettings?.ClientAdminWrite(ServerSettings.NetFlags.Misc, addedMissionType: missionType));
return true;
}
};
GUIButton deselectAllMissionsButton = new(new RectTransform(new Vector2(0.5f, 1f), buttonGroup.RectTransform), TextManager.Get("deselectall"))
{
OnClicked = (_, _) =>
{
IEnumerable<Identifier> validMissions = GetValidMissions();
// The server must have at least one mission selected, so ensure the first in the list is enabled.
GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, addedMissionType: validMissions.First());
validMissions.Skip(1).ForEach(missionType => GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, removedMissionType: missionType));
return true;
}
};
buttonGroup.RectTransform.MinSize = (0, buttonGroup.Children.Max(child => child.Rect.Height));
missionTypeTickBoxes = new GUITickBox[missionTypes.Count]; missionTypeTickBoxes = new GUITickBox[missionTypes.Count];
int index = 0; int index = 0;
foreach (var missionType in missionTypes.OrderBy(t => TextManager.Get("MissionType." + t.Value).Value)) foreach (var missionType in missionTypes.OrderBy(t => TextManager.Get("MissionType." + t.Value).Value))
@@ -763,6 +787,13 @@ namespace Barotrauma
} }
else else
{ {
Identifier firstValidMission = GetValidMissions().First();
if (missionTypeTickBoxes.None(tickBox => tickBox.Selected && tickBox.Parent.Visible))
{
GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, addedMissionType: firstValidMission);
if ((Identifier)tickbox.UserData == firstValidMission) { return true; }
}
GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, removedMissionType: (Identifier)tickbox.UserData); GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, removedMissionType: (Identifier)tickbox.UserData);
} }
return true; return true;
@@ -771,9 +802,16 @@ namespace Barotrauma
frame.RectTransform.MinSize = missionTypeTickBoxes[index].RectTransform.MinSize; frame.RectTransform.MinSize = missionTypeTickBoxes[index].RectTransform.MinSize;
index++; index++;
} }
clientDisabledElements.Add(selectAllMissionsButton);
clientDisabledElements.Add(deselectAllMissionsButton);
clientDisabledElements.AddRange(missionTypeTickBoxes); clientDisabledElements.AddRange(missionTypeTickBoxes);
return gameModeSpecificFrame; return gameModeSpecificFrame;
IEnumerable<Identifier> GetValidMissions() => missionTypeTickBoxes
.Where(tickBox => tickBox.Parent.Visible)
.Select(tickBox => (Identifier)tickBox.UserData);
} }
private GUIFrame gameModeSettingsContent; private GUIFrame gameModeSettingsContent;
@@ -2884,6 +2922,11 @@ namespace Barotrauma
UserData = new JobVariant(jobPrefab, variant) UserData = new JobVariant(jobPrefab, variant)
}; };
jobVariantTooltip.RectTransform.AbsoluteOffset = new Point(parentSlot.Rect.Right, parentSlot.Rect.Y); jobVariantTooltip.RectTransform.AbsoluteOffset = new Point(parentSlot.Rect.Right, parentSlot.Rect.Y);
if (jobVariantTooltip.Rect.X < 0)
{
jobVariantTooltip.RectTransform.SetPosition(anchor: Anchor.TopLeft, pivot: Pivot.BottomLeft);
jobVariantTooltip.RectTransform.AbsoluteOffset = new Point(parentSlot.Rect.X, parentSlot.Rect.Y);
}
var content = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), jobVariantTooltip.RectTransform, Anchor.Center)) var content = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), jobVariantTooltip.RectTransform, Anchor.Center))
{ {
@@ -3049,6 +3092,8 @@ namespace Barotrauma
private void RefreshOutpostDropdown() private void RefreshOutpostDropdown()
{ {
Identifier randomOutpostIdentifier = "Random".ToIdentifier();
outpostDropdown.Parent.Visible = MissionTypeFrame.Visible; outpostDropdown.Parent.Visible = MissionTypeFrame.Visible;
if (!outpostDropdown.Parent.Visible) { return; } if (!outpostDropdown.Parent.Visible) { return; }
@@ -3057,7 +3102,7 @@ namespace Barotrauma
Identifier prevSelected = GameMain.NetworkMember?.ServerSettings.SelectedOutpostName ?? Identifier.Empty; Identifier prevSelected = GameMain.NetworkMember?.ServerSettings.SelectedOutpostName ?? Identifier.Empty;
outpostDropdown.ClearChildren(); outpostDropdown.ClearChildren();
outpostDropdown.AddItem(TextManager.Get("Random"), "Random".ToIdentifier()); outpostDropdown.AddItem(TextManager.Get("Random"), randomOutpostIdentifier);
HashSet<Identifier> validOutpostTagsForMissions = new HashSet<Identifier>(); HashSet<Identifier> validOutpostTagsForMissions = new HashSet<Identifier>();
IEnumerable<Type> suitableMissionClasses = IEnumerable<Type> suitableMissionClasses =
@@ -3081,12 +3126,22 @@ namespace Barotrauma
foreach (var submarineInfo in SubmarineInfo.SavedSubmarines.DistinctBy(s => s.Name)) foreach (var submarineInfo in SubmarineInfo.SavedSubmarines.DistinctBy(s => s.Name))
{ {
if (submarineInfo.Type == SubmarineType.Outpost && if (submarineInfo.Type == SubmarineType.Outpost &&
validOutpostTagsForMissions.Any(tag => submarineInfo.OutpostTags.Contains(tag))) validOutpostTagsForMissions.Any(submarineInfo.OutpostTags.Contains))
{ {
outpostDropdown.AddItem(submarineInfo.DisplayName, userData: submarineInfo.Name.ToIdentifier(), toolTip: submarineInfo.Description); outpostDropdown.AddItem(submarineInfo.DisplayName, userData: submarineInfo.Name.ToIdentifier(), toolTip: submarineInfo.Description);
} }
} }
outpostDropdown.ListBox.Select(prevSelected); if (!outpostDropdown.ListBox.Select(prevSelected))
{
//could not select the previously selected outpost (not suitable for the selected missions)
// -> choose random instead
if (outpostDropdown.SelectedData is Identifier selectedIdentifier &&
selectedIdentifier != randomOutpostIdentifier)
{
outpostDropdown.Flash(GUIStyle.Red);
}
outpostDropdown.ListBox.Select(randomOutpostIdentifier);
}
GameMain.Client.ServerSettings.AssignGUIComponent(nameof(ServerSettings.SelectedOutpostName), outpostDropdown); GameMain.Client.ServerSettings.AssignGUIComponent(nameof(ServerSettings.SelectedOutpostName), outpostDropdown);
} }
else else
@@ -4496,26 +4551,16 @@ namespace Barotrauma
void PositionJobSelectionFrame() void PositionJobSelectionFrame()
{ {
JobSelectionFrame.RectTransform.AbsoluteOffset = new Point(characterInfoFrame.Rect.Right - JobSelectionFrame.Rect.Width, characterInfoFrame.Rect.Bottom); //move to the left side of the info frame
if (characterInfoFrame.Rect.Bottom + JobSelectionFrame.Rect.Height > GameMain.GraphicsHeight) JobSelectionFrame.RectTransform.AbsoluteOffset = new Point(characterInfoFrame.Rect.X - JobSelectionFrame.Rect.Width, JobList.Rect.Y);
if (JobSelectionFrame.Rect.X < 0)
{ {
//move to the left side of the info frame if the bottom goes below the screen //scale if goes outside the screen horizontally
JobSelectionFrame.RectTransform.AbsoluteOffset = new Point(characterInfoFrame.Rect.X - JobSelectionFrame.Rect.Width, characterInfoFrame.Rect.Bottom - JobSelectionFrame.Rect.Height / 2); JobSelectionFrame.RectTransform.Resize(new Point(characterInfoFrame.Rect.X, JobSelectionFrame.Rect.Height));
if (JobSelectionFrame.Rect.X < 0) JobSelectionFrame.RectTransform.AbsoluteOffset = new Point(characterInfoFrame.Rect.X - JobSelectionFrame.Rect.Width, JobSelectionFrame.RectTransform.AbsoluteOffset.Y);
{ }
//scale if goes outside the screen horizontally
JobSelectionFrame.RectTransform.Resize(new Point(characterInfoFrame.Rect.X, JobSelectionFrame.Rect.Height));
JobSelectionFrame.RectTransform.AbsoluteOffset = new Point(characterInfoFrame.Rect.X - JobSelectionFrame.Rect.Width, JobSelectionFrame.RectTransform.AbsoluteOffset.Y);
}
}
} }
new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), JobSelectionFrame.RectTransform, anchor: Anchor.Center), style: "OuterGlow", color: Color.Black)
{
UserData = "outerglow",
CanBeFocused = false
};
var jobSelectionList = new GUIListBox(new RectTransform(Vector2.One * listBoxRelativeSize, JobSelectionFrame.RectTransform, Anchor.Center), style: "GUIFrameListBox") var jobSelectionList = new GUIListBox(new RectTransform(Vector2.One * listBoxRelativeSize, JobSelectionFrame.RectTransform, Anchor.Center), style: "GUIFrameListBox")
{ {
Padding = Vector4.One * GUI.IntScale(10) Padding = Vector4.One * GUI.IntScale(10)

View File

@@ -368,6 +368,7 @@ namespace Barotrauma
"DecorativeSprite", "DecorativeSprite",
"BarrelSprite", "BarrelSprite",
"RailSprite", "RailSprite",
"ChargeSprite",
"SchematicSprite", "SchematicSprite",
"WeldedSprite" "WeldedSprite"
}; };

View File

@@ -8,6 +8,7 @@ using Microsoft.Xna.Framework.Input;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Xml.Linq; using System.Xml.Linq;
@@ -37,6 +38,151 @@ namespace Barotrauma
Wiring Wiring
} }
#region Transform Editor
private const float TransformWidgetOffset = 300f;
private GUITickBox rotateToolToggle, scaleToolToggle;
public bool TransformWidgetSelected => TransformWidget.IsSelected;
private static Vector2 GetSelectionCenter()
{
IEnumerable<MapEntity> nonWireEntities = MapEntity.FilteredSelectedList.Where(static entity => (entity as Item)?.GetComponent<Wire>() is not { Drawable: true });
if (nonWireEntities.None()) { return Vector2.Zero; }
float minX = nonWireEntities.Min(static entity => entity.DrawPosition.X);
float minY = nonWireEntities.Min(static entity => entity.DrawPosition.Y);
float maxX = nonWireEntities.Max(static entity => entity.DrawPosition.X);
float maxY = nonWireEntities.Max(static entity => entity.DrawPosition.Y);
return new Vector2(minX + maxX, minY + maxY) / 2f;
}
private Vector2 oldWidgetWorldPos;
public record struct TransformData(float Scale, float RotationRad, Vector2 Pos, Rectangle Rect, Vector2? TexOffset,
Dictionary<Wire, (List<Vector2> Nodes, float Width)> Wires,
Dictionary<ItemLabel, float> TextScales,
Dictionary<LightComponent, float> LightRanges,
Dictionary<Turret, Vector2> TurretLimits);
private TransformToolCommand transformCommand;
private Widget transformWidget;
private Widget TransformWidget
{
get
{
if (transformWidget != null) { return transformWidget; }
int size = GUI.IntScale(16f);
transformWidget = new Widget("scale", size, WidgetShape.Rectangle)
{
Enabled = false,
Color = GUIStyle.Yellow,
InputAreaMargin = 20,
RequireMouseOn = false,
TooltipOffset = (size / 2f, -size / 2f),
IsFilled = true
};
transformWidget.PreUpdate += _ =>
{
transformWidget.Enabled = MapEntity.FilteredSelectedList.Any() && (rotateToolToggle.Selected || scaleToolToggle.Selected);
if (transformWidget.IsSelected && PlayerInput.PrimaryMouseButtonReleased())
{
if (MapEntity.EditingHUD.GetChild<GUIListBox>() is GUIListBox listBox)
{
SerializableEntityEditor.LockEditing = true;
listBox.Content.Children.OfType<SerializableEntityEditor>().ForEach(editor => editor.RefreshValues());
SerializableEntityEditor.LockEditing = false;
}
Widget.SelectedWidgets.Remove(transformWidget);
StoreCommand(transformCommand);
transformWidget.Color = Color.Yellow;
}
};
transformWidget.Selected += () =>
{
transformWidget.Color = GUIStyle.Blue;
IEnumerable<Item> containedItems = MapEntity.SelectedList.OfType<Item>().SelectManyRecursive(item => item.ContainedItems);
IEnumerable<MapEntity> allEntities = MapEntity.SelectedList.Concat(containedItems).Distinct();
Dictionary<MapEntity, TransformData> oldTransformData = allEntities.ToDictionary(static entity => entity, static entity =>
{
Item item = entity as Item;
Structure structure = entity as Structure;
float rotation = entity switch
{
Structure => MathHelper.ToRadians(structure.Rotation),
Item => item.RotationRad,
_ => 0f
};
return new TransformData(entity.Scale, rotation, entity.DrawPosition, entity.Rect, structure?.TextureOffset,
GetPropertyDict<Wire, (List<Vector2>, float)>(static wire => (wire.GetNodes(), wire.Width)),
GetPropertyDict<ItemLabel, float>(static label => label.TextScale),
GetPropertyDict<LightComponent, float>(static light => light.Range),
GetPropertyDict<Turret, Vector2>(static turret => turret.RotationLimits));
Dictionary<TComponent, TProperties> GetPropertyDict<TComponent, TProperties>(Func<TComponent, TProperties> propSelector) where TComponent : ItemComponent
=> item?.GetComponents<TComponent>().ToDictionary(static comp => comp, propSelector);
});
transformCommand = new TransformToolCommand(oldTransformData, GetSelectionCenter());
};
transformWidget.MouseHeld += _ =>
{
MapEntity.DisableSelect = true;
Vector2 widgetWorldPos = Cam.ScreenToWorld(transformWidget.DrawPos * 2f); // Scale position to account for camera zooming as well.
if (MathUtils.NearlyEqual(widgetWorldPos, oldWidgetWorldPos)) { return; }
oldWidgetWorldPos = widgetWorldPos;
transformCommand.RotationRad = null;
LocalizedString rotationString = null;
if (rotateToolToggle.Selected)
{
transformCommand.RotationRad = MathUtils.VectorToAngle(PlayerInput.MousePosition - Cam.WorldToScreen(transformCommand.Pivot));
rotationString = TextManager.GetWithVariable("SubEditor.TransformWidget.Rotation", "[value]", MathHelper.ToDegrees(transformCommand.RotationRad.Value).ToString("0.000", CultureInfo.CurrentCulture));
}
transformCommand.ScaleMult = null;
LocalizedString scaleString = null;
if (scaleToolToggle.Selected)
{
transformCommand.ScaleMult = Math.Clamp(Vector2.Distance(PlayerInput.MousePosition, Cam.WorldToScreen(transformCommand.Pivot)) / (TransformWidgetOffset * GUI.Scale), transformCommand.MinScale, transformCommand.MaxScale);
scaleString = TextManager.GetWithVariable("SubEditor.TransformWidget.Scale", "[value]", transformCommand.ScaleMult.Value.ToString("0.000", CultureInfo.CurrentCulture));
}
transformWidget.Tooltip = !rotationString.IsNullOrEmpty() && !scaleString.IsNullOrEmpty()
? LocalizedString.Join("\n", rotationString, scaleString)
: transformWidget.Tooltip = rotationString ?? scaleString;
transformCommand.Execute();
};
transformWidget.PreDraw += (sb, _) =>
{
Vector2 selectionCenterScreenPos = Cam.WorldToScreen(transformWidget.IsSelected ? transformCommand.Pivot : GetSelectionCenter());
if (!GameMain.Instance.Paused)
{
if (transformWidget.IsSelected && scaleToolToggle.Selected)
{
transformWidget.DrawPos = PlayerInput.MousePosition;
}
else
{
Vector2 dir = transformWidget.IsSelected ? Vector2.Normalize(PlayerInput.MousePosition - Cam.WorldToScreen(transformCommand.Pivot)) : Vector2.UnitX;
transformWidget.DrawPos = selectionCenterScreenPos + dir * TransformWidgetOffset * GUI.Scale;
}
}
GUI.DrawLine(sb, selectionCenterScreenPos, transformWidget.DrawPos, Color.Black, width: 7f);
GUI.DrawLine(sb, selectionCenterScreenPos, transformWidget.DrawPos, Color.Red, width: 3f);
};
return transformWidget;
}
}
#endregion
public enum WarningType public enum WarningType
{ {
NoWaypoints, NoWaypoints,
@@ -530,6 +676,16 @@ namespace Barotrauma
}; };
spacing = new GUIFrame(new RectTransform(new Vector2(0.02f, 1.0f), paddedTopPanel.RectTransform), style: null); spacing = new GUIFrame(new RectTransform(new Vector2(0.02f, 1.0f), paddedTopPanel.RectTransform), style: null);
new GUIFrame(new RectTransform(new Vector2(0.1f, 0.9f), spacing.RectTransform, Anchor.Center), style: "VerticalLine");
rotateToolToggle = new GUITickBox(new RectTransform(new Vector2(0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "SubEditorRotateToggle")
{
ToolTip = TextManager.Get("SubEditor.RotateToggleToolTip")
};
scaleToolToggle = new GUITickBox(new RectTransform(new Vector2(0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "SubEditorScaleToggle")
{
ToolTip = TextManager.Get("SubEditor.ScaleToggleToolTip")
};
var selectedLayerText = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), paddedTopPanel.RectTransform), var selectedLayerText = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), paddedTopPanel.RectTransform),
string.Empty, textAlignment: Alignment.Center); string.Empty, textAlignment: Alignment.Center);
@@ -1099,6 +1255,7 @@ namespace Barotrauma
{ {
Submarine.Unload(); Submarine.Unload();
GameMain.SubEditorScreen.Select(); GameMain.SubEditorScreen.Select();
GameMain.GameSession = null;
}; };
} }
@@ -1451,6 +1608,9 @@ namespace Barotrauma
GUI.ForceMouseOn(null); GUI.ForceMouseOn(null);
SetMode(Mode.Default); SetMode(Mode.Default);
rotateToolToggle.Selected = false;
scaleToolToggle.Selected = false;
if (backedUpSubInfo != null) if (backedUpSubInfo != null)
{ {
MainSub = new Submarine(backedUpSubInfo); MainSub = new Submarine(backedUpSubInfo);
@@ -1660,6 +1820,9 @@ namespace Barotrauma
}); });
ClearFilter(); ClearFilter();
Widget.SelectedWidgets.Remove(TransformWidget);
TransformWidget.Color = Color.Yellow;
} }
private void CreateDummyCharacter() private void CreateDummyCharacter()
@@ -4636,7 +4799,10 @@ namespace Barotrauma
{ {
if (itemPrefab.Name.IsNullOrEmpty() || itemPrefab.HideInMenus || itemPrefab.HideInEditors) { continue; } if (itemPrefab.Name.IsNullOrEmpty() || itemPrefab.HideInMenus || itemPrefab.HideInEditors) { continue; }
if (!itemPrefab.Tags.Contains(Tags.WireItem)) { continue; } if (!itemPrefab.Tags.Contains(Tags.WireItem)) { continue; }
if (CircuitBox.IsInGame() && itemPrefab.Tags.Contains(Tags.Thalamus)) { continue; } if (CircuitBox.IsInGame() && (itemPrefab.Tags.Contains(Tags.Thalamus) || itemPrefab.Tags.Contains("alien")))
{
continue;
}
wirePrefabs.Add(itemPrefab); wirePrefabs.Add(itemPrefab);
} }
@@ -6221,6 +6387,8 @@ namespace Barotrauma
CharacterHUD.Update((float)deltaTime, dummyCharacter, cam); CharacterHUD.Update((float)deltaTime, dummyCharacter, cam);
} }
TransformWidget.Update((float)deltaTime);
} }
/// <summary> /// <summary>
@@ -6350,6 +6518,11 @@ namespace Barotrauma
} }
MapEntity.DrawEditor(spriteBatch, cam); MapEntity.DrawEditor(spriteBatch, cam);
if (TransformWidget.Enabled)
{
TransformWidget.Draw(spriteBatch, (float)deltaTime);
}
GUI.Draw(Cam, spriteBatch); GUI.Draw(Cam, spriteBatch);
if (MeasurePositionStart != Vector2.Zero) if (MeasurePositionStart != Vector2.Zero)

View File

@@ -20,7 +20,7 @@ namespace Barotrauma
Steamworks.FriendState.Offline => FriendStatus.Offline, Steamworks.FriendState.Offline => FriendStatus.Offline,
Steamworks.FriendState.Invisible => FriendStatus.Offline, Steamworks.FriendState.Invisible => FriendStatus.Offline,
_ when steamFriend.IsPlayingThisGame => FriendStatus.PlayingBarotrauma, _ when steamFriend.IsPlayingThisGame => FriendStatus.PlayingBarotrauma,
_ when steamFriend.GameInfo is { GameID: > 0 } => FriendStatus.PlayingAnotherGame, _ when steamFriend.GameInfo is { GameID.Value: > 0 } => FriendStatus.PlayingAnotherGame,
_ => FriendStatus.NotPlaying _ => FriendStatus.NotPlaying
}, },
serverName: steamFriend.GetRichPresence("servername") ?? "", serverName: steamFriend.GetRichPresence("servername") ?? "",

View File

@@ -17,11 +17,15 @@ namespace Barotrauma.Sounds
private short[] sampleBuffer = Array.Empty<short>(); private short[] sampleBuffer = Array.Empty<short>();
private short[] muffleBuffer = Array.Empty<short>(); private short[] muffleBuffer = Array.Empty<short>();
private readonly double durationSeconds;
public override double? DurationSeconds => durationSeconds;
public OggSound(SoundManager owner, string filename, bool stream, ContentXElement xElement) : base(owner, filename, public OggSound(SoundManager owner, string filename, bool stream, ContentXElement xElement) : base(owner, filename,
stream, true, xElement) stream, true, xElement)
{ {
var reader = new VorbisReader(Filename); var reader = new VorbisReader(Filename);
durationSeconds = reader.TotalTime.TotalSeconds;
ALFormat = reader.Channels == 1 ? Al.FormatMono16 : Al.FormatStereo16; ALFormat = reader.Channels == 1 ? Al.FormatMono16 : Al.FormatStereo16;
SampleRate = reader.SampleRate; SampleRate = reader.SampleRate;

View File

@@ -1,8 +1,6 @@
using System; using Barotrauma.IO;
using OpenAL;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Barotrauma.IO; using System;
using System.Xml.Linq;
namespace Barotrauma.Sounds namespace Barotrauma.Sounds
{ {
@@ -24,6 +22,11 @@ namespace Barotrauma.Sounds
public readonly bool StreamsReliably; public readonly bool StreamsReliably;
/// <summary>
/// Length of the audio in seconds. Null if the length is unknown (e.g. a streaming audio source).
/// </summary>
public abstract double? DurationSeconds { get; }
public bool Loading { get; protected set; } public bool Loading { get; protected set; }
private readonly SoundManager.SourcePoolIndex sourcePoolIndex = SoundManager.SourcePoolIndex.Default; private readonly SoundManager.SourcePoolIndex sourcePoolIndex = SoundManager.SourcePoolIndex.Default;

View File

@@ -778,34 +778,36 @@ namespace Barotrauma.Sounds
while (!killThread) while (!killThread)
{ {
killThread = true; killThread = true;
for (int i = 0; i < playingChannels.Length; i++) for (int sourcePoolIndex = 0; sourcePoolIndex < playingChannels.Length; sourcePoolIndex++)
{ {
lock (playingChannels[i]) lock (playingChannels[sourcePoolIndex])
{ {
for (int j = 0; j < playingChannels[i].Length; j++) for (int channelIndex = 0; channelIndex < playingChannels[sourcePoolIndex].Length; channelIndex++)
{ {
if (playingChannels[i][j] == null) { continue; } var channel = playingChannels[sourcePoolIndex][channelIndex];
if (playingChannels[i][j].IsStream)
if (channel == null) { continue; }
if (channel.FadingOutAndDisposing)
{ {
if (playingChannels[i][j].IsPlaying) killThread = false;
channel.Gain -= 0.1f;
if (channel.Gain <= 0.0f)
{
channel.Dispose();
playingChannels[sourcePoolIndex][channelIndex] = null;
}
}
else if (channel.IsStream)
{
if (channel.IsPlaying)
{ {
killThread = false; killThread = false;
playingChannels[i][j].UpdateStream(); channel.UpdateStream();
} }
else else
{ {
playingChannels[i][j].Dispose(); channel.Dispose();
playingChannels[i][j] = null; playingChannels[sourcePoolIndex][channelIndex] = null;
}
}
else if (playingChannels[i][j].FadingOutAndDisposing)
{
killThread = false;
playingChannels[i][j].Gain -= 0.1f;
if (playingChannels[i][j].Gain <= 0.0f)
{
playingChannels[i][j].Dispose();
playingChannels[i][j] = null;
} }
} }
} }

View File

@@ -868,10 +868,28 @@ namespace Barotrauma
if (Level.IsLoadedOutpost) if (Level.IsLoadedOutpost)
{ {
// Only return music type for location types which have music tracks defined // Only return music type for location types which have music tracks defined
var locationType = Level.Loaded.StartLocation?.Type?.Identifier; var locationType = Level.Loaded?.StartLocation?.Type?.Identifier;
if (locationType.HasValue && locationType != Identifier.Empty && musicClips.Any(c => c.Type == locationType)) var backgroundMusicIdentifier = Level.Loaded?.StartLocation?.Type?.BackgroundMusicLocationType;
if (MatchesTrack(backgroundMusicIdentifier, out Identifier id) ||
MatchesTrack(locationType, out id))
{ {
return locationType.Value; return id;
}
bool MatchesTrack(Identifier? identifier, out Identifier idValue)
{
if (identifier.HasValue && identifier.Value != Identifier.Empty)
{
if (musicClips.Any(clip => clip.Type == identifier.Value))
{
idValue = identifier.Value;
return true;
}
}
idValue = Identifier.Empty;
return false;
} }
} }
} }

View File

@@ -1,23 +1,20 @@
using System; using Barotrauma.Media;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using OpenAL;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System.Runtime.InteropServices; using OpenAL;
using System.Threading; using System;
using Barotrauma.Media; using System.Collections.Generic;
namespace Barotrauma.Sounds namespace Barotrauma.Sounds
{ {
class VideoSound : Sound class VideoSound : Sound
{ {
private readonly object mutex; private readonly object mutex;
private Queue<short[]> sampleQueue; private readonly Queue<short[]> sampleQueue;
private SoundChannel soundChannel; private SoundChannel soundChannel;
private Video video; private readonly Video video;
public override double? DurationSeconds => null;
public VideoSound(SoundManager owner, string filename, int sampleRate, int channelCount, Video vid) : base(owner, filename, true, false) public VideoSound(SoundManager owner, string filename, int sampleRate, int channelCount, Video vid) : base(owner, filename, true, false)
{ {

View File

@@ -8,6 +8,8 @@ namespace Barotrauma.Sounds
{ {
class VoipSound : Sound class VoipSound : Sound
{ {
public override double? DurationSeconds => null;
public override SoundManager.SourcePoolIndex SourcePoolIndex public override SoundManager.SourcePoolIndex SourcePoolIndex
{ {
get get

View File

@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework; using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
namespace Barotrauma namespace Barotrauma
@@ -18,5 +19,15 @@ namespace Barotrauma
spriteBatch.Draw(texture, pos + offset, sourceRects[MathHelper.Clamp(spriteIndex, 0, sourceRects.Length - 1)], color, rotation + rotate, origin, scale, spriteEffect, depth == null ? this.depth : (float)depth); spriteBatch.Draw(texture, pos + offset, sourceRects[MathHelper.Clamp(spriteIndex, 0, sourceRects.Length - 1)], color, rotation + rotate, origin, scale, spriteEffect, depth == null ? this.depth : (float)depth);
} }
/// <summary>
/// When this spritesheet is used for an animation, returns the current spriteIndex based on the given animation speed.
/// </summary>
/// <param name="animationSpeed">Animation speed in frames per second</param>
/// <param name="animatePaused">Should the animation run when paused? Defaults to false.</param>
public int GetAnimatedSpriteIndex(float animationSpeed, bool animatePaused = false)
{
return (int)(Math.Floor((animatePaused ? Timing.TotalTime : Timing.TotalTimeUnpaused) * animationSpeed) % FrameCount);
}
} }
} }

View File

@@ -15,7 +15,8 @@ namespace Barotrauma
private readonly static HashSet<StatusEffect> ActiveLoopingSounds = new HashSet<StatusEffect>(); private readonly static HashSet<StatusEffect> ActiveLoopingSounds = new HashSet<StatusEffect>();
private static double LastMuffleCheckTime; private static double LastMuffleCheckTime;
private readonly List<RoundSound> sounds = new List<RoundSound>(); private readonly List<RoundSound> sounds = new List<RoundSound>();
public IEnumerable<RoundSound> Sounds { get { return sounds; } } public IEnumerable<RoundSound> Sounds => sounds;
private SoundSelectionMode soundSelectionMode; private SoundSelectionMode soundSelectionMode;
private SoundChannel soundChannel; private SoundChannel soundChannel;
private Entity soundEmitter; private Entity soundEmitter;
@@ -64,6 +65,16 @@ namespace Barotrauma
partial void ApplyProjSpecific(float deltaTime, Entity entity, IReadOnlyList<ISerializableEntity> targets, Hull hull, Vector2 worldPosition, bool playSound) partial void ApplyProjSpecific(float deltaTime, Entity entity, IReadOnlyList<ISerializableEntity> targets, Hull hull, Vector2 worldPosition, bool playSound)
{ {
if (steamTimeLineEventToTrigger != default)
{
SteamTimelineManager.AddTimelineEvent(
steamTimeLineEventToTrigger.title,
steamTimeLineEventToTrigger.description,
steamTimeLineEventToTrigger.icon,
priority: 1,
submarine: entity?.Submarine);
}
if (playSound) if (playSound)
{ {
PlaySound(entity, hull, worldPosition); PlaySound(entity, hull, worldPosition);
@@ -222,6 +233,7 @@ namespace Barotrauma
{ {
PlaySound(selectedSound); PlaySound(selectedSound);
} }
playSoundAfterLoadedCoroutine = null;
yield return CoroutineStatus.Success; yield return CoroutineStatus.Success;
} }

View File

@@ -0,0 +1,87 @@
#nullable enable
using System;
namespace Barotrauma
{
/// <summary>
/// Contains all available Steam timeline event icons as constants and helper methods for number icons.
/// </summary>
public static class SteamIcons
{
// Standard icons
public const string Marker = "steam_marker";
public const string Achievement = "steam_achievement";
public const string Attack = "steam_attack";
public const string Bolt = "steam_bolt";
public const string Bookmark = "steam_bookmark";
public const string Bug = "steam_bug";
public const string Cart = "steam_cart";
public const string Caution = "steam_caution";
public const string Chat = "steam_chat";
public const string Checkmark = "steam_checkmark";
public const string Chest = "steam_chest";
public const string Circle = "steam_circle";
public const string Combat = "steam_combat";
public const string Completed = "steam_completed";
public const string Crown = "steam_crown";
public const string Death = "steam_death";
public const string Defend = "steam_defend";
public const string Diamond = "steam_diamond";
public const string Edit = "steam_edit";
public const string Effect = "steam_effect";
public const string Explosion = "steam_explosion";
public const string Fix = "steam_fix";
public const string Flag = "steam_flag";
public const string Gem = "steam_gem";
public const string Group = "steam_group";
public const string Heart = "steam_heart";
public const string Info = "steam_info";
public const string Invalid = "steam_invalid";
public const string Minus = "steam_minus";
public const string Pair = "steam_pair";
public const string Person = "steam_person";
public const string Plus = "steam_plus";
public const string Purchase = "steam_purchase";
public const string Question = "steam_question";
public const string Ribbon = "steam_ribbon";
public const string Screenshot = "steam_screenshot";
public const string Scroll = "steam_scroll";
public const string Square = "steam_square";
public const string Star = "steam_star";
public const string Starburst = "steam_starburst";
public const string Timer = "steam_timer";
public const string Transfer = "steam_transfer";
public const string Triangle = "steam_triangle";
public const string Trophy = "steam_trophy";
public const string View = "steam_view";
public const string X = "steam_x";
// Common number icons
public const string Zero = "steam_0";
public const string One = "steam_1";
public const string Two = "steam_2";
public const string Three = "steam_3";
public const string Four = "steam_4";
public const string Five = "steam_5";
public const string Six = "steam_6";
public const string Seven = "steam_7";
public const string Eight = "steam_8";
public const string Nine = "steam_9";
public const string Ten = "steam_10";
/// <summary>
/// Gets the Steam icon name for a number between 0 and 99.
/// </summary>
/// <param name="number">The number to get the icon for (0-99)</param>
/// <returns>The Steam icon name in the format "steam_X"</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the number is less than 0 or greater than 99</exception>
public static string GetNumberIcon(int number)
{
if (number is < 0 or > 99)
{
throw new ArgumentOutOfRangeException(nameof(number), "Number must be between 0 and 99");
}
return $"steam_{number}";
}
}
}

View File

@@ -92,6 +92,8 @@ namespace Barotrauma.Steam
//Maybe I'm completely wrong! All I know is that we need to handle both! //Maybe I'm completely wrong! All I know is that we need to handle both!
} }
SteamTimelineManager.Initialize();
} }
public static bool NetworkingDebugLog { get; private set; } = false; public static bool NetworkingDebugLog { get; private set; } = false;

View File

@@ -0,0 +1,345 @@
#nullable enable
using Barotrauma.Steam;
using System;
using System.Collections.Generic;
using System.Linq;
using Steamworks.Data;
using Steamworks;
namespace Barotrauma
{
internal static class SteamTimelineManager
{
private static Screen? prevScreen;
private static TimelineGameMode gameMode = TimelineGameMode.LoadingScreen;
/// <summary>
/// The current submarine that the controlled character is in (and has been for at least the delay amount).
/// </summary>
private static Submarine? currentSubmarine = null;
/// <summary>
/// For tracking the instantaneous switch of submarines, to reset the delay timer
/// </summary>
private static Submarine? previousTrackedSubmarine;
private static Character? trackedCharacter = null;
/// <summary>
/// Delay in seconds before the submarine state change is considered valid, triggering events.
/// </summary>
private const float SubmarineStateChangeDelay = 2.0f;
private static float submarineStateChangeTimer = 0.0f;
public enum TimelineGameMode
{
Playing,
Staging,
Menus,
LoadingScreen
}
public static void Initialize()
{
SetTimelineGameMode(TimelineGameMode.LoadingScreen);
}
public static void Update(float deltaTime)
{
PollScreenChange();
PollCharacterChange(deltaTime);
PollSubmarineChange(deltaTime);
}
private static void PollScreenChange()
{
if (!SteamManager.IsInitialized) { return; }
if (Screen.Selected == prevScreen) { return; }
TimelineGameMode newMode = Screen.Selected switch
{
GameScreen _ => TimelineGameMode.Playing,
NetLobbyScreen _ => TimelineGameMode.Staging,
EditorScreen _ => TimelineGameMode.Playing,
MainMenuScreen _ => TimelineGameMode.Menus,
_ => TimelineGameMode.LoadingScreen // Default to Menus for other screens for now
};
if (GameMain.Instance != null && GameMain.Instance.LoadingScreenOpen)
{
newMode = TimelineGameMode.LoadingScreen;
}
if (newMode == gameMode) { return; }
SetTimelineGameMode(newMode);
gameMode = newMode;
DebugConsole.NewMessage($"Timeline game mode set to {newMode}");
prevScreen = Screen.Selected;
}
private static void PollCharacterChange(float deltaTime)
{
Character? controlledCharacter = Character.Controlled;
// reset current sub state if character changes
if (controlledCharacter != trackedCharacter)
{
InstantlySetCurrentSubmarine(controlledCharacter?.Submarine ?? null);
trackedCharacter = controlledCharacter;
}
}
private static void PollSubmarineChange(float deltaTime)
{
if (!SteamManager.IsInitialized) { return; }
if (trackedCharacter == null) { return; }
if (Screen.Selected is not GameScreen) { return; }
Submarine? trackedCharacterSubmarine = trackedCharacter.Submarine;
// timer makes sure only time-stable state changes are registered
if (submarineStateChangeTimer > 0f)
{
submarineStateChangeTimer -= deltaTime;
if (submarineStateChangeTimer <= 0f)
{
// actually register our pending state change
CharacterSubChanged(trackedCharacter, trackedCharacterSubmarine);
}
}
// detect instantaneous submarine change and start the delay timer
if (previousTrackedSubmarine != trackedCharacterSubmarine)
{
submarineStateChangeTimer = SubmarineStateChangeDelay;
}
previousTrackedSubmarine = trackedCharacterSubmarine;
}
private static void InstantlySetCurrentSubmarine(Submarine? submarine)
{
currentSubmarine = submarine;
previousTrackedSubmarine = submarine;
submarineStateChangeTimer = 0f;
}
private static void CharacterSubChanged(Character character, Submarine newSubmarine)
{
if (newSubmarine == currentSubmarine) { return; }
// currentSub to none
if (currentSubmarine != null && newSubmarine == null)
{
OnCharacterLeftSubmarine(character, currentSubmarine);
}
// currentSub to newSub
else if (currentSubmarine != null && newSubmarine != null)
{
OnCharacterMovedBetweenSubmarines(character, currentSubmarine, newSubmarine);
}
//none to newSub
else if (currentSubmarine == null && newSubmarine != null)
{
OnCharacterEnteredSubmarine(character, newSubmarine);
}
currentSubmarine = newSubmarine;
}
public static void SetTimelineGameMode(TimelineGameMode mode)
{
if (!SteamManager.IsInitialized) { return; }
Steamworks.TimelineGameMode steamMode = mode switch
{
TimelineGameMode.Playing => Steamworks.TimelineGameMode.Playing,
TimelineGameMode.Staging => Steamworks.TimelineGameMode.Staging,
TimelineGameMode.Menus => Steamworks.TimelineGameMode.Menus,
TimelineGameMode.LoadingScreen => Steamworks.TimelineGameMode.LoadingScreen,
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, message: null)
};
try
{
SteamTimeline.SetTimelineGameMode(steamMode);
}
catch (Exception e)
{
DebugConsole.ThrowError($"Failed to set timeline game mode to {mode}", e);
}
}
public static void OnPlayerDied(Character victim, CauseOfDeath causeOfDeath)
{
if (victim == null || causeOfDeath == null) { return; }
string eventTitle = $"{victim.DisplayName} died";
string causeOfDeathText = causeOfDeath.Affliction != null ?
causeOfDeath.Affliction.CauseOfDeathDescription.Value :
causeOfDeath.Type.ToString();
string eventDescription = $"{victim.DisplayName} died: {causeOfDeathText}";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Death, 1);
}
public static void OnSignificantEnemyDied(Character victim, CauseOfDeath causeOfDeath)
{
string eventTitle = $"{victim.DisplayName} has died!";
string causeOfDeathText = causeOfDeath.Affliction != null ?
causeOfDeath.Affliction.CauseOfDeathDescription.Value :
causeOfDeath.Type.ToString();
string eventDescription = $"{victim.DisplayName} died: {causeOfDeathText}";
if (causeOfDeath.Killer != null)
{
eventDescription = $"{victim.DisplayName} was killed by {causeOfDeath.Killer.DisplayName}";
}
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Attack, 2);
}
public static void OnRoundStarted()
{
string eventTitle = "Round Started";
string eventDescription = "The round has started";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Marker, 0);
}
public static void OnRoundEnded()
{
string eventTitle = "Round Ended";
string eventDescription = "The round has ended";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Completed, 0);
}
public static void OnCharacterLeftSubmarine(Character character, Submarine submarine)
{
string eventTitle = $"{character.Name} Went Diving Outside";
string eventDescription = $"{character.Name} left {submarine.Info.Name}";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Transfer, 1);
}
public static void OnCharacterMovedBetweenSubmarines(Character character, Submarine oldSubmarine, Submarine newSubmarine)
{
string eventTitle = $"{character.Name} Moved Between Locations";
string eventDescription = $"{character.Name} moved from {oldSubmarine.Info.Name} to {newSubmarine.Info.Name}";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Transfer, 1);
}
public static void OnCharacterEnteredSubmarine(Character character, Submarine submarine)
{
string eventTitle = $"{character.Name} Entered Hull";
string eventDescription = $"{character.Name} has entered {submarine.Info.Name}";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Transfer, 1);
}
public static void OnError(string errorMessage, Exception? e = null)
{
// these don't have localization support yet, use hardcoded strings
string eventTitle = "Error Occurred";
string eventDescription = $"An error was logged: {errorMessage}";
if (e != null) { eventDescription += $"\n{e.GetType().Name}"; }
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Bug, 3); // Higher priority for errors
}
public static void OnClientDisconnect(string disconnectInfo)
{
// these don't have localization support yet, use hardcoded strings
string eventTitle = $"Client Disconnected";
string eventDescription = $"{disconnectInfo}";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Bug, 2); // Maybe slightly lower priority than code errors
}
public static void OnMonsterMissionTargetsKilled(MonsterMission mission)
{
// these don't have localization support yet, use hardcoded strings
string eventTitle = $"Monsters Dispatched";
string eventDescription = $"{mission.Name}: All targets were eliminated.";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Attack, 2);
}
public static void OnScanSuccessful(ScanMission mission)
{
// these don't have localization support yet, use hardcoded strings
string eventTitle = "Scan Successful";
string eventDescription = $"{mission.Name}: A scanner has successfully scanned a target.";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Marker, 1);
}
public static void OnOutpostTargetEliminated(AbandonedOutpostMission mission)
{
// these don't have localization support yet, use hardcoded strings
string eventTitle = $"Target Character Eliminated";
string eventDescription = $"{mission.Name}: A target was eliminated.";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Attack, 2);
}
/// <summary>
/// How often can hull breach events be created? There's often multiple breaches very close to each other, not necessary to track all of them.
/// </summary>
const float HullBreachEventInterval = 10.0f;
private static double LastHullBreachTime;
public static void OnHullBreached(Structure structure)
{
if (LastHullBreachTime > Timing.TotalTime - HullBreachEventInterval) { return; }
// only trigger this event for player subs, since beacon stations can fill the requirements at level start
if (structure.Submarine?.Info is not { IsPlayer: true }) { return; }
string eventTitle = "Major Hull Breach";
string eventDescription = $"The hull of {structure.Submarine?.Info.Name ?? "Unknown Submarine"} suffered a major breach.";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Caution, 2);
LastHullBreachTime = Timing.TotalTime;
}
public static void OnMissionTargetRetrieved(Item item, Mission mission)
{
string eventTitle = $"Target Retrieved: {item.Name}";
string eventDescription = $"{mission.Name}: A target item {item.Name} was retrieved.";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Checkmark, 1);
}
public static void OnMissionTargetPickedUp(Item item, Mission mission)
{
string eventTitle = $"Target Picked Up: {item.Name}";
string eventDescription = $"{mission.Name}: A target item {item.Name} was picked up.";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Checkmark, 1);
}
public static void AddTimelineEvent(string title, string description, string icon, uint priority = 1, Submarine? submarine = null)
{
if (!SteamManager.IsInitialized) { return; }
// exit early if title, description or icon is empty
if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(description) || string.IsNullOrWhiteSpace(icon))
{
DebugConsole.ThrowError("Failed to add timeline event: title, description or icon is empty");
return;
}
if (submarine != null)
{
string submarineName = submarine.Info?.DisplayName.Value ?? "Unknown Submarine";
title = title.Replace("[sub]", submarineName);
description = description.Replace("[sub]", submarineName);
}
try
{
var eventHandle = Steamworks.SteamTimeline.AddInstantaneousTimelineEvent(
title,
description,
icon,
priority,
0.0f,
Steamworks.TimelineEventClipPriority.Standard);
}
catch (Exception e)
{
DebugConsole.ThrowError($"Failed to add timeline event", e);
}
}
}
}

View File

@@ -727,7 +727,9 @@ namespace Barotrauma.Steam
} }
if (selectedMods.All(ContentPackageManager.WorkshopPackages.Contains)) if (selectedMods.All(ContentPackageManager.WorkshopPackages.Contains))
{ {
if (parentList.AllSelected.All(c => c.GetChild<GUILayoutGroup>()?.GetAllChildren<GUIButton>().Last()?.Style?.Identifier == "WorkshopMenu.DownloadedIcon") && selectedMods.Length > 0 && SteamManager.IsInitialized) static Identifier? GetButtonIconStyle(GUIComponent c) => c.GetChild<GUILayoutGroup>()?.GetAllChildren<GUIButton>().Last()?.Style?.Identifier;
if (parentList.AllSelected.All(c => GetButtonIconStyle(c) is { } style && (style == "WorkshopMenu.DownloadedIcon" || style == "WorkshopMenu.InfoButtonUpdate")) &&
selectedMods.Length > 0 && SteamManager.IsInitialized)
{ {
contextMenuOptions.Add(new((selectedMods.Length > 1 ? "UnsubscribeFromAllSelected" : "WorkshopItemUnsubscribe").ToIdentifier(), true, () => contextMenuOptions.Add(new((selectedMods.Length > 1 ? "UnsubscribeFromAllSelected" : "WorkshopItemUnsubscribe").ToIdentifier(), true, () =>
{ {
@@ -774,8 +776,7 @@ namespace Barotrauma.Steam
return true; return true;
} }
}; };
msgBox.Buttons[0].OnClicked += msgBox.Close; msgBox.Buttons[0].OnClicked += (_, _) =>
msgBox.Buttons[1].OnClicked += (_, _) =>
{ {
if (textBox.Text == mod.Name) if (textBox.Text == mod.Name)
{ {
@@ -794,6 +795,7 @@ namespace Barotrauma.Steam
return false; return false;
} }
}; };
msgBox.Buttons[1].OnClicked += msgBox.Close;
} }
void CopyToLocal() void CopyToLocal()
{ {

View File

@@ -10,6 +10,7 @@ using Barotrauma.IO;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using Steamworks; using Steamworks;
using Steamworks.Ugc;
using Directory = Barotrauma.IO.Directory; using Directory = Barotrauma.IO.Directory;
using ItemOrPackage = Barotrauma.Either<Steamworks.Ugc.Item, Barotrauma.ContentPackage>; using ItemOrPackage = Barotrauma.Either<Steamworks.Ugc.Item, Barotrauma.ContentPackage>;
using Path = Barotrauma.IO.Path; using Path = Barotrauma.IO.Path;
@@ -335,9 +336,10 @@ namespace Barotrauma.Steam
.WithTags(tagButtons.Where(kvp => kvp.Value.Selected).Select(kvp => kvp.Key.Value)) .WithTags(tagButtons.Where(kvp => kvp.Value.Selected).Select(kvp => kvp.Key.Value))
.WithChangeLog(changeNoteTextBox.Text) .WithChangeLog(changeNoteTextBox.Text)
.WithMetaData($"gameversion={localPackage.GameVersion};modversion={versionTextBox.Text}") .WithMetaData($"gameversion={localPackage.GameVersion};modversion={versionTextBox.Text}")
.WithVisibility(visibility)
.WithPreviewFile(thumbnailPath); .WithPreviewFile(thumbnailPath);
ugcEditor.Visibility = visibility;
CoroutineManager.StartCoroutine( CoroutineManager.StartCoroutine(
MessageBoxCoroutine((currentStepText, messageBox) MessageBoxCoroutine((currentStepText, messageBox)
=> PublishItem(currentStepText, messageBox, versionTextBox.Text, ugcEditor, localPackage))); => PublishItem(currentStepText, messageBox, versionTextBox.Text, ugcEditor, localPackage)));

View File

@@ -4,6 +4,7 @@ using System.Collections.Immutable;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Xml.Linq; using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
@@ -298,7 +299,7 @@ namespace Barotrauma
case null: case null:
continue; continue;
case ItemContainer { Inventory: not null } newContainer when ic is ItemContainer { Inventory: not null } itemContainer: case ItemContainer { Inventory: not null } newContainer when ic is ItemContainer { Inventory: not null } itemContainer:
itemContainer.Inventory.GetReplacementOrThiS().ReplacedBy = newContainer.Inventory; itemContainer.Inventory.GetReplacementOrThis().ReplacedBy = newContainer.Inventory;
goto default; goto default;
default: default:
ic.GetReplacementOrThis().ReplacedBy = component; ic.GetReplacementOrThis().ReplacedBy = component;
@@ -321,7 +322,7 @@ namespace Barotrauma
{ {
if (slotRef.Item == receiver) if (slotRef.Item == receiver)
{ {
inventory.GetReplacementOrThiS().TryPutItem(it, slotRef.Slot, false, false, null, createNetworkEvent: false); inventory.GetReplacementOrThis().TryPutItem(it, slotRef.Slot, false, false, null, createNetworkEvent: false);
} }
} }
} }
@@ -416,7 +417,7 @@ namespace Barotrauma
} }
else else
{ {
Inventory.GetReplacementOrThiS().TryPutItem(item, slot, false, false, null, createNetworkEvent: false); Inventory.GetReplacementOrThis().TryPutItem(item, slot, false, false, null, createNetworkEvent: false);
} }
} }
} }
@@ -609,7 +610,7 @@ namespace Barotrauma
{ {
if (targetItem.GetReplacementOrThis() is Item item) if (targetItem.GetReplacementOrThis() is Item item)
{ {
newInventory?.GetReplacementOrThiS().TryPutItem(item, newSlot, true, false, null, createNetworkEvent: false); newInventory?.GetReplacementOrThis().TryPutItem(item, newSlot, true, false, null, createNetworkEvent: false);
} }
} }
@@ -617,7 +618,7 @@ namespace Barotrauma
{ {
if (targetItem.GetReplacementOrThis() is Item item) if (targetItem.GetReplacementOrThis() is Item item)
{ {
oldInventory?.GetReplacementOrThiS().TryPutItem(item, oldSlot, true, false, null, createNetworkEvent: false); oldInventory?.GetReplacementOrThis().TryPutItem(item, oldSlot, true, false, null, createNetworkEvent: false);
} }
} }
@@ -628,4 +629,86 @@ namespace Barotrauma
return TextManager.GetWithVariable("Undo.MovedItem", "[item]", targetItem.Name); return TextManager.GetWithVariable("Undo.MovedItem", "[item]", targetItem.Name);
} }
} }
/// <summary>
/// A command for applying changes from the <see cref="SubEditorScreen.TransformWidget"/>.
/// </summary>
internal class TransformToolCommand : Command
{
private readonly Dictionary<MapEntity, SubEditorScreen.TransformData> originalData;
public float? ScaleMult, RotationRad;
public readonly Vector2 Pivot;
private readonly Vector2 wirePivot;
public float MinScale, MaxScale;
public TransformToolCommand(Dictionary<MapEntity, SubEditorScreen.TransformData> data, Vector2 pivot)
{
originalData = data;
Pivot = pivot;
wirePivot = Pivot - Submarine.MainSub.HiddenSubPosition;
MinScale = 0.01f / Math.Max(data.Values.Min(data => data.Scale), 0.01f);
MaxScale = 10f / Math.Min(data.Values.Max(data => data.Scale), 10f);
}
public override void Execute() => UpdateTransforms(RotationRad ?? 0f, ScaleMult ?? 1f);
public override void UnExecute() => UpdateTransforms(0f, 1f);
public override void Cleanup() => originalData.Clear();
private void UpdateTransforms(float rotationRad, float scaleMult)
{
foreach ((MapEntity receiver, SubEditorScreen.TransformData data) in originalData)
{
if (RotationRad.HasValue && receiver is Item { Prefab.AllowRotatingInEditor: true } or Structure { Prefab.AllowRotatingInEditor: true })
{
int rotationDir = receiver is Structure && receiver.FlippedX ^ receiver.FlippedY ? -1 : 1;
float newRotation = MathHelper.ToDegrees(data.RotationRad + rotationRad * rotationDir);
switch (receiver)
{
case Item item:
item.Rotation = newRotation;
break;
case Structure structure:
structure.Rotation = newRotation;
break;
}
data.TurretLimits?.ForEach(pair => pair.Key.RotationLimits = pair.Value + new Vector2(MathHelper.ToDegrees(rotationRad)));
}
if (ScaleMult.HasValue)
{
receiver.Scale = data.Scale * scaleMult;
if (receiver.ResizeVertical || receiver.ResizeHorizontal)
{
if (receiver.ResizeVertical)
{
receiver.RectHeight = (int)(data.Rect.Height * scaleMult);
}
if (receiver.ResizeHorizontal)
{
receiver.RectWidth = (int)(data.Rect.Width * scaleMult);
}
if (receiver is Structure structure && data.TexOffset.HasValue)
{
structure.TextureOffset = data.TexOffset.Value * scaleMult;
}
}
data.TextScales?.ForEach(pair => pair.Key.TextScale = pair.Value * scaleMult);
data.LightRanges?.ForEach(pair => pair.Key.Range = pair.Value * scaleMult);
data.Wires?.ForEach(pair => pair.Key.Width = pair.Value.Width * scaleMult);
}
Vector2 newEntityPos = MathUtils.RotatePoint((data.Pos - Pivot) * scaleMult, -rotationRad) + Pivot;
receiver.Move(newEntityPos - receiver.DrawPosition);
data.Wires?.ForEach(pair => pair.Key.SetNodes(pair.Value.Nodes.Select(TransformWireNode)));
Vector2 TransformWireNode(Vector2 node) => MathUtils.RotatePoint((node - wirePivot) * scaleMult, -rotationRad) + wirePivot;
}
}
public override LocalizedString GetDescription() => originalData.Count > 1
? TextManager.GetWithVariable("Undo.ChangedTransformMultiple", "[amount]", originalData.Count.ToString())
: TextManager.GetWithVariable("Undo.ChangedTransform", "[item]", originalData.First().Key.Name);
}
} }

View File

@@ -2,11 +2,11 @@
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>1.8.8.1</Version> <Version>1.9.7.0</Version>
<Copyright>Copyright © FakeFish 2018-2024</Copyright> <Copyright>Copyright © FakeFish 2018-2024</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>

View File

@@ -2,11 +2,11 @@
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>1.8.8.1</Version> <Version>1.9.7.0</Version>
<Copyright>Copyright © FakeFish 2018-2024</Copyright> <Copyright>Copyright © FakeFish 2018-2024</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>

View File

@@ -2,11 +2,11 @@
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>1.8.8.1</Version> <Version>1.9.7.0</Version>
<Copyright>Copyright © FakeFish 2018-2024</Copyright> <Copyright>Copyright © FakeFish 2018-2024</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>

View File

@@ -2,11 +2,11 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<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>1.8.8.1</Version> <Version>1.9.7.0</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>

View File

@@ -2,11 +2,11 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<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>1.8.8.1</Version> <Version>1.9.7.0</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>

View File

@@ -677,6 +677,7 @@ namespace Barotrauma
{ {
msg.WriteUInt32(CauseOfDeath.Affliction.UintIdentifier); msg.WriteUInt32(CauseOfDeath.Affliction.UintIdentifier);
} }
msg.WriteUInt16(CauseOfDeath.Killer?.ID ?? NullEntityID);
msg.WriteBoolean(forceAfflictionData); msg.WriteBoolean(forceAfflictionData);
if (forceAfflictionData) if (forceAfflictionData)
{ {

View File

@@ -2143,9 +2143,33 @@ namespace Barotrauma
"freecam", "freecam",
(Client client, Vector2 cursorWorldPos, string[] args) => (Client client, Vector2 cursorWorldPos, string[] args) =>
{ {
client.UsingFreeCam = true; if (client.UsingFreeCam)
GameMain.Server.SetClientCharacter(client, null); {
client.SpectateOnly = true; // Exiting freecam - try to return to previous character
Character prevCharacter = null;
if (client.PreviousCharacter != null && client.PreviousCharacter.TryGetTarget(out prevCharacter) &&
prevCharacter != null && !prevCharacter.IsDead && !prevCharacter.Removed)
{
GameMain.Server.SendConsoleMessage($"{client.Name}: Exiting freecam mode", client, Color.Yellow);
client.UsingFreeCam = false;
GameMain.Server.SetClientCharacter(client, prevCharacter);
client.SpectateOnly = false;
}
else
{
GameMain.Server.SendConsoleMessage($"{client.Name}: Could not regain control of the previous character (dead or removed).", client, Color.Red);
}
}
else
{
// Entering freecam - store current character ID
GameMain.Server.SendConsoleMessage($"{client.Name}: Entering freecam mode", client, Color.Yellow);
Character currentCharacter = client.Character;
client.PreviousCharacter = new WeakReference<Character>(currentCharacter);
client.UsingFreeCam = true;
client.SpectateOnly = true;
GameMain.Server.SetClientCharacter(client, null);
}
} }
); );

View File

@@ -92,6 +92,7 @@ namespace Barotrauma
purchasedHullRepairs = value; purchasedHullRepairs = value;
PurchasedHullRepairsInLatestSave |= value; PurchasedHullRepairsInLatestSave |= value;
IncrementLastUpdateIdForFlag(NetFlags.Misc); IncrementLastUpdateIdForFlag(NetFlags.Misc);
DebugConsole.NewMessage("Set PurchasedHullRepairs to " + PurchasedHullRepairs, Color.Cyan);
} }
} }
public override bool PurchasedLostShuttles public override bool PurchasedLostShuttles
@@ -861,6 +862,13 @@ namespace Barotrauma
purchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall)); purchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall));
} }
if (purchasedUpgradeCount > 0 || purchasedItemSwapCount > 0)
{
//if the client attempted to purchase something, increment flag regardless of whether the upgrades were actually purchased or not
//so we can sync the correct state in case the client incorrectly assumed they can buy something (e.g. lost permissions just as they were purchasing)
IncrementLastUpdateIdForFlag(NetFlags.UpgradeManager);
}
int hullRepairCost = GetHullRepairCost(); int hullRepairCost = GetHullRepairCost();
int itemRepairCost = GetItemRepairCost(); int itemRepairCost = GetItemRepairCost();
int shuttleRetrieveCost = CampaignMode.ShuttleReplaceCost; int shuttleRetrieveCost = CampaignMode.ShuttleReplaceCost;
@@ -1100,7 +1108,7 @@ namespace Barotrauma
var characterList = GameSession.GetSessionCrewCharacters(CharacterType.Both); var characterList = GameSession.GetSessionCrewCharacters(CharacterType.Both);
foreach (var (prefab, category, _) in purchasedUpgrades) foreach (var (prefab, category, _) in purchasedUpgrades)
{ {
UpgradeManager.PurchaseUpgrade(prefab, category, client: sender); UpgradeManager.TryPurchaseUpgrade(prefab, category, client: sender);
// unstable logging // unstable logging
int price = prefab.Price.GetBuyPrice(prefab, UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation, characterList); int price = prefab.Price.GetBuyPrice(prefab, UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation, characterList);
@@ -1111,7 +1119,7 @@ namespace Barotrauma
{ {
if (purchasedItemSwap.ItemToInstall == null) if (purchasedItemSwap.ItemToInstall == null)
{ {
UpgradeManager.CancelItemSwap(purchasedItemSwap.ItemToRemove); UpgradeManager.CancelItemSwap(purchasedItemSwap.ItemToRemove, client: sender);
} }
else else
{ {
@@ -1502,11 +1510,13 @@ namespace Barotrauma
{ {
element.Add(new XAttribute("campaignid", CampaignID)); element.Add(new XAttribute("campaignid", CampaignID));
XElement modeElement = new XElement("MultiPlayerCampaign", XElement modeElement = new XElement("MultiPlayerCampaign",
new XAttribute("purchasedlostshuttles", PurchasedLostShuttles), new XAttribute("purchasedlostshuttles", PurchasedLostShuttlesInLatestSave),
new XAttribute("purchasedhullrepairs", PurchasedHullRepairs), new XAttribute("purchasedhullrepairs", PurchasedHullRepairsInLatestSave),
new XAttribute("purchaseditemrepairs", PurchasedItemRepairs), new XAttribute("purchaseditemrepairs", PurchasedItemRepairsInLatestSave),
new XAttribute("cheatsenabled", CheatsEnabled)); new XAttribute("cheatsenabled", CheatsEnabled));
DebugConsole.NewMessage("Saved PurchasedHullRepairs: "+ PurchasedHullRepairs+" (in last save "+PurchasedHullRepairsInLatestSave+")", Color.Magenta);
modeElement.Add(Settings.Save()); modeElement.Add(Settings.Save());
modeElement.Add(SaveStats()); modeElement.Add(SaveStats());
if (GameMain.Server?.TraitorManager is TraitorManager traitorManager) if (GameMain.Server?.TraitorManager is TraitorManager traitorManager)
@@ -1520,6 +1530,11 @@ namespace Barotrauma
modeElement.Add(GameMain.GameSession?.EventManager.Save()); modeElement.Add(GameMain.GameSession?.EventManager.Save());
} }
foreach (Identifier unlockedRecipe in GameMain.GameSession.UnlockedRecipes)
{
modeElement.Add(new XElement("unlockedrecipe", new XAttribute("identifier", unlockedRecipe)));
}
CampaignMetadata?.Save(modeElement); CampaignMetadata?.Save(modeElement);
Map.Save(modeElement); Map.Save(modeElement);
CargoManager?.SavePurchasedItems(modeElement); CargoManager?.SavePurchasedItems(modeElement);

View File

@@ -37,7 +37,7 @@ namespace Barotrauma.Items.Components
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
{ {
msg.WriteRangedSingle(Health, 0f, (float)MaxHealth, 8); msg.WriteRangedSingle(Health, 0f, (float)MaxWater, 8);
if (TryExtractEventData(extraData, out EventData eventData)) if (TryExtractEventData(extraData, out EventData eventData))
{ {
int offset = eventData.Offset; int offset = eventData.Offset;

View File

@@ -10,7 +10,7 @@ namespace Barotrauma.Items.Components
{ {
base.ServerEventWrite(msg, c, extraData); base.ServerEventWrite(msg, c, extraData);
bool writeAttachData = attachable && body != null; bool writeAttachData = attachable && originalBody != null;
msg.WriteBoolean(writeAttachData); msg.WriteBoolean(writeAttachData);
if (!writeAttachData) { return; } if (!writeAttachData) { return; }
@@ -22,8 +22,8 @@ namespace Barotrauma.Items.Components
} }
msg.WriteBoolean(Attached); msg.WriteBoolean(Attached);
msg.WriteSingle(body.SimPosition.X); msg.WriteSingle(originalBody.SimPosition.X);
msg.WriteSingle(body.SimPosition.Y); msg.WriteSingle(originalBody.SimPosition.Y);
msg.WriteUInt16(item.Submarine?.ID ?? Entity.NullEntityID); msg.WriteUInt16(item.Submarine?.ID ?? Entity.NullEntityID);
msg.WriteUInt16(attacherId); msg.WriteUInt16(attacherId);
} }
@@ -40,6 +40,9 @@ namespace Barotrauma.Items.Components
Drop(false, null); Drop(false, null);
item.SetTransform(simPosition, 0.0f, findNewHull: false); item.SetTransform(simPosition, 0.0f, findNewHull: false);
//don't find the new hull in SetTransform, because that'd also potentially change the submarine (teleport the item outside if it's attached outside)
//instead just find the hull, so the item is considered to be in the right hull
item.CurrentHull = Hull.FindHull(item.WorldPosition, item.CurrentHull);
AttachToWall(); AttachToWall();
OnUsed.Invoke(new ItemUseInfo(item, c.Character)); OnUsed.Invoke(new ItemUseInfo(item, c.Character));

View File

@@ -0,0 +1,33 @@
#nullable enable
using Barotrauma.Networking;
namespace Barotrauma.Items.Components
{
internal partial class PowerDistributor : PowerTransfer, IServerSerializable, IClientSerializable
{
#region Networking
public void ServerEventRead(IReadMessage msg, Client c)
{
SharedEventRead(msg, out EventType eventType, out PowerGroup powerGroup, out string newName, out float newRatio);
if (item.CanClientAccess(c))
{
switch (eventType)
{
case EventType.NameChange:
powerGroup.Name = newName;
break;
case EventType.RatioChange:
powerGroup.SupplyRatio = newRatio;
GameServer.Log($"{GameServer.CharacterLogName(c.Character)} changed supply ratio of power group \"{powerGroup.Name}\" to \"{powerGroup.SupplyRatio}\"", ServerLog.MessageType.ItemInteraction);
break;
}
}
item.CreateServerEvent(this, new EventData(powerGroup, eventType));
}
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData? extraData = null) => SharedEventWrite(msg, extraData);
#endregion
}
}

View File

@@ -274,6 +274,7 @@ namespace Barotrauma
msg.WriteByte(originalSlotIndex < 0 ? (byte)255 : (byte)originalSlotIndex); msg.WriteByte(originalSlotIndex < 0 ? (byte)255 : (byte)originalSlotIndex);
} }
msg.WriteBoolean(OnInsertedEffectsAppliedOnPreviousRound);
msg.WriteByte(body == null ? (byte)0 : (byte)body.BodyType); msg.WriteByte(body == null ? (byte)0 : (byte)body.BodyType);
msg.WriteBoolean(SpawnedInCurrentOutpost); msg.WriteBoolean(SpawnedInCurrentOutpost);
msg.WriteBoolean(AllowStealing); msg.WriteBoolean(AllowStealing);

View File

@@ -30,8 +30,8 @@ namespace Barotrauma
//don't create updates if all clients are very far from the hull //don't create updates if all clients are very far from the hull
float hullUpdateDistanceSqr = NetConfig.HullUpdateDistance * NetConfig.HullUpdateDistance; float hullUpdateDistanceSqr = NetConfig.HullUpdateDistance * NetConfig.HullUpdateDistance;
if (!GameMain.Server.ConnectedClients.Any(c => if (!GameMain.Server.ConnectedClients.Any(c =>
c.Character != null && (c.Character != null && Vector2.DistanceSquared(c.Character.WorldPosition, WorldPosition) < hullUpdateDistanceSqr) ||
Vector2.DistanceSquared(c.Character.WorldPosition, WorldPosition) < hullUpdateDistanceSqr)) (c.SpectatePos != null && Vector2.DistanceSquared(c.SpectatePos.Value, WorldPosition) < hullUpdateDistanceSqr)) )
{ {
return; return;
} }
@@ -76,6 +76,11 @@ namespace Barotrauma
} }
} }
public void ForceStatusUpdate()
{
statusUpdateTimer = NetConfig.SparseHullUpdateInterval;
}
public void CreateStatusEvent() public void CreateStatusEvent()
{ {
@@ -137,6 +142,12 @@ namespace Barotrauma
WaterVolume = newWaterVolume; WaterVolume = newWaterVolume;
if (newFireSources.Length != FireSources.Count)
{
//number of fire sources has changed, force a network update
ForceStatusUpdate();
}
for (int i = 0; i < newFireSources.Length; i++) for (int i = 0; i < newFireSources.Length; i++)
{ {
Vector2 pos = newFireSources[i].Position; Vector2 pos = newFireSources[i].Position;

View File

@@ -176,9 +176,7 @@ namespace Barotrauma.Networking
} }
else else
{ {
ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked").Value, ChatMessageType.Server, null); BlockBySpamFilter();
c.ChatSpamTimer = 10.0f;
GameMain.Server.SendDirectChatMessage(denyMsg, c);
} }
flaggedAsSpam = true; flaggedAsSpam = true;
return; return;
@@ -188,14 +186,20 @@ namespace Barotrauma.Networking
if (c.ChatSpamTimer > 0.0f && !isSpamExempt) if (c.ChatSpamTimer > 0.0f && !isSpamExempt)
{ {
ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked").Value, ChatMessageType.Server, null); BlockBySpamFilter();
c.ChatSpamTimer = 10.0f;
GameMain.Server.SendDirectChatMessage(denyMsg, c);
flaggedAsSpam = true; flaggedAsSpam = true;
return; return;
} }
flaggedAsSpam = false; flaggedAsSpam = false;
void BlockBySpamFilter()
{
ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked").Value, ChatMessageType.BlockedBySpamFilter, null);
c.ChatSpamTimer = BlockedBySpamFilterTime;
GameMain.Server.SendDirectChatMessage(denyMsg, c);
GameServer.Log(c.Name + " blocked by spam filter", ServerLog.MessageType.ServerMessage);
}
} }
public int EstimateLengthBytesServer(Client c) public int EstimateLengthBytesServer(Client c)

View File

@@ -138,6 +138,8 @@ namespace Barotrauma.Networking
get { return kickVoters.Count; } get { return kickVoters.Count; }
} }
public WeakReference<Character> PreviousCharacter;
partial void InitProjSpecific() partial void InitProjSpecific()
{ {
kickVoters = new List<Client>(); kickVoters = new List<Client>();

View File

@@ -4279,6 +4279,9 @@ namespace Barotrauma.Networking
public void GiveAchievement(Client client, Identifier achievementIdentifier) public void GiveAchievement(Client client, Identifier achievementIdentifier)
{ {
if (client.GivenAchievements.Contains(achievementIdentifier)) { return; } if (client.GivenAchievements.Contains(achievementIdentifier)) { return; }
DebugConsole.NewMessage($"Attempting to give the achievement {achievementIdentifier} to {client.Name}...");
client.GivenAchievements.Add(achievementIdentifier); client.GivenAchievements.Add(achievementIdentifier);
IWriteMessage msg = new WriteOnlyMessage(); IWriteMessage msg = new WriteOnlyMessage();
@@ -4288,6 +4291,17 @@ namespace Barotrauma.Networking
serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable);
} }
public void UnlockRecipe(Identifier identifier)
{
foreach (var client in connectedClients)
{
IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ServerPacketHeader.UNLOCKRECIPE);
msg.WriteIdentifier(identifier);
serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable);
}
}
public void IncrementStat(Client client, AchievementStat stat, int amount) public void IncrementStat(Client client, AchievementStat stat, int amount)
{ {
IWriteMessage msg = new WriteOnlyMessage(); IWriteMessage msg = new WriteOnlyMessage();

View File

@@ -2,11 +2,11 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<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>1.8.8.1</Version> <Version>1.9.7.0</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<contentpackage name="[DebugOnlyTest]PowerTestSub" modversion="1.0.9" corepackage="False" gameversion="1.8.0.0">
<Submarine file="%ModDir%/PowerTestSub.sub" />
</contentpackage>

View File

@@ -16,7 +16,7 @@ namespace Barotrauma
static class AchievementManager static class AchievementManager
{ {
private static readonly ImmutableHashSet<Identifier> SupportedAchievements = ImmutableHashSet.Create( public static readonly ImmutableHashSet<Identifier> SupportedAchievements = ImmutableHashSet.Create(
"killmoloch".ToIdentifier(), "killmoloch".ToIdentifier(),
"killhammerhead".ToIdentifier(), "killhammerhead".ToIdentifier(),
"killendworm".ToIdentifier(), "killendworm".ToIdentifier(),
@@ -146,6 +146,9 @@ namespace Barotrauma
public static void OnStartRound(Biome biome = null) public static void OnStartRound(Biome biome = null)
{ {
#if CLIENT
SteamTimelineManager.OnRoundStarted();
#endif
roundData = new RoundData(); roundData = new RoundData();
foreach (Item item in Item.ItemList) foreach (Item item in Item.ItemList)
{ {
@@ -161,6 +164,7 @@ namespace Barotrauma
if (GameMain.Client != null) { return; } if (GameMain.Client != null) { return; }
#endif #endif
if (biome != null && GameMain.GameSession?.GameMode is CampaignMode) if (biome != null && GameMain.GameSession?.GameMode is CampaignMode)
{ {
string shortBiomeIdentifier = biome.Identifier.Value.Replace(" ", ""); string shortBiomeIdentifier = biome.Identifier.Value.Replace(" ", "");
@@ -233,8 +237,8 @@ namespace Barotrauma
//convert submarine velocity to km/h //convert submarine velocity to km/h
Vector2 submarineVel = Physics.DisplayToRealWorldRatio * ConvertUnits.ToDisplayUnits(sub.Velocity) * 3.6f; Vector2 submarineVel = Physics.DisplayToRealWorldRatio * ConvertUnits.ToDisplayUnits(sub.Velocity) * 3.6f;
//achievement for going > 100 km/h //achievement for going > 50 km/h
if (Math.Abs(submarineVel.X) > 100.0f) if (Math.Abs(submarineVel.X) > 50.0f)
{ {
//all conscious characters inside the sub get an achievement //all conscious characters inside the sub get an achievement
UnlockAchievement("subhighvelocity".ToIdentifier(), true, c => c != null && c.Submarine == sub && !c.IsDead && !c.IsUnconscious); UnlockAchievement("subhighvelocity".ToIdentifier(), true, c => c != null && c.Submarine == sub && !c.IsDead && !c.IsUnconscious);
@@ -279,7 +283,7 @@ namespace Barotrauma
if (c == null || c.Removed) { return; } if (c == null || c.Removed) { return; }
if (c.HasEquippedItem("clownmask".ToIdentifier()) && if (c.HasEquippedItem("clownmask".ToIdentifier()) &&
c.HasEquippedItem("clowncostume".ToIdentifier())) c.HasEquippedItem("clowngear".ToIdentifier()))
{ {
UnlockAchievement(c, "clowncostume".ToIdentifier()); UnlockAchievement(c, "clowncostume".ToIdentifier());
} }
@@ -407,9 +411,33 @@ namespace Barotrauma
UnlockAchievement(reviver, "healcrit".ToIdentifier()); UnlockAchievement(reviver, "healcrit".ToIdentifier());
} }
#if CLIENT
private static void CheckSteamTimelineEvents(Character killedCharacter, CauseOfDeath causeOfDeath)
{
if (killedCharacter == Character.Controlled)
{
SteamTimelineManager.OnPlayerDied(killedCharacter, causeOfDeath);
return;
}
bool pvpkill = killedCharacter.IsHuman && GameMain.GameSession?.GameMode is PvPMode;
float combatStrength = killedCharacter.Params.AI?.CombatStrength ?? 0;
bool significantCombatStrength = combatStrength >= 300;
if (pvpkill || significantCombatStrength)
{
// note: sometimes the causeOfDeath.Killer is null in multiplayer
SteamTimelineManager.OnSignificantEnemyDied(killedCharacter, causeOfDeath);
}
}
#endif
public static void OnCharacterKilled(Character character, CauseOfDeath causeOfDeath) public static void OnCharacterKilled(Character character, CauseOfDeath causeOfDeath)
{ {
#if CLIENT #if CLIENT
CheckSteamTimelineEvents(character, causeOfDeath);
// If this is a multiplayer game, the client should let the server handle achievements // If this is a multiplayer game, the client should let the server handle achievements
if (GameMain.Client != null || GameMain.GameSession == null) { return; } if (GameMain.Client != null || GameMain.GameSession == null) { return; }
@@ -417,40 +445,41 @@ namespace Barotrauma
causeOfDeath.Killer != null && causeOfDeath.Killer != null &&
causeOfDeath.Killer == Character.Controlled) causeOfDeath.Killer == Character.Controlled)
{ {
IncrementStat(causeOfDeath.Killer, character.IsHuman ? AchievementStat.HumansKilled : AchievementStat.MonstersKilled , 1); IncrementStat(causeOfDeath.Killer, character.IsHuman ? AchievementStat.HumansKilled : AchievementStat.MonstersKilled, 1);
} }
#elif SERVER #elif SERVER
if (character != causeOfDeath.Killer && causeOfDeath.Killer != null) if (character != causeOfDeath.Killer && causeOfDeath.Killer != null)
{ {
IncrementStat(causeOfDeath.Killer, character.IsHuman ? AchievementStat.HumansKilled : AchievementStat.MonstersKilled , 1); IncrementStat(causeOfDeath.Killer, character.IsHuman ? AchievementStat.HumansKilled : AchievementStat.MonstersKilled, 1);
} }
#endif #endif
UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName}".ToIdentifier()); UnlockKillAchievement(causeOfDeath.Killer, character, $"kill{character.SpeciesName}".ToIdentifier());
if (character.CurrentHull != null) if (character.CurrentHull != null)
{ {
UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName}indoors".ToIdentifier()); UnlockKillAchievement(causeOfDeath.Killer, character, $"kill{character.SpeciesName}indoors".ToIdentifier());
} }
if (character.SpeciesName.EndsWith("boss")) if (character.SpeciesName.EndsWith("boss"))
{ {
UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("boss", "")}".ToIdentifier()); UnlockKillAchievement(causeOfDeath.Killer, character, $"kill{character.SpeciesName.Replace("boss", "")}".ToIdentifier());
if (character.CurrentHull != null) if (character.CurrentHull != null)
{ {
UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("boss", "")}indoors".ToIdentifier()); UnlockKillAchievement(causeOfDeath.Killer, character, $"kill{character.SpeciesName.Replace("boss", "")}indoors".ToIdentifier());
} }
} }
if (character.SpeciesName.EndsWith("_m")) if (character.SpeciesName.EndsWith("_m"))
{ {
UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("_m", "")}".ToIdentifier()); UnlockKillAchievement(causeOfDeath.Killer, character, $"kill{character.SpeciesName.Replace("_m", "")}".ToIdentifier());
if (character.CurrentHull != null) if (character.CurrentHull != null)
{ {
UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("_m", "")}indoors".ToIdentifier()); UnlockKillAchievement(causeOfDeath.Killer, character, $"kill{character.SpeciesName.Replace("_m", "")}indoors".ToIdentifier());
} }
} }
#if SERVER #if SERVER
if (character.SpeciesName == "Jove" && if (character.SpeciesName == "Jove" &&
GameMain.GameSession.Campaign is MultiPlayerCampaign && GameMain.GameSession.Campaign is MultiPlayerCampaign &&
GameMain.Server?.ServerSettings is { IronmanModeActive: true }) (GameMain.Server?.ServerSettings is { IronmanModeActive: true } or { RespawnMode: RespawnMode.Permadeath }))
{ {
UnlockAchievement( UnlockAchievement(
identifier: "europasfinest".ToIdentifier(), identifier: "europasfinest".ToIdentifier(),
@@ -464,8 +493,12 @@ namespace Barotrauma
causeOfDeath.Killer != character) causeOfDeath.Killer != character)
{ {
UnlockAchievement(causeOfDeath.Killer, "killclown".ToIdentifier()); UnlockAchievement(causeOfDeath.Killer, "killclown".ToIdentifier());
if (character.CharacterHealth?.GetAffliction("psychosis") != null)
{
UnlockAchievement(causeOfDeath.Killer, "whatsmirksbelow".ToIdentifier());
}
} }
if (character.CharacterHealth?.GetAffliction("psychoclown") != null && if (character.CharacterHealth?.GetAffliction("psychoclown") != null &&
character.CurrentHull?.Submarine.Info is { Type: SubmarineType.BeaconStation }) character.CurrentHull?.Submarine.Info is { Type: SubmarineType.BeaconStation })
{ {
@@ -516,6 +549,20 @@ namespace Barotrauma
#endif #endif
} }
private static void UnlockKillAchievement(Character killer, Character target, Identifier identifier)
{
if (killer != null &&
target.Params.UnlockKillAchievementForWholeCrew &&
GameSession.GetSessionCrewCharacters(CharacterType.Player).Contains(killer))
{
UnlockAchievement(identifier, unlockClients: true, characterConditions: c => c != null);
}
else
{
UnlockAchievement(killer, identifier);
}
}
public static void OnTraitorWin(Character character) public static void OnTraitorWin(Character character)
{ {
#if CLIENT #if CLIENT
@@ -525,9 +572,15 @@ namespace Barotrauma
UnlockAchievement(character, "traitorwin".ToIdentifier()); UnlockAchievement(character, "traitorwin".ToIdentifier());
} }
public static void OnRoundEnded(GameSession gameSession) public static void OnRoundEnded(GameSession gameSession, bool roundInterrupted = false)
{ {
#if CLIENT
SteamTimelineManager.OnRoundEnded();
#endif
if (CheatsEnabled) { return; } if (CheatsEnabled) { return; }
// no processing for achievements if player quit to menu or such.
if (roundInterrupted) { return; }
//made it to the destination //made it to the destination
if (gameSession?.Submarine != null && Level.Loaded != null && gameSession.Submarine.AtEndExit) if (gameSession?.Submarine != null && Level.Loaded != null && gameSession.Submarine.AtEndExit)
@@ -713,7 +766,9 @@ namespace Barotrauma
private static void UnlockAchievementsOnPlatforms(Identifier identifier) private static void UnlockAchievementsOnPlatforms(Identifier identifier)
{ {
if (unlockedAchievements.Contains(identifier)) { return; } if (unlockedAchievements.Contains(identifier)) { return; }
DebugConsole.NewMessage($"Attempting to unlock achievement {identifier}...");
if (SteamManager.IsInitialized) if (SteamManager.IsInitialized)
{ {
if (SteamManager.UnlockAchievement(identifier)) if (SteamManager.UnlockAchievement(identifier))

View File

@@ -4396,11 +4396,11 @@ namespace Barotrauma
else if (canAttackDoors && HasValidPath()) else if (canAttackDoors && HasValidPath())
{ {
var door = PathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? PathSteering.CurrentPath.NextNode?.ConnectedDoor; var door = PathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? PathSteering.CurrentPath.NextNode?.ConnectedDoor;
if (door is { CanBeTraversed: false } && !door.HasAccess(Character)) if (door is { CanBeTraversed: false } && !door.HasAccess(Character) && door.Item.AiTarget is { } doorAiTarget)
{ {
if (SelectedAiTarget != door.Item.AiTarget || State != AIState.Attack) if (SelectedAiTarget != doorAiTarget || State != AIState.Attack)
{ {
SelectTarget(door.Item.AiTarget, CurrentTargetMemory.Priority); SelectTarget(doorAiTarget, CurrentTargetMemory.Priority);
State = AIState.Attack; State = AIState.Attack;
return false; return false;
} }

View File

@@ -787,6 +787,13 @@ namespace Barotrauma
foreach (Item item in Character.HeldItems) foreach (Item item in Character.HeldItems)
{ {
if (item == null || !item.IsInteractable(Character)) { continue; } if (item == null || !item.IsInteractable(Character)) { continue; }
if (!item.UnequipAutomatically) { continue; }
//NPC set to operate the item they're holding, don't put it away
if (ObjectiveManager.CurrentObjective is AIObjectiveOperateItem operateItem &&
(operateItem.OperateTarget == item || operateItem.Component?.Item == item))
{
continue;
}
if (Character.TryPutItemInAnySlot(item)) { continue; } if (Character.TryPutItemInAnySlot(item)) { continue; }
if (Character.TryPutItemInBag(item)) { continue; } if (Character.TryPutItemInBag(item)) { continue; }
if (item.HasTag(Tags.Weapon)) if (item.HasTag(Tags.Weapon))

View File

@@ -69,6 +69,7 @@ namespace Barotrauma
} }
var getItemObjective = new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true) var getItemObjective = new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true)
{ {
IsFindDivingGearSubObjective = true,
AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _), AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _),
AllowToFindDivingGear = false, AllowToFindDivingGear = false,
AllowDangerousPressure = true, AllowDangerousPressure = true,

View File

@@ -49,6 +49,13 @@ namespace Barotrauma
public const float DefaultReach = 100; public const float DefaultReach = 100;
public const float MaxReach = 150; public const float MaxReach = 150;
/// <summary>
/// Is the goal of this objective to get diving gear (i.e. has it been created by <see cref="AIObjectiveFindDivingGear"/>)?
/// If so, the objective won't attempt to create another objective if the path requires diving gear
/// (wouldn't make sense to start looking for diving gear so the bot can get to a room they're trying to get diving gear from!)
/// </summary>
public bool IsFindDivingGearSubObjective;
public bool AllowToFindDivingGear { get; set; } = true; public bool AllowToFindDivingGear { get; set; } = true;
public bool MustBeSpecificItem { get; set; } public bool MustBeSpecificItem { get; set; }
@@ -378,6 +385,7 @@ namespace Barotrauma
{ {
return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear, closeEnough: DefaultReach) return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear, closeEnough: DefaultReach)
{ {
IsFindDivingGearSubObjective = IsFindDivingGearSubObjective,
// If the root container changes, the item is no longer where it was (taken by someone -> need to find another item) // If the root container changes, the item is no longer where it was (taken by someone -> need to find another item)
AbortCondition = obj => targetItem == null || (targetItem.GetRootInventoryOwner() is Entity owner && owner != moveToTarget && owner != character), AbortCondition = obj => targetItem == null || (targetItem.GetRootInventoryOwner() is Entity owner && owner != moveToTarget && owner != character),
SpeakIfFails = false, SpeakIfFails = false,

View File

@@ -14,6 +14,13 @@ namespace Barotrauma
public override bool KeepDivingGearOn => GetTargetHull() == null; public override bool KeepDivingGearOn => GetTargetHull() == null;
/// <summary>
/// Is the goal of this objective to get diving gear (i.e. has it been created by <see cref="AIObjectiveFindDivingGear"/>)?
/// If so, the objective won't attempt to create another objective if the path requires diving gear
/// (wouldn't make sense to start looking for diving gear so the bot can get to a room they're trying to get diving gear from!)
/// </summary>
public bool IsFindDivingGearSubObjective;
private AIObjectiveFindDivingGear findDivingGear; private AIObjectiveFindDivingGear findDivingGear;
private readonly bool repeat; private readonly bool repeat;
//how long until the path to the target is declared unreachable //how long until the path to the target is declared unreachable
@@ -379,85 +386,89 @@ namespace Barotrauma
} }
} }
if (Abandon) { return; } if (Abandon) { return; }
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure;
bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit); if (!IsFindDivingGearSubObjective)
bool tryToGetDivingSuit = needsDivingSuit;
Character followTarget = Target as Character;
if (Mimic && !character.IsImmuneToPressure)
{ {
if (HumanAIController.HasDivingSuit(followTarget)) bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure;
bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
bool tryToGetDivingSuit = needsDivingSuit;
Character followTarget = Target as Character;
if (Mimic && !character.IsImmuneToPressure)
{ {
tryToGetDivingGear = true; if (HumanAIController.HasDivingSuit(followTarget))
tryToGetDivingSuit = true; {
tryToGetDivingGear = true;
tryToGetDivingSuit = true;
}
else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1)
{
tryToGetDivingGear = true;
}
} }
else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1) bool needsEquipment = false;
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
if (tryToGetDivingSuit)
{ {
tryToGetDivingGear = true; needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen, requireSuitablePressureProtection: !objectiveManager.FailedToFindDivingGearForDepth);
} }
} else if (tryToGetDivingGear)
bool needsEquipment = false;
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
if (tryToGetDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen, requireSuitablePressureProtection: !objectiveManager.FailedToFindDivingGearForDepth);
}
else if (tryToGetDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
}
if (!getDivingGearIfNeeded)
{
if (needsEquipment)
{ {
// Don't try to reach the target without proper equipment. needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
Abandon = true;
return;
} }
} if (!getDivingGearIfNeeded)
else
{
if (character.LockHands)
{ {
cantFindDivingGear = true; if (needsEquipment)
{
// Don't try to reach the target without proper equipment.
Abandon = true;
return;
}
} }
if (cantFindDivingGear && needsDivingSuit) else
{ {
// Don't try to reach the target without a suit because it's lethal. if (character.LockHands)
Abandon = true; {
return; cantFindDivingGear = true;
} }
if (needsEquipment && !cantFindDivingGear) if (cantFindDivingGear && needsDivingSuit)
{ {
SteeringManager.Reset(); // Don't try to reach the target without a suit because it's lethal.
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager), Abandon = true;
onAbandon: () => return;
{ }
cantFindDivingGear = true; if (needsEquipment && !cantFindDivingGear)
if (needsDivingSuit) {
SteeringManager.Reset();
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager),
onAbandon: () =>
{ {
// Shouldn't try to reach the target without a suit, because it's lethal. cantFindDivingGear = true;
Abandon = true; if (needsDivingSuit)
} {
else // Shouldn't try to reach the target without a suit, because it's lethal.
{ Abandon = true;
// Try again without requiring the diving suit (or mask) }
RemoveSubObjective(ref findDivingGear); else
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: !tryToGetDivingSuit, objectiveManager), {
onAbandon: () => // Try again without requiring the diving suit (or mask)
{ RemoveSubObjective(ref findDivingGear);
Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null); TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: !tryToGetDivingSuit, objectiveManager),
RemoveSubObjective(ref findDivingGear); onAbandon: () =>
}, {
onCompleted: () => Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null);
{ RemoveSubObjective(ref findDivingGear);
RemoveSubObjective(ref findDivingGear); },
}); onCompleted: () =>
} {
}, RemoveSubObjective(ref findDivingGear);
onCompleted: () => RemoveSubObjective(ref findDivingGear)); });
return; }
},
onCompleted: () => RemoveSubObjective(ref findDivingGear));
return;
}
} }
} }
if (IsDoneFollowing()) if (IsDoneFollowing())
{ {
OnCompleted(); OnCompleted();

View File

@@ -347,7 +347,10 @@ namespace Barotrauma
useItemTimer = 0.05f; useItemTimer = 0.05f;
StartUsingItem(); StartUsingItem();
if (!allowMovement) //make the character move towards the item they're using...
if (!allowMovement &&
//...except if they've selected an item that controls the character's direction (e.g. a periscope)
character.SelectedSecondaryItem?.GetComponent<Controller>() is not { Direction: Direction.Left or Direction.Right })
{ {
TargetMovement = Vector2.Zero; TargetMovement = Vector2.Zero;
TargetDir = handWorldPos.X > character.WorldPosition.X ? Direction.Right : Direction.Left; TargetDir = handWorldPos.X > character.WorldPosition.X ? Direction.Right : Direction.Left;

View File

@@ -4,7 +4,11 @@ namespace Barotrauma
{ {
enum CauseOfDeathType enum CauseOfDeathType
{ {
Unknown, Pressure, Suffocation, Drowning, Affliction, Disconnected Unknown, Pressure, Suffocation, Drowning, Affliction, Disconnected,
/// <summary>
/// Special cause of death type returned by <see cref="Character.CauseOfDeathType"/> when the character is not dead.
/// </summary>
None
} }
class CauseOfDeath class CauseOfDeath

View File

@@ -592,6 +592,9 @@ namespace Barotrauma
public Identifier VariantOf => Prefab.VariantOf; public Identifier VariantOf => Prefab.VariantOf;
/// <summary>
/// Non-localized name of the character (for characters with info, their name, for monsters, their species). E.g. "Mudraptor_veteran", "John Smith".
/// </summary>
public string Name public string Name
{ {
get get
@@ -600,6 +603,9 @@ namespace Barotrauma
} }
} }
/// <summary>
/// Localized display name of the character (e.g. "Mudraptor Veteran", "John Smith") - this should generally be used in any the player sees.
/// </summary>
public string DisplayName public string DisplayName
{ {
get get
@@ -1210,6 +1216,11 @@ namespace Barotrauma
private set; private set;
} }
/// <summary>
/// Can be used by mods to check the cause of death of the character using conditionals (e.g. if some <see cref="OnDeath"/> effects should or should not be triggered by certain causes of death).
/// </summary>
public CauseOfDeathType CauseOfDeathType => CauseOfDeath?.Type ?? CauseOfDeathType.None;
//can other characters select (= grab) this character //can other characters select (= grab) this character
public bool CanBeSelected public bool CanBeSelected
{ {
@@ -2413,7 +2424,7 @@ namespace Barotrauma
{ {
if (!CanInteractWith(item)) { continue; } if (!CanInteractWith(item)) { continue; }
if (SelectedItem?.OwnInventory != null && SelectedItem.OwnInventory.CanBePut(item)) if (SelectedItem?.OwnInventory != null && !SelectedItem.OwnInventory.Locked && SelectedItem.OwnInventory.CanBePut(item))
{ {
SelectedItem.OwnInventory.TryPutItem(item, this); SelectedItem.OwnInventory.TryPutItem(item, this);
} }
@@ -5729,6 +5740,7 @@ namespace Barotrauma
public bool HasRecipeForItem(Identifier recipeIdentifier) public bool HasRecipeForItem(Identifier recipeIdentifier)
{ {
if (GameMain.GameSession != null && GameMain.GameSession.UnlockedRecipes.Contains(recipeIdentifier)) { return true; }
return characterTalents.Any(t => t.UnlockedRecipes.Contains(recipeIdentifier)); return characterTalents.Any(t => t.UnlockedRecipes.Contains(recipeIdentifier));
} }

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