Merge remote-tracking branch 'upstream/master' into develop

This commit is contained in:
EvilFactory
2024-06-18 12:19:13 -03:00
263 changed files with 7788 additions and 2849 deletions

View File

@@ -73,7 +73,7 @@ body:
label: Version
description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu.
options:
- v1.4.6.0 (Blood in the Water Update, hotfix 2)
- v1.5.7.0 (Summer Update)
- Other
validations:
required: true

View File

@@ -66,6 +66,8 @@ namespace Barotrauma
public Vector2 ShakePosition { get; private set; }
private float shakeTimer;
public float MovementLockTimer;
private float globalZoomScale = 1.0f;
//used to smooth out the movement when in freecam
@@ -257,13 +259,15 @@ namespace Barotrauma
float moveSpeed = 20.0f / zoom;
MovementLockTimer -= deltaTime;
Vector2 moveCam = Vector2.Zero;
if (TargetPos == Vector2.Zero)
{
Vector2 moveInput = Vector2.Zero;
if (allowMove && !Freeze)
{
if (GUI.KeyboardDispatcher.Subscriber == null && allowInput)
if (GUI.KeyboardDispatcher.Subscriber == null && allowInput && MovementLockTimer <= 0.0f)
{
if (PlayerInput.KeyDown(Keys.LeftShift)) { moveSpeed *= 2.0f; }
if (PlayerInput.KeyDown(Keys.LeftControl)) { moveSpeed *= 0.5f; }

View File

@@ -558,14 +558,17 @@ namespace Barotrauma
if (GameMain.Client != null) { chatMessage += " " + TextManager.Get("DeathChatNotification"); }
GameMain.NetworkMember.RespawnManager?.ShowRespawnPromptIfNeeded();
RespawnManager.ShowDeathPromptIfNeeded();
GameMain.NetworkMember.AddChatMessage(chatMessage.Value, ChatMessageType.Dead);
GameMain.LightManager.LosEnabled = false;
controlled = null;
if (!(Screen.Selected?.Cam is null))
if (Screen.Selected?.Cam is Camera cam)
{
Screen.Selected.Cam.TargetPos = Vector2.Zero;
cam.TargetPos = Vector2.Zero;
//briefly lock moving the camera with arrow keys
//(it's annoying to have the camera fly off when you die while trying to move to safety)
cam.MovementLockTimer = 2.0f;
Lights.LightManager.ViewTarget = null;
}
}

View File

@@ -289,7 +289,7 @@ namespace Barotrauma
if (character.Params.CanInteract && character.SelectedCharacter != null && character.SelectedCharacter.Inventory != null)
{
if (character.SelectedCharacter.CanInventoryBeAccessed)
if (character.SelectedCharacter.IsInventoryAccessibleTo(character))
{
character.SelectedCharacter.Inventory.Update(deltaTime, cam);
}
@@ -677,7 +677,7 @@ namespace Barotrauma
{
if (character.Params.CanInteract && character.SelectedCharacter != null && character.SelectedCharacter.Inventory != null)
{
if (character.SelectedCharacter.CanInventoryBeAccessed)
if (character.SelectedCharacter.IsInventoryAccessibleTo(character))
{
character.SelectedCharacter.Inventory.Locked = false;
character.SelectedCharacter.Inventory.CurrentLayout = CharacterInventory.Layout.Left;
@@ -759,7 +759,7 @@ namespace Barotrauma
textPos.Y += largeTextSize.Y;
}
if (character.FocusedCharacter.CanBeDragged)
if (character.FocusedCharacter.CanBeDraggedBy(character))
{
string text = character.CanEat ? "EatHint" : "GrabHint";
GUI.DrawString(spriteBatch, textPos, GetCachedHudText(text, InputType.Grab),
@@ -767,11 +767,7 @@ namespace Barotrauma
textPos.Y += largeTextSize.Y;
}
if (!character.DisableHealthWindow &&
character.IsFriendly(character.FocusedCharacter) &&
character.FocusedCharacter.CharacterHealth.UseHealthWindow &&
character.CanInteractWith(character.FocusedCharacter, 160f, false) &&
!character.IsClimbing)
if (character.FocusedCharacter.CanBeHealedBy(character))
{
GUI.DrawString(spriteBatch, textPos, GetCachedHudText("HealHint", InputType.Health),
GUIStyle.Green, Color.Black, 2, GUIStyle.SmallFont);

View File

@@ -141,7 +141,7 @@ namespace Barotrauma
{
Color textColor = Color.White * (0.5f + skill.Level / 200.0f);
var skillName = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillsArea.RectTransform), TextManager.Get("SkillName." + skill.Identifier), textColor: textColor, font: font) { Padding = Vector4.Zero };
var skillName = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillsArea.RectTransform), skill.DisplayName, textColor: textColor, font: font) { Padding = Vector4.Zero };
float modifiedSkillLevel = skill.Level;
if (Character != null)
@@ -582,6 +582,7 @@ namespace Barotrauma
ch.ExperiencePoints = inc.ReadInt32();
ch.AdditionalTalentPoints = inc.ReadRangedInteger(0, MaxAdditionalTalentPoints);
ch.PermanentlyDead = inc.ReadBoolean();
return ch;
}

View File

@@ -723,11 +723,19 @@ namespace Barotrauma
character.ReadStatus(inc);
}
if (character.IsHuman && character.TeamID != CharacterTeamType.FriendlyNPC && character.TeamID != CharacterTeamType.None && !character.IsDead)
if (character.IsHuman && character.TeamID != CharacterTeamType.FriendlyNPC && character.TeamID != CharacterTeamType.None)
{
CharacterInfo duplicateCharacterInfo = GameMain.GameSession.CrewManager.GetCharacterInfos().FirstOrDefault(c => c.ID == info.ID);
GameMain.GameSession.CrewManager.RemoveCharacterInfo(duplicateCharacterInfo);
GameMain.GameSession.CrewManager.AddCharacter(character);
if (character.isDead)
{
//just add the info if dead (displayed in the round summary, and crew list if the character is revived)
GameMain.GameSession.CrewManager.AddCharacterInfo(character.info);
}
else
{
GameMain.GameSession.CrewManager.AddCharacter(character);
}
}
if (GameMain.Client.SessionId == ownerId)

View File

@@ -152,8 +152,7 @@ namespace Barotrauma
if (value == null &&
Character.Controlled?.SelectedCharacter?.CharacterHealth != null &&
Character.Controlled.SelectedCharacter.CharacterHealth == prevOpenHealthWindow/* &&
!Character.Controlled.SelectedCharacter.CanInventoryBeAccessed*/)
Character.Controlled.SelectedCharacter.CharacterHealth == prevOpenHealthWindow)
{
Character.Controlled.DeselectCharacter();
}

View File

@@ -289,18 +289,19 @@ namespace Barotrauma
spriteAnimState.Add(decorativeSprite, new SpriteState());
}
TintMask = null;
float sourceRectScale = ragdoll.RagdollParams.SourceRectScale;
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "sprite":
Sprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.normalSpriteParams, ref _texturePath));
Sprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.normalSpriteParams, ref _texturePath), sourceRectScale: sourceRectScale);
break;
case "damagedsprite":
DamagedSprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.damagedSpriteParams, ref _damagedTexturePath));
DamagedSprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.damagedSpriteParams, ref _damagedTexturePath), sourceRectScale: sourceRectScale);
break;
case "conditionalsprite":
var conditionalSprite = new ConditionalSprite(subElement, GetConditionalTarget(), file: GetSpritePath(subElement, null, ref _texturePath));
var conditionalSprite = new ConditionalSprite(subElement, GetConditionalTarget(), file: GetSpritePath(subElement, null, ref _texturePath), sourceRectScale: sourceRectScale);
ConditionalSprites.Add(conditionalSprite);
if (conditionalSprite.DeformableSprite != null)
{
@@ -310,7 +311,7 @@ namespace Barotrauma
}
break;
case "deformablesprite":
_deformSprite = new DeformableSprite(subElement, filePath: GetSpritePath(subElement, Params.deformSpriteParams, ref _texturePath));
_deformSprite = new DeformableSprite(subElement, filePath: GetSpritePath(subElement, Params.deformSpriteParams, ref _texturePath), sourceRectScale: sourceRectScale);
var deformations = CreateDeformations(subElement);
Deformations.AddRange(deformations);
NonConditionalDeformations.AddRange(deformations);
@@ -339,7 +340,7 @@ namespace Barotrauma
ContentPath tintMaskPath = subElement.GetAttributeContentPath("texture");
if (!tintMaskPath.IsNullOrWhiteSpace())
{
TintMask = new Sprite(subElement, file: GetSpritePath(tintMaskPath));
TintMask = new Sprite(subElement, file: GetSpritePath(tintMaskPath), sourceRectScale: sourceRectScale);
TintHighlightThreshold = subElement.GetAttributeFloat("highlightthreshold", 0.6f);
TintHighlightMultiplier = subElement.GetAttributeFloat("highlightmultiplier", 0.8f);
}
@@ -348,7 +349,7 @@ namespace Barotrauma
ContentPath huskMaskPath = subElement.GetAttributeContentPath("texture");
if (!huskMaskPath.IsNullOrWhiteSpace())
{
HuskMask = new Sprite(subElement, file: GetSpritePath(huskMaskPath));
HuskMask = new Sprite(subElement, file: GetSpritePath(huskMaskPath), sourceRectScale: sourceRectScale);
}
break;
}
@@ -700,31 +701,34 @@ namespace Barotrauma
if (spriteParams == null || Alpha <= 0) { return; }
float burn = spriteParams.IgnoreTint ? 0 : burnOverLayStrength;
float brightness = Math.Max(1.0f - burn, 0.2f);
Color clr = spriteParams.Color;
Color tintedColor = spriteParams.Color;
if (!spriteParams.IgnoreTint)
{
clr = clr.Multiply(ragdoll.RagdollParams.Color);
tintedColor = tintedColor.Multiply(ragdoll.RagdollParams.Color);
if (character.Info != null)
{
clr = clr.Multiply(character.Info.Head.SkinColor);
tintedColor = tintedColor.Multiply(character.Info.Head.SkinColor);
}
if (character.CharacterHealth.FaceTint.A > 0 && type == LimbType.Head)
{
clr = Color.Lerp(clr, character.CharacterHealth.FaceTint.Opaque(), character.CharacterHealth.FaceTint.A / 255.0f);
tintedColor = Color.Lerp(tintedColor, character.CharacterHealth.FaceTint.Opaque(), character.CharacterHealth.FaceTint.A / 255.0f);
}
if (character.CharacterHealth.BodyTint.A > 0)
{
clr = Color.Lerp(clr, character.CharacterHealth.BodyTint.Opaque(), character.CharacterHealth.BodyTint.A / 255.0f);
tintedColor = Color.Lerp(tintedColor, character.CharacterHealth.BodyTint.Opaque(), character.CharacterHealth.BodyTint.A / 255.0f);
}
}
Color color = new Color((byte)(clr.R * brightness), (byte)(clr.G * brightness), (byte)(clr.B * brightness), clr.A);
Color color = new Color(tintedColor.Multiply(brightness), tintedColor.A);
Color colorWithoutTint = new Color(spriteParams.Color.Multiply(brightness), spriteParams.Color.A);
Color blankColor = new Color(brightness, brightness, brightness, 1);
if (deadTimer > 0)
{
color = Color.Lerp(color, spriteParams.DeadColor, MathUtils.InverseLerp(0, spriteParams.DeadColorTime, deadTimer));
colorWithoutTint = Color.Lerp(colorWithoutTint, spriteParams.DeadColor, MathUtils.InverseLerp(0, spriteParams.DeadColorTime, deadTimer));
}
color = overrideColor ?? color;
colorWithoutTint = overrideColor ?? colorWithoutTint;
blankColor = overrideColor ?? blankColor;
color *= Alpha;
blankColor *= Alpha;
@@ -739,6 +743,7 @@ namespace Barotrauma
else if (severedFadeOutTimer > SeveredFadeOutTime - 1.0f)
{
color *= SeveredFadeOutTime - severedFadeOutTimer;
colorWithoutTint *= SeveredFadeOutTime - severedFadeOutTimer;
}
}
@@ -956,7 +961,7 @@ namespace Barotrauma
{
DamagedSprite.Draw(spriteBatch,
new Vector2(body.DrawPosition.X, -body.DrawPosition.Y),
color * damageOverlayStrength, activeSprite.Origin,
colorWithoutTint * damageOverlayStrength, activeSprite.Origin,
-body.DrawRotation,
Scale, spriteEffect, activeSprite.Depth - depthStep * Math.Max(1, WearingItems.Count * 2)); // Multiply by 2 to get rid of z-fighting with some clothing combos
}

View File

@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System;
using System.Linq;
@@ -75,7 +75,7 @@ namespace Barotrauma
foreach (ItemComponent ic in Item.Components)
{
if (ic is Holdable) { continue; }
if (!ic.AllowInGameEditing) { continue; }
if (!ic.AllowInGameEditing && Screen.Selected is not { IsEditor: true }) { continue; }
if (SerializableProperty.GetProperties<InGameEditable>(ic).Count == 0 &&
!SerializableProperty.GetProperties<ConditionallyEditable>(ic).Any(p => p.GetAttribute<ConditionallyEditable>().IsEditable(ic)))
{

View File

@@ -29,6 +29,12 @@ namespace Barotrauma
Length = Rect.Width + Padding + Label.Size.X;
}
public void SetLabel(LocalizedString label, CircuitBoxNode node)
{
Label = new CircuitBoxLabel(label, GUIStyle.SubHeadingFont);
Length = Rect.Width + Padding + Label.Size.X;
}
public void Draw(SpriteBatch spriteBatch, Vector2 drawPos, Vector2 parentPos, Color color)
{
if (CircuitBox.UI is not { } circuitBoxUi) { return; }

View File

@@ -0,0 +1,84 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
internal sealed partial class CircuitBoxInputOutputNode
{
private const string PromptUserData = "InputOutputEditPrompt";
public void PromptEdit(GUIComponent parent)
{
CircuitBox.UI?.SetMenuVisibility(false);
GUIFrame backgroundBlocker = new(new RectTransform(Vector2.One, parent.RectTransform), style: "GUIBackgroundBlocker")
{
UserData = PromptUserData
};
GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.8f), backgroundBlocker.RectTransform, Anchor.Center), isHorizontal: false, childAnchor: Anchor.TopCenter);
GUIFrame labelArea = new(new RectTransform(new Vector2(1f, 0.8f), mainLayout.RectTransform, Anchor.Center));
GUILayoutGroup labelLayout = new GUILayoutGroup(new RectTransform(Vector2.One, labelArea.RectTransform), childAnchor: Anchor.Center);
GUIListBox labelList = new GUIListBox(new RectTransform(ToolBox.PaddingSizeParentRelative(labelLayout.RectTransform, 0.9f), labelLayout.RectTransform));
Dictionary<string, GUITextBox> textBoxes = new();
foreach (var conn in Connectors)
{
bool found = ConnectionLabelOverrides.TryGetValue(conn.Name, out string labelOverride);
GUILayoutGroup connLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.12f), labelList.Content.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft);
new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), connLayout.RectTransform), text: conn.Connection.DisplayName, font: GUIStyle.SubHeadingFont);
GUITextBox box = GUI.CreateTextBoxWithPlaceholder(new RectTransform(new Vector2(0.6f, 1f), connLayout.RectTransform), text: found ? labelOverride : string.Empty, conn.Connection.DisplayName.Value);
box.MaxTextLength = MaxConnectionLabelLength;
textBoxes.Add(conn.Name, box);
}
new GUIButton(new RectTransform(new Vector2(0.5f, 0.1f), mainLayout.RectTransform), text: TextManager.Get("confirm"))
{
OnClicked = (_, _) =>
{
var newOverrides = textBoxes.ToDictionary(
static pair => pair.Key,
static pair => pair.Value.Text);
foreach (var (key, value) in newOverrides.ToImmutableDictionary())
{
if (ConnectionLabelOverrides.TryGetValue(key, out string newValue))
{
if (newValue == value)
{
newOverrides.Remove(key);
}
}
else if (string.IsNullOrWhiteSpace(value))
{
newOverrides.Remove(key);
}
}
CircuitBox.SetConnectionLabelOverrides(this, newOverrides);
RemoveEditPrompt(parent);
return true;
}
};
new GUIButton(new RectTransform(new Vector2(0.5f, 0.1f), mainLayout.RectTransform), text: TextManager.Get("cancel"))
{
OnClicked = (_, _) =>
{
RemoveEditPrompt(parent);
return true;
}
};
}
public void RemoveEditPrompt(GUIComponent parent)
{
if (parent.FindChild(PromptUserData) is not { } promptParent) { return; }
parent.RemoveChild(promptParent);
}
}
}

View File

@@ -1,5 +1,6 @@
#nullable enable
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
@@ -80,6 +81,18 @@ namespace Barotrauma
return true;
};
var characterLimit = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), frame.RectTransform, Anchor.BottomRight) { RelativeOffset = new Vector2(0.03f, 0.02f) }, text: $"{bodyTextBox.Text.Length}/{NetLimitedString.MaxLength}", font: GUIStyle.SmallFont, textAlignment: Alignment.Right);
bodyTextBox.OnTextChanged += (textBox, _) =>
{
textBox.TextColor = textBox.TextBlock.SelectedTextColor = textBox.Text.Length > NetLimitedString.MaxLength
? GUIStyle.Red
: GUIStyle.TextColorNormal;
characterLimit.Text = $"{textBox.Text.Length}/{NetLimitedString.MaxLength}";
return true;
};
static void UpdateLabelColor(GUITextBox box)
{
bool found = TextManager.ContainsTag(box.Text);
@@ -97,8 +110,8 @@ namespace Barotrauma
}
}
bodyTextBox.OnDeselected += (textBox, _) => UpdateLabelColor(textBox);
headerTextBox.OnDeselected += (textBox, _) => UpdateLabelColor(textBox);
bodyTextBox.OnDeselected += static (textBox, _) => UpdateLabelColor(textBox);
headerTextBox.OnDeselected += static (textBox, _) => UpdateLabelColor(textBox);
UpdateLabelColor(bodyTextBox);
UpdateLabelColor(headerTextBox);

View File

@@ -87,6 +87,16 @@ namespace Barotrauma
SnapshotMoveAffectedNodes();
startClick = cursorPos;
}
public void ClearSnapshot()
{
lastNodesUnderCursor = ImmutableHashSet<CircuitBoxNode>.Empty;
lastSelectedComponents = ImmutableHashSet<CircuitBoxNode>.Empty;
moveAffectedComponents = ImmutableHashSet<CircuitBoxNode>.Empty;
LastConnectorUnderCursor = Option.None;
LastWireUnderCursor = Option.None;
LastResizeAffectedNode = Option.None;
}
/// <summary>
/// Finds all connections and gathers them into a single list for easier iteration.
@@ -168,38 +178,36 @@ namespace Barotrauma
LastResizeAffectedNode = FindResizeBorderUnderCursor(lastNodesUnderCursor, cursorPos);
}
private static Option<(CircuitBoxResizeDirection, CircuitBoxNode)> FindResizeBorderUnderCursor(ImmutableHashSet<CircuitBoxNode> nodes, Vector2 cursorPos)
private Option<(CircuitBoxResizeDirection, CircuitBoxNode)> FindResizeBorderUnderCursor(ImmutableHashSet<CircuitBoxNode> nodes, Vector2 cursorPos)
{
foreach (var node in nodes)
if (!nodes.Any()) { return Option.None; }
var node = circuitBoxUi.GetTopmostNode(nodes);
if (node is null || !node.IsResizable) { return Option.None; }
const float borderSize = 32f;
var rect = node.Rect;
RectangleF bottomBorder = new(rect.X, rect.Top, rect.Width, borderSize);
RectangleF rightBorder = new(rect.Right - borderSize, rect.Y, borderSize, rect.Height);
RectangleF leftBorder = new(rect.X, rect.Y, borderSize, rect.Height);
bool hoverBottom = bottomBorder.Contains(cursorPos),
hoverRight = rightBorder.Contains(cursorPos),
hoverLeft = leftBorder.Contains(cursorPos);
var dir = CircuitBoxResizeDirection.None;
if (hoverBottom) { dir |= CircuitBoxResizeDirection.Down; }
if (hoverRight) { dir |= CircuitBoxResizeDirection.Right; }
if (hoverLeft) { dir |= CircuitBoxResizeDirection.Left; }
if (dir is CircuitBoxResizeDirection.None)
{
if (!node.IsResizable) { continue; }
const float borderSize = 32f;
var rect = node.Rect;
RectangleF bottomBorder = new(rect.X, rect.Top, rect.Width, borderSize);
RectangleF rightBorder = new(rect.Right - borderSize, rect.Y, borderSize, rect.Height);
RectangleF leftBorder = new(rect.X, rect.Y, borderSize, rect.Height);
bool hoverBottom = bottomBorder.Contains(cursorPos),
hoverRight = rightBorder.Contains(cursorPos),
hoverLeft = leftBorder.Contains(cursorPos);
var dir = CircuitBoxResizeDirection.None;
if (hoverBottom) { dir |= CircuitBoxResizeDirection.Down; }
if (hoverRight) { dir |= CircuitBoxResizeDirection.Right; }
if (hoverLeft) { dir |= CircuitBoxResizeDirection.Left; }
if (dir is CircuitBoxResizeDirection.None)
{
continue;
}
return Option.Some((dir, node));
return Option.None;
}
return Option.None;
return Option.Some((dir, node));
}
/// <summary>
@@ -281,14 +289,14 @@ namespace Barotrauma
if (circuitBoxUi.Locked) { return; }
bool isDragThresholdExceeded = Vector2.DistanceSquared(startClick, cursorPos) > dragTreshold * dragTreshold;
if (LastResizeAffectedNode.IsSome())
{
IsResizing |= isDragThresholdExceeded;
}
else if (LastConnectorUnderCursor.IsSome())
if (LastConnectorUnderCursor.IsSome())
{
IsWiring |= isDragThresholdExceeded;
}
else if (LastResizeAffectedNode.IsSome())
{
IsResizing |= isDragThresholdExceeded;
}
else
{
IsDragging |= isDragThresholdExceeded;

View File

@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -350,6 +350,10 @@ namespace Barotrauma
{
component.Sprite.Draw(spriteBatch, PlayerInput.MousePosition);
}
if (PlayerInput.PrimaryMouseButtonHeld() && MouseSnapshotHandler.LastConnectorUnderCursor.IsSome())
{
CircuitBoxWire.SelectedWirePrefab.Sprite.Draw(spriteBatch, PlayerInput.MousePosition, CircuitBoxWire.SelectedWirePrefab.SpriteColor, scale: camera.Zoom);
}
foreach (var c in CircuitBox.Components)
{
@@ -360,11 +364,11 @@ namespace Barotrauma
{
n.DrawHUD(spriteBatch, camera);
}
if (Locked)
{
LocalizedString lockedText = TextManager.Get("CircuitBoxLocked")
.Fallback(TextManager.Get("ConnectionLocked"));
.Fallback(TextManager.Get("ConnectionLocked"), useDefaultLanguageIfFound: false);
Vector2 size = GUIStyle.LargeFont.MeasureString(lockedText);
Vector2 pos = new Vector2(screenRect.Center.X - size.X / 2, screenRect.Top + screenRect.Height * 0.05f);
@@ -579,9 +583,25 @@ namespace Barotrauma
if (isMouseOn)
{
if (CircuitBox.HeldComponent.IsNone() && PlayerInput.PrimaryMouseButtonDown())
if (PlayerInput.PrimaryMouseButtonDown())
{
MouseSnapshotHandler.StartDragging();
if (CircuitBox.HeldComponent.IsNone())
{
MouseSnapshotHandler.StartDragging();
}
else
{
MouseSnapshotHandler.ClearSnapshot();
}
}
if (PlayerInput.DoubleClicked() && MouseSnapshotHandler.FindWireUnderCursor(cursorPos).IsNone())
{
var topmostNode = GetTopmostNode(MouseSnapshotHandler.FindNodesUnderCursor(cursorPos));
if (topmostNode is CircuitBoxLabelNode label && circuitComponent is not null)
{
label.PromptEditText(circuitComponent);
}
}
if (PlayerInput.MidButtonHeld() || (PlayerInput.IsAltDown() && PlayerInput.PrimaryMouseButtonHeld()))
@@ -629,6 +649,7 @@ namespace Barotrauma
if (PlayerInput.PrimaryMouseButtonClicked())
{
bool selectedNode = false;
if (MouseSnapshotHandler.IsResizing && MouseSnapshotHandler.LastResizeAffectedNode.TryUnwrap(out var r))
{
var (dir, node) = r;
@@ -647,7 +668,7 @@ namespace Barotrauma
}
else if (!MouseSnapshotHandler.IsWiring)
{
TrySelectComponentsUnderCursor();
selectedNode = TrySelectComponentsUnderCursor();
}
}
@@ -658,8 +679,15 @@ namespace Barotrauma
CircuitBox.AddWire(one, two);
}
}
CircuitBox.SelectWires(MouseSnapshotHandler.LastWireUnderCursor.TryUnwrap(out var wire) ? ImmutableArray.Create(wire) : ImmutableArray<CircuitBoxWire>.Empty, !PlayerInput.IsShiftDown());
if (MouseSnapshotHandler.LastWireUnderCursor.TryUnwrap(out var wire) && !MouseSnapshotHandler.IsDragging && !selectedNode)
{
CircuitBox.SelectWires(ImmutableArray.Create(wire), !PlayerInput.IsShiftDown());
}
else if (CircuitBox.Wires.Any(static wire => wire.IsSelectedByMe))
{
CircuitBox.SelectWires(ImmutableArray<CircuitBoxWire>.Empty, !PlayerInput.IsShiftDown());
}
CircuitBox.HeldComponent = Option.None;
MouseSnapshotHandler.EndDragging();
@@ -732,11 +760,17 @@ namespace Barotrauma
}
}
private void TrySelectComponentsUnderCursor()
private bool TrySelectComponentsUnderCursor()
{
CircuitBoxNode? foundNode = GetTopmostNode(MouseSnapshotHandler.GetLastComponentsUnderCursor());
if (foundNode is CircuitBoxLabelNode && MouseSnapshotHandler.LastWireUnderCursor.IsSome())
{
foundNode = null;
}
CircuitBox.SelectComponents(foundNode is null ? ImmutableArray<CircuitBoxNode>.Empty : ImmutableArray.Create(foundNode), !PlayerInput.IsShiftDown());
return foundNode is not null;
}
private void OpenContextMenu()
@@ -767,17 +801,26 @@ namespace Barotrauma
var editLabel = new ContextMenuOption(TextManager.Get("circuitboxeditlabel"), isEnabled: nodeOption is CircuitBoxLabelNode && !Locked, () =>
{
if (nodeOption is not CircuitBoxLabelNode label || circuitComponent is null) { return; }
if (circuitComponent is null) { return; }
if (nodeOption is not CircuitBoxLabelNode label) { return; }
label.PromptEditText(circuitComponent);
});
var editConnections = new ContextMenuOption(TextManager.Get("circuitboxrenameconnections"), isEnabled: nodeOption is CircuitBoxInputOutputNode && !Locked, () =>
{
if (circuitComponent is null) { return; }
if (nodeOption is not CircuitBoxInputOutputNode io) { return; }
io.PromptEdit(circuitComponent);
});
var addLabelOption = new ContextMenuOption(TextManager.Get("circuitboxaddlabel"), isEnabled: !Locked, () =>
{
CircuitBox.AddLabel(cursorPos);
});
ContextMenuOption[] allOptions = { addLabelOption, editLabel, option };
ContextMenuOption[] allOptions = { addLabelOption, editLabel, editConnections, option };
// show component name in the header to better indicate what is about to be deleted
if (nodeOption is CircuitBoxComponent comp)

View File

@@ -114,7 +114,7 @@ namespace Barotrauma
textBox.MaxTextLength = maxLength;
textBox.OnKeyHit += (sender, key) =>
{
if (key != Keys.Tab)
if (key != Keys.Tab && key != Keys.LeftShift)
{
ResetAutoComplete();
}
@@ -181,7 +181,8 @@ namespace Barotrauma
if (PlayerInput.KeyHit(Keys.Tab) && !textBox.IsIMEActive)
{
textBox.Text = AutoComplete(textBox.Text, increment: string.IsNullOrEmpty(currentAutoCompletedCommand) ? 0 : 1 );
int increment = PlayerInput.KeyDown(Keys.LeftShift) ? -1 : 1;
textBox.Text = AutoComplete(textBox.Text, increment: string.IsNullOrEmpty(currentAutoCompletedCommand) ? 0 : increment );
}
if (PlayerInput.KeyDown(Keys.LeftControl) || PlayerInput.KeyDown(Keys.RightControl))
@@ -644,6 +645,20 @@ namespace Barotrauma
{
NewMessage("Ready checks can only be commenced in multiplayer.", Color.Red);
}));
commands.Add(new Command("setsalary", "setsalary [0-100] [character/default]: Sets the salary of a certain character or the default salary to a percentage.", (string[] args) =>
{
ThrowError("This command can only be used in multiplayer campaign.");
}, isCheat: true, getValidArgs: () =>
{
return new[]
{
new[]{ "0", "100" },
Enumerable.Union(
new string[] { "default" },
Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n)).ToArray(),
};
}));
commands.Add(new Command("bindkey", "bindkey [key] [command]: Binds a key to a command.", (string[] args) =>
{
@@ -803,6 +818,7 @@ namespace Barotrauma
AssignRelayToServer("money", true);
AssignRelayToServer("showmoney", true);
AssignRelayToServer("setskill", true);
AssignRelayToServer("setsalary", true);
AssignRelayToServer("readycheck", true);
commands.Add(new Command("debugjobassignment", "", (string[] args) => { }));
AssignRelayToServer("debugjobassignment", true);
@@ -848,11 +864,8 @@ namespace Barotrauma
AssignOnExecute("teleportcharacter|teleport", (string[] args) =>
{
Character tpCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args, false);
if (tpCharacter != null)
{
tpCharacter.TeleportTo(GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition));
}
Vector2 cursorWorldPos = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition);
TeleportCharacter(cursorWorldPos, Character.Controlled, args);
});
AssignOnExecute("spawn|spawncharacter", (string[] args) =>
@@ -1435,6 +1448,9 @@ namespace Barotrauma
AssignRelayToServer("water|editwater", false);
AssignRelayToServer("fire|editfire", false);
#if DEBUG
AssignRelayToServer("debugvoip", true);
#endif
commands.Add(new Command("mute", "mute [name]: Prevent the client from speaking to anyone through the voice chat. Using this command requires a permission from the server host.",
null,
@@ -2355,6 +2371,11 @@ namespace Barotrauma
}));
#if DEBUG
commands.Add(new Command("deathprompt", "Shows the death prompt for testing purposes.", (string[] args) =>
{
DeathPrompt.Create(delay: 1.0f);
}));
commands.Add(new Command("listspamfilters", "Lists filters that are in the global spam filter.", (string[] args) =>
{
if (!SpamServerFilters.GlobalSpamFilter.TryUnwrap(out var filter))
@@ -3103,12 +3124,12 @@ namespace Barotrauma
{
if (Screen.Selected == GameMain.GameScreen)
{
ThrowError("Reloading the package while in GameScreen may break things; to do it anyway, type 'reloadcorepackage [name] force'");
ThrowError("Reloading the package while in GameScreen may break things; to do it anyway, type 'reloadpackage [name] force'");
return;
}
if (Screen.Selected == GameMain.SubEditorScreen)
{
ThrowError("Reloading the core package while in sub editor may break thingg; to do it anyway, type 'reloadcorepackage [name] force'");
ThrowError("Reloading the core package while in sub editor may break things; to do it anyway, type 'reloadpackage [name] force'");
return;
}
}

View File

@@ -2,7 +2,7 @@ using Barotrauma.Networking;
namespace Barotrauma
{
partial class AlienRuinMission : Mission
partial class EliminateTargetsMission : Mission
{
public override bool DisplayAsCompleted => State > 0;
public override bool DisplayAsFailed => false;

View File

@@ -409,9 +409,9 @@ namespace Barotrauma
{
senderName = (message.Type == ChatMessageType.Private ? "[PM] " : "") + message.SenderName;
}
if (message.Sender?.Info?.Job != null)
if (message.SenderCharacter?.Info?.Job != null)
{
senderColor = Color.Lerp(message.Sender.Info.Job.Prefab.UIColor, Color.White, 0.25f);
senderColor = Color.Lerp(message.SenderCharacter.Info.Job.Prefab.UIColor, Color.White, 0.25f);
}
var msgHolder = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.0f), chatBox.Content.RectTransform, Anchor.TopCenter), style: null,

View File

@@ -14,6 +14,7 @@ namespace Barotrauma
private readonly CampaignUI campaignUI;
private readonly GUIComponent parentComponent;
private GUILayoutGroup pendingAndCrewGroup;
private GUIListBox hireableList, pendingList, crewList;
private GUIFrame characterPreviewFrame;
private GUIDropDown sortingDropDown;
@@ -24,7 +25,14 @@ namespace Barotrauma
private PlayerBalanceElement? playerBalanceElement;
private List<CharacterInfo> PendingHires => campaign.Map?.CurrentLocation?.HireManager?.PendingHires;
private bool HasPermission => CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageHires);
// Is the player hiring a new character for themselves instead of bots for the crew?
// The window can only be used for one of these purposes at the same time.
private static bool HiringNewCharacter => GameMain.NetworkMember?.ServerSettings is { RespawnMode: RespawnMode.Permadeath, IronmanMode: false } &&
GameMain.Client?.CharacterInfo is { PermanentlyDead: true };
private static bool HasPermissionToHire => CampaignMode.AllowedToManageCampaign(
HiringNewCharacter ? ClientPermissions.ManageMoney : ClientPermissions.ManageHires);
private Point resolutionWhenCreated;
@@ -60,20 +68,20 @@ namespace Barotrauma
RefreshCrewFrames(hireableList);
RefreshCrewFrames(crewList);
RefreshCrewFrames(pendingList);
if (clearAllButton != null) { clearAllButton.Enabled = HasPermission; }
if (clearAllButton != null) { clearAllButton.Enabled = HasPermissionToHire; }
}
private void RefreshCrewFrames(GUIListBox listBox)
{
if (listBox == null) { return; }
listBox.CanBeFocused = HasPermission;
listBox.CanBeFocused = HasPermissionToHire;
foreach (GUIComponent child in listBox.Content.Children)
{
if (child.FindChild(c => c is GUIButton && c.UserData is CharacterInfo, true) is GUIButton buyButton)
{
CharacterInfo characterInfo = buyButton.UserData as CharacterInfo;
bool enoughReputationToHire = EnoughReputationToHire(characterInfo);
buyButton.Enabled = HasPermission && enoughReputationToHire;
bool enougMoneyToHire = !HiringNewCharacter || campaign.CanAfford(HireManager.GetSalaryFor(characterInfo));
buyButton.Enabled = HasPermissionToHire && EnoughReputationToHire(characterInfo) && enougMoneyToHire;
foreach (GUITextBlock text in child.GetAllChildren<GUITextBlock>())
{
text.TextColor = new Color(text.TextColor, buyButton.Enabled ? 1.0f : 0.6f);
@@ -174,7 +182,7 @@ namespace Barotrauma
playerBalanceElement = CampaignUI.AddBalanceElement(pendingAndCrewMainGroup, new Vector2(1.0f, 0.75f / 14.0f));
var pendingAndCrewGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), anchor: Anchor.Center,
pendingAndCrewGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), anchor: Anchor.Center,
parent: new GUIFrame(new RectTransform(new Vector2(1.0f, 13.25f / 14.0f), pendingAndCrewMainGroup.RectTransform)
{
MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height)
@@ -222,7 +230,7 @@ namespace Barotrauma
{
ClickSound = GUISoundType.Cart,
ForceUpperCase = ForceUpperCase.Yes,
Enabled = HasPermission,
Enabled = HasPermissionToHire,
OnClicked = (b, o) => RemoveAllPendingHires()
};
GUITextBlock.AutoScaleAndNormalize(validateHiresButton.TextBlock, clearAllButton.TextBlock);
@@ -277,7 +285,7 @@ namespace Barotrauma
{
foreach (CharacterInfo c in hireableCharacters)
{
if (c == null) { continue; }
if (c == null || PendingHires.Contains(c)) { continue; }
CreateCharacterFrame(c, hireableList);
}
}
@@ -289,8 +297,8 @@ namespace Barotrauma
{
HireManager hireManager = location.HireManager;
if (hireManager == null) { return; }
int hireVal = hireManager.AvailableCharacters.Aggregate(0, (curr, hire) => curr + hire.GetIdentifier());
int newVal = availableHires.Aggregate(0, (curr, hire) => curr + hire.GetIdentifier());
int hireVal = hireManager.AvailableCharacters.Aggregate(0, (curr, hire) => curr + hire.ID);
int newVal = availableHires.Aggregate(0, (curr, hire) => curr + hire.ID);
if (hireVal != newVal)
{
location.HireManager.AvailableCharacters = availableHires;
@@ -371,7 +379,7 @@ namespace Barotrauma
}
}
private void CreateCharacterFrame(CharacterInfo characterInfo, GUIListBox listBox)
public GUIComponent CreateCharacterFrame(CharacterInfo characterInfo, GUIListBox listBox, bool hideSalary = false)
{
Skill skill = null;
Color? jobColor = null;
@@ -442,33 +450,41 @@ namespace Barotrauma
CanBeFocused = false
};
}
if (listBox != crewList)
if (!hideSalary)
{
new GUITextBlock(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform),
TextManager.FormatCurrency(HireManager.GetSalaryFor(characterInfo)),
textAlignment: Alignment.Center)
if (listBox != crewList)
{
CanBeFocused = false
};
}
else
{
// Just a bit of padding to make list layouts similar
new GUIFrame(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform), style: null) { CanBeFocused = false };
new GUITextBlock(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform),
TextManager.FormatCurrency(HireManager.GetSalaryFor(characterInfo)),
textAlignment: Alignment.Center)
{
CanBeFocused = false
};
}
else
{
// Just a bit of padding to make list layouts similar
new GUIFrame(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform), style: null) { CanBeFocused = false };
}
}
if (listBox == hireableList)
{
var hireButton = new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementAddButton")
{
ToolTip = TextManager.Get("hirebutton"),
ClickSound = GUISoundType.Cart,
UserData = characterInfo,
Enabled = CanHire(characterInfo),
Enabled = CanHire(characterInfo) && !HiringNewCharacter,
OnClicked = (b, o) => AddPendingHire(o as CharacterInfo)
};
hireButton.OnAddedToGUIUpdateList += (GUIComponent btn) =>
{
if (HiringNewCharacter)
{
return;
}
if (PendingHires.Count + campaign.CrewManager.GetCharacterInfos().Count() >= CrewManager.MaxCrewSize)
{
if (btn.Enabled)
@@ -483,6 +499,41 @@ namespace Barotrauma
btn.Enabled = CanHire(characterInfo);
}
};
if (HiringNewCharacter)
{
bool canHire = CanHire(characterInfo) && campaign.CanAfford(HireManager.GetSalaryFor(characterInfo));
var takeoverButton = new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementTakeControlButton")
{
ToolTip = canHire ? TextManager.Get("hireandtakecontrol") : TextManager.Get("hireandtakecontroldisabled"),
ClickSound = GUISoundType.ConfirmTransaction,
UserData = characterInfo,
Enabled = canHire,
OnClicked = (b, o) =>
{
if (GameMain.Client is not GameClient gameClient)
{
return false;
}
Client client = gameClient.ConnectedClients.FirstOrDefault(c => c.SessionId == gameClient.SessionId);
if (!campaign.TryPurchase(client, HireManager.GetSalaryFor(characterInfo)))
{
return false;
}
gameClient.SendTakeOverBotRequest(characterInfo);
needsHireableRefresh = true;
campaign.ShowCampaignUI = false;
return true;
}
};
takeoverButton.OnAddedToGUIUpdateList += (GUIComponent btn) =>
{
bool canHireCurrently = HiringNewCharacter && CanHire(characterInfo) && campaign.CanAfford(HireManager.GetSalaryFor(characterInfo));
btn.ToolTip = TextManager.Get(canHireCurrently ? "hireandtakecontrol" : "hireandtakecontroldisabled");
btn.Visible = GameMain.GameSession is { AllowHrManagerBotTakeover: true };
btn.Enabled = canHireCurrently;
};
}
}
else if (listBox == pendingList)
{
@@ -501,7 +552,7 @@ namespace Barotrauma
{
UserData = characterInfo,
//can't fire if there's only one character in the crew
Enabled = currentCrew.Contains(characterInfo) && currentCrew.Count() > 1 && HasPermission,
Enabled = currentCrew.Contains(characterInfo) && currentCrew.Count() > 1 && HasPermissionToHire,
OnClicked = (btn, obj) =>
{
var confirmDialog = new GUIMessageBox(
@@ -534,11 +585,13 @@ namespace Barotrauma
};
}
bool CanHire(CharacterInfo characterInfo)
bool CanHire(CharacterInfo thisCharacterInfo)
{
if (!HasPermission) { return false; }
return EnoughReputationToHire(characterInfo);
if (!HasPermissionToHire) { return false; }
return EnoughReputationToHire(thisCharacterInfo);
}
return frame;
}
private bool EnoughReputationToHire(CharacterInfo characterInfo)
@@ -709,10 +762,10 @@ namespace Barotrauma
totalBlock.Text = TextManager.FormatCurrency(total);
bool enoughMoney = campaign == null || campaign.CanAfford(total);
totalBlock.TextColor = enoughMoney ? Color.White : Color.Red;
validateHiresButton.Enabled = enoughMoney && HasPermission && pendingList.Content.RectTransform.Children.Any();
validateHiresButton.Enabled = enoughMoney && HasPermissionToHire && pendingList.Content.RectTransform.Children.Any();
}
public bool ValidateHires(List<CharacterInfo> hires, bool takeMoney = true, bool createNetworkEvent = false)
public bool ValidateHires(List<CharacterInfo> hires, bool takeMoney = true, bool createNetworkEvent = false, bool createNotification = true)
{
if (hires == null || hires.None()) { return false; }
@@ -750,11 +803,14 @@ namespace Barotrauma
{
UpdateLocationView(campaign.Map.CurrentLocation, true);
SelectCharacter(null, null, null);
var dialog = new GUIMessageBox(
TextManager.Get("newcrewmembers"),
TextManager.GetWithVariable("crewhiredmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.DisplayName),
new LocalizedString[] { TextManager.Get("Ok") });
dialog.Buttons[0].OnClicked += dialog.Close;
if (createNotification)
{
var dialog = new GUIMessageBox(
TextManager.Get("newcrewmembers"),
TextManager.GetWithVariable("crewhiredmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.DisplayName),
new LocalizedString[] { TextManager.Get("Ok") });
dialog.Buttons[0].OnClicked += dialog.Close;
}
}
if (createNetworkEvent)
@@ -767,7 +823,7 @@ namespace Barotrauma
private bool CreateRenamingComponent(GUIButton button, object userData)
{
if (!HasPermission || userData is not CharacterInfo characterInfo) { return false; }
if (!HasPermissionToHire || userData is not CharacterInfo characterInfo) { return false; }
var outerGlowFrame = new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), parentComponent.RectTransform, Anchor.Center),
style: "OuterGlow", color: Color.Black * 0.7f);
var frame = new GUIFrame(new RectTransform(new Vector2(0.33f, 0.4f), outerGlowFrame.RectTransform, anchor: Anchor.Center)
@@ -875,6 +931,9 @@ namespace Barotrauma
{
playerBalanceElement = CampaignUI.UpdateBalanceElement(playerBalanceElement);
}
// When showing this window to someone hiring a new character, the right side panels aren't needed
pendingAndCrewGroup.Visible = !HiringNewCharacter;
if (needsHireableRefresh)
{
@@ -949,7 +1008,7 @@ namespace Barotrauma
}
}
public void SetPendingHires(List<int> characterInfos, Location location)
public void SetPendingHires(List<UInt16> characterInfos, Location location)
{
List<CharacterInfo> oldHires = PendingHires.ToList();
foreach (CharacterInfo pendingHire in oldHires)
@@ -957,9 +1016,9 @@ namespace Barotrauma
RemovePendingHire(pendingHire, createNetworkMessage: false);
}
PendingHires.Clear();
foreach (int identifier in characterInfos)
foreach (UInt16 identifier in characterInfos)
{
CharacterInfo match = location.HireManager.AvailableCharacters.Find(info => info.GetIdentifierUsingOriginalName() == identifier);
CharacterInfo match = location.HireManager.AvailableCharacters.Find(info => info.ID == identifier);
if (match != null)
{
AddPendingHire(match, createNetworkMessage: false);
@@ -992,7 +1051,7 @@ namespace Barotrauma
msg.WriteUInt16((ushort)PendingHires.Count);
foreach (CharacterInfo pendingHire in PendingHires)
{
msg.WriteInt32(pendingHire.GetIdentifierUsingOriginalName());
msg.WriteUInt16(pendingHire.ID);
}
}
@@ -1002,17 +1061,16 @@ namespace Barotrauma
msg.WriteBoolean(validRenaming);
if (validRenaming)
{
int identifier = renameCharacter.info.GetIdentifierUsingOriginalName();
msg.WriteInt32(identifier);
msg.WriteUInt16(renameCharacter.info.ID);
msg.WriteString(renameCharacter.newName);
bool existingCrewMember = campaign.CrewManager?.GetCharacterInfos().Any(ci => ci.GetIdentifierUsingOriginalName() == identifier) ?? false;
bool existingCrewMember = campaign.CrewManager?.GetCharacterInfos().Any(ci => ci.ID == renameCharacter.info.ID) ?? false;
msg.WriteBoolean(existingCrewMember);
}
msg.WriteBoolean(firedCharacter != null);
if (firedCharacter != null)
{
msg.WriteInt32(firedCharacter.GetIdentifier());
msg.WriteUInt16(firedCharacter.ID);
}
GameMain.Client.ClientPeer?.Send(msg, DeliveryMethod.Reliable);

View File

@@ -0,0 +1,529 @@
#nullable enable
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma;
internal class DeathPrompt
{
private static CoroutineHandle? createPromptCoroutine;
private GUIComponent? skillPanel;
private GUIComponent? newCharacterPanel;
private GUIComponent? takeOverBotPanel;
private GUIComponent? content;
public static GUIComponent? takeOverBotPanelFrame;
/// <summary>
/// Private constructor, because these should only be created using the Show method
/// </summary>
private DeathPrompt() { }
public static void Create(float delay)
{
if (!RespawnManager.UseDeathPrompt) { return; }
if (GameMain.GameSession.DeathPrompt != null)
{
return;
}
if (createPromptCoroutine != null && CoroutineManager.IsCoroutineRunning(createPromptCoroutine)) { return; }
if ((GameMain.GameSession is not { IsRunning: true })) { return; }
createPromptCoroutine = CoroutineManager.Invoke(() =>
{
if (GameMain.GameSession != null)
{
GameMain.GameSession.DeathPrompt = new DeathPrompt();
GameMain.GameSession.DeathPrompt.CreatePrompt();
SoundPlayer.OverrideMusicType = "crewdead".ToIdentifier();
SoundPlayer.OverrideMusicDuration = 25.0f;
}
}, delay);
}
public void AddToGUIUpdateList()
{
content?.AddToGUIUpdateList();
}
private void CreatePrompt()
{
const float FadeInInterval = 1.0f;
const float FadeInDuration = 1.0f;
bool permadeath = GameMain.NetworkMember is { ServerSettings.RespawnMode: RespawnMode.Permadeath };
bool ironman = GameMain.NetworkMember is { ServerSettings: { RespawnMode: RespawnMode.Permadeath, IronmanMode: true } };
var background = new GUICustomComponent(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), onDraw: DrawBackground)
{
UserData = this
};
background.FadeIn(wait: 0, duration: 5.0f);
var foreground = new GUIImage(new RectTransform(new Vector2(1.0f, GUI.RelativeHorizontalAspectRatio), background.RectTransform, Anchor.BottomCenter) { AbsoluteOffset = new Point(0, GUI.IntScale(-20)) }, "DeathScreenForeground")
{
Color = Color.White
};
foreground.FadeIn(wait: 0, duration: 5.0f);
foreground.Pulsate(startScale: Vector2.One, Vector2.One * 0.8f, duration: 25.0f);
var frame = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.3f), background.RectTransform, Anchor.Center))
{
UserData = this
};
frame.FadeIn(wait: 0, duration: FadeInDuration);
new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.1f), background.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.2f) }, string.Empty, font: GUIStyle.LargeFont, textAlignment: Alignment.TopCenter)
{
TextGetter = () =>
{
return GameMain.Client.EndRoundTimeRemaining > 0.0f ?
TextManager.GetWithVariable("endinground", "[time]", ToolBox.SecondsToReadableTime(GameMain.Client.EndRoundTimeRemaining))
.Fallback(ToolBox.SecondsToReadableTime(GameMain.Client.EndRoundTimeRemaining), useDefaultLanguageIfFound: false) :
string.Empty;
}
};
var content = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.8f), frame.RectTransform, Anchor.Center))
{
Stretch = true,
RelativeSpacing = 0.05f
};
//"you have died" header
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), TextManager.Get("deathprompt.header"), font: GUIStyle.LargeFont, textAlignment: Alignment.Center)
.FadeIn(wait: 0, duration: FadeInDuration);
var causeOfDeath = GameMain.Client?.Character?.CauseOfDeath;
if (causeOfDeath != null && causeOfDeath.Type != CauseOfDeathType.Unknown)
{
var causeOfDeathDescription = causeOfDeath.Affliction != null ?
causeOfDeath.Affliction.SelfCauseOfDeathDescription :
TextManager.Get("Self_CauseOfDeathDescription." + causeOfDeath.Type.ToString(), "Self_CauseOfDeathDescription.Damage");
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), causeOfDeathDescription)
.FadeIn(wait: FadeInInterval * 2, duration: FadeInDuration);
}
if (permadeath)
{
if (ironman)
{
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform),
TextManager.Get("deathprompt.permadeathnotification") + "\n\n" + TextManager.Get("deathprompt.ironmanexplanation"), wrap: true)
.FadeIn(wait: FadeInInterval * 3, duration: FadeInDuration);
}
else
{
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform),
TextManager.Get("deathprompt.permadeathnotification") + '\n' + TextManager.Get("deathprompt.takeoverbotexplanation"), wrap: true)
.FadeIn(wait: FadeInInterval * 3, duration: FadeInDuration);
}
}
else if (RespawnManager.SkillLossPercentageOnDeath > 0)
{
string skillLossAmount = ((int)RespawnManager.SkillLossPercentageOnDeath).ToString();
string skillLossText = $"‖color: { XMLExtensions.ToStringHex(GUIStyle.Red)}‖{skillLossAmount}‖end‖";
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform),
RichString.Rich(TextManager.GetWithVariable("respawnskillpenalty", "[percentage]", skillLossText)))
.FadeIn(wait: FadeInInterval * 3, duration: FadeInDuration);
};
//"what do you want to do" buttons in the middle
//-------------------------------------------------------------------------------------------------------
var decisionButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), content.RectTransform), isHorizontal: true)
{
Stretch = true,
RelativeSpacing = 0.05f
};
if (ironman)
{
// The only option is to spectate
var buttonContainerMiddle = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), decisionButtonContainer.RectTransform), childAnchor: Anchor.Center);
new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonContainerMiddle.RectTransform), TextManager.Get("spectatebutton"))
{
OnClicked = (btn, userdata) =>
{
GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: true);
Close();
return true;
}
}.FadeIn(wait: FadeInInterval * 4, duration: FadeInDuration, alsoChildren: true);
}
else
{
var buttonContainerLeft = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), decisionButtonContainer.RectTransform));
var buttonContainerRight = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), decisionButtonContainer.RectTransform));
// The default "I'll wait" button
new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), buttonContainerLeft.RectTransform), TextManager.Get("respawnquestionpromptwait"))
{
OnClicked = (btn, userdata) =>
{
GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: true);
Close();
return true;
}
}.FadeIn(wait: FadeInInterval * 4, duration: FadeInDuration, alsoChildren: true);
if (permadeath)
{
if (GameMain.Client != null && GameMain.Client.ServerSettings.AllowBotTakeoverOnPermadeath)
{
new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), buttonContainerRight.RectTransform), TextManager.Get("deathprompt.takeoverbot"))
{
Enabled = false,
OnAddedToGUIUpdateList = (component) =>
{
component.Enabled = GetAvailableBots().Any();
},
OnClicked = (btn, userdata) =>
{
if (takeOverBotPanel == null)
{
CreateTakeOverBotPanel(frame, this);
}
else
{
takeOverBotPanel.Parent?.RemoveChild(takeOverBotPanel);
takeOverBotPanel = null;
}
return true;
}
}.FadeIn(wait: FadeInInterval * 4, duration: FadeInDuration, alsoChildren: true);
}
}
else
{
new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), buttonContainerRight.RectTransform), TextManager.Get("deathprompt.respawnnow"))
{
OnClicked = (btn, userdata) =>
{
GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: false);
Close();
return true;
},
Enabled = GameMain.NetworkMember is { ServerSettings.RespawnMode: RespawnMode.MidRound }
}.FadeIn(wait: FadeInInterval * 4, duration: FadeInDuration, alsoChildren: true);
}
//"info buttons" at the bottom
//-------------------------------------------------------------------------------------------------------
var infoButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), content.RectTransform), childAnchor: Anchor.TopRight)
{
Stretch = true,
RelativeSpacing = 0.025f
};
if (permadeath)
{
if (Level.IsLoadedFriendlyOutpost)
{
new GUIButton(new RectTransform(new Vector2(0.6f, 1.0f), infoButtonContainer.RectTransform), TextManager.Get("npctitle.hrmanager"), style: "GUIButtonSmall")
{
OnClicked = (btn, userdata) =>
{
if (GameMain.GameSession?.Campaign is { } campaign)
{
campaign.ShowCampaignUI = true;
campaign.CampaignUI?.SelectTab(CampaignMode.InteractionType.Crew);
}
Close();
return true;
}
}.FadeIn(wait: FadeInInterval * 5, duration: FadeInDuration, alsoChildren: true);
}
}
else
{
new GUIButton(new RectTransform(new Vector2(0.6f, 1.0f), infoButtonContainer.RectTransform), TextManager.Get("deathprompt.showskills"), style: "GUIButtonSmall")
{
OnClicked = (btn, userdata) =>
{
if (skillPanel == null)
{
CreateSkillPanel(frame, GameMain.Client?.Character?.Info ?? GameMain.Client?.CharacterInfo);
}
else
{
skillPanel.Parent?.RemoveChild(skillPanel);
skillPanel = null;
}
return true;
}
}.FadeIn(wait: FadeInInterval * 5, duration: FadeInDuration, alsoChildren: true);
new GUIButton(new RectTransform(new Vector2(0.6f, 1.0f), infoButtonContainer.RectTransform), TextManager.Get("deathprompt.newcharacter"), style: "GUIButtonSmall")
{
OnClicked = (btn, userdata) =>
{
if (newCharacterPanel == null)
{
CreateNewCharacterPanel(frame);
}
else
{
newCharacterPanel.Parent?.RemoveChild(newCharacterPanel);
newCharacterPanel = null;
}
return true;
}
}.FadeIn(wait: FadeInInterval * 5, duration: FadeInDuration, alsoChildren: true);
}
}
//TODO
/*new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), infoButtonContainer.RectTransform), "Respawn settings", style: "GUIButtonSmall")
{
OnClicked = (btn, userdata) =>
{
return true;
}
}.FadeIn(wait: FadeInInterval * 5, duration: FadeInDuration, alsoChildren: true);*/
this.content = background;
}
private void CreateSkillPanel(GUIComponent parent, CharacterInfo? characterInfo)
{
if (characterInfo == null) { return; }
var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), parent.RectTransform, Anchor.CenterRight, Pivot.CenterLeft));
var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.8f), frame.RectTransform, Anchor.Center), isHorizontal: true)
{
Stretch = true
};
var leftColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), content.RectTransform))
{
RelativeSpacing = 0.05f
};
var middleColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), content.RectTransform))
{
RelativeSpacing = 0.05f
};
var rightColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), content.RectTransform))
{
RelativeSpacing = 0.05f
};
var leftHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), leftColumn.RectTransform), TextManager.Get("Skills"), font: GUIStyle.SubHeadingFont, textColor: GUIStyle.TextColorBright);
var middleHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), middleColumn.RectTransform), TextManager.Get("deathprompt.SkillsLostHeader"), font: GUIStyle.SubHeadingFont, textColor: GUIStyle.TextColorBright);
var rightHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), rightColumn.RectTransform), TextManager.Get("deathprompt.respawnnow"), font: GUIStyle.SubHeadingFont, textColor: GUIStyle.TextColorBright);
GUITextBlock.AutoScaleAndNormalize(leftHeader, middleHeader, rightHeader);
foreach (var skill in characterInfo.Job.GetSkills().OrderByDescending(s => s.Level))
{
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), leftColumn.RectTransform), skill.DisplayName);
int previousSkill = (int)skill.HighestLevelDuringRound;
int reducedSkill = (int)RespawnManager.GetReducedSkill(characterInfo, skill, RespawnManager.SkillLossPercentageOnDeath);
int reducedSkillOnImmediateRespawn = (int)RespawnManager.GetReducedSkill(characterInfo, skill, RespawnManager.SkillLossPercentageOnImmediateRespawn, currentSkillLevel: reducedSkill);
int skillLoss = reducedSkill - previousSkill;
int skillLossOnImmediateRespawn = reducedSkillOnImmediateRespawn - previousSkill;
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), middleColumn.RectTransform),
RichString.Rich($"{reducedSkill} (‖color:{XMLExtensions.ToStringHex(GUIStyle.Red)}‖{skillLoss}‖end‖)"));
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), rightColumn.RectTransform),
RichString.Rich($"{reducedSkillOnImmediateRespawn} (‖color:{XMLExtensions.ToStringHex(GUIStyle.Red)}‖{skillLossOnImmediateRespawn}‖end‖)"));
}
new GUIButton(new RectTransform(new Vector2(1.0f, 0.15f), leftColumn.RectTransform, Anchor.BottomLeft), TextManager.Get("Close"), style: "GUIButtonSmall")
{
IgnoreLayoutGroups = true,
OnClicked = (btn, userdata) =>
{
frame.Parent?.RemoveChild(frame);
skillPanel = null;
return true;
}
};
skillPanel = frame;
}
private void CreateNewCharacterPanel(GUIComponent parent)
{
var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.5f), parent.RectTransform, Anchor.CenterRight, Pivot.CenterLeft));
var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: false)
{
Stretch = true,
RelativeSpacing = 0.05f
};
GameMain.NetLobbyScreen.CreatePlayerFrame(content, alwaysAllowEditing: true, createPendingText: false);
var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.15f), content.RectTransform), isHorizontal: true)
{
RelativeSpacing = 0.05f,
Stretch = true
};
new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonContainer.RectTransform, Anchor.BottomLeft), TextManager.Get("Cancel"), style: "GUIButtonSmall")
{
OnClicked = (btn, userdata) =>
{
frame.Parent?.RemoveChild(frame);
newCharacterPanel = null;
return true;
}
};
new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonContainer.RectTransform, Anchor.BottomLeft), TextManager.Get("ApplySettingsYes"), style: "GUIButtonSmall")
{
OnClicked = (btn, userdata) =>
{
GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(onYes: () =>
{
frame.Parent?.RemoveChild(frame);
newCharacterPanel = null;
});
return true;
}
};
newCharacterPanel = frame;
}
public static void CreateTakeOverBotPanel()
{
var panelHolder = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.3f), GUI.Canvas, Anchor.Center));
var takeOverBotPanel = CreateTakeOverBotPanel(panelHolder, deathPrompt: null);
if (takeOverBotPanel != null)
{
takeOverBotPanel.RectTransform.SetPosition(Anchor.Center);
GUIMessageBox.MessageBoxes.Add(panelHolder);
}
}
/// <summary>
/// Static because the "take over bot" panel can be accessed outside the death prompt too
/// </summary>
private static GUIComponent? CreateTakeOverBotPanel(GUIComponent parent, DeathPrompt? deathPrompt)
{
if (GameMain.GameSession?.CrewManager == null) { return null; }
if (GameMain.GameSession?.Campaign is not MultiPlayerCampaign campaign) { return null; }
if (campaign.CampaignUI == null) { campaign.InitCampaignUI(); }
var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), parent.RectTransform, Anchor.CenterRight, Pivot.CenterLeft));
takeOverBotPanelFrame = frame;
var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: false)
{
Stretch = true,
RelativeSpacing = 0.05f
};
var botList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.9f), content.RectTransform));
foreach (CharacterInfo c in GetAvailableBots())
{
var characterFrame = campaign.CampaignUI?.CrewManagement.CreateCharacterFrame(c, botList, hideSalary: true);
if (characterFrame != null)
{
characterFrame.UserData = c;
}
}
botList.UpdateScrollBarSize();
var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.15f), content.RectTransform), isHorizontal: true)
{
RelativeSpacing = 0.05f,
Stretch = true
};
new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonContainer.RectTransform, Anchor.BottomLeft), TextManager.Get("Cancel"), style: "GUIButtonSmall")
{
OnClicked = (btn, userdata) =>
{
GUIMessageBox.MessageBoxes.Remove(frame.Parent);
frame.Parent?.RemoveChild(frame);
if (deathPrompt != null)
{
deathPrompt.takeOverBotPanel = null;
}
return true;
}
};
new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonContainer.RectTransform, Anchor.BottomLeft), TextManager.Get("inputtype.select"), style: "GUIButtonSmall")
{
Enabled = false,
OnAddedToGUIUpdateList = (component) =>
{
component.Enabled = botList.SelectedData is CharacterInfo;
},
OnClicked = (btn, userdata) =>
{
if (botList.SelectedData is CharacterInfo selectedCharacter && GameMain.Client is GameClient client)
{
client.SendTakeOverBotRequest(selectedCharacter);
GUIMessageBox.MessageBoxes.Remove(frame.Parent);
deathPrompt?.Close();
return true;
}
else
{
DebugConsole.ThrowError($"Conditions for sending bot takeover request not met");
return false;
}
}
};
if (deathPrompt != null)
{
deathPrompt.takeOverBotPanel = frame;
}
return frame;
}
private static IEnumerable<CharacterInfo> GetAvailableBots()
{
if (GameMain.GameSession?.CrewManager is { } crewManager)
{
return crewManager.GetCharacterInfos().Where(c =>
/*either an alive bot */
c is { Character.IsBot: true, Character.IsDead: false } ||
/* or a newly hired bot that hasn't spawned yet */
(c.IsNewHire && c.Character == null));
}
else
{
return Enumerable.Empty<CharacterInfo>();
}
}
private void DrawBackground(SpriteBatch spriteBatch, GUICustomComponent guiCustomComponent)
{
var background = GUIStyle.GetComponentStyle("DeathScreenBackground");
if (background != null)
{
GUI.DrawBackgroundSprite(spriteBatch, background.GetDefaultSprite(), Color.White * (guiCustomComponent.Color.A / 255.0f));
}
}
public void Close()
{
if (GameMain.GameSession != null)
{
GameMain.GameSession.DeathPrompt = null;
}
}
public static void CloseBotPanel()
{
if (takeOverBotPanelFrame is GUIComponent frame)
{
GUIMessageBox.MessageBoxes.Remove(frame.Parent);
frame.Parent?.RemoveChild(frame);
}
takeOverBotPanelFrame = null;
}
}

View File

@@ -717,9 +717,9 @@ namespace Barotrauma
private static readonly Queue<GUIComponent> removals = new Queue<GUIComponent>();
private static readonly Queue<GUIComponent> additions = new Queue<GUIComponent>();
// A helpers list for all elements that have a draw order less than 0.
private static readonly List<GUIComponent> first = new List<GUIComponent>();
private static readonly List<GUIComponent> firstAdditions = new List<GUIComponent>();
// A helper list for all elements that have a draw order greater than 0.
private static readonly List<GUIComponent> last = new List<GUIComponent>();
private static readonly List<GUIComponent> lastAdditions = new List<GUIComponent>();
/// <summary>
/// Adds the component on the addition queue.
@@ -737,11 +737,11 @@ namespace Barotrauma
if (!component.Visible) { return; }
if (component.UpdateOrder < 0)
{
first.Add(component);
firstAdditions.Add(component);
}
else if (component.UpdateOrder > 0)
{
last.Add(component);
lastAdditions.Add(component);
}
else
{
@@ -800,9 +800,9 @@ namespace Barotrauma
RemoveFromUpdateList(component);
}
}
ProcessHelperList(first);
ProcessHelperList(firstAdditions);
ProcessAdditions();
ProcessHelperList(last);
ProcessHelperList(lastAdditions);
ProcessRemovals();
}
}
@@ -897,7 +897,7 @@ namespace Barotrauma
public static IEnumerable<GUIComponent> GetAdditions()
{
return additions;
return additions.Union(firstAdditions).Union(lastAdditions);
}
#endregion
@@ -2171,6 +2171,28 @@ namespace Barotrauma
return frame;
}
public static GUITextBox CreateTextBoxWithPlaceholder(RectTransform rectT, string text, LocalizedString placeholder)
{
var holder = new GUIFrame(rectT, style: null);
var textBox = new GUITextBox(new RectTransform(Vector2.One, holder.RectTransform, Anchor.CenterLeft), text, createClearButton: false);
var placeholderElement = new GUITextBlock(new RectTransform(Vector2.One, holder.RectTransform, Anchor.CenterLeft),
textColor: Color.DarkGray * 0.6f,
text: placeholder,
textAlignment: Alignment.CenterLeft)
{
CanBeFocused = false
};
new GUICustomComponent(new RectTransform(Vector2.Zero, holder.RectTransform),
onUpdate: delegate { placeholderElement.RectTransform.NonScaledSize = textBox.Frame.RectTransform.NonScaledSize; });
textBox.OnSelected += delegate { placeholderElement.Visible = false; };
textBox.OnDeselected += delegate { placeholderElement.Visible = textBox.Text.IsNullOrWhiteSpace(); };
placeholderElement.Visible = string.IsNullOrWhiteSpace(text);
return textBox;
}
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));

View File

@@ -815,7 +815,8 @@ namespace Barotrauma
protected virtual void SetAlpha(float a)
{
color = new Color(color.R / 255.0f, color.G / 255.0f, color.B / 255.0f, a);
hoverColor = new Color(hoverColor.R / 255.0f, hoverColor.G / 255.0f, hoverColor.B / 255.0f, a);;
hoverColor = new Color(hoverColor.R / 255.0f, hoverColor.G / 255.0f, hoverColor.B / 255.0f, a);
disabledColor = new Color(disabledColor.R / 255.0f, disabledColor.G / 255.0f, disabledColor.B / 255.0f, a);
}
public virtual void Flash(Color? color = null, float flashDuration = 1.5f, bool useRectangleFlash = false, bool useCircularFlash = false, Vector2? flashRectInflate = null)
@@ -835,15 +836,29 @@ namespace Barotrauma
flashColor = (color == null) ? GUIStyle.Red : (Color)color;
}
public void FadeOut(float duration, bool removeAfter, float wait = 0.0f, Action onRemove = null)
public void FadeOut(float duration, bool removeAfter, float wait = 0.0f, Action onRemove = null, bool alsoChildren = false)
{
CoroutineManager.StartCoroutine(LerpAlpha(0.0f, duration, removeAfter, wait, onRemove));
if (alsoChildren)
{
foreach (var child in Children)
{
child.FadeOut(duration, removeAfter, wait, onRemove, alsoChildren);
}
}
}
public void FadeIn(float wait, float duration)
public void FadeIn(float wait, float duration, bool alsoChildren = false)
{
SetAlpha(0.0f);
CoroutineManager.StartCoroutine(LerpAlpha(1.0f, duration, false, wait));
if (alsoChildren)
{
foreach (var child in Children)
{
child.FadeIn(wait, duration, alsoChildren);
}
}
}
public void SlideIn(float wait, float duration, int amount, SlideDirection direction)

View File

@@ -201,7 +201,7 @@ namespace Barotrauma
}
else if (sprite?.Texture is { IsDisposed: false })
{
spriteBatch.Draw(sprite.Texture, Rect.Center.ToVector2(), sourceRect, currentColor * (currentColor.A / 255.0f), Rotation, origin,
spriteBatch.Draw(sprite.Texture, new Vector2(Rect.X + Rect.Width / 2.0f, Rect.Y + Rect.Height / 2.0f), sourceRect, currentColor * (currentColor.A / 255.0f), Rotation, origin,
Scale, SpriteEffects, 0.0f);
}

View File

@@ -9,7 +9,6 @@ namespace Barotrauma
{
public class GUIMessageBox : GUIFrame
{
#warning TODO: change this to List<GUIMessageBox> and fix incorrect uses of this list
public readonly static List<GUIComponent> MessageBoxes = new List<GUIComponent>();
private static int DefaultWidth
{

View File

@@ -128,7 +128,7 @@ namespace Barotrauma
}
set
{
if (MathUtils.NearlyEqual(value, floatValue)) { return; }
if (Math.Abs(value - floatValue) < 0.0001f && MathUtils.NearlyEqual(value, floatValue)) { return; }
floatValue = value;
ClampFloatValue();
float newValue = floatValue;

View File

@@ -1,5 +1,6 @@
#nullable enable
using System;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
@@ -25,6 +26,11 @@ namespace Barotrauma
public delegate void OnValueChangedHandler(GUISelectionCarousel<T> carousel);
public OnValueChangedHandler? OnValueChanged;
/// <summary>
/// Are there some conditions for selecting a particular element?
/// </summary>
public Func<T, bool>? ElementSelectionCondition { get; set; }
public GUITextBlock TextBlock { get; private set; }
@@ -89,35 +95,9 @@ namespace Barotrauma
GUIStyle.Apply(TextBlock, "TextBlock", this);
RightButton = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), layoutGroup.RectTransform), style: "GUIButtonToggleRight");
GUIStyle.Apply(RightButton, "RightButton", this);
RightButton.OnClicked += (btn, userData) =>
{
if (elements.Count < 2) { return false; }
if (SelectedElement == null)
{
SelectElement(elements.First());
}
else
{
int newIndex = (elements.IndexOf(SelectedElement) + 1) % elements.Count;
SelectElement(elements[newIndex]);
}
return true;
};
LeftButton.OnClicked += (btn, userData) =>
{
if (elements.Count < 2) { return false; }
if (SelectedElement == null)
{
SelectElement(elements.First());
}
else
{
int newIndex = MathUtils.PositiveModulo((elements.IndexOf(SelectedElement) - 1), elements.Count);
SelectElement(elements[newIndex]);
}
return true;
};
RightButton.OnClicked += (_, _) => SelectNextValidElement();
LeftButton.OnClicked += (_, _) => SelectNextValidElement(directionLeft: true);
if (newElements != null && newElements.Any())
{
@@ -140,9 +120,11 @@ namespace Barotrauma
SelectElement(null);
return;
}
if (elements.FirstOrDefault(e => value.Equals(e.value)) is { } element)
var matchingElement = elements.Where(e => value.Equals(e.value)) // selection is in the set of possible values
.FirstOrDefault(e => ElementSelectionCondition == null || ElementSelectionCondition(e.value)); // selection matches extra conditions, if any
if (matchingElement != null)
{
SelectElement(element);
SelectElement(matchingElement);
}
}
@@ -187,5 +169,42 @@ namespace Barotrauma
SelectElement(newElement);
}
}
/// <summary>
/// Refresh the current selection, for example if there are conditions for which elements are valid, and those might have changed
/// </summary>
public void Refresh()
{
if (SelectedElement != null)
{
if (ElementSelectionCondition == null || ElementSelectionCondition(SelectedElement.value))
{
return;
}
}
SelectElement(elements.FirstOrDefault(e => ElementSelectionCondition == null || ElementSelectionCondition(e.value)));
}
private bool SelectNextValidElement(bool directionLeft = false)
{
if (elements.Count < 2) { return false; }
// Try to find a valid next/previous element
int currentIndex = SelectedElement == null ? -1 : elements.IndexOf(SelectedElement);
int newIndex = currentIndex;
for (int i = 0; i < elements.Count; i++)
{
newIndex = directionLeft ? MathUtils.PositiveModulo((newIndex - 1), elements.Count) : (newIndex + 1) % elements.Count;
if (ElementSelectionCondition == null || ElementSelectionCondition(elements[newIndex].value))
{
SelectElement(elements[newIndex]);
return true;
}
}
// No valid elements found
SelectElement(null);
return true;
}
}
}

View File

@@ -438,8 +438,11 @@ namespace Barotrauma
protected override void SetAlpha(float a)
{
// base.SetAlpha(a);
textColor = new Color(TextColor.R / 255.0f, TextColor.G / 255.0f, TextColor.B / 255.0f, a);
textColor = new Color(TextColor, a);
if (hoverTextColor.HasValue)
{
hoverTextColor = new Color(hoverTextColor.Value, a);
}
}
/// <summary>

View File

@@ -2108,13 +2108,13 @@ namespace Barotrauma
deliveryPrompt.Buttons[0].OnClicked = (btn, userdata) =>
{
ConfirmPurchase(deliverImmediately: true);
deliveryPrompt.Close();
deliveryPrompt?.Close();
return true;
};
deliveryPrompt.Buttons[1].OnClicked = (btn, userdata) =>
{
ConfirmPurchase(deliverImmediately: false);
deliveryPrompt.Close();
deliveryPrompt?.Close();
return true;
};
}

View File

@@ -742,8 +742,8 @@ namespace Barotrauma
private (LocalizedString header, LocalizedString body) GetItemTransferWarningText()
{
var header = TextManager.Get("itemtransferheader").Fallback("lowfuelheader");
var body = TextManager.Get("itemtransferwarning").Fallback("lowfuelwarning");
var header = TextManager.Get("itemtransferheader").Fallback("lowfuelheader", useDefaultLanguageIfFound: false);
var body = TextManager.Get("itemtransferwarning").Fallback("lowfuelwarning", useDefaultLanguageIfFound: false);
return (header, body);
}

View File

@@ -320,7 +320,61 @@ namespace Barotrauma
var reputationButton = createTabButton(InfoFrameTab.Reputation, "reputation");
var balanceFrame = new GUIFrame(new RectTransform(new Point(innerLayoutGroup.Rect.Width, innerLayoutGroup.Rect.Height - infoFrameHolderHeight), parent: innerLayoutGroup.RectTransform), style: "InnerFrame");
GUITextBlock balanceText = new GUITextBlock(new RectTransform(Vector2.One, balanceFrame.RectTransform), string.Empty, textAlignment: Alignment.Right);
GUILayoutGroup salaryFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.66f, 1f), balanceFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft);
GUIScrollBar salaryScrollBar = null;
GUITextBlock salaryPercentage = null;
if (GameMain.GameSession?.GameMode is MultiPlayerCampaign)
{
float value = campaignMode.Bank.RewardDistribution;
GUITextBlock salaryText = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), salaryFrame.RectTransform), TextManager.Get("defaultsalary"), textAlignment: Alignment.Center)
{
AutoScaleHorizontal = true
};
salaryScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.4f, 1f), salaryFrame.RectTransform), barSize: 0.1f, style: "GUISlider")
{
Range = new Vector2(0, 1),
BarScrollValue = value / 100f,
Step = 0.01f,
BarSize = 0.1f,
};
salaryPercentage = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1f), salaryFrame.RectTransform), "0", textAlignment: Alignment.Center)
{
Text = ValueToPercentage(RoundRewardDistribution(salaryScrollBar.BarScroll, salaryScrollBar.Step))
};
salaryScrollBar.OnMoved = (scrollBar, value) =>
{
salaryPercentage.Text = ValueToPercentage(RoundRewardDistribution(value, scrollBar.Step));
return true;
};
salaryScrollBar.OnReleased = (bar, scroll) =>
{
int newRewardDistribution = RoundRewardDistribution(scroll, bar.Step);
SetRewardDistribution(Option.None, newRewardDistribution);
return true;
};
var resetButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), salaryFrame.RectTransform), TextManager.Get("ResetSalaries"), style: "GUIButtonSmall")
{
TextBlock = { AutoScaleHorizontal = true },
ToolTip = TextManager.Get("resetsalaries.tooltip"),
OnClicked = (button, userData) =>
{
GUI.AskForConfirmation(TextManager.Get("ResetSalaries"), TextManager.Get("ResetSalaries.Warning"), onConfirm: ResetRewardDistributions);
return true;
}
};
void UpdateSliderEnabled()
=> salaryScrollBar.Enabled = resetButton.Enabled = CampaignMode.AllowedToManageWallets();
UpdateSliderEnabled();
Identifier defaultSalaryEventIdentifier = "DefaultSalarySlider".ToIdentifier();
GameMain.Client?.OnPermissionChanged?.RegisterOverwriteExisting(defaultSalaryEventIdentifier, _ => UpdateSliderEnabled());
}
GUITextBlock balanceText = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1f), balanceFrame.RectTransform, Anchor.TopRight), string.Empty, textAlignment: Alignment.Right);
if (GameMain.IsMultiplayer)
{
balanceText.ToolTip = TextManager.Get("bankdescription");
@@ -343,6 +397,13 @@ namespace Barotrauma
{
if (!e.Owner.IsNone()) { return; }
SetBalanceText(balanceText, e.Wallet.Balance);
if (salaryPercentage is not null && salaryScrollBar is not null)
{
float rewardDistribution = e.Wallet.RewardDistribution;
salaryScrollBar.BarScrollValue = rewardDistribution / 100f;
salaryPercentage.Text = ValueToPercentage(rewardDistribution);
}
});
registeredEvents.Add(eventIdentifier);
@@ -350,6 +411,9 @@ namespace Barotrauma
{
text.Text = TextManager.GetWithVariable("bankbalanceformat", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", balance));
}
LocalizedString ValueToPercentage(float value)
=> TextManager.GetWithVariable("percentageformat", "[value]", $"{(int)MathF.Round(value)}");
}
var submarineButton = createTabButton(InfoFrameTab.Submarine, "submarine");
@@ -1037,11 +1101,10 @@ namespace Barotrauma
{
int newRewardDistribution = RoundRewardDistribution(scroll, bar.Step);
if (newRewardDistribution == targetWallet.RewardDistribution) { return false; }
SetRewardDistribution(character, newRewardDistribution);
SetRewardDistribution(Option.Some(character), newRewardDistribution);
return true;
}
};
int RoundRewardDistribution(float scroll, float step) => (int)MathUtils.RoundTowardsClosest(scroll * 100, step * 100);
SetRewardText(targetWallet.RewardDistribution, rewardBlock);
@@ -1201,6 +1264,7 @@ namespace Barotrauma
{
moneyBlock.Text = TextManager.FormatCurrency(e.Info.Balance);
salarySlider.BarScrollValue = e.Info.RewardDistribution / 100f;
SetRewardText(e.Info.RewardDistribution, rewardBlock);
}
UpdateAllInputs();
@@ -1311,20 +1375,29 @@ namespace Barotrauma
transfer.Write(msg);
GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable);
}
static void SetRewardDistribution(Character character, int newValue)
{
INetSerializableStruct transfer = new NetWalletSetSalaryUpdate
{
Target = character.ID,
NewRewardDistribution = newValue
};
IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.REWARD_DISTRIBUTION);
transfer.Write(msg);
GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable);
}
}
static void SetRewardDistribution(Option<Character> character, int newValue)
{
INetSerializableStruct transfer = new NetWalletSetSalaryUpdate
{
Target = character.Select(c => c.ID),
NewRewardDistribution = newValue
};
IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.REWARD_DISTRIBUTION);
transfer.Write(msg);
GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable);
}
static void ResetRewardDistributions()
{
IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.RESET_REWARD_DISTRIBUTION);
GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable);
}
static int RoundRewardDistribution(float scroll, float step)
=> (int)MathUtils.RoundTowardsClosest(scroll * 100, step * 100);
private GUIComponent CreateClientInfoFrame(GUIFrame frame, Client client, Sprite permissionIcon = null)
{
GUIComponent paddedFrame;

View File

@@ -133,43 +133,47 @@ namespace Barotrauma
GUIFrame containerFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), characterLayout.RectTransform), style: null);
GUILayoutGroup playerFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), containerFrame.RectTransform, Anchor.TopCenter));
GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false);
GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), skillLayout.RectTransform, Anchor.BottomRight),
text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew"), style: "GUIButtonSmall")
// TODO: What is CampaignCharacterDiscarded and can it be relevant in permadeath mode?
if (!GameMain.NetLobbyScreen.PermadeathMode)
{
IgnoreLayoutGroups = false,
TextBlock =
GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), skillLayout.RectTransform, Anchor.BottomRight),
text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew"), style: "GUIButtonSmall")
{
AutoScaleHorizontal = true
}
};
newCharacterBox.OnClicked = (button, o) =>
{
if (!GameMain.NetLobbyScreen.CampaignCharacterDiscarded)
{
GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(() =>
IgnoreLayoutGroups = false,
TextBlock =
{
newCharacterBox.Text = TextManager.Get("settings");
if (TabMenu.PendingChangesFrame != null)
{
NetLobbyScreen.CreateChangesPendingFrame(TabMenu.PendingChangesFrame);
}
AutoScaleHorizontal = true
}
};
OpenMenu();
});
return true;
}
OpenMenu();
return true;
void OpenMenu()
newCharacterBox.OnClicked = (button, o) =>
{
characterSettingsFrame!.Visible = true;
content.Visible = false;
}
};
if (!GameMain.NetLobbyScreen.CampaignCharacterDiscarded)
{
GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(() =>
{
newCharacterBox.Text = TextManager.Get("settings");
if (TabMenu.PendingChangesFrame != null)
{
NetLobbyScreen.CreateChangesPendingFrame(TabMenu.PendingChangesFrame);
}
OpenMenu();
});
return true;
}
OpenMenu();
return true;
void OpenMenu()
{
characterSettingsFrame!.Visible = true;
content.Visible = false;
}
};
}
GUILayoutGroup characterCloseButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), characterLayout.RectTransform), childAnchor: Anchor.BottomCenter);
new GUIButton(new RectTransform(new Vector2(0.4f, 1f), characterCloseButtonLayout.RectTransform), TextManager.Get("ApplySettingsButton")) //TODO: Is this text appropriate for this circumstance for all languages?
@@ -177,6 +181,7 @@ namespace Barotrauma
OnClicked = (button, o) =>
{
GameMain.Client?.SendCharacterInfo(GameMain.Client.PendingName);
GameMain.NetLobbyScreen.CampaignCharacterDiscarded = false;
characterSettingsFrame.Visible = false;
content.Visible = true;
return true;

View File

@@ -773,7 +773,7 @@ namespace Barotrauma
subItems ??= GetSubItems();
return subItems.Any(i =>
i.Prefab.SwappableItem != null &&
!i.HiddenInGame && i.AllowSwapping &&
!i.IsHidden && i.AllowSwapping &&
(i.Prefab.SwappableItem.CanBeBought || ItemPrefab.Prefabs.Any(ip => ip.SwappableItem?.ReplacementOnUninstall == i.Prefab.Identifier)) &&
Submarine.MainSub.IsEntityFoundOnThisSub(i, true) && category.ItemTags.Any(t => i.HasTag(t)));
}
@@ -876,7 +876,7 @@ namespace Barotrauma
{
parent.Content.ClearChildren();
currentUpgradeCategory = category;
var entitiesOnSub = submarine.GetItems(true).Where(i => submarine.IsEntityFoundOnThisSub(i, true) && !i.HiddenInGame && i.AllowSwapping && i.Prefab.SwappableItem != null && category.ItemTags.Any(t => i.HasTag(t))).ToList();
var entitiesOnSub = submarine.GetItems(true).Where(i => submarine.IsEntityFoundOnThisSub(i, true) && !i.IsHidden && i.AllowSwapping && i.Prefab.SwappableItem != null && category.ItemTags.Any(t => i.HasTag(t))).ToList();
foreach (Item item in entitiesOnSub)
{

View File

@@ -115,7 +115,6 @@ namespace Barotrauma
if (IsMouseOver || (!RequireMouseOn && SelectedWidgets.Contains(this) && PlayerInput.PrimaryMouseButtonHeld()))
{
Hovered?.Invoke();
System.Diagnostics.Debug.WriteLine("hovered");
if (RequireMouseOn || PlayerInput.PrimaryMouseButtonDown())
{
if ((multiselect && !SelectedWidgets.Contains(this)) || SelectedWidgets.None())

View File

@@ -1147,19 +1147,14 @@ namespace Barotrauma
if (save)
{
GUI.SetSavingIndicatorState(true);
GameSession.Campaign?.HandleSaveAndQuit();
if (GameSession.Submarine != null && !GameSession.Submarine.Removed)
{
GameSession.SubmarineInfo = new SubmarineInfo(GameSession.Submarine);
}
if (GameSession.Campaign is CampaignMode campaign)
{
if (campaign is SinglePlayerCampaign spCampaign && Level.IsLoadedFriendlyOutpost)
{
spCampaign.UpdateStoreStock();
}
GameSession.EventManager?.RegisterEventHistory(registerFinishedOnly: true);
campaign.End();
}
GameSession.Campaign?.End();
SaveUtil.SaveGame(GameSession.SavePath);
}

View File

@@ -678,12 +678,12 @@ namespace Barotrauma
/// <summary>
/// Adds the message to the single player chatbox.
/// </summary>
public void AddSinglePlayerChatMessage(LocalizedString senderName, LocalizedString text, ChatMessageType messageType, Character sender)
public void AddSinglePlayerChatMessage(LocalizedString senderName, LocalizedString text, ChatMessageType messageType, Entity sender)
{
AddSinglePlayerChatMessage(senderName.Value, text.Value, messageType, sender);
}
public void AddSinglePlayerChatMessage(string senderName, string text, ChatMessageType messageType, Character sender)
public void AddSinglePlayerChatMessage(string senderName, string text, ChatMessageType messageType, Entity sender)
{
if (!IsSinglePlayer)
{
@@ -692,9 +692,13 @@ namespace Barotrauma
}
if (string.IsNullOrEmpty(text)) { return; }
if (sender != null)
if (sender is Character character)
{
GameMain.GameSession.CrewManager.SetCharacterSpeaking(sender);
GameMain.GameSession.CrewManager?.SetCharacterSpeaking(character);
if (!character.IsBot)
{
character.TextChatVolume = 1f;
}
}
ChatBox.AddMessage(ChatMessage.Create(senderName, text, messageType, sender));
}
@@ -708,9 +712,9 @@ namespace Barotrauma
}
if (string.IsNullOrEmpty(message.Text)) { return; }
if (message.Sender != null)
if (message.SenderCharacter != null)
{
GameMain.GameSession.CrewManager.SetCharacterSpeaking(message.Sender);
GameMain.GameSession.CrewManager?.SetCharacterSpeaking(message.SenderCharacter);
}
ChatBox.AddMessage(message);
}
@@ -3688,6 +3692,9 @@ namespace Barotrauma
crewList.ClearChildren();
}
/// <summary>
/// Saves the current crew. Note that this is client-only code (only used in the single player campaign) - saving in multiplayer is handled in the server-side code of <see cref="MultiPlayerCampaign"/>.
/// </summary>
public XElement Save(XElement parentElement)
{
var element = new XElement("crew");

View File

@@ -254,7 +254,7 @@ namespace Barotrauma
buttonText = TextManager.Get("map");
}
else if (prevCampaignUIAutoOpenType != availableTransition &&
(availableTransition == TransitionType.ProgressToNextEmptyLocation || availableTransition == TransitionType.ReturnToPreviousEmptyLocation))
availableTransition == TransitionType.ProgressToNextEmptyLocation)
{
HintManager.OnAvailableTransition(availableTransition);
//opening the campaign map pauses the game and prevents HintManager from running -> update it manually to get the hint to show up immediately

View File

@@ -162,7 +162,7 @@ namespace Barotrauma
};
}
private void InitCampaignUI()
public void InitCampaignUI()
{
campaignUIContainer = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: "InnerGlow", color: Color.Black);
CampaignUI = new CampaignUI(this, campaignUIContainer)
@@ -720,7 +720,7 @@ namespace Barotrauma
}
else
{
campaign.UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall, force: true);
campaign.UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall, isNetworkMessage: true);
}
}
foreach (Item item in Item.ItemList.ToList())
@@ -906,6 +906,8 @@ namespace Barotrauma
public void ClientReadCrew(IReadMessage msg)
{
bool createNotification = msg.ReadBoolean();
ushort availableHireLength = msg.ReadUInt16();
List<CharacterInfo> availableHires = new List<CharacterInfo>();
for (int i = 0; i < availableHireLength; i++)
@@ -916,10 +918,10 @@ namespace Barotrauma
}
ushort pendingHireLength = msg.ReadUInt16();
List<int> pendingHires = new List<int>();
List<UInt16> pendingHires = new List<UInt16>();
for (int i = 0; i < pendingHireLength; i++)
{
pendingHires.Add(msg.ReadInt32());
pendingHires.Add(msg.ReadUInt16());
}
ushort hiredLength = msg.ReadUInt16();
@@ -934,30 +936,40 @@ namespace Barotrauma
bool renameCrewMember = msg.ReadBoolean();
if (renameCrewMember)
{
int renamedIdentifier = msg.ReadInt32();
UInt16 renamedIdentifier = msg.ReadUInt16();
string newName = msg.ReadString();
CharacterInfo renamedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.GetIdentifierUsingOriginalName() == renamedIdentifier);
CharacterInfo renamedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.ID == renamedIdentifier);
if (renamedCharacter != null) { CrewManager.RenameCharacter(renamedCharacter, newName); }
}
bool fireCharacter = msg.ReadBoolean();
if (fireCharacter)
{
int firedIdentifier = msg.ReadInt32();
CharacterInfo firedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.GetIdentifier() == firedIdentifier);
UInt16 firedIdentifier = msg.ReadUInt16();
CharacterInfo firedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.ID == firedIdentifier);
// this one might and is allowed to be null since the character is already fired on the original sender's game
if (firedCharacter != null) { CrewManager.FireCharacter(firedCharacter); }
}
if (map?.CurrentLocation?.HireManager != null && CampaignUI?.CrewManagement != null &&
/*can't apply until we have the latest save file*/
!NetIdUtils.IdMoreRecent(pendingSaveID, LastSaveID))
if (map?.CurrentLocation?.HireManager != null && CampaignUI?.CrewManagement != null)
{
CampaignUI.CrewManagement.SetHireables(map.CurrentLocation, availableHires);
if (hiredCharacters.Any()) { CampaignUI.CrewManagement.ValidateHires(hiredCharacters, takeMoney: false); }
CampaignUI.CrewManagement.SetPendingHires(pendingHires, map.CurrentLocation);
if (renameCrewMember || fireCharacter) { CampaignUI.CrewManagement.UpdateCrew(); }
//can't apply until we have the latest save file
if (!NetIdUtils.IdMoreRecent(pendingSaveID, LastSaveID))
{
CampaignUI.CrewManagement.SetHireables(map.CurrentLocation, availableHires);
if (hiredCharacters.Any()) { CampaignUI.CrewManagement.ValidateHires(hiredCharacters, takeMoney: false, createNotification: createNotification); }
CampaignUI.CrewManagement.SetPendingHires(pendingHires, map.CurrentLocation);
if (renameCrewMember || fireCharacter) { CampaignUI.CrewManagement.UpdateCrew(); }
}
}
else
{
//This is pretty nasty: setting hireables is handled through CrewManagement,
//which is part of the Campaign UI that might not exist when the client is still initializing the round.
//If that's the case, let's force the available hires here so they're available when the UI is created
CurrentLocation?.ForceHireableCharacters(availableHires);
}
}
public void ClientReadMoney(IReadMessage inc)
@@ -979,6 +991,7 @@ namespace Barotrauma
else
{
Bank.Balance = info.Balance;
Bank.RewardDistribution = info.RewardDistribution;
TryInvokeEvent(Bank, transaction.ChangedData, info);
}
}

View File

@@ -1,6 +1,8 @@
using System;
using Barotrauma.Abilities;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
namespace Barotrauma
{
@@ -43,13 +45,19 @@ namespace Barotrauma
private GUIButton crewListButton, commandButton, tabMenuButton;
private GUIImage talentPointNotification;
private GUIComponent respawnInfoFrame, respawnButtonContainer;
private GUIComponent deathChoiceInfoFrame, deathChoiceButtonContainer;
private GUITextBlock respawnInfoText;
private GUITickBox respawnTickBox;
private GUITickBox deathChoiceTickBox;
private GUIButton takeOverBotButton;
private GUIButton hrManagerButton;
public DeathPrompt DeathPrompt;
private GUIImage eventLogNotification;
private Point prevTopLeftButtonsResolution;
public bool AllowHrManagerBotTakeover => GameMain.NetworkMember?.ServerSettings is { RespawnMode: RespawnMode.Permadeath, IronmanMode: false }
&& Level.IsLoadedFriendlyOutpost;
private void CreateTopLeftButtons()
{
@@ -96,30 +104,63 @@ namespace Barotrauma
talentPointNotification = CreateNotificationIcon(tabMenuButton);
eventLogNotification = CreateNotificationIcon(tabMenuButton);
respawnInfoFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 1.0f), parent: topLeftButtonGroup.RectTransform)
{ MaxSize = new Point(HUDLayoutSettings.ButtonAreaTop.Width / 3, int.MaxValue) }, style: null)
// The visibility of the following contents of deathChoiceInfoFrame is controlled by SetRespawnInfo()
deathChoiceInfoFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 1.0f), parent: topLeftButtonGroup.RectTransform)
{ MaxSize = new Point(HUDLayoutSettings.ButtonAreaTop.Width / 3, int.MaxValue) }, style: null)
{
Visible = false
};
respawnInfoText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), respawnInfoFrame.RectTransform), "", wrap: true);
respawnButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), respawnInfoFrame.RectTransform, Anchor.CenterRight), isHorizontal: true, childAnchor: Anchor.CenterLeft)
respawnInfoText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), deathChoiceInfoFrame.RectTransform), "", wrap: true);
deathChoiceButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), deathChoiceInfoFrame.RectTransform, Anchor.CenterRight), isHorizontal: true, childAnchor: Anchor.CenterLeft)
{
AbsoluteSpacing = HUDLayoutSettings.Padding,
Stretch = true,
Visible = false
};
respawnTickBox = new GUITickBox(new RectTransform(Vector2.One * 0.9f, respawnButtonContainer.RectTransform, Anchor.Center), TextManager.Get("respawnquestionpromptrespawn"))
takeOverBotButton = new GUIButton(new RectTransform(Vector2.One * 0.9f, deathChoiceButtonContainer.RectTransform, Anchor.Center),
TextManager.Get("takeoverbotquestionprompttakeoverbot"), style: "GUIButtonSmall")
{
ToolTip = TextManager.GetWithVariable(
"respawnquestionprompt", "[percentage]",
(Math.Round(Networking.RespawnManager.SkillLossPercentageOnImmediateRespawn).ToString())),
OnSelected = (tickbox) =>
OnClicked = (btn, userdata) =>
{
GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: !tickbox.Selected);
DeathPrompt.CreateTakeOverBotPanel();
return true;
}
};
takeOverBotButton.TextBlock.AutoScaleHorizontal = true;
hrManagerButton = new GUIButton(new RectTransform(Vector2.One * 0.9f, deathChoiceButtonContainer.RectTransform, Anchor.Center),
TextManager.Get("npctitle.hrmanager"), style: "GUIButtonSmall")
{
OnClicked = (btn, userdata) =>
{
if (GameMain.GameSession?.Campaign is { } campaign)
{
campaign.ShowCampaignUI = true;
campaign.CampaignUI?.SelectTab(CampaignMode.InteractionType.Crew);
}
return true;
}
};
hrManagerButton.TextBlock.AutoScaleHorizontal = true;
var questionText =
TextManager.GetWithVariable(
"respawnquestionprompt", "[percentage]",
((int)Math.Round(RespawnManager.SkillLossPercentageOnImmediateRespawn)).ToString());
deathChoiceTickBox = new GUITickBox(new RectTransform(Vector2.One * 0.9f, deathChoiceButtonContainer.RectTransform, Anchor.Center),
TextManager.Get("respawnquestionpromptrespawn"))
{
ToolTip = questionText,
OnSelected = (tickbox) =>
{
GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: !tickbox.Selected);
return true;
}
};
prevTopLeftButtonsResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
}
@@ -150,6 +191,8 @@ namespace Barotrauma
GameMain.NetLobbyScreen.CharacterAppearanceCustomizationMenu?.AddToGUIUpdateList();
GameMain.NetLobbyScreen?.JobSelectionFrame?.AddToGUIUpdateList();
}
DeathPrompt?.AddToGUIUpdateList();
}
public static GUIImage CreateNotificationIcon(GUIComponent parent, bool offset = true)
@@ -230,16 +273,67 @@ namespace Barotrauma
HintManager.Update();
ObjectiveManager.VideoPlayer.Update();
}
public void SetRespawnInfo(bool visible, string text, Color textColor, bool buttonsVisible, bool waitForNextRoundRespawn)
/// <summary>
/// This method controls the content and visibility logic of the respawn-related GUI elements at the top left of the game screen.
/// </summary>
/// <param name="waitForNextRoundRespawn">Has the player chosen to wait until next round</param>
/// <param name="hideButtons">Hide the respawn buttons even if they would otherwise be visible</param>
public void SetRespawnInfo(string text, Color textColor, bool waitForNextRoundRespawn, bool hideButtons = false)
{
if (topLeftButtonGroup == null) { return; }
respawnInfoFrame.Visible = visible;
if (!visible) { return; }
bool permadeathMode = GameMain.NetworkMember?.ServerSettings is { RespawnMode: RespawnMode.Permadeath };
bool ironmanMode = GameMain.NetworkMember is { ServerSettings: { RespawnMode: RespawnMode.Permadeath, IronmanMode: true } };
bool hasRespawnOptions;
if (permadeathMode)
{
// In permadeath mode you can (in ironman, must) always at least wait, and possibly buy a new character from HR or take control of a bot
hasRespawnOptions = !ironmanMode &&
GameMain.Client is GameClient client && (client.CharacterInfo == null || client.CharacterInfo.PermanentlyDead);
}
else // "classic" respawn modes
{
//can choose between midround respawning with a penalty or waiting
//if we're in a non-outpost level, and either don't have an existing character or have already spawned during the round
//(otherwise, e.g. when joining a campaign in which we have an existing character, we can respawn mid-round "for free" and there's no reason to make a choice)
hasRespawnOptions = Level.Loaded?.Type != LevelData.LevelType.Outpost &&
(GameMain.Client is GameClient client && (client.CharacterInfo == null || client.HasSpawned));
}
// Are the death choice elements shown at all, at least with the text?
deathChoiceInfoFrame.Visible = !text.IsNullOrEmpty() || hasRespawnOptions;
if (!deathChoiceInfoFrame.Visible) { return; }
respawnInfoText.Text = text;
respawnInfoText.TextColor = textColor;
respawnButtonContainer.Visible = buttonsVisible;
respawnTickBox.Selected = !waitForNextRoundRespawn;
// Determine if we even bother considering showing the buttons
if (GameMain.GameSession.GameMode is not CampaignMode || Character.Controlled != null)
{
// Disable the button container in case it was left visible earlier
deathChoiceButtonContainer.Visible = false;
return;
}
deathChoiceButtonContainer.Visible = hasRespawnOptions && !hideButtons;
if (deathChoiceButtonContainer.Visible)
{
hrManagerButton.Visible = AllowHrManagerBotTakeover;
if (permadeathMode && ironmanMode)
{
takeOverBotButton.Visible = false;
deathChoiceTickBox.Visible = false;
deathChoiceTickBox.Selected = false;
}
else
{
takeOverBotButton.Visible = permadeathMode && GameMain.NetworkMember?.ServerSettings is { AllowBotTakeoverOnPermadeath: true };
deathChoiceTickBox.Visible = !permadeathMode;
deathChoiceTickBox.Selected = !waitForNextRoundRespawn;
}
}
}
public void Draw(SpriteBatch spriteBatch)

View File

@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System;
using System.Collections.Generic;
@@ -81,6 +81,22 @@ namespace Barotrauma
public void Update(float deltaTime)
{
processAfflictionChangesTimer -= deltaTime;
if (processAfflictionChangesTimer <= 0.0f)
{
foreach (var character in charactersWithAfflictionChanges)
{
if (GameMain.NetworkMember is null)
{
ImmutableArray<NetAffliction> afflictions = GetAllAfflictions(character.CharacterHealth);
ui?.UpdateAfflictions(new NetCrewMember(character.Info, afflictions));
}
ui?.UpdateCrewPanel();
}
charactersWithAfflictionChanges.Clear();
processAfflictionChangesTimer = ProcessAfflictionChangesInterval;
}
DateTimeOffset now = DateTimeOffset.Now;
UpdateQueue(afflictionRequests, now, onTimeout: static callback => { callback(new AfflictionRequest(RequestResult.Timeout, ImmutableArray<NetAffliction>.Empty)); });
UpdateQueue(pendingHealRequests, now, onTimeout: static callback => { callback(new PendingRequest(RequestResult.Timeout, NetCollection<NetCrewMember>.Empty)); });

View File

@@ -785,7 +785,7 @@ namespace Barotrauma
{
if (item.Container == null || character.Inventory.FindIndex(item.Container) == -1) // Not a subinventory in the character's inventory
{
if (character.HeldItems.Any(i => i.OwnInventory != null && i.OwnInventory.CanBePut(item)))
if (character.HeldItems.Any(i => i.OwnInventory != null && i.OwnInventory.CanBePut(item) && character.CanAccessInventory(i.OwnInventory)))
{
return QuickUseAction.PutToEquippedItem;
}
@@ -843,13 +843,14 @@ namespace Barotrauma
else if (character.HeldItems.FirstOrDefault(i =>
i.OwnInventory != null &&
i.OwnInventory.Container.DrawInventory &&
character.CanAccessInventory(i.OwnInventory) &&
(i.OwnInventory.CanBePut(item) || ((i.OwnInventory.Capacity == 1 || i.OwnInventory.Container.HasSubContainers) && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item)))) is { } equippedContainer)
{
if (allowEquip)
{
if (!character.HasEquippedItem(item))
{
if (equippedContainer.GetComponent<ItemContainer>() is { QuickUseMovesItemsInside: false})
if (equippedContainer.GetComponent<ItemContainer>() is { QuickUseMovesItemsInside: false })
{
//put the item in a hand slot if that hand is free
if ((item.AllowedSlots.Contains(InvSlotType.RightHand) && character.Inventory.GetItemInLimbSlot(InvSlotType.RightHand) == null) ||

View File

@@ -79,7 +79,8 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, IsPropertySaveable.No, description: "If true, the contained state indicator calculates how full the item is based on the total amount of items that can be stacked inside it, as opposed to how many of the inventory slots are occupied.")]
[Serialize(false, IsPropertySaveable.No, description: "If true, the contained state indicator calculates how full the item is based on the total amount of items that can be stacked inside it, as opposed to how many of the inventory slots are occupied." +
" Note that only items in the main container or in the subcontainer are counted, depending on which container the first containable item match is found in. The item determining this can be defined with ContainedStateIndicatorSlot")]
public bool ShowTotalStackCapacityInContainedStateIndicator { get; set; }
[Serialize(false, IsPropertySaveable.No, description: "Should the inventory of this item be kept open when the item is equipped by a character.")]
@@ -274,8 +275,14 @@ namespace Barotrauma.Items.Components
}
}
itemsPerSlot.Sort((i1, i2) => i1.First().Name.CompareTo(i2.First().Name));
foreach (var items in itemsPerSlot)
var sortedItems = itemsPerSlot
.OrderBy(i => i.First().Name)
//if there's multiple items with the same name, sort largest stacks first
.ThenByDescending(i => i.Count)
//same name and stack size, sort items with most items inside first
.ThenByDescending(i => i.First().ContainedItems.Count());
foreach (var items in sortedItems)
{
int firstFreeSlot = -1;
for (int i = 0; i < Inventory.Capacity; i++)
@@ -591,7 +598,8 @@ namespace Barotrauma.Items.Components
contained.Item.Scale,
spriteEffects,
depth: containedSpriteDepth);
contained.Item.DrawDecorativeSprites(spriteBatch, itemPos, flipX,flipY, (contained.Item.body == null ? 0.0f : contained.Item.body.DrawRotation), containedSpriteDepth);
contained.Item.DrawDecorativeSprites(spriteBatch, itemPos, flipX,flipY, (contained.Item.body == null ? 0.0f : contained.Item.body.DrawRotation),
containedSpriteDepth, overrideColor);
foreach (ItemContainer ic in contained.Item.GetComponents<ItemContainer>())
{

View File

@@ -40,7 +40,7 @@ namespace Barotrauma.Items.Components
partial void SetLightSourceState(bool enabled, float brightness)
{
if (Light == null) { return; }
if (item.HiddenInGame) { enabled = false; }
if (item.IsHidden) { enabled = false; }
Light.Enabled = enabled;
lightColorMultiplier = brightness;
if (enabled)

View File

@@ -429,7 +429,7 @@ namespace Barotrauma.Items.Components
{
if (it?.Submarine == null) { return false; }
if (item.Submarine == null || !item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true)) { return false; }
if (it.NonInteractable || it.HiddenInGame) { return false; }
if (it.NonInteractable || it.IsHidden) { return false; }
if (it.GetComponent<Pickable>() == null) { return false; }
var holdable = it.GetComponent<Holdable>();
@@ -470,10 +470,10 @@ namespace Barotrauma.Items.Components
scissorComponent = new GUIScissorComponent(new RectTransform(Vector2.One, submarineContainer.RectTransform, Anchor.Center));
miniMapContainer = new GUIFrame(new RectTransform(Vector2.One, scissorComponent.Content.RectTransform, Anchor.Center), style: null) { CanBeFocused = false };
ImmutableHashSet<Item> hullPointsOfInterest = Item.ItemList.Where(it => item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true) && !it.HiddenInGame && !it.NonInteractable && it.Prefab.ShowInStatusMonitor && (it.GetComponent<Door>() != null || it.GetComponent<Turret>() != null)).ToImmutableHashSet();
ImmutableHashSet<Item> hullPointsOfInterest = Item.ItemList.Where(it => item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true) && !it.IsHidden && !it.NonInteractable && it.Prefab.ShowInStatusMonitor && (it.GetComponent<Door>() != null || it.GetComponent<Turret>() != null)).ToImmutableHashSet();
miniMapFrame = CreateMiniMap(item.Submarine, submarineContainer, MiniMapSettings.Default, hullPointsOfInterest, out hullStatusComponents);
IEnumerable<Item> electricalPointsOfInterest = Item.ItemList.Where(it => item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true) && !it.HiddenInGame && !it.NonInteractable && it.GetComponent<Repairable>() != null);
IEnumerable<Item> electricalPointsOfInterest = Item.ItemList.Where(it => item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true) && !it.IsHidden && !it.NonInteractable && it.GetComponent<Repairable>() != null);
electricalFrame = CreateMiniMap(item.Submarine, miniMapContainer, new MiniMapSettings(createHullElements: false), electricalPointsOfInterest, out electricalMapComponents);
Dictionary<MiniMapGUIComponent, GUIComponent> electricChildren = new Dictionary<MiniMapGUIComponent, GUIComponent>();
@@ -566,7 +566,7 @@ namespace Barotrauma.Items.Components
displayedSubs.Add(item.Submarine);
displayedSubs.AddRange(item.Submarine.DockedTo.Where(s => s.TeamID == item.Submarine.TeamID));
subEntities = MapEntity.MapEntityList.Where(me => (item.Submarine is { } sub && sub.IsEntityFoundOnThisSub(me, includingConnectedSubs: true, allowDifferentType: false)) && !me.HiddenInGame).OrderByDescending(w => w.SpriteDepth).ToList();
subEntities = MapEntity.MapEntityList.Where(me => (item.Submarine is { } sub && sub.IsEntityFoundOnThisSub(me, includingConnectedSubs: true, allowDifferentType: false)) && !me.IsHidden).OrderByDescending(w => w.SpriteDepth).ToList();
BakeSubmarine(item.Submarine, parentRect);
elementSize = GuiFrame.Rect.Size;
@@ -763,7 +763,7 @@ namespace Barotrauma.Items.Components
worldBorders.Location += item.Submarine.WorldPosition.ToPoint();
foreach (Gap gap in Gap.GapList)
{
if (gap.IsRoomToRoom || gap.linkedTo.Count == 0 || gap.Submarine != item.Submarine || gap.ConnectedDoor != null || gap.HiddenInGame) { continue; }
if (gap.IsRoomToRoom || gap.linkedTo.Count == 0 || gap.Submarine != item.Submarine || gap.ConnectedDoor != null || gap.IsHidden) { continue; }
RectangleF entityRect = ScaleRectToUI(gap, miniMapFrame.Rect, worldBorders);
Vector2 scale = new Vector2(entityRect.Size.X / spriteSize.X, entityRect.Size.Y / spriteSize.Y) * 2.0f;
@@ -930,7 +930,7 @@ namespace Barotrauma.Items.Components
if (DisplayAsSameItem(it.Prefab, searchedPrefab))
{
// ignore items on players and hidden inventories
if (it.FindParentInventory(inv => inv is CharacterInventory || inv is ItemInventory { Owner: Item { HiddenInGame: true }}) is { }) { continue; }
if (it.FindParentInventory(inv => inv is CharacterInventory || inv is ItemInventory { Owner: Item { IsHidden: true }}) is { }) { continue; }
if (it.FindParentInventory(inventory => inventory is ItemInventory { Owner: Item { ParentInventory: null } }) is ItemInventory parent)
{
@@ -1112,7 +1112,7 @@ namespace Barotrauma.Items.Components
if (ShowHullIntegrity)
{
float amount = 1f + hullData.LinkedHulls.Count;
gapOpenSum = hull.ConnectedGaps.Concat(hullData.LinkedHulls.SelectMany(h => h.ConnectedGaps)).Where(g => g.linkedTo.Count == 1 && !g.HiddenInGame).Sum(g => g.Open) / amount;
gapOpenSum = hull.ConnectedGaps.Concat(hullData.LinkedHulls.SelectMany(h => h.ConnectedGaps)).Where(g => g.linkedTo.Count == 1 && !g.IsHidden).Sum(g => g.Open) / amount;
borderColor = Color.Lerp(neutralColor, GUIStyle.Red, Math.Min(gapOpenSum, 1.0f));
}
@@ -1557,7 +1557,7 @@ namespace Barotrauma.Items.Components
{
if (linkedEntity is Hull linkedHull)
{
if (linkedHulls.Contains(linkedHull) || linkedHull.HiddenInGame) { continue; }
if (linkedHulls.Contains(linkedHull) || linkedHull.IsHidden) { continue; }
linkedHulls.Add(linkedHull);
GetLinkedHulls(linkedHull, linkedHulls);
}
@@ -1737,7 +1737,7 @@ namespace Barotrauma.Items.Components
bool IsPartofSub(MapEntity entity)
{
if (entity.Submarine != sub && !connectedSubs.Contains(entity.Submarine) || entity.HiddenInGame) { return false; }
if (entity.Submarine != sub && !connectedSubs.Contains(entity.Submarine) || entity.IsHidden) { return false; }
return sub.IsEntityFoundOnThisSub(entity, true);
}

View File

@@ -1186,7 +1186,7 @@ namespace Barotrauma.Items.Components
foreach (DockingPort dockingPort in DockingPort.List)
{
if (Level.Loaded != null && dockingPort.Item.Submarine.WorldPosition.Y > Level.Loaded.Size.Y) { continue; }
if (dockingPort.Item.HiddenInGame) { continue; }
if (dockingPort.Item.IsHidden) { continue; }
if (dockingPort.Item.Submarine == null) { continue; }
if (dockingPort.Item.Submarine.Info.IsWreck) { continue; }
// docking ports should be shown even if defined as not, if the submarine is the same as the sonar's

View File

@@ -60,7 +60,7 @@ namespace Barotrauma.Items.Components
public override bool ShouldDrawHUD(Character character)
{
if (item.HiddenInGame) { return false; }
if (item.IsHidden) { return false; }
if (!HasRequiredItems(character, false) || character.SelectedItem != item) { return false; }
if (character.IsTraitor && item.ConditionPercentage > MinSabotageCondition) { return true; }
if (item.ConditionPercentageRelativeToDefaultMaxCondition < RepairThreshold) { return true; }
@@ -224,7 +224,7 @@ namespace Barotrauma.Items.Components
partial void UpdateProjSpecific(float deltaTime)
{
if (item.HiddenInGame) { return; }
if (item.IsHidden) { return; }
if (FakeBrokenTimer > 0.0f)
{
item.FakeBroken = true;
@@ -397,6 +397,12 @@ namespace Barotrauma.Items.Components
GUI.DrawString(spriteBatch,
new Vector2(item.DrawPosition.X, -item.DrawPosition.Y + 20), "Condition: " + (int)item.Condition + "/" + (int)item.MaxCondition,
GUIStyle.Orange);
if (MaxStressDeteriorationMultiplier > 1.0f)
{
GUI.DrawString(spriteBatch,
new Vector2(item.DrawPosition.X, -item.DrawPosition.Y + 40), "Stress multiplier: " + StressDeteriorationMultiplier.ToString("0.00"),
GUIStyle.Red);
}
}
}

View File

@@ -303,6 +303,17 @@ namespace Barotrauma.Items.Components
CreateClientEvent(new CircuitBoxRenameLabelEvent(label.ID, color, header, body));
}
public void SetConnectionLabelOverrides(CircuitBoxInputOutputNode node, Dictionary<string, string> newOverrides)
{
if (GameMain.NetworkMember is null)
{
node.ReplaceAllConnectionLabelOverrides(newOverrides);
return;
}
CreateClientEvent(new CircuitBoxRenameConnectionLabelsEvent(node.NodeType, newOverrides.ToNetDictionary()));
}
public void ResizeNode(CircuitBoxNode node, CircuitBoxResizeDirection dir, Vector2 amount)
{
if (Locked) { return; }
@@ -528,6 +539,12 @@ namespace Barotrauma.Items.Components
_ => node.Position
};
}
foreach (var labelOverride in data.LabelOverrides)
{
RenameConnectionLabelsInternal(labelOverride.Type, labelOverride.Override.ToDictionary());
}
wasInitializedByServer = true;
break;
}
@@ -556,6 +573,12 @@ namespace Barotrauma.Items.Components
ResizeLabelInternal(data.ID, data.Position, data.Size);
break;
}
case CircuitBoxOpcode.RenameConnections:
{
var data = INetSerializableStruct.Read<CircuitBoxRenameConnectionLabelsEvent>(msg);
RenameConnectionLabelsInternal(data.Type, data.Override.ToDictionary());
break;
}
default:
throw new ArgumentOutOfRangeException(nameof(header), header, "This opcode cannot be handled using entity events");
}

View File

@@ -292,8 +292,17 @@ namespace Barotrauma.Items.Components
if (wire.HiddenInGame && Screen.Selected == GameMain.GameScreen) { continue; }
Connection recipient = wire.OtherConnection(this);
LocalizedString label = recipient == null ? "" : recipient.item.Name + $" ({recipient.DisplayName})";
if (wire.Locked) { label += "\n" + TextManager.Get("ConnectionLocked"); }
LocalizedString label;
if (wire.Item.IsLayerHidden)
{
label = TextManager.Get("ConnectionLocked");
}
else
{
label = recipient == null ? "" : recipient.item.Name + $" ({recipient.DisplayName})";
if (wire.Locked) { label += "\n" + TextManager.Get("ConnectionLocked"); }
}
DrawWire(spriteBatch, wire, position, wirePosition, equippedWire, panel, label);
wirePosition.Y += wireInterval;
@@ -494,7 +503,7 @@ namespace Barotrauma.Items.Components
ConnectionPanel.HighlightedWire = wire;
bool allowRewiring = GameMain.NetworkMember?.ServerSettings == null || GameMain.NetworkMember.ServerSettings.AllowRewiring || panel.AlwaysAllowRewiring;
if (allowRewiring && (!wire.Locked && !panel.Locked && !panel.TemporarilyLocked || Screen.Selected == GameMain.SubEditorScreen))
if (allowRewiring && (!wire.Locked && !wire.Item.IsLayerHidden && !panel.Locked && !panel.TemporarilyLocked || Screen.Selected == GameMain.SubEditorScreen))
{
//start dragging the wire
if (PlayerInput.PrimaryMouseButtonHeld()) { DraggingConnected = wire; }

View File

@@ -283,12 +283,12 @@ namespace Barotrauma.Items.Components
texts.Add(CharacterHUD.GetCachedHudText("PlayHint", InputType.Use));
textColors.Add(GUIStyle.Green);
}
if (target.CharacterHealth.UseHealthWindow && !target.DisableHealthWindow && equipper?.FocusedCharacter == target && equipper.CanInteractWith(target, 160f, false))
if (equipper?.FocusedCharacter == target && target.CanBeHealedBy(equipper, checkFriendlyTeam: false))
{
texts.Add(CharacterHUD.GetCachedHudText("HealHint", InputType.Health));
textColors.Add(GUIStyle.Green);
}
if (target.CanBeDragged)
if (target.CanBeDraggedBy(Character.Controlled))
{
texts.Add(CharacterHUD.GetCachedHudText("GrabHint", InputType.Grab));
textColors.Add(GUIStyle.Green);

View File

@@ -196,7 +196,7 @@ namespace Barotrauma.Items.Components
Vector2 particlePos = GetRelativeFiringPosition();
foreach (ParticleEmitter emitter in particleEmitters)
{
emitter.Emit(1.0f, particlePos, hullGuess: null, angle: -rotation, particleRotation: rotation);
emitter.Emit(1.0f, particlePos, hullGuess: null, angle: -Rotation, particleRotation: Rotation);
}
}
@@ -213,7 +213,7 @@ namespace Barotrauma.Items.Components
if (crosshairSprite != null)
{
Vector2 itemPos = cam.WorldToScreen(new Vector2(item.WorldRect.X + transformedBarrelPos.X, item.WorldRect.Y - transformedBarrelPos.Y));
Vector2 turretDir = new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation));
Vector2 turretDir = new Vector2((float)Math.Cos(Rotation), (float)Math.Sin(Rotation));
Vector2 mouseDiff = itemPos - PlayerInput.MousePosition;
crosshairPos = new Vector2(
@@ -268,7 +268,7 @@ namespace Barotrauma.Items.Components
foreach (ParticleEmitter emitter in particleEmitterCharges)
{
// color is currently not connected to ammo type, should be updated when ammo is changed
emitter.Emit(deltaTime, particlePos, hullGuess: null, angle: -rotation, particleRotation: rotation, sizeMultiplier: sizeMultiplier, colorMultiplier: emitter.Prefab.Properties.ColorMultiplier);
emitter.Emit(deltaTime, particlePos, hullGuess: null, angle: -Rotation, particleRotation: Rotation, sizeMultiplier: sizeMultiplier, colorMultiplier: emitter.Prefab.Properties.ColorMultiplier);
}
if (chargeSoundChannel == null || !chargeSoundChannel.IsPlaying)
@@ -339,7 +339,7 @@ namespace Barotrauma.Items.Components
if (crosshairSprite != null)
{
Vector2 itemPos = cam.WorldToScreen(item.WorldPosition);
Vector2 turretDir = new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation));
Vector2 turretDir = new Vector2((float)Math.Cos(Rotation), (float)Math.Sin(Rotation));
Vector2 mouseDiff = itemPos - PlayerInput.MousePosition;
crosshairPos = new Vector2(
@@ -372,7 +372,7 @@ namespace Barotrauma.Items.Components
recoilOffset = RecoilDistance;
}
}
return new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation)) * recoilOffset;
return new Vector2((float)Math.Cos(Rotation), (float)Math.Sin(Rotation)) * recoilOffset;
}
public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1, Color? overrideColor = null)
@@ -388,13 +388,13 @@ namespace Barotrauma.Items.Components
railSprite?.Draw(spriteBatch,
drawPos,
overrideColor ?? item.SpriteColor,
rotation + MathHelper.PiOver2, item.Scale,
Rotation + MathHelper.PiOver2, item.Scale,
SpriteEffects.None, item.SpriteDepth + (railSprite.Depth - item.Sprite.Depth));
barrelSprite?.Draw(spriteBatch,
drawPos - GetRecoilOffset() * item.Scale,
overrideColor ?? item.SpriteColor,
rotation + MathHelper.PiOver2, item.Scale,
Rotation + MathHelper.PiOver2, item.Scale,
SpriteEffects.None, item.SpriteDepth + (barrelSprite.Depth - item.Sprite.Depth));
float chargeRatio = currentChargeTime / MaxChargeTime;
@@ -402,9 +402,9 @@ namespace Barotrauma.Items.Components
foreach ((Sprite chargeSprite, Vector2 position) in chargeSprites)
{
chargeSprite?.Draw(spriteBatch,
drawPos - MathUtils.RotatePoint(new Vector2(position.X * chargeRatio, position.Y * chargeRatio) * item.Scale, rotation + MathHelper.PiOver2),
drawPos - MathUtils.RotatePoint(new Vector2(position.X * chargeRatio, position.Y * chargeRatio) * item.Scale, Rotation + MathHelper.PiOver2),
item.SpriteColor,
rotation + MathHelper.PiOver2, item.Scale,
Rotation + MathHelper.PiOver2, item.Scale,
SpriteEffects.None, item.SpriteDepth + (chargeSprite.Depth - item.Sprite.Depth));
}
@@ -427,9 +427,9 @@ namespace Barotrauma.Items.Components
float newPositionOffset = barrelPositionModifier * SpinningBarrelDistance;
spinningBarrel.Draw(spriteBatch,
drawPos - MathUtils.RotatePoint(new Vector2(newPositionOffset, 0f) * item.Scale, rotation + MathHelper.PiOver2),
drawPos - MathUtils.RotatePoint(new Vector2(newPositionOffset, 0f) * item.Scale, Rotation + MathHelper.PiOver2),
Color.Lerp(overrideColor ?? item.SpriteColor, newColorModifier, 0.8f),
rotation + MathHelper.PiOver2, item.Scale,
Rotation + MathHelper.PiOver2, item.Scale,
SpriteEffects.None, newDepth);
}
}
@@ -475,9 +475,9 @@ namespace Barotrauma.Items.Components
{
spriteBatch.DrawLine(drawPos, drawPos + center * circleRadius, GUIStyle.Green, thickness: lineThickness);
}
else if (radians > Math.PI * 2)
else if (radians >= MathHelper.TwoPi)
{
spriteBatch.DrawCircle(drawPos, circleRadius, 180, GUIStyle.Red, thickness: lineThickness);
spriteBatch.DrawCircle(drawPos, circleRadius, 180, GUIStyle.Green, thickness: lineThickness);
}
else
{
@@ -510,7 +510,12 @@ namespace Barotrauma.Items.Components
};
widget.MouseHeld += (deltaTime) =>
{
minRotation = GetRotationAngle(GetDrawPos());
float newMinRotation = GetRotationAngle(GetDrawPos());
AngleWrapAdjustment(minRotation, newMinRotation, ref maxRotation);
// clamp value here to keep widget movement within max range
minRotation = MathHelper.Clamp(newMinRotation, maxRotation - MathHelper.TwoPi, maxRotation);
UpdateBarrel();
MapEntity.DisableSelect = true;
};
@@ -554,7 +559,12 @@ namespace Barotrauma.Items.Components
};
widget.MouseHeld += (deltaTime) =>
{
maxRotation = GetRotationAngle(GetDrawPos());
float newMaxRotation = GetRotationAngle(GetDrawPos());
AngleWrapAdjustment(maxRotation, newMaxRotation, ref minRotation);
// clamp value here to keep widget movement within max range
maxRotation = MathHelper.Clamp(newMaxRotation, minRotation, minRotation + MathHelper.TwoPi);
UpdateBarrel();
MapEntity.DisableSelect = true;
};
@@ -580,10 +590,44 @@ namespace Barotrauma.Items.Components
void UpdateBarrel()
{
rotation = (minRotation + maxRotation) / 2;
Rotation = (minRotation + maxRotation) / 2;
}
}
private static void AngleWrapAdjustment(float currentRotation, float newRotation, ref float rangeLockedRotation)
{
if (DetectAngleWrapAround(currentRotation, newRotation))
{
// if there's a wrap-around, also wrap the other rotation limit to keep range
if (newRotation < currentRotation)
{
rangeLockedRotation -= MathHelper.TwoPi;
}
else
{
rangeLockedRotation += MathHelper.TwoPi;
}
}
}
private static bool DetectAngleWrapAround(float rotation, float newRotation)
{
float deltaRotation = MathF.Abs(rotation - newRotation);
// turret angle wraps around to 0 from -2Pi and 2Pi.
// Detect wrap-around when dragging the widgets, where usual rotation delta is small,
// so a large jump in rotation (here, an arbitrary big value in the range of 0 to 2Pi)
// is considered a wrap-around for this purpose.
// NOTE: this is not a reliable way to detect angle wrap-around in general, and is only intended for
// the angle widgets!
if (deltaRotation > MathHelper.TwoPi * 0.8f)
{
return true;
}
return false;
}
public Vector2 GetDrawPos()
{
Vector2 drawPos = new Vector2(item.Rect.X + transformedBarrelPos.X, item.Rect.Y - transformedBarrelPos.Y);
@@ -764,7 +808,7 @@ namespace Barotrauma.Items.Components
if (projectileID == 0) { return; }
//ID ushort.MaxValue = launched without a projectile
if (projectileID == ushort.MaxValue)
if (projectileID == LaunchWithoutProjectileId)
{
Launch(null, user);
}

View File

@@ -319,7 +319,7 @@ namespace Barotrauma
}
}
string colorStr = (item.SpawnedInCurrentOutpost && !item.AllowStealing ? GUIStyle.Red : Color.White).ToStringHex();
string colorStr = (item.Illegitimate ? GUIStyle.Red : Color.White).ToStringHex();
toolTip = $"‖color:{colorStr}‖{name}‖color:end‖";
if (item.GetComponent<Quality>() != null)
@@ -478,10 +478,11 @@ namespace Barotrauma
{
int row = (int)Math.Floor((double)i / slotsPerRow);
int slotsPerThisRow = Math.Min(slotsPerRow, capacity - row * slotsPerRow);
int slotNumberOnThisRow = i - row * slotsPerRow;
int rowWidth = (int)(rectSize.X * slotsPerThisRow + spacing.X * (slotsPerThisRow - 1));
slotRect.X = (int)(center.X) - rowWidth / 2;
slotRect.X += (int)((rectSize.X + spacing.X) * (i % slotsPerThisRow));
slotRect.X += (int)((rectSize.X + spacing.X) * (slotNumberOnThisRow % slotsPerThisRow));
slotRect.Y = (int)(topLeft.Y + (rectSize.Y + spacing.Y) * row);
visualSlots[i] = new VisualSlot(slotRect);
@@ -1185,6 +1186,7 @@ namespace Barotrauma
{
DraggingItems.RemoveAll(it => !Character.Controlled.CanInteractWith(it));
}
if (DraggingItems.Any() && PlayerInput.PrimaryMouseButtonReleased())
{
Character.Controlled.ClearInputs();
@@ -1193,198 +1195,234 @@ namespace Barotrauma
if (!DetermineMouseOnInventory(ignoreDraggedItem: true) &&
(CharacterHealth.OpenHealthWindow != null || mouseOnPortrait))
{
bool dropSuccessful = false;
foreach (Item item in DraggingItems)
if (TryPortraitAndHealthDrop(mouseOnPortrait))
{
var inventory = item.ParentInventory;
var indices = inventory?.FindIndices(item);
dropSuccessful |= (CharacterHealth.OpenHealthWindow ?? Character.Controlled.CharacterHealth).OnItemDropped(item, ignoreMousePos: mouseOnPortrait);
if (dropSuccessful)
{
if (indices != null && inventory.visualSlots != null)
{
foreach (int i in indices)
{
inventory.visualSlots[i]?.ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.4f);
}
}
break;
}
}
if (dropSuccessful)
{
DraggingItems.Clear();
return;
}
}
if (selectedSlot == null)
{
if (DraggingItemToWorld &&
Character.Controlled.FocusedItem is { OwnInventory: { } inventory } item && item.GetComponent<ItemContainer>() is { } container &&
container.HasRequiredItems(Character.Controlled, addMessage: false) &&
container.AllowDragAndDrop &&
inventory.CanBePut(DraggingItems.FirstOrDefault()))
HandleOutsideInventoryDrop();
}
else if (!DraggingItems.Any(it => selectedSlot.ParentInventory.slots[selectedSlot.SlotIndex].Contains(it)))
{
HandleInventorySlotDrop();
}
DraggingItems.Clear();
}
if (selectedSlot != null && !CanSelectSlot(selectedSlot))
{
selectedSlot = null;
}
bool TryPortraitAndHealthDrop(bool mouseOnPortrait)
{
bool dropSuccessful = false;
foreach (Item item in DraggingItems)
{
var inventory = item.ParentInventory;
var indices = inventory?.FindIndices(item);
dropSuccessful |= (CharacterHealth.OpenHealthWindow ?? Character.Controlled.CharacterHealth).OnItemDropped(item, ignoreMousePos: mouseOnPortrait);
if (dropSuccessful)
{
bool anySuccess = false;
foreach (Item it in DraggingItems)
if (indices != null && inventory.visualSlots != null)
{
bool success = Character.Controlled.FocusedItem.OwnInventory.TryPutItem(it, Character.Controlled);
if (!success) { break; }
anySuccess |= success;
}
if (anySuccess) { SoundPlayer.PlayUISound(GUISoundType.PickItem); }
}
else
{
if (Screen.Selected is SubEditorScreen)
{
if (DraggingItems.First()?.ParentInventory != null)
foreach (int i in indices)
{
SubEditorScreen.StoreCommand(new InventoryPlaceCommand(DraggingItems.First().ParentInventory, new List<Item>(DraggingItems), true));
inventory.visualSlots[i]?.ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.4f);
}
}
SoundPlayer.PlayUISound(GUISoundType.DropItem);
bool removed = false;
if (Screen.Selected is SubEditorScreen editor)
break;
}
}
if (dropSuccessful)
{
DraggingItems.Clear();
return true;
}
return false;
}
void HandleOutsideInventoryDrop()
{
bool isTargetingValidContainer = Character.Controlled.FocusedItem is { OwnInventory: { } inventory } item &&
item.GetComponent<ItemContainer>() is { } container &&
container.HasRequiredItems(Character.Controlled, addMessage: false) &&
container.AllowDragAndDrop &&
inventory.CanBePut(DraggingItems.FirstOrDefault());
bool isTargetingValidCharacter = IsValidTargetForDragDropGive(Character.Controlled, Character.Controlled.FocusedCharacter);
if (DraggingItemToWorld && (isTargetingValidContainer || isTargetingValidCharacter))
{
bool anySuccess = false;
foreach (Item it in DraggingItems)
{
bool success = false;
if (isTargetingValidContainer)
{
if (editor.EntityMenu.Rect.Contains(PlayerInput.MousePosition))
success = Character.Controlled.FocusedItem.OwnInventory.TryPutItem(it, Character.Controlled);
}
if (!success && isTargetingValidCharacter)
{
success = Character.Controlled.FocusedCharacter.Inventory.TryPutItem(it, Character.Controlled, CharacterInventory.AnySlot);
}
if (!success) { break; }
anySuccess = true;
}
if (anySuccess) { SoundPlayer.PlayUISound(GUISoundType.PickItem); }
}
else
{
if (Screen.Selected is SubEditorScreen)
{
if (DraggingItems.First()?.ParentInventory != null)
{
SubEditorScreen.StoreCommand(new InventoryPlaceCommand(DraggingItems.First().ParentInventory, new List<Item>(DraggingItems), true));
}
}
SoundPlayer.PlayUISound(GUISoundType.DropItem);
bool removed = false;
if (Screen.Selected is SubEditorScreen editor)
{
if (editor.EntityMenu.Rect.Contains(PlayerInput.MousePosition))
{
DraggingItems.ForEachMod(it => it.Remove());
removed = true;
}
else
{
if (editor.WiringMode)
{
DraggingItems.ForEachMod(it => it.Remove());
removed = true;
}
else
{
if (editor.WiringMode)
{
DraggingItems.ForEachMod(it => it.Remove());
removed = true;
}
else
{
DraggingItems.ForEachMod(it => it.Drop(Character.Controlled));
}
DraggingItems.ForEachMod(it => it.Drop(Character.Controlled));
}
}
else
{
DraggingItems.ForEachMod(it => it.Drop(Character.Controlled));
DraggingItems.First().CreateDroppedStack(DraggingItems, allowClientExecute: false);
}
SoundPlayer.PlayUISound(removed ? GUISoundType.PickItem : GUISoundType.DropItem);
}
}
else if (!DraggingItems.Any(it => selectedSlot.ParentInventory.slots[selectedSlot.SlotIndex].Contains(it)))
{
Inventory oldInventory = DraggingItems.First().ParentInventory;
Inventory selectedInventory = selectedSlot.ParentInventory;
int slotIndex = selectedSlot.SlotIndex;
int oldSlot = oldInventory == null ? 0 : Array.IndexOf(oldInventory.slots, DraggingItems);
//if attempting to drop into an invalid slot in the same inventory, try to move to the correct slot
if (selectedInventory.slots[slotIndex].Empty() &&
selectedInventory == Character.Controlled.Inventory &&
!DraggingItems.First().AllowedSlots.Any(a => a.HasFlag(Character.Controlled.Inventory.SlotTypes[slotIndex])) &&
DraggingItems.Any(it => selectedInventory.TryPutItem(it, Character.Controlled, it.AllowedSlots)))
else
{
if (selectedInventory.visualSlots != null)
DraggingItems.ForEachMod(it => it.Drop(Character.Controlled));
DraggingItems.First().CreateDroppedStack(DraggingItems, allowClientExecute: false);
}
SoundPlayer.PlayUISound(removed ? GUISoundType.PickItem : GUISoundType.DropItem);
}
}
void HandleInventorySlotDrop()
{
Inventory oldInventory = DraggingItems.First().ParentInventory;
Inventory selectedInventory = selectedSlot.ParentInventory;
int slotIndex = selectedSlot.SlotIndex;
int oldSlot = oldInventory == null ? 0 : Array.IndexOf(oldInventory.slots, DraggingItems);
//if attempting to drop into an invalid slot in the same inventory, try to move to the correct slot
if (selectedInventory.slots[slotIndex].Empty() &&
selectedInventory == Character.Controlled.Inventory &&
!DraggingItems.First().AllowedSlots.Any(a => a.HasFlag(Character.Controlled.Inventory.SlotTypes[slotIndex])) &&
DraggingItems.Any(it => selectedInventory.TryPutItem(it, Character.Controlled, it.AllowedSlots)))
{
if (selectedInventory.visualSlots != null)
{
for (int i = 0; i < selectedInventory.visualSlots.Length; i++)
{
for (int i = 0; i < selectedInventory.visualSlots.Length; i++)
if (DraggingItems.Any(it => selectedInventory.slots[i].Contains(it)))
{
if (DraggingItems.Any(it => selectedInventory.slots[i].Contains(it)))
selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(Color.White, 0.1f, 0.4f);
}
}
selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f);
}
SoundPlayer.PlayUISound(GUISoundType.PickItem);
}
else
{
bool anySuccess = false;
//if we're dragging a stack of partial items or trying to drag to a stack of partial items
//(which should not normally exist, but can happen when e.g. fire damages a stack of items)
//don't allow combining because it leads to weird behavior (stack of items of mixed quality)
bool allowCombine = !(DraggingItems.Count(it => !it.IsFullCondition && it.Condition > 0.0f) > 1 ||
selectedInventory.GetItemsAt(slotIndex).Count(it => !it.IsFullCondition && it.Condition > 0.0f) > 1);
int itemCount = 0;
foreach (Item item in DraggingItems)
{
if (selectedInventory.GetItemAt(slotIndex)?.OwnInventory?.Container is { } container &&
container.Inventory.CanBePut(item))
{
if (!container.AllowDragAndDrop || !container.AllowAccess)
{
allowCombine = false;
}
}
bool success = selectedInventory.TryPutItem(item, slotIndex, allowSwapping: !anySuccess, allowCombine, Character.Controlled);
if (success)
{
anySuccess = true;
itemCount++;
}
if (!success || itemCount >= item.Prefab.GetMaxStackSize(selectedInventory))
{
break;
}
}
if (anySuccess)
{
highlightedSubInventorySlots.RemoveWhere(s => s.ParentInventory == oldInventory || s.ParentInventory == selectedInventory);
if (SubEditorScreen.IsSubEditor())
{
foreach (Item draggingItem in DraggingItems)
{
if (selectedInventory.slots[slotIndex].Contains(draggingItem))
{
selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(Color.White, 0.1f, 0.4f);
SubEditorScreen.StoreCommand(new InventoryMoveCommand(oldInventory, selectedInventory, draggingItem, oldSlot, slotIndex));
}
}
selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f);
}
if (selectedInventory.visualSlots != null) { selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(Color.White, 0.1f, 0.4f); }
SoundPlayer.PlayUISound(GUISoundType.PickItem);
}
else
{
bool anySuccess = false;
bool allowCombine = true;
//if we're dragging a stack of partial items or trying to drag to a stack of partial items
//(which should not normally exist, but can happen when e.g. fire damages a stack of items)
//don't allow combining because it leads to weird behavior (stack of items of mixed quality)
if (DraggingItems.Count(it => !it.IsFullCondition && it.Condition > 0.0f) > 1 ||
selectedInventory.GetItemsAt(slotIndex).Count(it => !it.IsFullCondition && it.Condition > 0.0f) > 1)
{
allowCombine = false;
}
int itemCount = 0;
foreach (Item item in DraggingItems)
{
if (selectedInventory.GetItemAt(slotIndex)?.OwnInventory?.Container is { } container &&
container.Inventory.CanBePut(item))
{
if (!container.AllowDragAndDrop || !container.AllowAccess)
{
allowCombine = false;
}
}
bool success = selectedInventory.TryPutItem(item, slotIndex, allowSwapping: !anySuccess, allowCombine, Character.Controlled);
if (success)
{
anySuccess = true;
itemCount++;
}
if (!success || itemCount >= item.Prefab.GetMaxStackSize(selectedInventory))
{
break;
}
}
if (anySuccess)
{
highlightedSubInventorySlots.RemoveWhere(s => s.ParentInventory == oldInventory || s.ParentInventory == selectedInventory);
if (SubEditorScreen.IsSubEditor())
{
foreach (Item draggingItem in DraggingItems)
{
if (selectedInventory.slots[slotIndex].Contains(draggingItem))
{
SubEditorScreen.StoreCommand(new InventoryMoveCommand(oldInventory, selectedInventory, draggingItem, oldSlot, slotIndex));
}
}
}
if (selectedInventory.visualSlots != null) { selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(Color.White, 0.1f, 0.4f); }
SoundPlayer.PlayUISound(GUISoundType.PickItem);
}
else
{
if (selectedInventory.visualSlots != null){ selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f); }
SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
}
if (selectedInventory.visualSlots != null){ selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f); }
SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
}
selectedInventory.HideTimer = 2.0f;
if (selectedSlot.ParentInventory?.Owner is Item parentItem && parentItem.ParentInventory != null)
{
for (int i = 0; i < parentItem.ParentInventory.capacity; i++)
{
if (parentItem.ParentInventory.HideSlot(i)) { continue; }
if (parentItem.ParentInventory.slots[i].FirstOrDefault() != parentItem) { continue; }
highlightedSubInventorySlots.Add(new SlotReference(
parentItem.ParentInventory, parentItem.ParentInventory.visualSlots[i],
i, false, selectedSlot.ParentInventory));
break;
}
}
DraggingItems.Clear();
DraggingSlot = null;
}
DraggingItems.Clear();
}
selectedInventory.HideTimer = 2.0f;
if (selectedSlot.ParentInventory?.Owner is Item parentItem && parentItem.ParentInventory != null)
{
for (int i = 0; i < parentItem.ParentInventory.capacity; i++)
{
if (parentItem.ParentInventory.HideSlot(i)) { continue; }
if (parentItem.ParentInventory.slots[i].FirstOrDefault() != parentItem) { continue; }
if (selectedSlot != null && !CanSelectSlot(selectedSlot))
{
selectedSlot = null;
}
highlightedSubInventorySlots.Add(new SlotReference(
parentItem.ParentInventory, parentItem.ParentInventory.visualSlots[i],
i, false, selectedSlot.ParentInventory));
break;
}
}
DraggingItems.Clear();
DraggingSlot = null;
}
}
private static bool IsValidTargetForDragDropGive(Character giver, Character receiver)
{
if (giver == null || receiver == null) { return false; }
if (receiver == giver) { return false; }
return receiver.IsInventoryAccessibleTo(giver, IsDragAndDropGiveAllowed ? CharacterInventory.AccessLevel.Allowed : CharacterInventory.AccessLevel.Limited);
}
private static bool CanSelectSlot(SlotReference selectedSlot)
@@ -1504,6 +1542,31 @@ namespace Barotrauma
}
if (DraggingItems.Any())
{
DrawDragRelated();
}
if (selectedSlot != null && selectedSlot.Item != null)
{
Rectangle slotRect = selectedSlot.Slot.Rect;
slotRect.Location += selectedSlot.Slot.DrawOffset.ToPoint();
if (selectedSlot.TooltipNeedsRefresh())
{
selectedSlot.RefreshTooltip();
}
if (!slotIconTooltip.IsNullOrEmpty())
{
DrawToolTip(spriteBatch, slotIconTooltip, slotRect);
}
else
{
DrawToolTip(spriteBatch, selectedSlot.Tooltip, slotRect);
}
slotIconTooltip = string.Empty;
}
void DrawDragRelated()
{
if (DraggingSlot == null || (!DraggingSlot.MouseOn()))
{
@@ -1521,10 +1584,8 @@ namespace Barotrauma
if ((GUI.MouseOn == null || mouseOnHealthInterface) && selectedSlot == null)
{
var shadowSprite = GUIStyle.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0];
LocalizedString toolTip = mouseOnHealthInterface ? TextManager.Get("QuickUseAction.UseTreatment") :
Character.Controlled.FocusedItem != null ?
TextManager.GetWithVariable("PutItemIn", "[itemname]", Character.Controlled.FocusedItem.Name, FormatCapitals.Yes) :
TextManager.Get(Screen.Selected is SubEditorScreen editor && editor.EntityMenu.Rect.Contains(PlayerInput.MousePosition) ? "Delete" : "DropItem");
(LocalizedString toolTip, Color toolTipColor) = GetDragLabelTextAndColor(mouseOnHealthInterface);
Vector2 nameSize = GUIStyle.Font.MeasureString(DraggingItems.First().Name);
Vector2 toolTipSize = GUIStyle.SmallFont.MeasureString(toolTip);
@@ -1544,7 +1605,7 @@ namespace Barotrauma
GUI.DrawString(spriteBatch, textPos + new Vector2(nameSize.X * textOffset, -iconSize / 2), DraggingItems.First().Name, Color.White);
GUI.DrawString(spriteBatch, textPos + new Vector2(toolTipSize.X * textOffset, 0), toolTip,
color: Character.Controlled.FocusedItem == null && !mouseOnHealthInterface ? GUIStyle.Red : Color.LightGreen,
color: toolTipColor,
font: GUIStyle.SmallFont);
}
@@ -1587,24 +1648,31 @@ namespace Barotrauma
}
}
if (selectedSlot != null && selectedSlot.Item != null)
(LocalizedString, Color) GetDragLabelTextAndColor(bool mouseOnHealthInterface)
{
Rectangle slotRect = selectedSlot.Slot.Rect;
slotRect.Location += selectedSlot.Slot.DrawOffset.ToPoint();
if (selectedSlot.TooltipNeedsRefresh())
bool useDragDropGive = IsValidTargetForDragDropGive(Character.Controlled, Character.Controlled.FocusedCharacter);
Color toolTipColor = Color.LightGreen;
LocalizedString toolTip;
if (mouseOnHealthInterface)
{
selectedSlot.RefreshTooltip();
toolTip = TextManager.Get("QuickUseAction.UseTreatment");
}
if (!slotIconTooltip.IsNullOrEmpty())
else if (Character.Controlled.FocusedItem != null)
{
DrawToolTip(spriteBatch, slotIconTooltip, slotRect);
toolTip = TextManager.GetWithVariable("PutItemIn", "[itemname]", Character.Controlled.FocusedItem.Name, FormatCapitals.Yes);
}
else if (useDragDropGive)
{
toolTip = TextManager.GetWithVariable("GiveItemTo", "[character]", Character.Controlled.FocusedCharacter.Name, FormatCapitals.Yes);
}
else
{
DrawToolTip(spriteBatch, selectedSlot.Tooltip, slotRect);
toolTipColor = GUIStyle.Red;
toolTip = TextManager.Get(Screen.Selected is SubEditorScreen editor && editor.EntityMenu.Rect.Contains(PlayerInput.MousePosition) ? "Delete" : "DropItem");
}
slotIconTooltip = string.Empty;
return (toolTip, toolTipColor);
}
}
@@ -1801,7 +1869,7 @@ namespace Barotrauma
DrawSideIcon(deconstructOrder.SymbolSprite, Direction.Right, TextManager.Get("tooltip.markedfordeconstruction"), GUIStyle.Red, out bool mouseOn);
if (mouseOn) { availableContextualOrder = (item, Tags.DontDeconstructThis); }
}
else if (((item.SpawnedInCurrentOutpost && !item.AllowStealing) || (inventory != null && inventory.slots[slotIndex].Items.Any(it => it.SpawnedInCurrentOutpost && !it.AllowStealing))) && CharacterInventory.LimbSlotIcons.ContainsKey(InvSlotType.LeftHand))
else if ((item.Illegitimate || (inventory != null && inventory.slots[slotIndex].Items.Any(it => it.Illegitimate))) && CharacterInventory.LimbSlotIcons.ContainsKey(InvSlotType.LeftHand))
{
DrawSideIcon(CharacterInventory.LimbSlotIcons[InvSlotType.LeftHand], Direction.Left, TextManager.Get("tooltip.stolenitem"), GUIStyle.Red, out _);
}

View File

@@ -326,7 +326,7 @@ namespace Barotrauma
public void Draw(SpriteBatch spriteBatch, bool editing, bool back = true, Color? overrideColor = null)
{
if (!Visible || (!editing && HiddenInGame) || !SubEditorScreen.IsLayerVisible(this)) { return; }
if (!Visible || (!editing && IsHidden) || !SubEditorScreen.IsLayerVisible(this)) { return; }
if (editing)
{
@@ -424,7 +424,7 @@ namespace Barotrauma
textureScale: Vector2.One * Scale,
depth: d);
}
DrawDecorativeSprites(spriteBatch, DrawPosition, flippedX && Prefab.CanSpriteFlipX, flippedY && Prefab.CanSpriteFlipY, rotation: 0, depth);
DrawDecorativeSprites(spriteBatch, DrawPosition, flippedX && Prefab.CanSpriteFlipX, flippedY && Prefab.CanSpriteFlipY, rotation: 0, depth, overrideColor);
}
}
else
@@ -445,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);
}
DrawDecorativeSprites(spriteBatch, DrawPosition, flippedX && Prefab.CanSpriteFlipX, flippedY && Prefab.CanSpriteFlipY, -RotationRad, depth);
DrawDecorativeSprites(spriteBatch, DrawPosition, flippedX && Prefab.CanSpriteFlipX, flippedY && Prefab.CanSpriteFlipY, -RotationRad, depth, overrideColor);
}
}
else if (body.Enabled)
@@ -456,30 +456,49 @@ namespace Barotrauma
//don't draw the item on hands if it's also being worn
if (GetComponent<Wearable>() is { IsActive: true }) { return; }
if (!back) { return; }
float depthStep = 0.000001f;
if (holdable.Picker.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == this)
{
Limb holdLimb = holdable.Picker.AnimController.GetLimb(LimbType.RightArm);
if (holdLimb?.ActiveSprite != null)
{
depth = holdLimb.ActiveSprite.Depth + holdable.Picker.AnimController.GetDepthOffset() + depthStep * 2;
foreach (WearableSprite wearableSprite in holdLimb.WearingItems)
{
if (!wearableSprite.InheritLimbDepth && wearableSprite.Sprite != null) { depth = Math.Max(wearableSprite.Sprite.Depth + depthStep, depth); }
}
}
depth = GetHeldItemDepth(LimbType.RightHand, depth);
}
else if (holdable.Picker.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == this)
{
Limb holdLimb = holdable.Picker.AnimController.GetLimb(LimbType.LeftArm);
depth = GetHeldItemDepth(LimbType.LeftHand, depth);
}
float GetHeldItemDepth(LimbType limb, float depth)
{
//offset used to make sure the item draws just slightly behind the right hand, or slightly in front of the left hand
float limbDepthOffset = 0.000001f;
float depthOffset = holdable.Picker.AnimController.GetDepthOffset();
//use the upper arm as a reference, to ensure the item gets drawn behind / in front of the whole arm (not just the forearm)
Limb holdLimb = holdable.Picker.AnimController.GetLimb(limb == LimbType.RightHand ? LimbType.RightArm : LimbType.LeftArm);
if (holdLimb?.ActiveSprite != null)
{
depth = holdLimb.ActiveSprite.Depth + holdable.Picker.AnimController.GetDepthOffset() - depthStep * 2;
depth =
holdLimb.ActiveSprite.Depth
+ depthOffset
+ limbDepthOffset * 2 * (limb == LimbType.RightHand ? 1 : -1);
foreach (WearableSprite wearableSprite in holdLimb.WearingItems)
{
if (!wearableSprite.InheritLimbDepth && wearableSprite.Sprite != null) { depth = Math.Min(wearableSprite.Sprite.Depth - depthStep, depth); }
if (!wearableSprite.InheritLimbDepth && wearableSprite.Sprite != null)
{
depth =
limb == LimbType.RightHand ?
Math.Max(wearableSprite.Sprite.Depth + limbDepthOffset, depth) :
Math.Min(wearableSprite.Sprite.Depth - limbDepthOffset, depth);
}
}
var head = holdable.Picker.AnimController.GetLimb(LimbType.Head);
if (head != null)
{
//ensure the holdable item is always drawn in front of the head no matter what the wearables or whatnot do with the sprite depths
depth =
limb == LimbType.RightHand ?
Math.Min(head.Sprite.Depth + depthOffset - limbDepthOffset, depth) :
Math.Max(head.Sprite.Depth + depthOffset + limbDepthOffset, depth);
}
}
return depth;
}
}
Vector2 origin = GetSpriteOrigin(activeSprite);
@@ -489,7 +508,7 @@ namespace Barotrauma
float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f);
body.Draw(spriteBatch, fadeInBrokenSprite.Sprite, color * fadeInBrokenSpriteAlpha, d, Scale);
}
DrawDecorativeSprites(spriteBatch, body.DrawPosition, flipX: body.Dir < 0, flipY: false, rotation: body.Rotation, depth: depth);
DrawDecorativeSprites(spriteBatch, body.DrawPosition, flipX: body.Dir < 0, flipY: false, rotation: body.Rotation, depth, overrideColor);
}
foreach (var upgrade in Upgrades)
@@ -617,11 +636,11 @@ namespace Barotrauma
}
}
public void DrawDecorativeSprites(SpriteBatch spriteBatch, Vector2 drawPos, bool flipX, bool flipY, float rotation, float depth)
public void DrawDecorativeSprites(SpriteBatch spriteBatch, Vector2 drawPos, bool flipX, bool flipY, float rotation, float depth, Color? overrideColor = null)
{
foreach (var decorativeSprite in Prefab.DecorativeSprites)
{
Color decorativeSpriteColor = GetSpriteColor(decorativeSprite.Color).Multiply(GetSpriteColor(spriteColor));
Color decorativeSpriteColor = overrideColor ?? GetSpriteColor(decorativeSprite.Color).Multiply(GetSpriteColor(spriteColor));
if (!spriteAnimState[decorativeSprite].IsActive) { continue; }
Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier,

View File

@@ -215,7 +215,9 @@ namespace Barotrauma
}
else
{
if (Math.Sign(flowTargetHull.Rect.Y - rect.Y) != Math.Sign(lerpedFlowForce.Y)) { return; }
//do not emit particles unless water is flowing towards the target hull
//(using lerpedFlowForce smooths out "flickers" when the direction of flow is rapidly changing)
if (Math.Sign(flowTargetHull.WorldPosition.Y - WorldPosition.Y) != Math.Sign(lerpedFlowForce.Y)) { return; }
float particlesPerSec = Math.Max(open * rect.Width * particleAmountMultiplier, 10.0f);
float emitInterval = 1.0f / particlesPerSec;

View File

@@ -210,7 +210,9 @@ namespace Barotrauma
{
bool primaryMouseButtonHeld = PlayerInput.PrimaryMouseButtonHeld();
bool secondaryMouseButtonHeld = PlayerInput.SecondaryMouseButtonHeld();
if (!primaryMouseButtonHeld && !secondaryMouseButtonHeld) { return; }
bool doubleClicked = PlayerInput.DoubleClicked();
bool secondaryDoubleClicked = PlayerInput.SecondaryDoubleClicked();
if (!primaryMouseButtonHeld && !secondaryMouseButtonHeld && !doubleClicked && !secondaryDoubleClicked) { return; }
Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition);
Hull hull = FindHull(position);
@@ -218,29 +220,67 @@ namespace Barotrauma
if (hull == null || hull.IdFreed) { return; }
if (EditWater)
{
const float waterIncrement = 100000.0f;
if (primaryMouseButtonHeld)
{
ShowHulls = true;
hull.WaterVolume += 100000.0f * deltaTime;
hull.networkUpdatePending = true;
hull.serverUpdateDelay = 0.5f;
SetWaterVolume(hull.WaterVolume + waterIncrement * deltaTime);
}
else if (secondaryMouseButtonHeld)
{
hull.WaterVolume -= 100000.0f * deltaTime;
SetWaterVolume(hull.WaterVolume - waterIncrement * deltaTime);
}
if (doubleClicked)
{
SetWaterVolume(hull.Volume * MaxCompress);
}
else if (secondaryDoubleClicked)
{
SetWaterVolume(0f);
}
void SetWaterVolume(float newVolume)
{
ShowHulls = true;
hull.WaterVolume = newVolume;
hull.networkUpdatePending = true;
hull.serverUpdateDelay = 0.5f;
}
}
else if (EditFire)
{
bool networkUpdate = false;
if (primaryMouseButtonHeld)
{
new FireSource(position, hull, isNetworkMessage: true);
networkUpdate = true;
}
else if (secondaryMouseButtonHeld || secondaryDoubleClicked)
{
for (int index = hull.FireSources.Count - 1; index >= 0; index--)
{
var currentFireSource = hull.FireSources[index];
if (secondaryMouseButtonHeld)
{
const float extinguishAmount = 120f;
currentFireSource.Extinguish(deltaTime, extinguishAmount);
networkUpdate = true;
}
else
{
currentFireSource.Remove();
networkUpdate = true;
}
}
}
if (networkUpdate)
{
hull.networkUpdatePending = true;
hull.serverUpdateDelay = 0.5f;
}
}
}
}

View File

@@ -1,4 +1,4 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System.Collections.Generic;
using System.Linq;
@@ -70,7 +70,7 @@ namespace Barotrauma.Lights
public bool LightingEnabled = true;
public bool ObstructVision;
public float ObstructVisionAmount;
private readonly Texture2D visionCircle;
@@ -498,7 +498,7 @@ namespace Barotrauma.Lights
{
foreach (MapEntity e in (Submarine.VisibleEntities ?? MapEntity.MapEntityList))
{
if (e is Item item && !item.HiddenInGame && item.GetComponent<Wire>() is Wire wire)
if (e is Item item && !item.IsHidden && item.GetComponent<Wire>() is Wire wire)
{
wire.DebugDraw(spriteBatch, alpha: 0.4f);
}
@@ -664,7 +664,7 @@ namespace Barotrauma.Lights
visibleHulls.Clear();
foreach (Hull hull in Hull.HullList)
{
if (hull.HiddenInGame) { continue; }
if (hull.IsHidden) { continue; }
var drawRect =
hull.Submarine == null ?
hull.Rect :
@@ -682,12 +682,12 @@ namespace Barotrauma.Lights
public void UpdateObstructVision(GraphicsDevice graphics, SpriteBatch spriteBatch, Camera cam, Vector2 lookAtPosition)
{
if ((!LosEnabled || LosMode == LosMode.None) && !ObstructVision) { return; }
if ((!LosEnabled || LosMode == LosMode.None) && ObstructVisionAmount <= 0.0f) { return; }
if (ViewTarget == null) return;
graphics.SetRenderTarget(LosTexture);
if (ObstructVision)
if (ObstructVisionAmount > 0.0f)
{
graphics.Clear(Color.Black);
Vector2 diff = lookAtPosition - ViewTarget.WorldPosition;
@@ -697,13 +697,14 @@ namespace Barotrauma.Lights
//the visible area stretches to the maximum when the cursor is this far from the character
const float MaxOffset = 256.0f;
const float MinHorizontalScale = 2.2f;
const float MaxHorizontalScale = 2.8f;
const float VerticalScale = 2.5f;
//the magic numbers here are just based on experimentation
float MinHorizontalScale = MathHelper.Lerp(3.5f, 1.5f, ObstructVisionAmount);
float MaxHorizontalScale = MinHorizontalScale * 1.25f;
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.22f; //Increasing this value moves the origin further behind the character
float originStartPosition = visionCircle.Width * relativeOriginStartPosition;
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;

View File

@@ -1115,13 +1115,23 @@ namespace Barotrauma
float subCrushDepth = SubmarineInfo.GetSubCrushDepth(SubmarineSelection.CurrentOrPendingSubmarine(), ref pendingSubInfo);
string crushDepthWarningIconStyle = null;
if (connection.LevelData.InitialDepth * Physics.DisplayToRealWorldRatio > subCrushDepth)
var levelData = connection.LevelData;
float spawnDepth =
levelData.InitialDepth +
//base the warning on the start or end position of the level, whichever is deeper
levelData.Size.Y * Math.Max(levelData.GenerationParams.StartPosition.Y, levelData.GenerationParams.EndPosition.Y);
//"high warning" if the sub spawns at/below crush depth
if (spawnDepth * Physics.DisplayToRealWorldRatio > subCrushDepth)
{
iconCount++;
crushDepthWarningIconStyle = "CrushDepthWarningHighIcon";
tooltip = "crushdepthwarninghigh";
}
else if ((connection.LevelData.InitialDepth + connection.LevelData.Size.Y) * Physics.DisplayToRealWorldRatio > subCrushDepth)
//"low warning" if the spawn position is less than the level's height away from crush depth
//(i.e. the crush depth is pretty close to the spawn pos, possibly inside the level or at least close enough that many parts of the abyss are unreachable)
else if ((spawnDepth + connection.LevelData.Size.Y) * Physics.DisplayToRealWorldRatio > subCrushDepth)
{
iconCount++;
crushDepthWarningIconStyle = "CrushDepthWarningLowIcon";

View File

@@ -382,7 +382,10 @@ namespace Barotrauma
if (!HasBody && !ShowStructures) { return; }
if (HasBody && !ShowWalls) { return; }
}
else if (HiddenInGame) { return; }
else if (IsHidden)
{
return;
}
Color color = IsIncludedInSelection && editing ? GUIStyle.Blue : IsHighlighted ? GUIStyle.Orange * Math.Max(spriteColor.A / (float) byte.MaxValue, 0.1f) : spriteColor;

View File

@@ -20,13 +20,13 @@ namespace Barotrauma
public override bool SelectableInEditor
{
get { return !IsHidden(); }
get { return ShouldDrawIcon(); }
}
public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true)
{
if (!editing && (!GameMain.DebugDraw || Screen.Selected.Cam.Zoom < 0.1f)) { return; }
if (IsHidden()) { return; }
if (!ShouldDrawIcon()) { return; }
Vector2 drawPos = Position;
if (Submarine != null) { drawPos += Submarine.DrawPosition; }
@@ -59,8 +59,10 @@ namespace Barotrauma
Color.White);
}
Sprite sprite = iconSprites[SpawnType.ToString()];
Sprite sprite2 = null;
//there are no sprites for all possible combinations of SpawnType flags, but in the vanilla game the only possible combination is
//SpawnType.Disabled + some other flag, in which case it's fine to just not show the icon.
iconSprites.TryGetValue(SpawnType.ToString(), out Sprite sprite);
if (spawnType == SpawnType.Human && AssignedJob?.Icon != null)
{
sprite = iconSprites["Path"];
@@ -87,9 +89,12 @@ namespace Barotrauma
sprite = iconSprites["Ladder"];
}
float spriteScale = iconSize / (float)sprite.SourceRect.Width;
sprite.Draw(spriteBatch, drawPos, clr, origin: sprite.size / 2, scale: spriteScale, depth: 0.001f);
sprite2?.Draw(spriteBatch, drawPos + sprite.size * spriteScale * 0.5f, clr, origin: sprite2.size / 2, scale: spriteScale, depth: 0.001f);
if (sprite != null)
{
float spriteScale = iconSize / (float)sprite.SourceRect.Width;
sprite.Draw(spriteBatch, drawPos, clr, origin: sprite.size / 2, scale: spriteScale, depth: 0.001f);
sprite2?.Draw(spriteBatch, drawPos + sprite.size * spriteScale * 0.5f, clr, origin: sprite2.size / 2, scale: spriteScale, depth: 0.001f);
}
if (spawnType == SpawnType.Human && AssignedJob?.Icon != null)
{
@@ -160,22 +165,22 @@ namespace Barotrauma
public override bool IsMouseOn(Vector2 position)
{
if (IsHidden()) { return false; }
if (!ShouldDrawIcon()) { return false; }
float dist = Vector2.DistanceSquared(position, WorldPosition);
float radius = (SpawnType == SpawnType.Path ? WaypointSize : SpawnPointSize) * 0.6f;
return dist < radius * radius;
}
private bool IsHidden()
private bool ShouldDrawIcon()
{
if (!SubEditorScreen.IsLayerVisible(this)) { return true; }
if (!SubEditorScreen.IsLayerVisible(this)) { return false; }
if (spawnType == SpawnType.Path)
{
return (!GameMain.DebugDraw && !ShowWayPoints);
return GameMain.DebugDraw || ShowWayPoints;
}
else
{
return (!GameMain.DebugDraw && !ShowSpawnPoints);
return GameMain.DebugDraw || ShowSpawnPoints;
}
}

View File

@@ -30,6 +30,7 @@ namespace Barotrauma.Networking
txt = msg.ReadString();
string senderName = msg.ReadString();
Entity sender = null;
Character senderCharacter = null;
Client senderClient = null;
bool hasSenderClient = msg.ReadBoolean();
@@ -40,13 +41,14 @@ namespace Barotrauma.Networking
=> c.SessionOrAccountIdMatches(userId));
if (senderClient != null) { senderName = senderClient.Name; }
}
bool hasSenderCharacter = msg.ReadBoolean();
if (hasSenderCharacter)
bool hasSender = msg.ReadBoolean();
if (hasSender)
{
senderCharacter = Entity.FindEntityByID(msg.ReadUInt16()) as Character;
if (senderCharacter != null)
sender = Entity.FindEntityByID(msg.ReadUInt16());
senderCharacter = sender as Character;
if (sender is Character or Item)
{
senderName = senderCharacter.Name;
senderName = OrderChatMessage.NameFromEntityOrNull(sender);
}
}
@@ -180,7 +182,7 @@ namespace Barotrauma.Networking
GameMain.Client.ServerSettings.ServerLog?.WriteLine(txt, messageType);
break;
default:
GameMain.Client.AddChatMessage(txt, type, senderName, senderClient, senderCharacter, changeType, textColor: textColor);
GameMain.Client.AddChatMessage(txt, type, senderName, senderClient, sender, changeType, textColor: textColor);
if (type == ChatMessageType.Radio && CanUseRadio(senderCharacter, out WifiComponent radio))
{
Signal s = new Signal(txt, sender: senderCharacter, source: radio.Item);

View File

@@ -21,6 +21,10 @@ namespace Barotrauma.Networking
public override bool IsClient => true;
public override bool IsServer => false;
#if DEBUG
public float DebugServerVoipAmplitude;
#endif
public override Voting Voting { get; }
@@ -112,6 +116,8 @@ namespace Barotrauma.Networking
//has the client been given a character to control this round
public bool HasSpawned;
public float EndRoundTimeRemaining { get; private set; }
public LocalizedString TraitorFirstObjective;
public TraitorEventPrefab TraitorMission = null;
@@ -198,20 +204,6 @@ namespace Barotrauma.Networking
CanBeFocused = false
};
cameraFollowsSub = new GUITickBox(new RectTransform(new Vector2(0.05f, 0.05f), inGameHUD.RectTransform, anchor: Anchor.TopCenter, pivot: Pivot.CenterLeft)
{
AbsoluteOffset = new Point(0, HUDLayoutSettings.ButtonAreaTop.Y + HUDLayoutSettings.ButtonAreaTop.Height / 2),
MaxSize = new Point(GUI.IntScale(25))
}, TextManager.Get("CamFollowSubmarine"))
{
Selected = Camera.FollowSub,
OnSelected = (tbox) =>
{
Camera.FollowSub = tbox.Selected;
return true;
}
};
chatBox = new ChatBox(inGameHUD, isSinglePlayer: false);
chatBox.OnEnterMessage += EnterChatMessage;
chatBox.InputBox.OnTextChanged += TypingChatMessage;
@@ -250,6 +242,19 @@ namespace Barotrauma.Networking
}
};
ShowLogButton.TextBlock.AutoScaleHorizontal = true;
cameraFollowsSub = new GUITickBox(new RectTransform(new Vector2(0.1f, 0.4f), buttonContainer.RectTransform)
{
MinSize = new Point(150, 0)
}, TextManager.Get("CamFollowSubmarine"))
{
Selected = Camera.FollowSub,
OnSelected = (tbox) =>
{
Camera.FollowSub = tbox.Selected;
return true;
}
};
GameMain.DebugDraw = false;
Hull.EditFire = false;
@@ -676,6 +681,11 @@ namespace Barotrauma.Networking
VoipClient.Read(inc);
break;
#if DEBUG
case ServerPacketHeader.VOICE_AMPLITUDE_DEBUG:
GameMain.Client.DebugServerVoipAmplitude = inc.ReadRangedSingle(min: 0, max: 1, bitCount: 8);
break;
#endif
case ServerPacketHeader.QUERY_STARTGAME:
DebugConsole.Log("Received QUERY_STARTGAME packet.");
string subName = inc.ReadString();
@@ -1384,6 +1394,7 @@ namespace Barotrauma.Networking
ServerSettings.AllowRewiring = inc.ReadBoolean();
ServerSettings.AllowImmediateItemDelivery = inc.ReadBoolean();
ServerSettings.AllowFriendlyFire = inc.ReadBoolean();
ServerSettings.AllowDragAndDropGive = inc.ReadBoolean();
ServerSettings.LockAllDefaultWires = inc.ReadBoolean();
ServerSettings.AllowLinkingWifiToChat = inc.ReadBoolean();
ServerSettings.MaximumMoneyTransferRequest = inc.ReadInt32();
@@ -1814,6 +1825,8 @@ namespace Barotrauma.Networking
GameStarted = inc.ReadBoolean();
bool allowSpectating = inc.ReadBoolean();
bool permadeathMode = inc.ReadBoolean();
bool ironmanMode = inc.ReadBoolean();
ReadPermissions(inc);
@@ -1821,8 +1834,17 @@ namespace Barotrauma.Networking
{
if (Screen.Selected != GameMain.GameScreen)
{
new GUIMessageBox(TextManager.Get("PleaseWait"), TextManager.Get(allowSpectating ? "RoundRunningSpectateEnabled" : "RoundRunningSpectateDisabled"));
if (Screen.Selected is not ModDownloadScreen) { GameMain.NetLobbyScreen.Select(); }
LocalizedString message;
if (permadeathMode)
{
message = TextManager.Get(ironmanMode ? "RoundRunningIronman" : "RoundRunningPermadeath");
}
else
{
message = TextManager.Get(allowSpectating ? "RoundRunningSpectateEnabled" : "RoundRunningSpectateDisabled");
}
new GUIMessageBox(TextManager.Get("PleaseWait"), message);
if (!(Screen.Selected is ModDownloadScreen)) { GameMain.NetLobbyScreen.Select(); }
}
}
}
@@ -2105,6 +2127,8 @@ namespace Barotrauma.Networking
float sendingTime = inc.ReadSingle() - 0.0f;//TODO: reimplement inc.SenderConnection.RemoteTimeOffset;
EndRoundTimeRemaining = inc.ReadSingle();
SegmentTableReader<ServerNetSegment>.Read(inc,
segmentDataReader: (segment, inc) =>
{
@@ -2375,7 +2399,16 @@ namespace Barotrauma.Networking
WaitForNextRoundRespawn = waitForNextRoundRespawn;
IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ClientPacketHeader.READY_TO_SPAWN);
msg.WriteBoolean((bool)waitForNextRoundRespawn);
msg.WriteBoolean(GameMain.NetLobbyScreen.Spectating);
msg.WriteBoolean(waitForNextRoundRespawn);
ClientPeer?.Send(msg, DeliveryMethod.Reliable);
}
public void SendTakeOverBotRequest(CharacterInfo bot)
{
IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ClientPacketHeader.TAKEOVERBOT);
msg.WriteUInt16(bot.ID);
ClientPeer?.Send(msg, DeliveryMethod.Reliable);
}
@@ -2711,16 +2744,19 @@ namespace Barotrauma.Networking
if (should != null && should.Value) { return; }
if (string.IsNullOrEmpty(message.Text)) { return; }
if (message.Sender != null && !message.Sender.IsDead)
if (message.SenderCharacter is { IsDead: false } sender)
{
if (message.Text.IsNullOrEmpty())
{
message.Sender.ShowTextlessSpeechBubble(2.0f, message.Color);
sender.ShowTextlessSpeechBubble(2.0f, message.Color);
}
else
{
message.Sender.ShowSpeechBubble(message.Color, message.Text);
sender.ShowSpeechBubble(message.Color, message.Text);
if (!sender.IsBot)
{
sender.TextChatVolume = 1f;
}
}
}
GameMain.NetLobbyScreen.NewChatMessage(message);
@@ -3204,19 +3240,19 @@ namespace Barotrauma.Networking
{
LocalizedString respawnText = string.Empty;
Color textColor = Color.White;
bool canChooseRespawn =
GameMain.GameSession.GameMode is CampaignMode &&
Character.Controlled == null &&
Level.Loaded?.Type != LevelData.LevelType.Outpost &&
(characterInfo == null || HasSpawned);
bool hideRespawnButtons = false;
if (EndRoundTimeRemaining > 0)
{
respawnText = TextManager.GetWithVariable("endinground", "[time]", ToolBox.SecondsToReadableTime(EndRoundTimeRemaining))
.Fallback(ToolBox.SecondsToReadableTime(EndRoundTimeRemaining), useDefaultLanguageIfFound: false);
}
if (RespawnManager.CurrentState == RespawnManager.State.Waiting)
{
if (RespawnManager.RespawnCountdownStarted)
{
float timeLeft = (float)(RespawnManager.RespawnTime - DateTime.Now).TotalSeconds;
respawnText = TextManager.GetWithVariable(
RespawnManager.UsingShuttle && !RespawnManager.ForceSpawnInMainSub ?
"RespawnShuttleDispatching" : "RespawningIn", "[time]", ToolBox.SecondsToReadableTime(timeLeft));
respawnText = TextManager.GetWithVariable("RespawningIn", "[time]", ToolBox.SecondsToReadableTime(timeLeft));
}
else if (RespawnManager.PendingRespawnCount > 0)
{
@@ -3239,12 +3275,12 @@ namespace Barotrauma.Networking
//textScale = 1.0f + phase * 0.5f;
textColor = Color.Lerp(GUIStyle.Red, Color.White, 1.0f - phase);
}
canChooseRespawn = false;
hideRespawnButtons = true;
}
GameMain.GameSession?.SetRespawnInfo(
visible: !respawnText.IsNullOrEmpty() || canChooseRespawn, text: respawnText.Value, textColor: textColor,
buttonsVisible: canChooseRespawn, waitForNextRoundRespawn: (WaitForNextRoundRespawn ?? true));
GameMain.GameSession.SetRespawnInfo(
text: respawnText.Value, textColor: textColor,
waitForNextRoundRespawn: (WaitForNextRoundRespawn ?? true), hideButtons: hideRespawnButtons);
}
if (!ShowNetStats) { return; }

View File

@@ -1,5 +1,4 @@
using System;
using System.Linq;
namespace Barotrauma.Networking
{
@@ -23,6 +22,14 @@ namespace Barotrauma.Networking
get; private set;
}
public static void ShowDeathPromptIfNeeded(float delay = 1.0f)
{
if (UseDeathPrompt)
{
DeathPrompt.Create(delay);
}
}
partial void UpdateTransportingProjSpecific(float deltaTime)
{
if (GameMain.Client?.Character == null || GameMain.Client.Character.Submarine != RespawnShuttle) { return; }
@@ -37,72 +44,10 @@ namespace Barotrauma.Networking
}
}
private CoroutineHandle respawnPromptCoroutine;
public void ShowRespawnPromptIfNeeded(float delay = 5.0f)
{
if (!UseRespawnPrompt) { return; }
if (CoroutineManager.IsCoroutineRunning(respawnPromptCoroutine) || GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "respawnquestionprompt"))
{
return;
}
respawnPromptCoroutine = CoroutineManager.Invoke(() =>
{
if (Character.Controlled != null || (GameMain.GameSession is not { IsRunning: true })) { return; }
LocalizedString text;
GUIMessageBox respawnPrompt;
if (SkillLossPercentageOnImmediateRespawn > 0)
{
// Respawn asap with extra skill loss?
text = TextManager.GetWithVariable("respawnquestionprompt", "[percentage]", ((int)Math.Round(SkillLossPercentageOnImmediateRespawn)).ToString());
respawnPrompt = new GUIMessageBox(
TextManager.Get("tutorial.tryagainheader"), text,
new LocalizedString[] { TextManager.Get("respawnquestionpromptrespawn"), TextManager.Get("respawnquestionpromptwait") })
{
UserData = "respawnquestionprompt"
};
}
else
{
// Respawn asap?
text = TextManager.Get("respawnquestionpromptnoloss");
respawnPrompt = new GUIMessageBox(
TextManager.Get("tutorial.tryagainheader"), text,
new LocalizedString[] { TextManager.Get("respawnquestionpromptrespawnnoloss"), TextManager.Get("respawnquestionpromptwait") })
{
UserData = "respawnquestionprompt"
};
}
if (SkillLossPercentageOnDeath > 0)
{
// You have died... etc added BEFORE the above text
text =
TextManager.GetWithVariable("respawnskillpenalty", "[percentage]", ((int)SkillLossPercentageOnDeath).ToString()) +
"\n\n" + text;
};
respawnPrompt.Buttons[0].OnClicked += (btn, userdata) =>
{
GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: false);
respawnPrompt.Close();
return true;
};
respawnPrompt.Buttons[1].OnClicked += (btn, userdata) =>
{
GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: true);
respawnPrompt.Close();
return true;
};
}, delay: delay);
}
public void ClientEventRead(IReadMessage msg, float sendingTime)
{
bool respawnPromptPending = false;
var newState = (State)msg.ReadRangedInteger(0, Enum.GetNames(typeof(State)).Length);
ForceSpawnInMainSub = false;
switch (newState)
{
case State.Transporting:
@@ -122,7 +67,6 @@ namespace Barotrauma.Networking
RequiredRespawnCount = msg.ReadUInt16();
respawnPromptPending = msg.ReadBoolean();
RespawnCountdownStarted = msg.ReadBoolean();
ForceSpawnInMainSub = msg.ReadBoolean();
ResetShuttle();
float newRespawnTime = msg.ReadSingle();
RespawnTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, milliseconds: (int)(newRespawnTime * 1000.0f));
@@ -136,7 +80,7 @@ namespace Barotrauma.Networking
if (respawnPromptPending)
{
GameMain.Client.HasSpawned = true;
ShowRespawnPromptIfNeeded(delay: 1.0f);
DeathPrompt.Create(delay: 1.0f);
}
msg.ReadPadBits();

View File

@@ -468,11 +468,28 @@ namespace Barotrauma.Networking
};
AssignGUIComponent(nameof(SaveServerLogs), saveLogsBox);
LocalizedString newCampaignDefaultSalaryLabel = TextManager.Get("ServerSettingsNewCampaignDefaultSalary");
NetLobbyScreen.CreateLabeledSlider(listBox.Content, headerTag: "ServerSettingsNewCampaignDefaultSalary", valueLabelTag: "ServerSettingsKickVotesRequired", tooltipTag: "ServerSettingsNewCampaignDefaultSalaryToolTip",
out var defaultSalarySlider, out var defaultSalarySliderLabel);
defaultSalarySlider.Range = new Vector2(0, 100);
defaultSalarySlider.StepValue = 1;
defaultSalarySlider.OnMoved = (scrollBar, _) =>
{
if (scrollBar.UserData is not GUITextBlock text) { return false; }
text.Text = TextManager.AddPunctuation(
':',
newCampaignDefaultSalaryLabel,
TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollBar.BarScrollValue, digits: 0)).ToString()));
return true;
};
AssignGUIComponent(nameof(NewCampaignDefaultSalary), defaultSalarySlider);
defaultSalarySlider.OnMoved(defaultSalarySlider, defaultSalarySlider.BarScroll);
//--------------------------------------------------------------------------------
// game settings
//--------------------------------------------------------------------------------
GUILayoutGroup buttonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform), isHorizontal: true)
GUILayoutGroup buttonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform), isHorizontal: true, childAnchor: Anchor.BottomLeft)
{
Stretch = true,
RelativeSpacing = 0.05f
@@ -683,7 +700,7 @@ namespace Barotrauma.Networking
// antigriefing
//--------------------------------------------------------------------------------
var tickBoxContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.25f), listBox.Content.RectTransform))
var tickBoxContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.268f), listBox.Content.RectTransform))
{
AutoHideScrollBar = true,
UseGridLayout = true
@@ -693,6 +710,10 @@ namespace Barotrauma.Networking
var allowFriendlyFire = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform),
TextManager.Get("ServerSettingsAllowFriendlyFire"));
AssignGUIComponent(nameof(AllowFriendlyFire), allowFriendlyFire);
var allowDragAndDropGive = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform),
TextManager.Get("ServerSettingsAllowDragAndDropGive"));
AssignGUIComponent(nameof(AllowDragAndDropGive), allowDragAndDropGive);
var killableNPCs = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform),
TextManager.Get("ServerSettingsKillableNPCs"));

View File

@@ -1,19 +1,10 @@
using Concentus.Enums;
using Concentus.Structs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Barotrauma.Networking
{
static partial class VoipConfig
{
public const int FREQUENCY = 48000; //48Khz
public const int BITRATE = 16000; //16Kbps
public const int BUFFER_SIZE = (8 * MAX_COMPRESSED_SIZE * FREQUENCY) / BITRATE; //20ms window
public static OpusEncoder CreateEncoder()
{
var encoder = new OpusEncoder(FREQUENCY, 1, OpusApplication.OPUS_APPLICATION_VOIP);
@@ -22,10 +13,5 @@ namespace Barotrauma.Networking
encoder.SignalType = OpusSignal.OPUS_SIGNAL_VOICE;
return encoder;
}
public static OpusDecoder CreateDecoder()
{
return new OpusDecoder(FREQUENCY, 1);
}
}
}

View File

@@ -228,8 +228,11 @@ namespace Barotrauma
static MouseState latestMouseState; //the absolute latest state, do NOT use for player interaction
static KeyboardState keyboardState, oldKeyboardState;
static double timeSinceClick;
static Point lastClickPosition;
static double timeSincePrimaryClick;
static Point lastPrimaryClickPosition;
static double timeSinceSecondaryClick;
static Point lastSecondaryClickPosition;
const float DoubleClickDelay = 0.4f;
public static float MaxDoubleClickDistance
@@ -237,7 +240,8 @@ namespace Barotrauma
get { return Math.Max(15.0f * Math.Max(GameMain.GraphicsHeight / 1920.0f, GameMain.GraphicsHeight / 1080.0f), 10.0f); }
}
static bool doubleClicked;
static bool primaryDoubleClicked;
static bool secondaryDoubleClicked;
static bool allowInput;
static bool wasWindowActive;
@@ -406,7 +410,12 @@ namespace Barotrauma
public static bool DoubleClicked()
{
return AllowInput && doubleClicked;
return AllowInput && primaryDoubleClicked;
}
public static bool SecondaryDoubleClicked()
{
return AllowInput && secondaryDoubleClicked;
}
public static bool KeyHit(InputType inputType)
@@ -466,7 +475,8 @@ namespace Barotrauma
public static void Update(double deltaTime)
{
timeSinceClick += deltaTime;
timeSincePrimaryClick += deltaTime;
timeSinceSecondaryClick += deltaTime;
if (!GameMain.WindowActive)
{
@@ -497,11 +507,33 @@ namespace Barotrauma
MouseSpeedPerSecond = MouseSpeed / (float)deltaTime;
// Split into two to not accept drag & drop releasing as part of a double-click
doubleClicked = false;
primaryDoubleClicked = false;
if (PrimaryMouseButtonClicked())
{
float dist = (mouseState.Position - lastClickPosition).ToVector2().Length();
primaryDoubleClicked = UpdateDoubleClicking(ref lastPrimaryClickPosition, ref timeSincePrimaryClick);
}
if (PrimaryMouseButtonDown())
{
lastPrimaryClickPosition = mouseState.Position;
}
secondaryDoubleClicked = false;
if (SecondaryMouseButtonClicked())
{
secondaryDoubleClicked = UpdateDoubleClicking(ref lastSecondaryClickPosition, ref timeSinceSecondaryClick);
}
if (SecondaryMouseButtonDown())
{
lastSecondaryClickPosition = mouseState.Position;
}
bool UpdateDoubleClicking(ref Point lastClickPosition, ref double timeSinceClick)
{
bool doubleClicked = false;
float dist = (mouseState.Position - lastClickPosition).ToVector2().Length();
if (timeSinceClick < DoubleClickDelay && dist < MaxDoubleClickDistance)
{
doubleClicked = true;
@@ -515,11 +547,8 @@ namespace Barotrauma
{
timeSinceClick = 0.0;
}
}
if (PrimaryMouseButtonDown())
{
lastClickPosition = mouseState.Position;
return doubleClicked;
}
}

View File

@@ -781,7 +781,7 @@ namespace Barotrauma.CharacterEditor
// Lightmaps
if (GameMain.LightManager.LightingEnabled && Character.Controlled != null)
{
GameMain.LightManager.ObstructVision = Character.Controlled.ObstructVision;
GameMain.LightManager.ObstructVisionAmount = Character.Controlled.ObstructVisionAmount;
GameMain.LightManager.RenderLightMap(graphics, spriteBatch, cam);
GameMain.LightManager.UpdateObstructVision(graphics, spriteBatch, cam, Character.Controlled.CursorWorldPosition);
}

View File

@@ -1,4 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Barotrauma.Lights;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
@@ -178,10 +178,15 @@ namespace Barotrauma
Stopwatch sw = new Stopwatch();
sw.Start();
GameMain.LightManager.ObstructVision =
Character.Controlled != null &&
Character.Controlled.ObstructVision &&
(Character.Controlled.ViewTarget == Character.Controlled || Character.Controlled.ViewTarget == null);
if (Character.Controlled != null &&
(Character.Controlled.ViewTarget == Character.Controlled || Character.Controlled.ViewTarget == null))
{
GameMain.LightManager.ObstructVisionAmount = Character.Controlled.ObstructVisionAmount;
}
else
{
GameMain.LightManager.ObstructVisionAmount = 0.0f;
}
GameMain.LightManager.UpdateObstructVision(graphics, spriteBatch, cam, Character.Controlled?.CursorWorldPosition ?? Vector2.Zero);

View File

@@ -71,7 +71,7 @@ namespace Barotrauma
currentLevelData = LevelData.CreateRandom(seedBox.Text, generationParams: selectedParams);
editorContainer.ClearChildren();
SortLevelObjectsList(currentLevelData);
new SerializableEntityEditor(editorContainer.Content.RectTransform, selectedParams, false, true, elementHeight: 20);
new SerializableEntityEditor(editorContainer.Content.RectTransform, selectedParams, inGame: false, showName: true, elementHeight: 20, titleFont: GUIStyle.LargeFont);
return true;
};
@@ -996,7 +996,7 @@ namespace Barotrauma
{
foreach (Item item in Item.ItemList)
{
if (item == null || item.HiddenInGame) { continue; }
if (item == null || item.IsHidden) { continue; }
foreach (var light in item.GetComponents<Items.Components.LightComponent>())
{
light.Update((float)deltaTime, Cam);

View File

@@ -63,6 +63,9 @@ namespace Barotrauma
private GUITickBox spectateBox;
public bool Spectating => spectateBox is { Selected: true, Visible: true };
public bool PermadeathMode => GameMain.Client?.ServerSettings?.RespawnMode == RespawnMode.Permadeath;
public bool PermanentlyDead => campaignCharacterInfo?.PermanentlyDead ?? false;
private GUILayoutGroup playerInfoContent;
private GUIComponent changesPendingText;
private bool createPendingChangesText = true;
@@ -87,7 +90,14 @@ namespace Barotrauma
private GUIFrame characterInfoFrame;
private GUIFrame appearanceFrame;
private readonly List<GUIComponent> respawnSettingsElements = new List<GUIComponent>();
private GUISelectionCarousel<RespawnMode> respawnModeSelection;
private GUITextBlock respawnModeLabel;
private GUIComponent respawnIntervalElement;
private readonly List<GUIComponent> midRoundRespawnSettings = new List<GUIComponent>();
private readonly List<GUIComponent> permadeathEnabledRespawnSettings = new List<GUIComponent>();
private readonly List<GUIComponent> permadeathDisabledRespawnSettings = new List<GUIComponent>();
private readonly List<GUIComponent> ironmanDisabledRespawnSettings = new List<GUIComponent>();
private readonly List<GUIComponent> campaignDisabledElements = new List<GUIComponent>();
public CharacterInfo.AppearanceCustomizationMenu CharacterAppearanceCustomizationMenu { get; set; }
@@ -191,7 +201,7 @@ namespace Barotrauma
public bool UsingShuttle
{
get { return shuttleTickBox.Selected; }
get { return shuttleTickBox.Selected && !PermadeathMode; }
set { shuttleTickBox.Selected = value; }
}
@@ -955,19 +965,17 @@ namespace Barotrauma
// ------------------------------------------------------------------
var respawnBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), settingsContent.RectTransform) { AbsoluteOffset = new Point((int)respawnSettingsHeader.Padding.X, 0) },
TextManager.Get("ServerSettingsAllowRespawning"))
var respawnModeHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true };
respawnModeLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 0.0f), respawnModeHolder.RectTransform), TextManager.Get("RespawnMode"), wrap: true);
respawnModeSelection = new GUISelectionCarousel<RespawnMode>(new RectTransform(new Vector2(0.6f, 1.0f), respawnModeHolder.RectTransform));
foreach (var respawnMode in Enum.GetValues(typeof(RespawnMode)).Cast<RespawnMode>())
{
ToolTip = TextManager.Get("RespawnExplanation"),
OnSelected = (tickbox) =>
{
GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
RefreshEnabledElements();
return true;
}
};
AssignComponentToServerSetting(respawnBox, nameof(ServerSettings.AllowRespawn));
clientDisabledElements.Add(respawnBox);
respawnModeSelection.AddElement(respawnMode, TextManager.Get($"respawnmode.{respawnMode}"), TextManager.Get($"respawnmode.{respawnMode}.tooltip"));
}
respawnModeSelection.ElementSelectionCondition += (value) => value != RespawnMode.Permadeath || SelectedMode == GameModePreset.MultiPlayerCampaign;
respawnModeSelection.OnValueChanged += (_) => GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
AssignComponentToServerSetting(respawnModeSelection, nameof(ServerSettings.RespawnMode));
GUILayoutGroup shuttleHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), settingsContent.RectTransform), isHorizontal: true)
{
@@ -977,7 +985,7 @@ namespace Barotrauma
shuttleTickBox = new GUITickBox(new RectTransform(Vector2.One, shuttleHolder.RectTransform), TextManager.Get("RespawnShuttle"))
{
ToolTip = TextManager.Get("RespawnShuttleExplanation"),
Selected = true,
Selected = !PermadeathMode,
OnSelected = (GUITickBox box) =>
{
GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
@@ -985,7 +993,7 @@ namespace Barotrauma
}
};
AssignComponentToServerSetting(shuttleTickBox, nameof(ServerSettings.UseRespawnShuttle));
respawnSettingsElements.Add(shuttleTickBox);
midRoundRespawnSettings.Add(shuttleTickBox);
shuttleTickBox.TextBlock.RectTransform.SizeChanged += () =>
{
@@ -1008,9 +1016,9 @@ namespace Barotrauma
};
ShuttleList.ListBox.RectTransform.MinSize = new Point(250, 0);
shuttleHolder.RectTransform.MinSize = new Point(0, ShuttleList.RectTransform.Children.Max(c => c.MinSize.Y));
respawnSettingsElements.Add(ShuttleList);
midRoundRespawnSettings.Add(ShuttleList);
var respawnIntervalElement = CreateLabeledSlider(settingsContent, "ServerSettingsRespawnInterval", "", "", out var respawnIntervalSlider, out var respawnIntervalSliderLabel,
respawnIntervalElement = CreateLabeledSlider(settingsContent, "ServerSettingsRespawnInterval", "", "", out var respawnIntervalSlider, out var respawnIntervalSliderLabel,
range: new Vector2(10.0f, 600.0f));
LocalizedString intervalLabel = respawnIntervalSliderLabel.Text;
respawnIntervalSlider.StepValue = 10.0f;
@@ -1026,7 +1034,6 @@ namespace Barotrauma
return true;
};
respawnIntervalSlider.OnMoved(respawnIntervalSlider, respawnIntervalSlider.BarScroll);
respawnSettingsElements.AddRange(respawnIntervalElement.GetAllChildren());
AssignComponentToServerSetting(respawnIntervalSlider, nameof(ServerSettings.RespawnInterval));
var minRespawnElement = CreateLabeledSlider(settingsContent, "ServerSettingsMinRespawn", "", "ServerSettingsMinRespawnToolTip", out var minRespawnSlider, out var minRespawnSliderLabel,
@@ -1043,7 +1050,7 @@ namespace Barotrauma
return true;
};
minRespawnSlider.OnMoved(minRespawnSlider, minRespawnSlider.BarScroll);
respawnSettingsElements.AddRange(minRespawnElement.GetAllChildren());
midRoundRespawnSettings.AddRange(minRespawnElement.GetAllChildren());
AssignComponentToServerSetting(minRespawnSlider, nameof(ServerSettings.MinRespawnRatio));
var respawnDurationElement = CreateLabeledSlider(settingsContent, "ServerSettingsRespawnDuration", "", "ServerSettingsRespawnDurationTooltip", out var respawnDurationSlider, out var respawnDurationSliderLabel,
@@ -1068,7 +1075,7 @@ namespace Barotrauma
return value <= 0.0f ? 1.0f : (value - scrollBar.Range.X) / (scrollBar.Range.Y - scrollBar.Range.X);
};
respawnDurationSlider.OnMoved(respawnDurationSlider, respawnDurationSlider.BarScroll);
respawnSettingsElements.AddRange(respawnDurationElement.GetAllChildren());
midRoundRespawnSettings.AddRange(respawnDurationElement.GetAllChildren());
AssignComponentToServerSetting(respawnDurationSlider, nameof(ServerSettings.MaxTransportTime));
var skillLossElement = CreateLabeledSlider(settingsContent, "ServerSettingsSkillLossPercentageOnDeath", "", "ServerSettingsSkillLossPercentageOnDeathToolTip",
@@ -1085,7 +1092,8 @@ namespace Barotrauma
GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
return true;
};
respawnSettingsElements.AddRange(skillLossElement.GetAllChildren());
permadeathDisabledRespawnSettings.AddRange(skillLossElement.GetAllChildren());
clientDisabledElements.AddRange(skillLossElement.GetAllChildren());
AssignComponentToServerSetting(skillLossSlider, nameof(ServerSettings.SkillLossPercentageOnDeath));
skillLossSlider.OnMoved(skillLossSlider, skillLossSlider.BarScroll);
@@ -1103,11 +1111,41 @@ namespace Barotrauma
GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
return true;
};
respawnSettingsElements.AddRange(skillLossImmediateRespawnElement.GetAllChildren());
midRoundRespawnSettings.AddRange(skillLossImmediateRespawnElement.GetAllChildren());
permadeathDisabledRespawnSettings.AddRange(skillLossImmediateRespawnElement.GetAllChildren());
AssignComponentToServerSetting(skillLossImmediateRespawnSlider, nameof(ServerSettings.SkillLossPercentageOnImmediateRespawn));
skillLossImmediateRespawnSlider.OnMoved(skillLossImmediateRespawnSlider, skillLossImmediateRespawnSlider.BarScroll);
foreach (var respawnElement in respawnSettingsElements)
var allowBotTakeoverTickbox = new GUITickBox(new RectTransform(Vector2.One, settingsContent.RectTransform), TextManager.Get("AllowBotTakeover"))
{
ToolTip = TextManager.Get("AllowBotTakeover.Tooltip"),
Selected = GameMain.Client != null && GameMain.Client.ServerSettings.AllowBotTakeoverOnPermadeath,
OnSelected = (GUITickBox box) =>
{
GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
return true;
}
};
AssignComponentToServerSetting(allowBotTakeoverTickbox, nameof(ServerSettings.AllowBotTakeoverOnPermadeath));
permadeathEnabledRespawnSettings.Add(allowBotTakeoverTickbox);
ironmanDisabledRespawnSettings.Add(allowBotTakeoverTickbox);
clientDisabledElements.Add(allowBotTakeoverTickbox);
var ironmanTickbox = new GUITickBox(new RectTransform(Vector2.One, settingsContent.RectTransform), TextManager.Get("IronmanMode").ToUpper())
{
ToolTip = TextManager.Get("IronmanMode.Tooltip"),
Selected = GameMain.Client != null && GameMain.Client.ServerSettings.IronmanMode,
OnSelected = (GUITickBox box) =>
{
GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
return true;
}
};
AssignComponentToServerSetting(ironmanTickbox, nameof(ServerSettings.IronmanMode));
permadeathEnabledRespawnSettings.Add(ironmanTickbox);
clientDisabledElements.Add(ironmanTickbox);
foreach (var respawnElement in midRoundRespawnSettings)
{
if (!clientDisabledElements.Contains(respawnElement))
{
@@ -1650,19 +1688,31 @@ namespace Barotrauma
bool campaignStarted = CampaignFrame.Visible;
bool gameStarted = client != null && client.GameStarted;
//disable elements the client doesn't have access to
// First, enable or disable elements based on client permissions
foreach (var element in clientDisabledElements)
{
element.Enabled = manageSettings;
}
// Then disable elements depending on other conditions
traitorElements.ForEach(e => e.Enabled &= settings.TraitorProbability > 0);
SetTraitorDangerIndicators(settings.TraitorDangerLevel);
respawnSettingsElements.ForEach(e => e.Enabled &= settings.AllowRespawn);
respawnModeSelection.Enabled = respawnModeLabel.Enabled = manageSettings && !gameStarted;
midRoundRespawnSettings.ForEach(e => e.Enabled &= settings.RespawnMode == RespawnMode.MidRound);
permadeathDisabledRespawnSettings.ForEach(e => e.Enabled &= settings.RespawnMode != RespawnMode.Permadeath);
permadeathEnabledRespawnSettings.ForEach(e => e.Enabled &= settings.RespawnMode == RespawnMode.Permadeath && !gameStarted);
ironmanDisabledRespawnSettings.ForEach(e => e.Enabled &= !settings.IronmanMode);
// The respawn interval is used even if the shuttle is not
respawnIntervalElement.GetAllChildren().ForEach(e => e.Enabled = settings.RespawnMode != RespawnMode.BetweenRounds && manageSettings);
//go through the individual elements that are only enabled in a specific context
shuttleTickBox.Enabled &= !gameStarted;
if (ShuttleList != null)
{
ShuttleList.Enabled = ShuttleList.ButtonEnabled = HasPermission(ClientPermissions.SelectSub) && !gameStarted && settings.AllowRespawn;
// Shuttle list depends on shuttle tickbox
ShuttleList.Enabled &= shuttleTickBox.Enabled && HasPermission(ClientPermissions.SelectSub);
ShuttleList.ButtonEnabled = ShuttleList.Enabled;
}
if (SubList != null)
{
@@ -1672,7 +1722,6 @@ namespace Barotrauma
{
ModeList.Enabled = !gameStarted && (settings.AllowModeVoting || HasPermission(ClientPermissions.SelectMode));
}
shuttleTickBox.Enabled &= !gameStarted;
RefreshStartButtonVisibility();
@@ -1750,6 +1799,10 @@ namespace Barotrauma
private void UpdatePlayerFrame(CharacterInfo characterInfo, bool allowEditing, GUIComponent parent, bool createPendingText = true)
{
if (GameMain.Client == null) { return; }
// When permanently dead and still characterless, spectating is the only option
spectateBox.Enabled = !PermanentlyDead;
createPendingChangesText = createPendingText;
if (characterInfo == null || CampaignCharacterDiscarded)
{
@@ -1780,41 +1833,57 @@ namespace Barotrauma
MaxTextLength = Client.MaxNameLength,
OverflowClip = true
};
CharacterNameBox.OnEnterPressed += (tb, text) => { CharacterNameBox.Deselect(); return true; };
CharacterNameBox.OnDeselected += (tb, key) =>
if (PermanentlyDead)
{
if (GameMain.Client == null) { return; }
string newName = Client.SanitizeName(tb.Text);
if (newName == GameMain.Client.Name) return;
if (string.IsNullOrWhiteSpace(newName))
CharacterNameBox.Readonly = true;
CharacterNameBox.Enabled = false;
}
else
{
CharacterNameBox.OnEnterPressed += (tb, text) =>
{
tb.Text = GameMain.Client.Name;
}
else
CharacterNameBox.Deselect();
return true;
};
CharacterNameBox.OnDeselected += (tb, key) =>
{
if (isGameRunning)
if (GameMain.Client == null)
{
GameMain.Client.PendingName = tb.Text;
TabMenu.PendingChanges = true;
if (createPendingText)
{
CreateChangesPendingText();
}
return;
}
string newName = Client.SanitizeName(tb.Text);
if (newName == GameMain.Client.Name) { return; }
if (string.IsNullOrWhiteSpace(newName))
{
tb.Text = GameMain.Client.Name;
}
else
{
ReadyToStartBox.Selected = false;
if (isGameRunning)
{
GameMain.Client.PendingName = tb.Text;
TabMenu.PendingChanges = true;
if (createPendingText)
{
CreateChangesPendingText();
}
}
else
{
ReadyToStartBox.Selected = false;
}
GameMain.Client.SetName(tb.Text);
}
GameMain.Client.SetName(tb.Text);
}
};
};
}
//spacing
new GUIFrame(new RectTransform(new Vector2(1.0f, 0.006f), parent.RectTransform), style: null);
if (allowEditing)
if (allowEditing && (!PermadeathMode || !isGameRunning))
{
GUILayoutGroup characterInfoTabs = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), parent.RectTransform), isHorizontal: true)
{
@@ -1892,37 +1961,70 @@ namespace Barotrauma
{
characterInfo.CreateIcon(new RectTransform(new Vector2(1.0f, 0.16f), parent.RectTransform, Anchor.TopCenter));
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), characterInfo.Job.Name, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont, wrap: true)
if (PermanentlyDead)
{
HoverColor = Color.Transparent,
SelectedColor = Color.Transparent
};
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), TextManager.Get("Skills"), font: GUIStyle.SubHeadingFont);
foreach (Skill skill in characterInfo.Job.GetSkills())
new GUITextBlock(
new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform),
TextManager.Get("deceased"),
textAlignment: Alignment.Center, font: GUIStyle.LargeFont);
if (GameMain.Client?.ServerSettings is { IronmanMode: true })
{
new GUITextBlock(
new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform),
TextManager.Get("lobby.ironmaninfo"),
textAlignment: Alignment.Center, wrap: true);
}
else
{
new GUITextBlock(
new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform),
TextManager.Get("lobby.permadeathinfo"),
textAlignment: Alignment.Center, wrap: true);
new GUITextBlock(
new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform),
TextManager.Get("lobby.permadeathoptionsexplanation"),
textAlignment: Alignment.Center, wrap: true);
}
}
else
{
Color textColor = Color.White * (0.5f + skill.Level / 200.0f);
var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform),
" - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + skill.Identifier), ((int)skill.Level).ToString()),
textColor,
font: GUIStyle.SmallFont);
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), characterInfo.Job.Name, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont, wrap: true)
{
HoverColor = Color.Transparent,
SelectedColor = Color.Transparent
};
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), TextManager.Get("Skills"), font: GUIStyle.SubHeadingFont);
foreach (Skill skill in characterInfo.Job.GetSkills())
{
Color textColor = Color.White * (0.5f + skill.Level / 200.0f);
var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform),
" - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + skill.Identifier), ((int)skill.Level).ToString()),
textColor,
font: GUIStyle.SmallFont);
}
}
// Spacing
new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), parent.RectTransform), style: null);
new GUIButton(new RectTransform(new Vector2(0.8f, 0.1f), parent.RectTransform, Anchor.BottomCenter), TextManager.Get("CreateNew"))
if (GameMain.Client?.ServerSettings?.RespawnMode != RespawnMode.Permadeath)
{
IgnoreLayoutGroups = true,
OnClicked = (btn, userdata) =>
// Button to create new character
new GUIButton(new RectTransform(new Vector2(0.8f, 0.1f), parent.RectTransform, Anchor.BottomCenter), TextManager.Get("CreateNew"))
{
TryDiscardCampaignCharacter(() =>
IgnoreLayoutGroups = true,
OnClicked = (btn, userdata) =>
{
UpdatePlayerFrame(null, true, parent);
});
return true;
}
};
TryDiscardCampaignCharacter(() =>
{
UpdatePlayerFrame(null, true, parent);
});
return true;
}
};
}
}
TeamPreferenceListBox = null;
@@ -2095,14 +2197,20 @@ namespace Barotrauma
{
if (GameMain.Client == null) { return; }
spectateBox.Selected = spectate;
if (spectate)
{
playerInfoContent.ClearChildren();
GameMain.Client.CharacterInfo?.Remove();
GameMain.Client.CharacterInfo = null;
GameMain.Client.Character?.Remove();
GameMain.Client.Character = null;
// TODO: The following lines are ancient, unexplained, and they cause a client spectating because of permadeath
// to get kicked from the server at round transition because the server expects to be in control of
// removing Characters and the client to still have one. Commenting these lines out for now, but
// if no side-effects occur, they can just be deleted.
//GameMain.Client.Character?.Remove();
//GameMain.Client.Character = null;
playerInfoContent.ClearChildren();
new GUITextBlock(new RectTransform(Vector2.One, playerInfoContent.RectTransform, Anchor.Center),
TextManager.Get("PlayingAsSpectator"),
textAlignment: Alignment.Center);
@@ -2118,6 +2226,10 @@ namespace Barotrauma
// Server owner is allowed to spectate regardless of the server settings
if (GameMain.Client != null && GameMain.Client.IsServerOwner) { return; }
// A client whose character has faced permadeath and hasn't chosen a new
// character yet has no choice but to spectate
if (campaignCharacterInfo != null && campaignCharacterInfo.PermanentlyDead) { return; }
// Show the player config menu if spectating is not allowed
if (spectateBox.Selected && !allowSpectating) { spectateBox.Selected = false; }
@@ -3609,6 +3721,7 @@ namespace Barotrauma
GameMain.GameSession = null;
}
respawnModeSelection.Refresh(); // not all respawn modes are compatible with all game modes
RefreshGameModeContent();
RefreshEnabledElements();
}

View File

@@ -27,38 +27,8 @@ namespace Barotrauma
get => Submarine.MainSub;
set => Submarine.MainSub = value;
}
private enum LayerVisibility
{
Visible,
Invisible
}
private enum LayerLinkage
{
Unlinked,
Linked
}
private readonly struct LayerData
{
public readonly LayerVisibility Visible;
public readonly LayerLinkage Linkage;
public static readonly LayerData Default = new LayerData(LayerVisibility.Visible, LayerLinkage.Unlinked);
public LayerData(LayerVisibility visible, LayerLinkage linkage)
{
Visible = visible;
Linkage = linkage;
}
public void Deconstruct(out LayerVisibility isvisible, out LayerLinkage islinked)
{
isvisible = Visible;
islinked = Linkage;
}
}
private readonly record struct LayerData(bool IsVisible = true, bool IsGrouped = false);
public enum Mode
{
@@ -1105,12 +1075,22 @@ namespace Barotrauma
GameSession gameSession = new GameSession(backedUpSubInfo, "", GameModePreset.TestMode, CampaignSettings.Empty, null);
gameSession.StartRound(null, false);
(gameSession.GameMode as TestGameMode).OnRoundEnd = () =>
foreach ((string layerName, LayerData layerData) in Layers)
{
Submarine.Unload();
GameMain.SubEditorScreen.Select();
};
Identifier identifier = layerName.ToIdentifier();
bool enabled = layerData.IsVisible;
MainSub.SetLayerEnabled(identifier, enabled);
}
if (gameSession.GameMode is TestGameMode testGameMode)
{
testGameMode.OnRoundEnd = () =>
{
Submarine.Unload();
GameMain.SubEditorScreen.Select();
};
}
return true;
}
@@ -1472,6 +1452,7 @@ namespace Barotrauma
{
var subInfo = new SubmarineInfo();
MainSub = new Submarine(subInfo, showErrorMessages: false);
ReconstructLayers();
}
MainSub.UpdateTransform(interpolate: false);
@@ -1507,7 +1488,10 @@ namespace Barotrauma
}
ImageManager.OnEditorSelected();
ReconstructLayers();
if (Layers.None())
{
ReconstructLayers();
}
}
public override void OnFileDropped(string filePath, string extension)
@@ -1664,7 +1648,6 @@ namespace Barotrauma
});
ClearFilter();
ClearLayers();
}
private void CreateDummyCharacter()
@@ -2168,32 +2151,32 @@ namespace Barotrauma
if (Layers.Any())
{
var layerVisibilityGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.01f), leftColumn.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft);
var visibleLayers = Layers.Where(l => !MainSub.Info.LayersHiddenByDefault.Contains(l.Key.ToIdentifier()));
LocalizedString visibleLayersString = LocalizedString.Join(", ", visibleLayers.Select(l => TextManager.Capitalize(l.Key)) ?? ((LocalizedString)"None").ToEnumerable());
new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), layerVisibilityGroup.RectTransform), TextManager.Get("editor.layer.visiblebydefault"), textAlignment: Alignment.CenterLeft);
var layerVisibilityDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), layerVisibilityGroup.RectTransform),
text: LocalizedString.Join(", ", Layers.Where(l => !Submarine.MainSub?.Info?.LayersHiddenByDefault?.Contains(l.ToIdentifier()) ?? false).Select(lt => TextManager.Capitalize(lt.Key)) ?? ((LocalizedString)"None").ToEnumerable()), selectMultiple: true);
foreach (string layerName in Layers.Keys)
var layerVisibilityDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), layerVisibilityGroup.RectTransform), text: visibleLayersString, selectMultiple: true);
foreach (var layer in Layers)
{
string layerName = layer.Key;
layerVisibilityDropDown.AddItem(TextManager.Capitalize(layerName), layerName);
if (MainSub?.Info == null) { continue; }
if (!MainSub.Info.LayersHiddenByDefault.Contains(layerName.ToIdentifier()))
if (visibleLayers.Contains(layer))
{
layerVisibilityDropDown.SelectItem(layerName);
}
}
layerVisibilityDropDown.OnSelected += (_, __) =>
layerVisibilityDropDown.OnSelected += (button, obj) =>
{
if (MainSub.Info == null) { return false; }
MainSub.Info.LayersHiddenByDefault.Clear();
foreach (string layerName in Layers.Keys)
string layerName = (string)obj;
bool isVisible = layerVisibilityDropDown.SelectedDataMultiple.Contains(obj);
if (isVisible)
{
//selected as visible = not hidden
if (layerVisibilityDropDown.SelectedDataMultiple.Any(o => o as string == layerName))
{
continue;
}
MainSub.Info.LayersHiddenByDefault.Add(layerName.ToIdentifier());
MainSub.Info.LayersHiddenByDefault.Remove(layerName.ToIdentifier());
}
else
{
MainSub.Info.LayersHiddenByDefault.Add(layerName.ToIdentifier());
}
UpdateLayerPanel();
layerVisibilityDropDown.Text = ToolBox.LimitString(layerVisibilityDropDown.Text.Value, layerVisibilityDropDown.Font, layerVisibilityDropDown.Rect.Width);
return true;
};
@@ -2511,6 +2494,15 @@ namespace Barotrauma
return true;
}
};
new GUITickBox(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), TextManager.Get("allowdamageddevices"))
{
Selected = MainSub?.Info?.BeaconStationInfo?.AllowDamagedDevices ?? true,
OnSelected = (tb) =>
{
MainSub.Info.BeaconStationInfo.AllowDamagedDevices = tb.Selected;
return true;
}
};
new GUITickBox(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), TextManager.Get("allowdisconnectedwires"))
{
Selected = MainSub?.Info?.BeaconStationInfo?.AllowDisconnectedWires ?? true,
@@ -3935,15 +3927,14 @@ namespace Barotrauma
MapEntity.HighlightedEntities.ToList() :
new List<MapEntity>(MapEntity.SelectedList);
Item target = null;
var single = targets.Count == 1 ? targets.Single() : null;
if (single is Item item && item.Components.Any(static ic => ic is not ConnectionPanel && ic is not Repairable && ic.GuiFrame != null))
{
// Do not offer the ability to open the inventory if the inventory should never be drawn
var containers = item.GetComponents<ItemContainer>();
if (containers.Any(static c => c.DrawInventory) || item.GetComponent<CircuitBox>() is not null) { target = item; }
}
bool allowOpening = false;
var targetItem = (targets.Count == 1 ? targets.Single() : null) as Item;
// Do not offer the ability to open the inventory if the inventory should never be drawn
allowOpening = targetItem is not null && targetItem.Components.Any(static ic =>
ic is not ConnectionPanel &&
ic is not Repairable &&
ic is not ItemContainer { DrawInventory: false } &&
ic.GuiFrame != null);
bool hasTargets = targets.Count > 0;
@@ -3987,7 +3978,6 @@ namespace Barotrauma
}
else
{
List<ContextMenuOption> availableLayers = new List<ContextMenuOption>
{
new ContextMenuOption("editor.layer.nolayer", true, onSelected: () => { MoveToLayer(null, targets); })
@@ -3995,7 +3985,8 @@ namespace Barotrauma
availableLayers.AddRange(Layers.Select(layer => new ContextMenuOption(layer.Key, true, onSelected: () => { MoveToLayer(layer.Key, targets); })));
List<ContextMenuOption> availableLayerOptions = new List<ContextMenuOption>
{ new ContextMenuOption("editor.layer.movetolayer", isEnabled: hasTargets, availableLayers.ToArray()),
{
new ContextMenuOption("editor.layer.movetolayer", isEnabled: hasTargets, availableLayers.ToArray()),
new ContextMenuOption("editor.layer.createlayer", isEnabled: hasTargets, onSelected: () => { CreateNewLayer(null, targets); }),
new ContextMenuOption("editor.layer.selectall", isEnabled: hasTargets, onSelected: () =>
{
@@ -4009,7 +4000,7 @@ namespace Barotrauma
availableLayerOptions.AddRange(Layers.Select(layer => new ContextMenuOption(layer.Key, true, onSelected: () => { MoveToLayer(layer.Key, targets); })));
GUIContextMenu.CreateContextMenu(
new ContextMenuOption("label.openlabel", isEnabled: target != null, onSelected: () => OpenItem(target)),
new ContextMenuOption("label.openlabel", isEnabled: allowOpening, onSelected: () => OpenItem(targetItem)),
new ContextMenuOption("editor.cut", isEnabled: hasTargets, onSelected: () => MapEntity.Cut(targets)),
new ContextMenuOption("editor.copytoclipboard", isEnabled: hasTargets, onSelected: () => MapEntity.Copy(targets)),
new ContextMenuOption("editor.paste", isEnabled: MapEntity.CopiedList.Any(), onSelected: () => MapEntity.Paste(cam.ScreenToWorld(PlayerInput.MousePosition))),
@@ -4064,13 +4055,13 @@ namespace Barotrauma
MoveToLayer(name, content);
}
Layers.Add(name, LayerData.Default);
Layers.Add(name, new LayerData());
UpdateLayerPanel();
}
private void RenameLayer(string original, string newName)
{
Layers.Remove(original);
Layers.Remove(original, out LayerData originalData);
foreach (MapEntity entity in MapEntity.MapEntityList.Where(entity => entity.Layer == original))
{
@@ -4079,7 +4070,7 @@ namespace Barotrauma
if (!string.IsNullOrWhiteSpace(newName))
{
Layers.TryAdd(newName, LayerData.Default);
Layers.TryAdd(newName, originalData);
}
UpdateLayerPanel();
}
@@ -4091,7 +4082,7 @@ namespace Barotrauma
{
if (!string.IsNullOrWhiteSpace(entity.Layer))
{
Layers.TryAdd(entity.Layer, LayerData.Default);
Layers.TryAdd(entity.Layer, new LayerData(!entity.IsLayerHidden));
}
}
UpdateLayerPanel();
@@ -4102,6 +4093,18 @@ namespace Barotrauma
Layers.Clear();
UpdateLayerPanel();
}
private static void SetLayerVisibility(string layerName, bool isVisible)
{
if (Layers.Remove(layerName, out LayerData layerData))
{
Layers.Add(layerName, layerData with { IsVisible = isVisible });
}
else
{
Layers.Add(layerName, new LayerData(isVisible));
}
}
private void PasteAssembly(string text = null, Vector2? pos = null)
{
@@ -4495,39 +4498,39 @@ namespace Barotrauma
}
/// <summary>
/// Tries to open an item container in the submarine editor using the dummy character
/// Tries to open an item in the submarine editor using the dummy character
/// </summary>
/// <param name="itemContainer">The item we want to open</param>
private void OpenItem(Item itemContainer)
/// <param name="item">The item we want to open</param>
private void OpenItem(Item item)
{
if (dummyCharacter == null || itemContainer == null) { return; }
if (dummyCharacter == null || item == null) { return; }
if ((itemContainer.GetComponent<Holdable>() is { Attached: false } || itemContainer.GetComponent<Wearable>() != null) && itemContainer.GetComponent<ItemContainer>() != null)
if ((item.GetComponent<Holdable>() is { Attached: false } || item.GetComponent<Wearable>() != null) && item.GetComponent<ItemContainer>() != null)
{
// We teleport our dummy character to the item so it appears as the entity stays still when in reality the dummy is holding it
oldItemPosition = itemContainer.SimPosition;
oldItemPosition = item.SimPosition;
TeleportDummyCharacter(oldItemPosition);
// Override this so we can be sure the container opens
var container = itemContainer.GetComponent<ItemContainer>();
var container = item.GetComponent<ItemContainer>();
if (container != null) { container.KeepOpenWhenEquipped = true; }
// We accept any slots except "Any" since that would take priority
List<InvSlotType> allowedSlots = new List<InvSlotType>();
itemContainer.AllowedSlots.ForEach(type =>
item.AllowedSlots.ForEach(type =>
{
if (type != InvSlotType.Any) { allowedSlots.Add(type); }
});
// Try to place the item in the dummy character's inventory
bool success = dummyCharacter.Inventory.TryPutItem(itemContainer, dummyCharacter, allowedSlots);
if (success) { OpenedItem = itemContainer; }
bool success = dummyCharacter.Inventory.TryPutItem(item, dummyCharacter, allowedSlots);
if (success) { OpenedItem = item; }
else { return; }
}
MapEntity.SelectedList.Clear();
MapEntity.FilteredSelectedList.Clear();
MapEntity.SelectEntity(itemContainer);
dummyCharacter.SelectedItem = itemContainer;
MapEntity.SelectEntity(item);
dummyCharacter.SelectedItem = item;
FilterEntities(entityFilterBox.Text);
MapEntity.StopSelection();
}
@@ -5179,7 +5182,7 @@ namespace Barotrauma
};
new GUIButton(new RectTransform(new Vector2(0.6f, 1f), buttonHeaders.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes };
foreach (var (layer, (visibility, linkage)) in Layers)
foreach ((string layer, (bool isVisible, bool isGrouped)) in Layers)
{
GUIFrame parent = new GUIFrame(new RectTransform(new Vector2(1f, 0.1f), layerList.Content.RectTransform), style: "ListBoxElement")
{
@@ -5191,7 +5194,7 @@ namespace Barotrauma
GUILayoutGroup layerVisibilityLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.25f, 1f), layerGroup.RectTransform), childAnchor: Anchor.Center);
GUITickBox layerVisibleButton = new GUITickBox(new RectTransform(Vector2.One, layerVisibilityLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), string.Empty)
{
Selected = visibility == LayerVisibility.Visible,
Selected = isVisible,
OnSelected = box =>
{
if (!Layers.TryGetValue(layer, out LayerData data))
@@ -5199,12 +5202,15 @@ namespace Barotrauma
UpdateLayerPanel();
return false;
}
//hiding a layer automatically deselects it (can't edit a hidden layer)
if (!box.Selected && layerList.SelectedData as string == layer)
{
layerList.Deselect();
//hiding a layer automatically deselects it (can't edit a hidden layer)
if (!box.Selected)
{
layerList.Deselect();
}
}
Layers[layer] = new LayerData(box.Selected ? LayerVisibility.Visible : LayerVisibility.Invisible, data.Linkage);
Layers[layer] = data with { IsVisible = box.Selected };
return true;
}
};
@@ -5212,7 +5218,7 @@ namespace Barotrauma
GUILayoutGroup layerChainLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.15f, 1f), layerGroup.RectTransform), childAnchor: Anchor.Center);
GUITickBox layerChainButton = new GUITickBox(new RectTransform(Vector2.One, layerChainLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), string.Empty)
{
Selected = linkage == LayerLinkage.Linked,
Selected = isGrouped,
OnSelected = box =>
{
if (!Layers.TryGetValue(layer, out LayerData data))
@@ -5221,7 +5227,7 @@ namespace Barotrauma
return false;
}
Layers[layer] = new LayerData(data.Visible, box.Selected ? LayerLinkage.Linked : LayerLinkage.Unlinked);
Layers[layer] = data with { IsGrouped = box.Selected };
return true;
}
};
@@ -5252,7 +5258,6 @@ namespace Barotrauma
btn.ToolTip = originalBtnText;
}
}
}
public void UpdateUndoHistoryPanel()
@@ -6306,11 +6311,11 @@ namespace Barotrauma
if (!Layers.TryGetValue(entity.Layer, out LayerData data))
{
Layers.TryAdd(entity.Layer, LayerData.Default);
Layers.TryAdd(entity.Layer, new LayerData(!entity.IsLayerHidden));
return true;
}
return data.Visible == LayerVisibility.Visible;
return data.IsVisible;
}
public static bool IsLayerLinked(MapEntity entity)
@@ -6319,11 +6324,11 @@ namespace Barotrauma
if (!Layers.TryGetValue(entity.Layer, out LayerData data))
{
Layers.TryAdd(entity.Layer, LayerData.Default);
Layers.TryAdd(entity.Layer, new LayerData(!entity.IsLayerHidden));
return true;
}
return data.Linkage == LayerLinkage.Linked;
return data.IsGrouped;
}
public static ImmutableHashSet<MapEntity> GetEntitiesInSameLayer(MapEntity entity)

View File

@@ -326,7 +326,7 @@ namespace Barotrauma
public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, IEnumerable<SerializableProperty> properties, bool showName, string style = "", int elementHeight = 24, GUIFont titleFont = null)
: base(style, new RectTransform(Vector2.One, parent))
{
this.elementHeight = (int)(elementHeight * GUI.Scale);
elementHeight = (int)(elementHeight * GUI.Scale);
var tickBoxStyle = GUIStyle.GetComponentStyle("GUITickBox");
var textBoxStyle = GUIStyle.GetComponentStyle("GUITextBox");
var numberInputStyle = GUIStyle.GetComponentStyle("GUINumberInput");
@@ -343,7 +343,50 @@ namespace Barotrauma
Color = Color.Black
};
}
properties.ForEach(ep => CreateNewField(ep, entity));
List<Header> headers = new List<Header>()
{
//"no header" comes first = properties under no header are listed first
null
};
//check which header each property is under
Dictionary<SerializableProperty, Header> propertyHeaders = new Dictionary<SerializableProperty, Header>();
Header prevHeader = null;
foreach (var property in properties)
{
var header = property.GetAttribute<Header>();
if (header != null)
{
prevHeader = header;
//Attribute.Equals is based on the equality of the fields,
//so in practice we treat identical headers split into different files/classes as the same header
if (!headers.Contains(header))
{
//collect headers into a list in the order they're encountered in
//(to keep them in the same order as they're defined in the code, as the dictionary is not in any particular order)
headers.Add(header);
}
}
propertyHeaders[property] = prevHeader;
}
prevHeader = null;
foreach (Header header in headers)
{
//go through all the properties that belong under this header
foreach (var property in properties)
{
if (!Equals(propertyHeaders[property], header)) { continue; }
//don't create a header if the previous header has the same text as this one (= if we already created this header before)
if (header != null && !Equals(header, prevHeader))
{
new GUITextBlock(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true),
header.Text, textColor: GUIStyle.TextColorBright, font: GUIStyle.SubHeadingFont);
prevHeader = header;
}
CreateNewField(property, entity);
}
}
//scale the size of this component and the layout group to fit the children
Recalculate();

View File

@@ -137,22 +137,10 @@ namespace Barotrauma.Sounds
{
for (int i = 0; i < length; i++)
{
outBuffer[i] = FloatToShort(inBuffer[i]);
outBuffer[i] = ToolBox.FloatToShortAudioSample(inBuffer[i]);
}
}
static protected short FloatToShort(float fVal)
{
int temp = (int)(32767 * fVal);
if (temp > short.MaxValue) temp = short.MaxValue;
else if (temp < short.MinValue) temp = short.MinValue;
return (short)temp;
}
static protected float ShortToFloat(short shortVal)
{
return shortVal / 32767f;
}
public abstract int FillStreamBuffer(int samplePos, short[] buffer);
public abstract float GetAmplitudeAtPlaybackPos(int playbackPos);

View File

@@ -107,7 +107,7 @@ namespace Barotrauma.Sounds
float finalGain = gain * GameSettings.CurrentConfig.Audio.VoiceChatVolume * client.VoiceVolume;
for (int i = 0; i < readSamples; i++)
{
float fVal = ShortToFloat(buffer[i]);
float fVal = ToolBox.ShortAudioSampleToFloat(buffer[i]);
if (finalGain > 1.0f) //TODO: take distance into account?
{
@@ -128,7 +128,7 @@ namespace Barotrauma.Sounds
fVal = Math.Clamp(filter.Process(fVal) * PostRadioFilterBoost, -1f, 1f);
}
}
buffer[i] = FloatToShort(fVal);
buffer[i] = ToolBox.FloatToShortAudioSample(fVal);
}
}

View File

@@ -99,10 +99,10 @@ namespace Barotrauma.Steam
{
currentLobby?.SetData("EosEndpoint", puids[0].Value);
}
DebugConsole.Log("Lobby updated!");
}
private static void SetServerListInfo(Identifier key, object value)
{
switch (value)
@@ -115,7 +115,7 @@ namespace Barotrauma.Steam
.JoinEscaped(','));
return;
}
currentLobby?.SetData(key.Value.ToLowerInvariant(), value.ToString());
}

View File

@@ -6,8 +6,8 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>1.4.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Version>1.5.7.0</Version>
<Copyright>Copyright © FakeFish 2018-2024</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>
<ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon>

View File

@@ -6,8 +6,8 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>1.4.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Version>1.5.7.0</Version>
<Copyright>Copyright © FakeFish 2018-2024</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>
<ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon>

View File

@@ -6,8 +6,8 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>1.4.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Version>1.5.7.0</Version>
<Copyright>Copyright © FakeFish 2018-2024</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>
<ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon>

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>1.4.6.0</Version>
<Version>1.5.7.0</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>

View File

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

View File

@@ -1,4 +1,5 @@
using Barotrauma.Networking;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
@@ -26,6 +27,27 @@ namespace Barotrauma
}
}
if (GameMain.Server is { ServerSettings.RespawnMode: RespawnMode.Permadeath } &&
GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign &&
causeOfDeath != CauseOfDeathType.Disconnected)
{
Client ownerClient = GameMain.Server.ConnectedClients.FirstOrDefault(c => c.Character == this);
if (ownerClient != null)
{
ownerClient.SpectateOnly = true;
CharacterCampaignData matchingData = mpCampaign.GetClientCharacterData(ownerClient);
if (matchingData != null)
{
matchingData.ApplyPermadeath();
if (GameMain.Server is { ServerSettings.IronmanMode: true })
{
mpCampaign.SaveSingleCharacter(matchingData);
}
}
}
}
if (HasAbilityFlag(AbilityFlags.RetainExperienceForNewCharacter))
{
var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this);

View File

@@ -96,6 +96,7 @@ namespace Barotrauma
msg.WriteInt32(ExperiencePoints);
msg.WriteRangedInteger(AdditionalTalentPoints, 0, MaxAdditionalTalentPoints);
msg.WriteBoolean(PermanentlyDead);
}
}
}

View File

@@ -964,7 +964,7 @@ namespace Barotrauma
}
client.Muted = true;
GameMain.Server.SendDirectChatMessage(TextManager.Get("MutedByServer").Value, client, ChatMessageType.MessageBox);
},
},
() =>
{
if (GameMain.Server == null) return null;
@@ -1635,6 +1635,19 @@ namespace Barotrauma
NewMessage("Disabled RequireClientNameMatch");
}
}));
AssignOnExecute("debugvoip", _ =>
{
VoipServerDecoder.DebugVoip = !VoipServerDecoder.DebugVoip;
NewMessage("Debugging voice chat is now " + (VoipServerDecoder.DebugVoip ? "enabled" : "disabled"), Color.White);
});
AssignOnClientRequestExecute("debugvoip", (client, _, _) =>
{
VoipServerDecoder.DebugVoip = !VoipServerDecoder.DebugVoip;
NewMessage("Debugging voice chat is now " + (VoipServerDecoder.DebugVoip ? "enabled" : "disabled") + " by " + client.Name, Color.White);
GameMain.Server.SendConsoleMessage("Debugging voice chat is now " + (VoipServerDecoder.DebugVoip ? "enabled" : "disabled"), client);
});
#endif
AssignOnClientRequestExecute(
@@ -1797,11 +1810,7 @@ namespace Barotrauma
"teleportcharacter|teleport",
(Client client, Vector2 cursorWorldPos, string[] args) =>
{
Character tpCharacter = (args.Length == 0) ? client.Character : FindMatchingCharacter(args, false);
if (tpCharacter != null)
{
tpCharacter.TeleportTo(cursorWorldPos);
}
TeleportCharacter(cursorWorldPos, client.Character, args);
}
);
@@ -1958,6 +1967,17 @@ namespace Barotrauma
foreach (Client c in GameMain.Server.ConnectedClients)
{
if (c.Character != revivedCharacter) { continue; }
// If killed in ironman mode, the character has been wiped from the save mid-round, so its
// original data needs to be restored to the save file (without making a backup of the dead character)
if (GameMain.Server.ServerSettings.IronmanMode && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)
{
if (mpCampaign.RestoreSingleCharacterFromBackup(c) is CharacterCampaignData characterToRestore)
{
characterToRestore.CharacterInfo.PermanentlyDead = false;
mpCampaign.SaveSingleCharacter(characterToRestore, skipBackup: true);
}
}
//clients stop controlling the character when it dies, force control back
GameMain.Server.SetClientCharacter(c, revivedCharacter);
@@ -2581,6 +2601,91 @@ namespace Barotrauma
}
);
commands.Add(new Command("setsalary", "setsalary [0-100] [character/default]: Sets the salary of a certain character or the default salary to a percentage.", (string[] args) =>
{
if (args.Length < 2)
{
NewMessage($"Missing arguments. Expected at least 2 but got {args.Length} (amount, character)", Color.Red);
return;
}
if (GameMain.GameSession?.Campaign is not MultiPlayerCampaign mpCampaign)
{
NewMessage("No campaign active.", Color.Red);
return;
}
if (!int.TryParse(args[0], out int amount))
{
NewMessage($"{args[0]} is not a valid amount.", Color.Red);
return;
}
if (args[1].Equals("default", StringComparison.OrdinalIgnoreCase))
{
mpCampaign.Bank.SetRewardDistribution(amount);
NewMessage($"Set the default salary to {amount}%", Color.White);
return;
}
Character character = FindMatchingCharacter(args.Skip(1).ToArray());
if (character is null)
{
NewMessage($"Character not found \"{args[1]}\".", Color.Red);
return;
}
character.Wallet.SetRewardDistribution(amount);
NewMessage($"Set {character.Name}'s salary to {amount}%", Color.White);
}));
AssignOnClientRequestExecute(
"setsalary",
(senderClient, cursorWorldPos, args) =>
{
if (args.Length < 2)
{
GameMain.Server.SendConsoleMessage($"Missing arguments. Expected at least 2 but got {args.Length} (amount, character)", senderClient, Color.Red);
return;
}
if (!CampaignMode.AllowedToManageWallets(senderClient))
{
GameMain.Server.SendConsoleMessage("You are not allowed to manage wallets.", senderClient, Color.Red);
return;
}
if (GameMain.GameSession?.Campaign is not MultiPlayerCampaign mpCampaign)
{
GameMain.Server.SendConsoleMessage("No campaign active.", senderClient, Color.Red);
return;
}
if (!int.TryParse(args[0], out int amount))
{
GameMain.Server.SendConsoleMessage($"{args[0]} is not a valid amount.", senderClient, Color.Red);
return;
}
if (args[1].Equals("default", StringComparison.OrdinalIgnoreCase))
{
mpCampaign.Bank.SetRewardDistribution(amount);
GameMain.Server.SendConsoleMessage($"Set the default salary to {amount}%", senderClient);
return;
}
Character character = FindMatchingCharacter(args.Skip(1).ToArray());
if (character is null)
{
GameMain.Server.SendConsoleMessage($"Character not found \"{args[1]}\".", senderClient, Color.Red);
return;
}
character.Wallet.SetRewardDistribution(amount);
GameMain.Server.SendConsoleMessage($"Set {character.Name}'s salary to {amount}%.", senderClient);
}
);
commands.Add(new Command("readycheck", "Commence a ready check.", (string[] args) =>
{
if (Screen.Selected == GameMain.GameScreen && GameMain.NetworkMember != null)
@@ -2643,7 +2748,7 @@ namespace Barotrauma
}));
#endif
}
public static void ServerRead(IReadMessage inc, Client sender)
{
string consoleCommand = inc.ReadString();

View File

@@ -2,7 +2,7 @@ using Barotrauma.Networking;
namespace Barotrauma
{
partial class AlienRuinMission : Mission
partial class EliminateTargetsMission : Mission
{
public override void ServerWriteInitial(IWriteMessage msg, Client c)
{

View File

@@ -123,6 +123,12 @@ namespace Barotrauma
public bool IsDuplicate(CharacterCampaignData other)
{
#if DEBUG
if (RequireClientNameMatch)
{
return AccountId == other.AccountId && other.ClientAddress == ClientAddress && Name == other.Name;
}
#endif
return AccountId == other.AccountId && other.ClientAddress == ClientAddress;
}
@@ -133,6 +139,13 @@ namespace Barotrauma
WalletData = null;
}
public void ApplyPermadeath()
{
Reset();
CharacterInfo.PermanentlyDead = true;
DebugConsole.NewMessage($"Permadeath applied on {Name}'s CharacterCampaignData.CharacterInfo.");
}
public void SpawnInventoryItems(Character character, Inventory inventory)
{
if (character == null)
@@ -158,7 +171,7 @@ namespace Barotrauma
public void ApplyWalletData(Character character)
{
character.Wallet = new Wallet(Option<Character>.Some(character), WalletData);
character.Wallet = new Wallet(Option.Some(character), WalletData);
}
public XElement Save()
@@ -167,7 +180,6 @@ namespace Barotrauma
new XAttribute("name", Name),
new XAttribute("address", ClientAddress),
new XAttribute("accountid", AccountId.TryUnwrap(out var accountId) ? accountId.StringRepresentation : ""));
CharacterInfo?.Save(element);
if (itemData != null) { element.Add(itemData); }
if (healthData != null) { element.Add(healthData); }

View File

@@ -16,6 +16,12 @@ namespace Barotrauma
private readonly HashSet<NetWalletTransaction> transactions = new HashSet<NetWalletTransaction>();
private const float clientCheckInterval = 10;
private float clientCheckTimer = clientCheckInterval;
/// <summary>
/// Temporary backup storage for characters that have been overwritten by SaveSingleCharacter, this will be gone
/// once the round ends or the server closes. Currently needed to enable the console command "revive" in ironman mode.
/// </summary>
public List<CharacterCampaignData> replacedCharacterDataBackup = new List<CharacterCampaignData>();
public override Wallet GetWallet(Client client = null)
{
@@ -295,6 +301,12 @@ namespace Barotrauma
MoveDiscardedCharacterBalancesToBank();
characterData.ForEach(cd => cd.HasSpawned = false);
foreach (var cd in characterData)
{
//remove from crewmanager - we don't need to save the data there if it's been saved as CharacterCampaignData
//(e.g. if a client has taken over a bot, we need to do this to prevent it being saved twice)
CrewManager.RemoveCharacterInfo(cd.CharacterInfo);
}
SavePets();
@@ -380,6 +392,9 @@ namespace Barotrauma
GameMain.GameSession.EventManager.RegisterEventHistory();
}
//store the currently active missions at this point so we can communicate their states to clients, they're cleared in EndRound
List<Mission> missions = GameMain.GameSession.Missions.ToList();
GameMain.GameSession.EndRound("", transitionType);
//--------------------------------------
@@ -407,7 +422,7 @@ namespace Barotrauma
//--------------------------------------
GameMain.Server.EndGame(transitionType, wasSaved: true);
GameMain.Server.EndGame(transitionType, wasSaved: true, missions);
ForceMapUI = false;
@@ -513,6 +528,9 @@ namespace Barotrauma
Map?.Radiation?.UpdateRadiation(deltaTime);
base.Update(deltaTime);
MedicalClinic?.Update(deltaTime);
if (Level.Loaded != null)
{
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection)
@@ -1165,42 +1183,59 @@ namespace Barotrauma
if (!AllowedToManageWallets(sender)) { return; }
Character targetCharacter = Character.CharacterList.FirstOrDefault(c => c.ID == update.Target);
targetCharacter?.Wallet.SetRewardDistribution(update.NewRewardDistribution);
GameServer.Log($"{sender.Name} changed the salary of {targetCharacter?.Name ?? "the bank"} to {update.NewRewardDistribution}%.", ServerLog.MessageType.Money);
if (update.Target.TryUnwrap(out ushort id))
{
Character targetCharacter = Character.CharacterList.FirstOrDefault(c => c.ID == id);
targetCharacter?.Wallet.SetRewardDistribution(update.NewRewardDistribution);
GameServer.Log($"{sender.Name} changed the salary of {targetCharacter?.Name} to {update.NewRewardDistribution}%.", ServerLog.MessageType.Money);
return;
}
Bank.SetRewardDistribution(update.NewRewardDistribution);
GameServer.Log($"{sender.Name} changed the default salary to {update.NewRewardDistribution}%.", ServerLog.MessageType.Money);
}
public void ResetSalaries(Client sender)
{
if (!AllowedToManageWallets(sender)) { return; }
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Player))
{
character.Wallet.SetRewardDistribution(Bank.RewardDistribution);
}
}
public void ServerReadCrew(IReadMessage msg, Client sender)
{
int[] pendingHires = null;
UInt16[] pendingHires = null;
bool updatePending = msg.ReadBoolean();
if (updatePending)
{
ushort pendingHireLength = msg.ReadUInt16();
pendingHires = new int[pendingHireLength];
pendingHires = new UInt16[pendingHireLength];
for (int i = 0; i < pendingHireLength; i++)
{
pendingHires[i] = msg.ReadInt32();
pendingHires[i] = msg.ReadUInt16();
}
}
bool validateHires = msg.ReadBoolean();
bool renameCharacter = msg.ReadBoolean();
int renamedIdentifier = -1;
UInt16 renamedIdentifier = 0;
string newName = null;
bool existingCrewMember = false;
if (renameCharacter)
{
renamedIdentifier = msg.ReadInt32();
renamedIdentifier = msg.ReadUInt16();
newName = msg.ReadString();
existingCrewMember = msg.ReadBoolean();
}
bool fireCharacter = msg.ReadBoolean();
int firedIdentifier = -1;
if (fireCharacter) { firedIdentifier = msg.ReadInt32(); }
if (fireCharacter) { firedIdentifier = msg.ReadUInt16(); }
Location location = map?.CurrentLocation;
CharacterInfo firedCharacter = null;
@@ -1209,7 +1244,7 @@ namespace Barotrauma
{
if (fireCharacter)
{
firedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.GetIdentifier() == firedIdentifier);
firedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.ID == firedIdentifier);
if (firedCharacter != null && (firedCharacter.Character?.IsBot ?? true))
{
CrewManager.FireCharacter(firedCharacter);
@@ -1225,11 +1260,11 @@ namespace Barotrauma
CharacterInfo characterInfo = null;
if (existingCrewMember && CrewManager != null)
{
characterInfo = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.GetIdentifierUsingOriginalName() == renamedIdentifier);
characterInfo = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.ID == renamedIdentifier);
}
else if(!existingCrewMember && location.HireManager != null)
{
characterInfo = location.HireManager.AvailableCharacters.FirstOrDefault(info => info.GetIdentifierUsingOriginalName() == renamedIdentifier);
characterInfo = location.HireManager.AvailableCharacters.FirstOrDefault(info => info.ID == renamedIdentifier);
}
if (characterInfo != null && (characterInfo.Character?.IsBot ?? true))
@@ -1262,9 +1297,9 @@ namespace Barotrauma
if (updatePending)
{
List<CharacterInfo> pendingHireInfos = new List<CharacterInfo>();
foreach (int identifier in pendingHires)
foreach (UInt16 identifier in pendingHires)
{
CharacterInfo match = location.GetHireableCharacters().FirstOrDefault(info => info.GetIdentifierUsingOriginalName() == identifier);
CharacterInfo match = location.GetHireableCharacters().FirstOrDefault(info => info.ID == identifier);
if (match == null)
{
DebugConsole.ThrowError($"Tried to add a character that doesn't exist ({identifier}) to pending hires");
@@ -1311,7 +1346,7 @@ namespace Barotrauma
/// the client and the server when there's only one person on the server but when a second person joins both of
/// their available hires are different from the server.
/// </remarks>
public void SendCrewState((int id, string newName) renamedCrewMember = default, CharacterInfo firedCharacter = null)
public void SendCrewState((ushort id, string newName) renamedCrewMember = default, CharacterInfo firedCharacter = null, bool createNotification = true)
{
List<CharacterInfo> availableHires = new List<CharacterInfo>();
List<CharacterInfo> pendingHires = new List<CharacterInfo>();
@@ -1327,6 +1362,8 @@ namespace Barotrauma
IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ServerPacketHeader.CREW);
msg.WriteBoolean(createNotification);
msg.WriteUInt16((ushort)availableHires.Count);
foreach (CharacterInfo hire in availableHires)
{
@@ -1337,7 +1374,7 @@ namespace Barotrauma
msg.WriteUInt16((ushort)pendingHires.Count);
foreach (CharacterInfo pendingHire in pendingHires)
{
msg.WriteInt32(pendingHire.GetIdentifierUsingOriginalName());
msg.WriteUInt16(pendingHire.ID);
}
var hiredCharacters = CrewManager.GetCharacterInfos().Where(ci => ci.IsNewHire);
@@ -1348,16 +1385,16 @@ namespace Barotrauma
msg.WriteInt32(info.Salary);
}
bool validRenaming = renamedCrewMember.id > -1 && !string.IsNullOrEmpty(renamedCrewMember.newName);
bool validRenaming = renamedCrewMember.id > 0 && !string.IsNullOrEmpty(renamedCrewMember.newName);
msg.WriteBoolean(validRenaming);
if (validRenaming)
{
msg.WriteInt32(renamedCrewMember.id);
msg.WriteUInt16(renamedCrewMember.id);
msg.WriteString(renamedCrewMember.newName);
}
msg.WriteBoolean(firedCharacter != null);
if (firedCharacter != null) { msg.WriteInt32(firedCharacter.GetIdentifier()); }
if (firedCharacter != null) { msg.WriteUInt16(firedCharacter.ID); }
GameMain.Server.ServerPeer.Send(msg, client.Connection, DeliveryMethod.Reliable);
}
@@ -1475,5 +1512,57 @@ namespace Barotrauma
lastSaveID++;
DebugConsole.Log("Campaign saved, save ID " + lastSaveID);
}
/// <summary>
/// Load the current character save file and add/replace a single character's data with a new version immediately.
/// </summary>
/// <param name="newData">New character to insert. If it matches one existing in the save, that will get replaced.</param>
/// <param name="skipBackup">By default, replaced characters will be temporarily backed up, but that might be unwanted
/// eg. when using this method to save a character itself restored from the backup.</param>
public void SaveSingleCharacter(CharacterCampaignData newData, bool skipBackup = false)
{
string characterDataPath = GetCharacterDataSavePath();
if (!File.Exists(characterDataPath))
{
DebugConsole.ThrowError($"Failed to load the character data for the campaign. Could not find the file \"{characterDataPath}\".");
}
else
{
var loadedCharacterData = XMLExtensions.TryLoadXml(characterDataPath);
if (loadedCharacterData?.Root == null) { return; }
var oldData = loadedCharacterData.Root.Elements()
.FirstOrDefault(subElement => new CharacterCampaignData(subElement).IsDuplicate(newData));
if (oldData != null)
{
if (!skipBackup)
{
replacedCharacterDataBackup.Add(new CharacterCampaignData(oldData));
}
oldData.Remove();
}
loadedCharacterData.Root.Add(newData.Save());
try
{
loadedCharacterData.SaveSafe(characterDataPath);
}
catch (Exception e)
{
DebugConsole.ThrowError("Saving multiplayer campaign characters to \"" + characterDataPath + "\" failed!", e);
}
}
}
public CharacterCampaignData RestoreSingleCharacterFromBackup(Client client)
{
if (replacedCharacterDataBackup.Find(cd => cd.MatchesClient(client)) is CharacterCampaignData characterToRestore)
{
replacedCharacterDataBackup.Remove(characterToRestore);
return characterToRestore;
}
return default;
}
}
}

View File

@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System;
using System.Collections.Generic;
@@ -22,6 +22,36 @@ namespace Barotrauma
private readonly List<AfflictionSubscriber> afflictionSubscribers = new();
public void Update(float deltaTime)
{
processAfflictionChangesTimer -= deltaTime;
if (processAfflictionChangesTimer <= 0.0f)
{
foreach (var character in charactersWithAfflictionChanges)
{
ImmutableArray<NetAffliction> afflictions = GetAllAfflictions(character.CharacterHealth);
foreach (AfflictionSubscriber sub in afflictionSubscribers.ToList())
{
if (sub.Expiry < DateTimeOffset.Now)
{
afflictionSubscribers.Remove(sub);
continue;
}
if (sub.Target == character.Info)
{
ServerSend(new NetCrewMember(character.Info, afflictions),
header: NetworkHeader.AFFLICTION_UPDATE,
deliveryMethod: DeliveryMethod.Unreliable,
targetClient: sub.Subscriber);
}
}
}
charactersWithAfflictionChanges.Clear();
processAfflictionChangesTimer = ProcessAfflictionChangesInterval;
}
}
public void ServerRead(IReadMessage inc, Client sender)
{
NetworkHeader header = (NetworkHeader)inc.ReadByte();
@@ -141,7 +171,7 @@ namespace Barotrauma
if (foundInfo is { Character.CharacterHealth: { } health })
{
pendingAfflictions = GetAllAfflictions(health);
infoId = foundInfo.GetIdentifierUsingOriginalName();
infoId = foundInfo.ID;
}
INetSerializableStruct writeCrewMember = new NetCrewMember

View File

@@ -18,10 +18,13 @@ namespace Barotrauma.Items.Components
private bool needsServerInitialization;
/// <summary>
/// When in multiplayer and the circuit box is loaded from the players inventory,
/// We only load the components from XML on server side since only the server has access to CharacterCampaignData
/// and then send a network event syncing the loaded properties. But circuit box properties are too complex to
/// sync using the existing syncing logic so we instead send the state using <see cref="CircuitBoxInitializeStateFromServerEvent"/>.
/// When in multiplayer and the circuit box are loaded from the player inventory,
/// We only load the components from XML on the server side
/// since only the server has access to CharacterCampaignData
/// and then send a network event syncing the loaded properties.
/// But circuit box properties are too complex to
/// sync using the existing syncing logic,
/// so we instead send the state using <see cref="CircuitBoxInitializeStateFromServerEvent"/>.
/// </summary>
public void MarkServerRequiredInitialization()
=> needsServerInitialization = true;
@@ -280,6 +283,15 @@ namespace Barotrauma.Items.Components
CreateServerEvent(data with { Size = Vector2.Max(data.Size, CircuitBoxLabelNode.MinSize) });
break;
}
case CircuitBoxOpcode.RenameConnections:
{
var data = INetSerializableStruct.Read<CircuitBoxRenameConnectionLabelsEvent>(msg);
if (!CanAccessAndUnlocked(c)) { break; }
RenameConnectionLabelsInternal(data.Type, data.Override.ToDictionary());
CreateServerEvent(data);
break;
}
default:
throw new ArgumentOutOfRangeException(nameof(header), header, "This opcode cannot be handled using entity events");
}
@@ -327,6 +339,7 @@ namespace Barotrauma.Items.Components
Components: Components.Select(EventFromComponent).ToImmutableArray(),
Wires: Wires.Select(EventFromWire).ToImmutableArray(),
Labels: Labels.Select(EventFromLabel).ToImmutableArray(),
LabelOverrides: InputOutputNodes.Select(EventFromLabelOverride).ToImmutableArray(),
InputPos: inputPos,
OutputPos: outputPos);
@@ -347,6 +360,9 @@ namespace Barotrauma.Items.Components
static CircuitBoxServerAddLabelEvent EventFromLabel(CircuitBoxLabelNode label)
=> new(label.ID, label.Position, label.Size, label.Color, label.HeaderText, label.BodyText);
static CircuitBoxRenameConnectionLabelsEvent EventFromLabelOverride(CircuitBoxInputOutputNode node)
=> new(node.NodeType, node.ConnectionLabelOverrides.ToNetDictionary());
}
// we don't care about updating the view on server

View File

@@ -98,7 +98,7 @@ namespace Barotrauma.Items.Components
//existing wire not in the list of new wires -> disconnect it
if (!wires[i].Contains(existingWire))
{
if (existingWire.Locked)
if (existingWire.Locked || existingWire.Item.IsLayerHidden)
{
//this should not be possible unless the client is running a modified version of the game
GameServer.Log(GameServer.CharacterLogName(c.Character) + " attempted to disconnect a locked wire from " +
@@ -166,18 +166,6 @@ namespace Barotrauma.Items.Components
}
}
foreach (Wire disconnectedWire in DisconnectedWires.ToList())
{
if (disconnectedWire.Connections[0] == null &&
disconnectedWire.Connections[1] == null &&
!clientSideDisconnectedWires.Contains(disconnectedWire) &&
disconnectedWire.Item.ParentInventory == null)
{
disconnectedWire.Item.Drop(c.Character);
GameServer.Log(GameServer.CharacterLogName(c.Character) + " dropped " + disconnectedWire.Name, ServerLog.MessageType.Inventory);
}
}
//go through new wires
for (int i = 0; i < Connections.Count; i++)
{
@@ -205,6 +193,18 @@ namespace Barotrauma.Items.Components
}
}
}
foreach (Wire disconnectedWire in DisconnectedWires.ToList())
{
if (disconnectedWire.Connections[0] == null &&
disconnectedWire.Connections[1] == null &&
!clientSideDisconnectedWires.Contains(disconnectedWire) &&
disconnectedWire.Item.ParentInventory == null)
{
disconnectedWire.Item.Drop(c.Character);
GameServer.Log(GameServer.CharacterLogName(c.Character) + " dropped " + disconnectedWire.Name, ServerLog.MessageType.Inventory);
}
}
}
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)

View File

@@ -11,153 +11,42 @@ namespace Barotrauma
{
private readonly Dictionary<Client, List<ushort>[]> receivedItemIds = new Dictionary<Client, List<ushort>[]>();
public void ServerEventRead(IReadMessage msg, Client c)
public void ServerEventRead(IReadMessage msg, Client sender)
{
if (!receivedItemIds.TryGetValue(c, out List<ushort>[] receivedItemIdsFromClient))
// if the dictionary doesn't contain the client entry, create a new one
if (!receivedItemIds.TryGetValue(sender, out List<ushort>[] receivedItemIdsFromClient))
{
receivedItemIdsFromClient = new List<ushort>[capacity];
receivedItemIds.Add(c, receivedItemIdsFromClient);
receivedItemIds.Add(sender, receivedItemIdsFromClient);
}
// Read some item ids from the message. readyToApply waits for all the data from possible multiple messages.
SharedRead(msg, receivedItemIdsFromClient, out bool readyToApply);
if (!readyToApply) { return; }
if (c == null || c.Character == null) { return; }
bool accessible = c.Character.CanAccessInventory(this);
if (this is CharacterInventory characterInventory && accessible)
if (sender == null || sender.Character == null) { return; }
if (!IsInventoryAccessible())
{
if (Owner == null || Owner is not Character ownerCharacter)
{
accessible = false;
}
else if (!characterInventory.AccessibleWhenAlive && !ownerCharacter.IsDead && !characterInventory.AccessibleByOwner)
{
accessible = false;
}
}
if (!accessible)
{
//create a network event to correct the client's inventory state
//otherwise they may have an item in their inventory they shouldn't have been able to pick up,
//and receiving an event for that inventory later will cause the item to be dropped
CreateNetworkEvent();
for (int i = 0; i < capacity; i++)
{
foreach (ushort id in receivedItemIdsFromClient[i])
{
if (Entity.FindEntityByID(id) is not Item item) { continue; }
item.PositionUpdateInterval = 0.0f;
if (item.ParentInventory != null && item.ParentInventory != this)
{
item.ParentInventory.CreateNetworkEvent();
}
}
}
CreateCorrectiveNetworkEvent();
return;
}
//we need to check which of the items the client can access at this point, before we start shuffling things around
//otherwise if you're e.g. holding an item to access a cabinet, and picking up an item from the cabinet unequips the item you're holding,
//you would fail to pick up the item because it gets unequipped before checking whether you can access the cabinet.
Dictionary<Item, bool> canAccessItem = new Dictionary<Item, bool>();
for (int i = 0; i < capacity; i++)
{
foreach (ushort id in receivedItemIdsFromClient[i])
{
if (Entity.FindEntityByID(id) is not Item item) { continue; }
canAccessItem[item] = item.CanClientAccess(c);
}
}
List<Item> prevItems = new List<Item>(AllItems.Distinct());
List<Inventory> prevItemInventories = new List<Inventory>() { this };
for (int i = 0; i < capacity; i++)
{
foreach (Item item in slots[i].Items.ToList())
{
if (!receivedItemIdsFromClient[i].Contains(item.ID) && item.IsInteractable(c.Character))
{
Item droppedItem = item;
Entity prevOwner = Owner;
Inventory previousInventory = droppedItem.ParentInventory;
droppedItem.Drop(null);
droppedItem.PreviousParentInventory = previousInventory;
var previousCharacterInventory = prevOwner switch
{
Item itemInventory => itemInventory.FindParentInventory(inventory => inventory is CharacterInventory) as CharacterInventory,
Character character => character.Inventory,
_ => null
};
//we need to check which of the items the client (sender) can access at this point, before we start shuffling things around
//otherwise if you're e.g. holding an item to access a cabinet, and picking up an item from the cabinet unequips the item you're holding,
//you would fail to pick up the item because it gets unequipped before checking whether you can access the cabinet.
var itemAccessibility = GetItemAccessibility();
HandleRemovedItems();
if (previousCharacterInventory != null && previousCharacterInventory != c.Character?.Inventory)
{
GameMain.Server?.KarmaManager.OnItemTakenFromPlayer(previousCharacterInventory, c, droppedItem);
}
if (droppedItem.body != null && prevOwner != null)
{
droppedItem.body.SetTransform(prevOwner.SimPosition, 0.0f);
}
}
}
HandleAddedItems();
foreach (ushort id in receivedItemIdsFromClient[i])
{
Item newItem = id == 0 ? null : Entity.FindEntityByID(id) as Item;
prevItemInventories.Add(newItem?.ParentInventory);
}
}
EnsureItemsInBothHands(sender.Character);
for (int i = 0; i < capacity; i++)
{
foreach (ushort id in receivedItemIdsFromClient[i])
{
if (Entity.FindEntityByID(id) is not Item item || slots[i].Contains(item)) { continue; }
if (item.GetComponent<Pickable>() is not Pickable pickable ||
(pickable.IsAttached && !pickable.PickingDone) || item.AllowedSlots.None() || !item.IsInteractable(c.Character))
{
DebugConsole.AddWarning($"Client {c.Name} tried to pick up a non-pickable item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"})",
item.Prefab.ContentPackage);
continue;
}
if (GameMain.Server != null)
{
var holdable = item.GetComponent<Holdable>();
if (holdable != null && !holdable.CanBeDeattached()) { continue; }
if (!prevItems.Contains(item) && !canAccessItem[item] &&
(c.Character == null || item.PreviousParentInventory == null || !c.Character.CanAccessInventory(item.PreviousParentInventory)))
{
#if DEBUG || UNSTABLE
DebugConsole.NewMessage($"Client {c.Name} failed to pick up item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"}). No access.", Color.Yellow);
#endif
if (item.body != null && !c.PendingPositionUpdates.Contains(item))
{
c.PendingPositionUpdates.Enqueue(item);
}
item.PositionUpdateInterval = 0.0f;
continue;
}
}
TryPutItem(item, i, true, true, c.Character, false);
for (int j = 0; j < capacity; j++)
{
if (slots[j].Contains(item) && !receivedItemIdsFromClient[j].Contains(item.ID))
{
slots[j].RemoveItem(item);
}
}
}
}
EnsureItemsInBothHands(c.Character);
receivedItemIds.Remove(c);
receivedItemIds.Remove(sender);
CreateNetworkEvent();
foreach (Inventory prevInventory in prevItemInventories.Distinct())
@@ -165,43 +54,211 @@ namespace Barotrauma
if (prevInventory != this) { prevInventory?.CreateNetworkEvent(); }
}
foreach (Item item in AllItems.DistinctBy(it => it.Prefab))
ServerLogAddedItems();
ServerLogRemovedItems();
#region local functions
bool IsInventoryAccessible() => sender.Character.CanAccessInventory(this, IsDragAndDropGiveAllowed ? CharacterInventory.AccessLevel.Allowed : CharacterInventory.AccessLevel.Limited);
void CreateCorrectiveNetworkEvent()
{
if (item == null) { continue; }
if (!prevItems.Contains(item))
// create a network event to correct the client's inventory state.
// Otherwise they may have an item in their inventory they shouldn't have been able to pick up,
// and receiving an event for that inventory later will cause the item to be dropped
CreateNetworkEvent();
for (int i = 0; i < capacity; i++)
{
int amount = AllItems.Count(it => it.Prefab == item.Prefab && !prevItems.Contains(it));
string amountText = amount > 1 ? $"x{amount} " : string.Empty;
if (Owner == c.Character)
foreach (ushort itemId in receivedItemIdsFromClient[i])
{
HumanAIController.ItemTaken(item, c.Character);
GameServer.Log($"{GameServer.CharacterLogName(c.Character)} picked up {amountText}{item.Name}", ServerLog.MessageType.Inventory);
if (Entity.FindEntityByID(itemId) is not Item item) { continue; }
item.PositionUpdateInterval = 0.0f;
if (item.ParentInventory != null && item.ParentInventory != this)
{
item.ParentInventory.CreateNetworkEvent();
}
}
}
}
Dictionary<Item, bool> GetItemAccessibility()
{
Dictionary<Item, bool> itemAccessibility = new Dictionary<Item, bool>();
for (int i = 0; i < capacity; i++)
{
// for every item that the new inventory state contains
foreach (ushort itemId in receivedItemIdsFromClient[i])
{
// if there is no such item, skip
if (Entity.FindEntityByID(itemId) is not Item item) { continue; }
// add entry: can the sender access the item?
itemAccessibility[item] = item.CanClientAccess(sender);
}
}
// we now have accessibility for every item in the new inventory state
// but not for the items that were in the inventory before and perhaps dropped, so let's add those as well
foreach (var item in prevItems)
{
if (!itemAccessibility.ContainsKey(item))
{
itemAccessibility[item] = item.CanClientAccess(sender);
}
}
return itemAccessibility;
}
void HandleRemovedItems()
{
for (int slotIndex = 0; slotIndex < capacity; slotIndex++)
{
foreach (Item item in slots[slotIndex].Items.ToList())
{
bool shouldBeRemoved = !receivedItemIdsFromClient[slotIndex].Contains(item.ID) &&
item.IsInteractable(sender.Character); // item is interactable to sender: not hidden and player team
if (shouldBeRemoved)
{
bool itemAccessDenied = prevItems.Contains(item) && // if the item was in the inventory before
!itemAccessibility[item] && // and the sender is not allowed to access it
(item.PreviousParentInventory == null || // and either the item has no previous inventory
!sender.Character.CanAccessInventory(item.PreviousParentInventory)); // or the sender can't access the previous inventory
if (itemAccessDenied)
{
#if DEBUG || UNSTABLE
DebugConsole.NewMessage($"Client {sender.Name} failed to drop item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"}). No access.", Color.Yellow);
#endif
continue;
}
Item droppedItem = item;
Entity prevOwner = Owner;
Inventory previousInventory = droppedItem.ParentInventory;
droppedItem.Drop(null);
droppedItem.PreviousParentInventory = previousInventory;
var previousCharacterInventory = prevOwner switch
{
Item itemInventory => itemInventory.FindParentInventory(inventory => inventory is CharacterInventory) as CharacterInventory,
Character character => character.Inventory,
_ => null
};
if (previousCharacterInventory != null && previousCharacterInventory != sender.Character?.Inventory)
{
GameMain.Server?.KarmaManager.OnItemTakenFromPlayer(previousCharacterInventory, sender, droppedItem);
}
if (droppedItem.body != null && prevOwner != null)
{
droppedItem.body.SetTransform(prevOwner.SimPosition, 0.0f);
}
}
}
foreach (ushort id in receivedItemIdsFromClient[slotIndex])
{
Item newItem = id == 0 ? null : Entity.FindEntityByID(id) as Item;
prevItemInventories.Add(newItem?.ParentInventory);
}
}
}
void HandleAddedItems()
{
for (int slotIndex = 0; slotIndex < capacity; slotIndex++)
{
foreach (ushort id in receivedItemIdsFromClient[slotIndex])
{
if (Entity.FindEntityByID(id) is not Item item || slots[slotIndex].Contains(item)) { continue; }
if (item.GetComponent<Pickable>() is not Pickable pickable ||
(pickable.IsAttached && !pickable.PickingDone) || item.AllowedSlots.None() || !item.IsInteractable(sender.Character))
{
DebugConsole.AddWarning($"Client {sender.Name} tried to pick up a non-pickable item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"})",
item.Prefab.ContentPackage);
continue;
}
if (GameMain.Server != null)
{
var holdable = item.GetComponent<Holdable>();
if (holdable != null && !holdable.CanBeDeattached()) { continue; }
bool itemAccessDenied = !prevItems.Contains(item) && !itemAccessibility[item] &&
(sender.Character == null || item.PreviousParentInventory == null || !sender.Character.CanAccessInventory(item.PreviousParentInventory));
if (itemAccessDenied)
{
#if DEBUG || UNSTABLE
DebugConsole.NewMessage($"Client {sender.Name} failed to pick up item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"}). No access.", Color.Yellow);
#endif
if (item.body != null && !sender.PendingPositionUpdates.Contains(item))
{
sender.PendingPositionUpdates.Enqueue(item);
}
item.PositionUpdateInterval = 0.0f;
continue;
}
}
TryPutItem(item, slotIndex, true, true, sender.Character, false);
for (int j = 0; j < capacity; j++)
{
if (slots[j].Contains(item) && !receivedItemIdsFromClient[j].Contains(item.ID))
{
slots[j].RemoveItem(item);
}
}
}
}
}
void ServerLogAddedItems()
{
foreach (Item item in AllItems.DistinctBy(it => it.Prefab))
{
if (item == null) { continue; }
if (!prevItems.Contains(item))
{
int amount = AllItems.Count(it => it.Prefab == item.Prefab && !prevItems.Contains(it));
string amountText = amount > 1 ? $"x{amount} " : string.Empty;
if (Owner == sender.Character)
{
HumanAIController.ItemTaken(item, sender.Character);
GameServer.Log($"{GameServer.CharacterLogName(sender.Character)} picked up {amountText}{item.Name}", ServerLog.MessageType.Inventory);
}
else
{
GameServer.Log($"{GameServer.CharacterLogName(sender.Character)} placed {amountText}{item.Name} in the inventory of {Owner}", ServerLog.MessageType.Inventory);
}
}
}
}
void ServerLogRemovedItems()
{
var droppedItems = prevItems.Where(it => it != null && !AllItems.Contains(it));
foreach (Item item in droppedItems.DistinctBy(it => it.Prefab))
{
var matchingItems = prevItems.Where(it => it.Prefab == item.Prefab && !AllItems.Contains(it));
int amount = matchingItems.Count();
string amountText = amount > 1 ? $"x{amount} " : string.Empty;
if (Owner == sender.Character)
{
GameServer.Log($"{GameServer.CharacterLogName(sender.Character)} dropped {amountText}{item.Name}", ServerLog.MessageType.Inventory);
}
else
{
GameServer.Log($"{GameServer.CharacterLogName(c.Character)} placed {amountText}{item.Name} in {Owner}", ServerLog.MessageType.Inventory);
GameServer.Log($"{GameServer.CharacterLogName(sender.Character)} removed {amountText}{item.Name} from the inventory of {Owner}", ServerLog.MessageType.Inventory);
}
item.CreateDroppedStack(matchingItems, allowClientExecute: true);
}
}
var droppedItems = prevItems.Where(it => it != null && !AllItems.Contains(it));
foreach (Item item in droppedItems.DistinctBy(it => it.Prefab))
{
var matchingItems = prevItems.Where(it => it.Prefab == item.Prefab && !AllItems.Contains(it));
int amount = matchingItems.Count();
string amountText = amount > 1 ? $"x{amount} " : string.Empty;
if (Owner == c.Character)
{
GameServer.Log($"{GameServer.CharacterLogName(c.Character)} dropped {amountText}{item.Name}", ServerLog.MessageType.Inventory);
}
else
{
GameServer.Log($"{GameServer.CharacterLogName(c.Character)} removed {amountText}{item.Name} from {Owner}", ServerLog.MessageType.Inventory);
}
item.CreateDroppedStack(matchingItems, allowClientExecute: true);
}
#endregion
}
private void EnsureItemsInBothHands(Character character)
{
if (this is not CharacterInventory charInv) { return; }

View File

@@ -98,7 +98,7 @@ namespace Barotrauma.Networking
if (c.Character == null || c.Character.SpeechImpediment >= 100.0f || c.Character.IsDead) { return; }
if (orderMsg.Order.IsReport)
{
HumanAIController.ReportProblem(orderMsg.Sender, orderMsg.Order);
HumanAIController.ReportProblem(orderMsg.Sender as Character, orderMsg.Order);
}
if (order != null)
{

View File

@@ -1,4 +1,5 @@
using System;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -8,6 +9,8 @@ namespace Barotrauma.Networking
{
public bool VoiceEnabled = true;
public VoipServerDecoder VoipServerDecoder;
public UInt16 LastRecvClientListUpdate
= NetIdUtils.GetIdOlderThan(GameMain.Server.LastClientListUpdateID);
@@ -15,7 +18,7 @@ namespace Barotrauma.Networking
= NetIdUtils.GetIdOlderThan(GameMain.Server.ServerSettings.LastUpdateIdForFlag[ServerSettings.NetFlags.Properties]);
public UInt16 LastRecvServerSettingsUpdate
= NetIdUtils.GetIdOlderThan(GameMain.Server.ServerSettings.LastUpdateIdForFlag[ServerSettings.NetFlags.Properties]);
public UInt16 LastRecvLobbyUpdate
= NetIdUtils.GetIdOlderThan(GameMain.NetLobbyScreen.LastUpdateID);
@@ -129,6 +132,7 @@ namespace Barotrauma.Networking
JobPreferences = new List<JobVariant>();
VoipQueue = new VoipQueue(SessionId, true, true);
VoipServerDecoder = new VoipServerDecoder(VoipQueue, this);
GameMain.Server.VoipServer.RegisterQueue(VoipQueue);
//initialize to infinity, gets set to a proper value when initializing midround syncing
@@ -277,5 +281,47 @@ namespace Barotrauma.Networking
{
return Permissions.HasFlag(permission);
}
public bool TryTakeOverBot(Character botCharacter)
{
if (GameMain.Server == null)
{
DebugConsole.ThrowError($"TryTakeOverBot: Client {Name} requested to take over a bot but GameMain.Server is null!");
return false;
}
if (GameMain.NetworkMember is not { ServerSettings.RespawnMode: RespawnMode.Permadeath })
{
DebugConsole.ThrowError($"Client {Name} requested to take over a bot but Permadeath is not enabled!");
GameMain.Server.SendConsoleMessage($"Permadeath mode is not enabled, cannot take over a bot.", this, Color.Red);
return false;
}
if (CharacterInfo == null)
{
DebugConsole.ThrowError($"Permadeath: Client {Name} requested to take over a bot, but they don't seem to have a character at all yet.");
GameMain.Server.SendConsoleMessage($"Permadeath: Taking over a bot requires having a character that died first.", this, Color.Red);
return false;
}
if (CharacterInfo is not { PermanentlyDead: true })
{
DebugConsole.ThrowError($"Permadeath: Client {Name} requested to take over a bot, but their character has not been permanently killed.");
GameMain.Server.SendConsoleMessage($"Permadeath: Could not take over the bot, previous character not permanently killed.", this, Color.Red);
return false;
}
if (!botCharacter.IsBot)
{
DebugConsole.ThrowError($"Permadeath: {Name} requested to take over a bot character, but the target character is not a bot!");
GameMain.Server.SendConsoleMessage($"Permadeath: Could not take over the target character because it is not a bot.", this, Color.Red);
return false;
}
// Now that the old permanently killed character will be replaced, we can fully discard it
if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)
{
mpCampaign.DiscardClientCharacterData(this);
}
GameMain.Server.SetClientCharacter(this, botCharacter);
SpectateOnly = false;
return true;
}
}
}

View File

@@ -60,7 +60,10 @@ namespace Barotrauma.Networking
private bool wasReadyToStartAutomatically;
private bool autoRestartTimerRunning;
private float endRoundTimer;
public float EndRoundTimer { get; private set; }
public float EndRoundDelay { get; private set; }
public float EndRoundTimeRemaining => EndRoundTimer > 0 ? EndRoundDelay - EndRoundTimer : 0;
/// <summary>
/// Chat messages that get sent to the owner of the server when the owner is determined
@@ -360,6 +363,10 @@ namespace Barotrauma.Networking
if (ServerSettings.VoiceChatEnabled)
{
VoipServer.SendToClients(connectedClients);
foreach (var c in connectedClients)
{
c.VoipServerDecoder.DebugUpdate(deltaTime);
}
}
if (GameStarted)
@@ -367,6 +374,7 @@ namespace Barotrauma.Networking
RespawnManager?.Update(deltaTime);
entityEventManager.Update(connectedClients);
bool permadeathMode = ServerSettings.RespawnMode == RespawnMode.Permadeath;
//go through the characters backwards to give rejoining clients control of the latest created character
for (int i = Character.CharacterList.Count - 1; i >= 0; i--)
@@ -377,7 +385,8 @@ namespace Barotrauma.Networking
Client owner = connectedClients.Find(c => (c.Character == null || c.Character == character) && character.IsClientOwner(c));
bool canOwnerTakeControl =
owner != null && owner.InGame && !owner.NeedsMidRoundSync &&
(!ServerSettings.AllowSpectating || !owner.SpectateOnly);
(!ServerSettings.AllowSpectating || !owner.SpectateOnly ||
(permadeathMode && (!character.IsDead || character.CauseOfDeath?.Type == CauseOfDeathType.Disconnected)));
if (!character.IsDead)
{
if (!GameMain.LuaCs.Game.disableDisconnectCharacter)
@@ -386,7 +395,8 @@ namespace Barotrauma.Networking
character.SetStun(1.0f);
}
if ((OwnerConnection == null || owner?.Connection != OwnerConnection) && character.KillDisconnectedTimer > ServerSettings.KillDisconnectedTime)
if ((OwnerConnection == null || owner?.Connection != OwnerConnection) &&
character.KillDisconnectedTimer > (permadeathMode ? ServerSettings.DespawnDisconnectedPermadeathTime : ServerSettings.KillDisconnectedTime))
{
character.Kill(CauseOfDeathType.Disconnected, null);
continue;
@@ -411,8 +421,10 @@ namespace Barotrauma.Networking
Voting.Update(deltaTime);
bool isCrewDead =
bool isCrewDown =
connectedClients.All(c => !c.UsingFreeCam && (c.Character == null || c.Character.IsDead || c.Character.IsIncapacitated));
bool isSomeoneIncapacitatedNotDead =
connectedClients.Any(c => !c.UsingFreeCam && c.Character is { IsDead: false, IsIncapacitated: true });
bool subAtLevelEnd = false;
if (Submarine.MainSub != null && GameMain.GameSession.GameMode is not PvPMode)
@@ -441,45 +453,58 @@ namespace Barotrauma.Networking
}
}
float endRoundDelay = 1.0f;
if (ServerSettings.AutoRestart && isCrewDead)
EndRoundDelay = 1.0f;
if (permadeathMode && isCrewDown)
{
endRoundDelay = 5.0f;
endRoundTimer += deltaTime;
if (EndRoundTimer <= 0.0f)
{
CreateEntityEvent(RespawnManager);
}
EndRoundDelay = 120.0f;
EndRoundTimer += deltaTime;
}
else if (ServerSettings.AutoRestart && isCrewDown)
{
EndRoundDelay = isSomeoneIncapacitatedNotDead ? 120.0f : 5.0f;
EndRoundTimer += deltaTime;
}
else if (subAtLevelEnd && GameMain.GameSession?.GameMode is not CampaignMode)
{
endRoundDelay = 5.0f;
endRoundTimer += deltaTime;
EndRoundDelay = 5.0f;
EndRoundTimer += deltaTime;
}
else if (isCrewDead && (RespawnManager == null || !RespawnManager.CanRespawnAgain))
else if (isCrewDown && (RespawnManager == null || !RespawnManager.CanRespawnAgain))
{
#if !DEBUG
if (endRoundTimer <= 0.0f)
if (EndRoundTimer <= 0.0f)
{
SendChatMessage(TextManager.GetWithVariable("CrewDeadNoRespawns", "[time]", "60").Value, ChatMessageType.Server);
SendChatMessage(TextManager.GetWithVariable("CrewDeadNoRespawns", "[time]", "120").Value, ChatMessageType.Server);
}
endRoundDelay = 60.0f;
endRoundTimer += deltaTime;
EndRoundDelay = 120.0f;
EndRoundTimer += deltaTime;
#endif
}
else if (isCrewDead && (GameMain.GameSession?.GameMode is CampaignMode))
else if (isCrewDown && (GameMain.GameSession?.GameMode is CampaignMode))
{
#if !DEBUG
endRoundDelay = 2.0f;
endRoundTimer += deltaTime;
EndRoundDelay = isSomeoneIncapacitatedNotDead ? 120.0f : 2.0f;
EndRoundTimer += deltaTime;
#endif
}
else
{
endRoundTimer = 0.0f;
EndRoundTimer = 0.0f;
}
if (endRoundTimer >= endRoundDelay)
if (EndRoundTimer >= EndRoundDelay)
{
if (ServerSettings.AutoRestart && isCrewDead)
if (permadeathMode && isCrewDown)
{
Log("Ending round (entire crew dead)", ServerLog.MessageType.ServerMessage);
Log("Ending round (entire crew dead or down and did not acquire new characters in time)", ServerLog.MessageType.ServerMessage);
}
else if (ServerSettings.AutoRestart && isCrewDown)
{
Log("Ending round (entire crew down)", ServerLog.MessageType.ServerMessage);
}
else if (subAtLevelEnd)
{
@@ -487,11 +512,11 @@ namespace Barotrauma.Networking
}
else if (RespawnManager == null)
{
Log("Ending round (no living players left and respawning is not enabled during this round)", ServerLog.MessageType.ServerMessage);
Log("Ending round (no players left standing and respawning is not enabled during this round)", ServerLog.MessageType.ServerMessage);
}
else
{
Log("Ending round (no living players left)", ServerLog.MessageType.ServerMessage);
Log("Ending round (no players left standing)", ServerLog.MessageType.ServerMessage);
}
EndGame(wasSaved: false);
return;
@@ -836,7 +861,7 @@ namespace Barotrauma.Networking
#endif
return;
}
connectedClient.VoipQueue.Read(inc);
VoipServer.Read(inc, connectedClient);
}
break;
case ClientPacketHeader.SERVER_SETTINGS:
@@ -854,6 +879,9 @@ namespace Barotrauma.Networking
case ClientPacketHeader.REWARD_DISTRIBUTION:
ReadRewardDistributionMessage(inc, connectedClient);
break;
case ClientPacketHeader.RESET_REWARD_DISTRIBUTION:
ResetRewardDistribution(connectedClient);
break;
case ClientPacketHeader.MEDICAL:
ReadMedicalMessage(inc, connectedClient);
break;
@@ -866,6 +894,9 @@ namespace Barotrauma.Networking
case ClientPacketHeader.READY_TO_SPAWN:
ReadReadyToSpawnMessage(inc, connectedClient);
break;
case ClientPacketHeader.TAKEOVERBOT:
ReadTakeOverBotMessage(inc, connectedClient);
break;
case ClientPacketHeader.FILE_REQUEST:
if (ServerSettings.AllowFileTransfers)
{
@@ -1319,6 +1350,14 @@ namespace Barotrauma.Networking
mpCampaign.ServerReadRewardDistribution(inc, sender);
}
}
private void ResetRewardDistribution(Client client)
{
if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)
{
mpCampaign.ResetSalaries(client);
}
}
private void ReadMedicalMessage(IReadMessage inc, Client sender)
{
@@ -1354,6 +1393,80 @@ namespace Barotrauma.Networking
}
}
private void ReadTakeOverBotMessage(IReadMessage inc, Client sender)
{
UInt16 botId = inc.ReadUInt16();
if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign) { return; }
if (ServerSettings.IronmanMode)
{
DebugConsole.ThrowError($"Client {sender.Name} has requested to take over a bot in Ironman mode!");
return;
}
if (campaign.CurrentLocation.GetHireableCharacters().FirstOrDefault(c => c.ID == botId) is CharacterInfo hireableCharacter)
{
if (campaign.TryHireCharacter(campaign.CurrentLocation, hireableCharacter, takeMoney: true, sender))
{
campaign.CurrentLocation.RemoveHireableCharacter(hireableCharacter);
SpawnAndTakeOverBot(campaign, hireableCharacter, sender);
campaign.SendCrewState(createNotification: false);
}
else
{
SendConsoleMessage($"Could not hire the bot {hireableCharacter.Name}.", sender, Color.Red);
DebugConsole.ThrowError($"Client {sender.Name} failed to hire the bot {hireableCharacter.Name}.");
}
}
else
{
CharacterInfo botInfo = GameMain.GameSession.CrewManager?.GetCharacterInfos()?.FirstOrDefault(i => i.ID == botId);
if (botInfo is { IsNewHire: true, Character: null })
{
SpawnAndTakeOverBot(campaign, botInfo, sender);
}
else if (botInfo?.Character == null || !botInfo.Character.IsBot)
{
SendConsoleMessage($"Could not find a bot with the id {botId}.", sender, Color.Red);
DebugConsole.ThrowError($"Client {sender.Name} failed to take over a bot (Could not find a bot with the id {botId}).");
return;
}
else if (ServerSettings.AllowBotTakeoverOnPermadeath)
{
sender.TryTakeOverBot(botInfo.Character);
}
else
{
SendConsoleMessage($"Failed to take over a bot (taking control of bots is disallowed).", sender, Color.Red);
DebugConsole.ThrowError($"Client {sender.Name} failed to take over a bot (taking control of bots is disallowed).");
}
}
}
private static void SpawnAndTakeOverBot(CampaignMode campaign, CharacterInfo botInfo, Client client)
{
var mainSubSpawnpoint = WayPoint.SelectCrewSpawnPoints(botInfo.ToEnumerable().ToList(), Submarine.MainSub).FirstOrDefault();
var spawnWaypoint = campaign.CrewManager.GetOutpostSpawnpoints()?.FirstOrDefault() ?? mainSubSpawnpoint;
if (spawnWaypoint == null)
{
DebugConsole.ThrowError("SpawnAndTakeOverBot: Unable to find any spawn waypoints inside the sub");
return;
}
Entity.Spawner.AddCharacterToSpawnQueue(botInfo.SpeciesName, spawnWaypoint.WorldPosition, botInfo, onSpawn: newCharacter =>
{
if (newCharacter == null)
{
DebugConsole.ThrowError("SpawnAndTakeOverBot: newCharacter is null somehow");
return;
}
campaign.CrewManager.RemoveCharacterInfo(botInfo);
newCharacter.TeamID = CharacterTeamType.Team1;
campaign.CrewManager.InitializeCharacter(newCharacter, mainSubSpawnpoint, spawnWaypoint);
client.TryTakeOverBot(newCharacter);
});
}
private void ClientReadServerCommand(IReadMessage inc)
{
Client sender = ConnectedClients.Find(x => x.Connection == inc.Sender);
@@ -1462,9 +1575,8 @@ namespace Barotrauma.Networking
if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save)
{
mpCampaign.SavePlayers();
mpCampaign.HandleSaveAndQuit();
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine);
mpCampaign.UpdateStoreStock();
GameMain.GameSession?.EventManager?.RegisterEventHistory(registerFinishedOnly: true);
SaveUtil.SaveGame(GameMain.GameSession.SavePath);
}
else
@@ -1698,6 +1810,8 @@ namespace Barotrauma.Networking
outmsg.WriteBoolean(GameStarted);
outmsg.WriteBoolean(ServerSettings.AllowSpectating);
outmsg.WriteBoolean(ServerSettings.RespawnMode == RespawnMode.Permadeath);
outmsg.WriteBoolean(ServerSettings.IronmanMode);
c.WritePermissions(outmsg);
}
@@ -1788,6 +1902,7 @@ namespace Barotrauma.Networking
IWriteMessage outmsg = new WriteOnlyMessage();
outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_INGAME);
outmsg.WriteSingle((float)NetTime.Now);
outmsg.WriteSingle(EndRoundTimeRemaining);
using (var segmentTable = SegmentTableWriter<ServerNetSegment>.StartWriting(outmsg))
{
@@ -1870,7 +1985,8 @@ namespace Barotrauma.Networking
{
outmsg = new WriteOnlyMessage();
outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_INGAME);
outmsg.WriteSingle((float)Lidgren.Network.NetTime.Now);
outmsg.WriteSingle((float)NetTime.Now);
outmsg.WriteSingle(EndRoundTimeRemaining);
using (var segmentTable = SegmentTableWriter<ServerNetSegment>.StartWriting(outmsg))
{
@@ -2341,17 +2457,18 @@ namespace Barotrauma.Networking
yield return CoroutineStatus.Failure;
}
bool missionAllowRespawn = !(GameMain.GameSession.GameMode is MissionMode missionMode) || !missionMode.Missions.Any(m => !m.AllowRespawn);
bool missionAllowRespawn = GameMain.GameSession.GameMode is not MissionMode missionMode || !missionMode.Missions.Any(m => !m.AllowRespawn);
bool isOutpost = campaign != null && campaign.NextLevel?.Type == LevelData.LevelType.Outpost;
if (ServerSettings.AllowRespawn && missionAllowRespawn)
if (ServerSettings.RespawnMode != RespawnMode.BetweenRounds && missionAllowRespawn)
{
RespawnManager = new RespawnManager(this, ServerSettings.UseRespawnShuttle && !isOutpost ? selectedShuttle : null);
}
if (campaign != null)
{
campaign.CargoManager.CreatePurchasedItems();
campaign.SendCrewState();
//midround-joining clients need to be informed of pending/new hires at outposts
if (isOutpost) { campaign.SendCrewState(); }
}
Level.Loaded?.SpawnNPCs();
@@ -2392,6 +2509,8 @@ namespace Barotrauma.Networking
}
//always allow the server owner to spectate even if it's disallowed in server settings
teamClients.RemoveAll(c => c.Connection == OwnerConnection && c.SpectateOnly);
// Clients with last character permanently dead spectate regardless of server settings
teamClients.RemoveAll(c => c.CharacterInfo != null && c.CharacterInfo.PermanentlyDead);
//if (!teamClients.Any() && n > 0) { continue; }
@@ -2460,6 +2579,7 @@ namespace Barotrauma.Networking
wp.Submarine == Level.Loaded.StartOutpost &&
wp.CurrentHull?.OutpostModuleTags != null &&
wp.CurrentHull.OutpostModuleTags.Contains("airlock".ToIdentifier()));
while (spawnWaypoints.Count > characterInfos.Count)
{
spawnWaypoints.RemoveAt(Rand.Int(spawnWaypoints.Count));
@@ -2507,13 +2627,17 @@ namespace Barotrauma.Networking
characterData.ApplyWalletData(spawnedCharacter);
spawnedCharacter.GiveIdCardTags(mainSubWaypoints[i]);
spawnedCharacter.LoadTalents();
characterData.HasSpawned = true;
}
if (GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign && spawnedCharacter.Info != null)
{
spawnedCharacter.Info.SetExperience(Math.Max(spawnedCharacter.Info.ExperiencePoints, mpCampaign.GetSavedExperiencePoints(teamClients[i])));
mpCampaign.ClearSavedExperiencePoints(teamClients[i]);
if (spawnedCharacter.Info.LastRewardDistribution.TryUnwrap(out int salary))
{
spawnedCharacter.Wallet.SetRewardDistribution(salary);
}
}
spawnedCharacter.SetOwnerClient(teamClients[i]);
@@ -2614,11 +2738,12 @@ namespace Barotrauma.Networking
msg.WriteInt32(seed);
msg.WriteIdentifier(gameSession.GameMode.Preset.Identifier);
bool missionAllowRespawn = GameMain.GameSession.GameMode is not MissionMode missionMode || !missionMode.Missions.Any(m => !m.AllowRespawn);
msg.WriteBoolean(ServerSettings.AllowRespawn && missionAllowRespawn);
msg.WriteBoolean(ServerSettings.RespawnMode != RespawnMode.BetweenRounds && missionAllowRespawn);
msg.WriteBoolean(ServerSettings.AllowDisguises);
msg.WriteBoolean(ServerSettings.AllowRewiring);
msg.WriteBoolean(ServerSettings.AllowImmediateItemDelivery);
msg.WriteBoolean(ServerSettings.AllowFriendlyFire);
msg.WriteBoolean(ServerSettings.AllowDragAndDropGive);
msg.WriteBoolean(ServerSettings.LockAllDefaultWires);
msg.WriteBoolean(ServerSettings.AllowLinkingWifiToChat);
msg.WriteInt32(ServerSettings.MaximumMoneyTransferRequest);
@@ -2726,7 +2851,7 @@ namespace Barotrauma.Networking
GameMain.GameSession.CrewManager?.ServerWriteActiveOrders(msg);
}
public void EndGame(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, bool wasSaved = false)
public void EndGame(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, bool wasSaved = false, IEnumerable<Mission> missions = null)
{
if (GameStarted)
{
@@ -2742,7 +2867,7 @@ namespace Barotrauma.Networking
}
string endMessage = TextManager.FormatServerMessage("RoundSummaryRoundHasEnded");
List<Mission> missions = GameMain.GameSession.Missions.ToList();
missions ??= GameMain.GameSession.Missions.ToList();
if (GameMain.GameSession is { IsRunning: true })
{
GameMain.GameSession.EndRound(endMessage);
@@ -2758,7 +2883,7 @@ namespace Barotrauma.Networking
}
}
endRoundTimer = 0.0f;
EndRoundTimer = 0.0f;
if (ServerSettings.AutoRestart)
{
@@ -2794,7 +2919,7 @@ namespace Barotrauma.Networking
msg.WriteByte((byte)transitionType);
msg.WriteBoolean(wasSaved);
msg.WriteString(endMessage);
msg.WriteByte((byte)missions.Count);
msg.WriteByte((byte)missions.Count());
foreach (Mission mission in missions)
{
msg.WriteBoolean(mission.Completed);
@@ -2838,6 +2963,12 @@ namespace Barotrauma.Networking
{
logMsg = message.TextWithSender;
}
if (message.Sender is Character sender)
{
sender.TextChatVolume = 1f;
}
Log(logMsg, ServerLog.MessageType.Chat);
}
@@ -3377,24 +3508,24 @@ namespace Barotrauma.Networking
public void SendOrderChatMessage(OrderChatMessage message)
{
if (message.Sender == null || message.Sender.SpeechImpediment >= 100.0f) { return; }
if (message.SenderCharacter == null || message.SenderCharacter.SpeechImpediment >= 100.0f) { return; }
//check which clients can receive the message and apply distance effects
foreach (Client client in ConnectedClients)
{
if (message.Sender != null && client.Character != null && !client.Character.IsDead)
if (message.SenderCharacter != null && client.Character != null && !client.Character.IsDead)
{
//too far to hear the msg -> don't send
if (!client.Character.CanHearCharacter(message.Sender)) { continue; }
if (!client.Character.CanHearCharacter(message.SenderCharacter)) { continue; }
}
SendDirectChatMessage(new OrderChatMessage(message.Order, message.Text, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder), client);
}
if (!string.IsNullOrWhiteSpace(message.Text))
{
AddChatMessage(new OrderChatMessage(message.Order, message.Text, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder));
if (ChatMessage.CanUseRadio(message.Sender, out var senderRadio))
if (ChatMessage.CanUseRadio(message.SenderCharacter, out var senderRadio))
{
//send to chat-linked wifi components
Signal s = new Signal(message.Text, sender: message.Sender, source: senderRadio.Item);
Signal s = new Signal(message.Text, sender: message.SenderCharacter, source: senderRadio.Item);
senderRadio.TransmitSignal(s, sentFromChat: true);
}
}
@@ -3738,6 +3869,14 @@ namespace Barotrauma.Networking
}
}
// If a CharacterInfo for this Client already exists on the server, make sure it is used, and prevent the Client from replacing it
var existingCampaignData = (GameMain.GameSession?.Campaign as MultiPlayerCampaign)?.GetClientCharacterData(sender);
if (existingCampaignData != null)
{
sender.CharacterInfo = existingCampaignData.CharacterInfo;
return;
}
sender.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, newName);
sender.CharacterInfo.RecreateHead(
@@ -3900,7 +4039,7 @@ namespace Barotrauma.Networking
foreach (Client c in unassigned)
{
//find all jobs that are still available
var remainingJobs = jobList.FindAll(jp => assignedClientCount[jp] < jp.MaxNumber && c.Karma >= jp.MinKarma);
var remainingJobs = jobList.FindAll(jp => !jp.HiddenJob && assignedClientCount[jp] < jp.MaxNumber && c.Karma >= jp.MinKarma);
//all jobs taken, give a random job
if (remainingJobs.Count == 0)
@@ -3945,9 +4084,15 @@ namespace Barotrauma.Networking
public void AssignBotJobs(List<CharacterInfo> bots, CharacterTeamType teamID)
{
//shuffle first so the parts where we go through the prefabs
//and find ones there's too few of don't always pick the same job
List<JobPrefab> shuffledPrefabs = JobPrefab.Prefabs.Where(static jp => !jp.HiddenJob).ToList();
shuffledPrefabs.Shuffle();
Dictionary<JobPrefab, int> assignedPlayerCount = new Dictionary<JobPrefab, int>();
foreach (JobPrefab jp in JobPrefab.Prefabs)
foreach (JobPrefab jp in shuffledPrefabs)
{
if (jp.HiddenJob) { continue; }
assignedPlayerCount.Add(jp, 0);
}
@@ -3966,53 +4111,55 @@ namespace Barotrauma.Networking
}
List<CharacterInfo> unassignedBots = new List<CharacterInfo>(bots);
List<WayPoint> spawnPoints = WayPoint.WayPointList.FindAll(wp =>
wp.SpawnType == SpawnType.Human &&
wp.Submarine != null && wp.Submarine.TeamID == teamID)
.OrderBy(sp => Rand.Int(int.MaxValue))
.OrderBy(sp => sp.AssignedJob == null ? 0 : 1)
.ToList();
bool canAssign = false;
do
while (unassignedBots.Count > 0)
{
canAssign = false;
foreach (WayPoint spawnPoint in spawnPoints)
//if there's any jobs left that must be included in the crew, assign those
var jobsBelowMinNumber = shuffledPrefabs.Where(jp => assignedPlayerCount[jp] < jp.MinNumber);
if (jobsBelowMinNumber.Any())
{
if (unassignedBots.Count == 0) { break; }
JobPrefab jobPrefab = spawnPoint.AssignedJob ?? JobPrefab.Prefabs.GetRandomUnsynced();
if (assignedPlayerCount[jobPrefab] >= jobPrefab.MaxNumber) { continue; }
var variant = Rand.Range(0, jobPrefab.Variants, Rand.RandSync.ServerAndClient);
unassignedBots[0].Job = new Job(jobPrefab, Rand.RandSync.ServerAndClient, variant);
assignedPlayerCount[jobPrefab]++;
unassignedBots.Remove(unassignedBots[0]);
canAssign = true;
AssignJob(unassignedBots[0], jobsBelowMinNumber.GetRandomUnsynced());
}
} while (unassignedBots.Count > 0 && canAssign);
else
{
//if there's any jobs left that are below the normal number of bots initially in the crew, assign those
var jobsBelowInitialCount = shuffledPrefabs.Where(jp => assignedPlayerCount[jp] < jp.InitialCount);
if (jobsBelowInitialCount.Any())
{
AssignJob(unassignedBots[0], jobsBelowInitialCount.GetRandomUnsynced());
}
else
{
//no "must-have-jobs" left, break and start assigning randomly
break;
}
}
}
//find a suitable job for the rest of the bots
foreach (CharacterInfo c in unassignedBots)
foreach (CharacterInfo c in unassignedBots.ToList())
{
//find all jobs that are still available
var remainingJobs = JobPrefab.Prefabs.Where(jp => assignedPlayerCount[jp] < jp.MaxNumber);
var remainingJobs = shuffledPrefabs.Where(jp => assignedPlayerCount[jp] < jp.MaxNumber);
//all jobs taken, give a random job
if (remainingJobs.None())
{
DebugConsole.ThrowError("Failed to assign a suitable job for bot \"" + c.Name + "\" (all jobs already have the maximum numbers of players). Assigning a random job...");
c.Job = Job.Random(Rand.RandSync.ServerAndClient);
assignedPlayerCount[c.Job.Prefab]++;
AssignJob(c, shuffledPrefabs.GetRandomUnsynced());
}
else //some jobs still left, choose one of them by random
{
var job = remainingJobs.GetRandomUnsynced();
var variant = Rand.Range(0, job.Variants);
c.Job = new Job(job, Rand.RandSync.Unsynced, variant);
assignedPlayerCount[c.Job.Prefab]++;
else
{
//some jobs still left, choose one of them by random (preferring ones there's the least of in the crew)
var selectedJob = remainingJobs.GetRandomByWeight(jp => 1.0f / Math.Max(assignedPlayerCount[jp], 0.01f), Rand.RandSync.Unsynced);
AssignJob(c, selectedJob);
}
}
void AssignJob(CharacterInfo bot, JobPrefab job)
{
int variant = Rand.Range(0, job.Variants);
bot.Job = new Job(job, Rand.RandSync.Unsynced, variant);
assignedPlayerCount[bot.Job.Prefab]++;
unassignedBots.Remove(bot);
}
}
private Client FindClientWithJobPreference(List<Client> clients, JobPrefab job, bool forceAssign = false)

View File

@@ -468,15 +468,35 @@ namespace Barotrauma.Networking
RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationFailed));
}
if (authenticators is null &&
GameMain.Server.ServerSettings.RequireAuthentication)
{
DebugConsole.NewMessage(
"The server is configured to require authentication from clients, but there are no authenticators available. " +
$"If you're for example trying to host a server in a local network without being connected to Steam or Epic Online Services, please set {nameof(GameMain.Server.ServerSettings.RequireAuthentication)} to false in the server settings.",
Microsoft.Xna.Framework.Color.Yellow);
}
if (authenticators is null
|| !packet.AuthTicket.TryUnwrap(out var authTicket)
|| !authenticators.TryGetValue(authTicket.Kind, out var authenticator))
{
{
#if DEBUG
DebugConsole.NewMessage($"Debug server accepts unauthenticated connections", Microsoft.Xna.Framework.Color.Yellow);
acceptClient(new AccountInfo(packet.AccountId));
DebugConsole.NewMessage("Debug server accepts unauthenticated connections", Microsoft.Xna.Framework.Color.Yellow);
acceptClient(new AccountInfo(new UnauthenticatedAccountId(packet.Name)));
#else
rejectClient();
if (GameMain.Server.ServerSettings.RequireAuthentication)
{
DebugConsole.NewMessage(
"A client attempted to join without an authentication ticket, but the server is configured to require authentication. " +
$"If you're for example trying to host a server in a local network without being connected to Steam or Epic Online Services, please set {nameof(GameMain.Server.ServerSettings.RequireAuthentication)} to false in the server settings.",
Microsoft.Xna.Framework.Color.Yellow);
rejectClient();
}
else
{
acceptClient(new AccountInfo(new UnauthenticatedAccountId(packet.Name)));
}
#endif
return;
}

View File

@@ -15,6 +15,8 @@ namespace Barotrauma.Networking
private int pendingRespawnCount, requiredRespawnCount;
private int prevPendingRespawnCount, prevRequiredRespawnCount;
public bool IsShuttleInsideLevel => RespawnShuttle != null && RespawnShuttle.WorldPosition.Y < Level.Loaded.Size.Y;
private IEnumerable<Client> GetClientsToRespawn()
{
MultiPlayerCampaign campaign = GameMain.GameSession.GameMode as MultiPlayerCampaign;
@@ -27,18 +29,27 @@ namespace Barotrauma.Networking
if (c.SpectateOnly && (GameMain.Server.ServerSettings.AllowSpectating || GameMain.Server.OwnerConnection == c.Connection)) { continue; }
if (c.Character != null && !c.Character.IsDead) { continue; }
//don't allow respawn if the client already has a character (they'll regain control once they're in sync)
var matchingData = campaign?.GetClientCharacterData(c);
//don't allow respawn if the client already has a character (they'll regain control once they're in sync)
if (matchingData != null && matchingData.HasSpawned &&
Character.CharacterList.Any(c => c.Info == matchingData.CharacterInfo && !c.IsDead))
{
continue;
}
if (UseRespawnPrompt)
// Respawning might also be needed in permadeath mode for disconnected characters, but never for permanently dead ones
if (GameMain.NetworkMember?.ServerSettings is { RespawnMode: RespawnMode.Permadeath } &&
(matchingData?.CharacterInfo is { PermanentlyDead: true } || c.Character is { IsDead: true }))
{
continue;
}
if (campaign != null)
{
if (matchingData != null && matchingData.HasSpawned)
{
//in the campaign mode, wait for the client to choose whether they want to spawn
if (!c.WaitForNextRoundRespawn.HasValue || c.WaitForNextRoundRespawn.Value) { continue; }
}
}
@@ -47,9 +58,9 @@ namespace Barotrauma.Networking
}
}
private static bool IsRespawnPromptPendingForClient(Client c)
private static bool IsRespawnDecisionPendingForClient(Client c)
{
if (!UseRespawnPrompt || !(GameMain.GameSession.GameMode is MultiPlayerCampaign campaign)) { return false; }
if (Level.Loaded == null || GameMain.GameSession.GameMode is not MultiPlayerCampaign campaign) { return false; }
if (!c.InGame) { return false; }
if (c.SpectateOnly && (GameMain.Server.ServerSettings.AllowSpectating || GameMain.Server.OwnerConnection == c.Connection)) { return false; }
@@ -58,7 +69,9 @@ namespace Barotrauma.Networking
var matchingData = campaign.GetClientCharacterData(c);
if (matchingData != null && matchingData.HasSpawned)
{
if (Character.CharacterList.Any(c => c.Info == matchingData.CharacterInfo && !c.IsDead))
if (Character.CharacterList.Any(c =>
c.Info == matchingData.CharacterInfo &&
(!c.IsDead || c.CauseOfDeath is { Type: CauseOfDeathType.Disconnected })))
{
return false;
}
@@ -197,26 +210,30 @@ namespace Barotrauma.Networking
shuttleSteering.TargetVelocity = Vector2.Zero;
}
GameServer.Log("Dispatching the respawn shuttle.", ServerLog.MessageType.Spawning);
Vector2 spawnPos = FindSpawnPos();
if (!GameMain.LuaCs.Game.overrideRespawnSub)
{
RespawnCharacters(spawnPos);
RespawnCharacters(spawnPos, out bool anyCharacterSpawnedInShuttle);
}
CoroutineManager.StopCoroutines("forcepos");
if (spawnPos.Y > Level.Loaded.Size.Y)
if (anyCharacterSpawnedInShuttle)
{
CoroutineManager.StartCoroutine(ForceShuttleToPos(Level.Loaded.StartPosition - Vector2.UnitY * Level.ShaftHeight, 100.0f), "forcepos");
GameServer.Log("Dispatching the respawn shuttle.", ServerLog.MessageType.Spawning);
CoroutineManager.StopCoroutines("forcepos");
if (spawnPos.Y > Level.Loaded.Size.Y)
{
CoroutineManager.StartCoroutine(ForceShuttleToPos(Level.Loaded.StartPosition - Vector2.UnitY * Level.ShaftHeight, 100.0f), "forcepos");
}
else
{
RespawnShuttle.SetPosition(spawnPos);
RespawnShuttle.Velocity = Vector2.Zero;
RespawnShuttle.NeutralizeBallast();
RespawnShuttle.EnableMaintainPosition();
}
}
else
{
RespawnShuttle.SetPosition(spawnPos);
RespawnShuttle.Velocity = Vector2.Zero;
RespawnShuttle.NeutralizeBallast();
RespawnShuttle.EnableMaintainPosition();
GameServer.Log("Respawning everyone in main sub.", ServerLog.MessageType.Spawning);
}
}
else
@@ -225,7 +242,7 @@ namespace Barotrauma.Networking
GameServer.Log("Respawning everyone in main sub.", ServerLog.MessageType.Spawning);
GameMain.Server.CreateEntityEvent(this);
RespawnCharacters(null);
RespawnCharacters(shuttlePos: null, out _);
}
}
@@ -265,7 +282,7 @@ namespace Barotrauma.Networking
}
}
if (RespawnShuttle.WorldPosition.Y > Level.Loaded.Size.Y || DateTime.Now > despawnTime)
if (!IsShuttleInsideLevel || DateTime.Now > despawnTime)
{
CoroutineManager.StopCoroutines("forcepos");
@@ -310,7 +327,10 @@ namespace Barotrauma.Networking
if (DateTime.Now > ReturnTime)
{
GameServer.Log("The respawn shuttle is leaving.", ServerLog.MessageType.ServerMessage);
if (IsShuttleInsideLevel)
{
GameServer.Log("The respawn shuttle is leaving.", ServerLog.MessageType.ServerMessage);
}
CurrentState = State.Returning;
GameMain.Server.CreateEntityEvent(this);
@@ -332,8 +352,8 @@ namespace Barotrauma.Networking
}
return shuttleEmptyTimer > 1.0f;
}
partial void RespawnCharactersProjSpecific(Vector2? shuttlePos)
private void RespawnCharacters(Vector2? shuttlePos, out bool anyCharacterSpawnedInShuttle)
{
respawnedCharacters.Clear();
@@ -358,7 +378,7 @@ namespace Barotrauma.Networking
//all characters are in Team 1 in game modes/missions with only one team.
//if at some point we add a game mode with multiple teams where respawning is possible, this needs to be reworked
c.TeamID = CharacterTeamType.Team1;
if (c.CharacterInfo == null) { c.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, c.Name); }
c.CharacterInfo ??= new CharacterInfo(CharacterPrefab.HumanSpeciesName, c.Name);
}
List<CharacterInfo> characterInfos = clients.Select(c => c.CharacterInfo).ToList();
@@ -399,6 +419,8 @@ namespace Barotrauma.Networking
var cargoSp = WayPoint.WayPointList.Find(wp => wp.Submarine == respawnSub && wp.SpawnType == SpawnType.Cargo);
anyCharacterSpawnedInShuttle = false;
for (int i = 0; i < characterInfos.Count; i++)
{
bool bot = i >= clients.Count;
@@ -433,10 +455,19 @@ namespace Barotrauma.Networking
}
}
if (!forceSpawnInMainSub)
{
anyCharacterSpawnedInShuttle = true;
}
var character = Character.Create(characterInfos[i], (forceSpawnInMainSub ? mainSubSpawnPoints[i] : shuttleSpawnPoints[i]).WorldPosition, characterInfos[i].Name, isRemotePlayer: !bot, hasAi: bot);
characterCampaignData?.ApplyWalletData(character);
character.TeamID = CharacterTeamType.Team1;
character.LoadTalents();
if (characterInfos[i].LastRewardDistribution.TryUnwrap(out int salary))
{
character.Wallet.SetRewardDistribution(salary);
}
respawnedCharacters.Add(character);
@@ -467,7 +498,7 @@ namespace Barotrauma.Networking
$"Respawning {GameServer.ClientLogName(clients[i])} ({clients[i].Connection.Endpoint}) as {characterInfos[i].Job.Name}", ServerLog.MessageType.Spawning);
}
if (RespawnShuttle != null)
if (RespawnShuttle != null && anyCharacterSpawnedInShuttle)
{
List<Item> newRespawnItems = new List<Item>();
Vector2 pos = cargoSp?.Position ?? character.Position;
@@ -588,9 +619,7 @@ namespace Barotrauma.Networking
foreach (Skill skill in characterInfo.Job.GetSkills())
{
var skillPrefab = characterInfo.Job.Prefab.Skills.Find(s => skill.Identifier == s.Identifier);
if (skillPrefab == null || skill.Level < skillPrefab.LevelRange.End) { continue; }
skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.End, skillLossPercentage / 100.0f);
skill.Level = GetReducedSkill(characterInfo, skill, skillLossPercentage);
}
}
@@ -606,14 +635,10 @@ namespace Barotrauma.Networking
msg.WriteSingle((float)(ReturnTime - DateTime.Now).TotalSeconds);
break;
case State.Waiting:
MultiPlayerCampaign campaign = GameMain.GameSession.GameMode as MultiPlayerCampaign;
var matchingData = campaign?.GetClientCharacterData(c);
bool forceSpawnInMainSub = matchingData != null && !matchingData.HasSpawned;
msg.WriteUInt16((ushort)pendingRespawnCount);
msg.WriteUInt16((ushort)requiredRespawnCount);
msg.WriteBoolean(IsRespawnPromptPendingForClient(c));
msg.WriteBoolean(IsRespawnDecisionPendingForClient(c));
msg.WriteBoolean(RespawnCountdownStarted);
msg.WriteBoolean(forceSpawnInMainSub);
msg.WriteSingle((float)(RespawnTime - DateTime.Now).TotalSeconds);
break;
case State.Returning:

View File

@@ -132,5 +132,21 @@ namespace Barotrauma.Networking
return garbleAmount < range;
}
}
public static void Read(IReadMessage inc, Client connectedClient)
{
var queue = connectedClient.VoipQueue;
if (queue.Read(inc, discardData: false))
{
connectedClient.VoipServerDecoder.OnNewVoiceReceived();
}
#if DEBUG
var msg = new WriteOnlyMessage().WithHeader(ServerPacketHeader.VOICE_AMPLITUDE_DEBUG);
msg.WriteRangedSingle(connectedClient.VoipServerDecoder.Amplitude, min: 0, max: 1, bitCount: 8);
GameMain.Server?.ServerPeer?.Send(msg, connectedClient.Connection, DeliveryMethod.Unreliable);
#endif
}
}
}

View File

@@ -0,0 +1,230 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.IO;
using System.Text;
using Barotrauma.Networking;
using Concentus.Structs;
namespace Barotrauma
{
internal sealed class VoipServerDecoder
{
private readonly OpusDecoder decoder;
private readonly VoipQueue queue;
private int lastRetrievedBufferID;
public float Amplitude { get; private set; }
private readonly Client ownerClient;
public VoipServerDecoder(VoipQueue q, Client owner)
{
ownerClient = owner;
decoder = VoipConfig.CreateDecoder();
queue = q;
lastRetrievedBufferID = q.LatestBufferID;
}
private static bool debugVoip;
/// <summary>
/// When set to true the server will write VOIP into an audio file for debugging purposes.
/// Useful if you're modifying this part of the code and want to be able to hear what the server "hears"
/// </summary>
public static bool DebugVoip
{
get => debugVoip;
set
{
#if !DEBUG
debugVoip = false;
if (value)
{
DebugConsole.ThrowError("DebugVoip is only available in debug builds of the game");
}
#else
debugVoip = value;
if (!value)
{
if (GameMain.Server is null) { return; }
foreach (var c in GameMain.Server.ConnectedClients)
{
c.VoipServerDecoder.ClearStoredDebugSamples();
}
}
#endif
}
}
private readonly List<short[]> debugStoredSamples = new();
private float debugWriteTimerBacking;
private float DebugWriteTimer
{
get => debugWriteTimerBacking;
set => debugWriteTimerBacking = Math.Clamp(value, min: 0, max: DebugWriteTimeout);
}
private bool shouldWriteDebugFile;
private const float DebugWriteTimeout = 3f; // 3 seconds of no data before writing to file
public void OnNewVoiceReceived()
{
float amplitude = 0.0f;
for (int i = lastRetrievedBufferID + 1; i <= queue.LatestBufferID; i++)
{
queue.RetrieveBuffer(i, out int compressedSize, out byte[] compressedBuffer);
if (compressedSize <= 0) { continue; }
short[] buffer = new short[VoipConfig.BUFFER_SIZE];
decoder.Decode(compressedBuffer, 0, compressedSize, buffer, 0, VoipConfig.BUFFER_SIZE);
amplitude = Math.Max(amplitude, GetAmplitude(buffer));
lastRetrievedBufferID = i;
if (!DebugVoip) { continue; }
lock (debugStoredSamples) { debugStoredSamples.Add(buffer); }
}
Amplitude = amplitude;
if (DebugVoip)
{
DebugWriteTimer = DebugWriteTimeout;
}
}
public void DebugUpdate(float deltaTime)
{
if (!DebugVoip) { return; }
if (DebugWriteTimer > 0)
{
DebugWriteTimer -= deltaTime;
if (DebugWriteTimer <= 0)
{
shouldWriteDebugFile = true;
}
return;
}
if (!shouldWriteDebugFile) { return; }
lock (debugStoredSamples)
{
#if DEBUG
WriteSamplesToWaveFile(debugStoredSamples,
filename: $"voip_{ownerClient.Name}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}.wav",
sampleRate: VoipConfig.FREQUENCY,
channels: 1);
#endif
debugStoredSamples.Clear();
shouldWriteDebugFile = false;
}
}
private static float GetAmplitude(short[] values)
{
float max = 0;
foreach (short v in values)
{
max = Math.Max(max, ToolBox.ShortAudioSampleToFloat(v));
}
return max;
}
/// <summary>
/// Writes the given audio samples to a wave file.
/// </summary>
/// <param name="samples">The audio samples to write.</param>
/// <param name="filename">The name of the wave file to create.</param>
/// <param name="sampleRate">The sample rate of the audio.</param>
/// <param name="channels">The number of channels in the audio.</param>
private static void WriteSamplesToWaveFile(IReadOnlyList<short[]> samples, string filename, int sampleRate, short channels)
{
if (!samples.Any()) { return; }
var path = Path.Combine(Path.GetFullPath("AudioDebug"));
if (!Directory.Exists(path))
{
var dir = Directory.CreateDirectory(path);
if (dir is not { Exists: true }) { return; }
}
using var outFile = File.Create(Path.Combine(path, ToolBox.RemoveInvalidFileNameChars(filename)));
if (outFile is null)
{
DebugConsole.ThrowError("Failed to create audio debug file");
return;
}
// wave file format: https://docs.fileformat.com/audio/wav/
using var writer = new System.IO.BinaryWriter(outFile);
const short pcmFormat = 1; // PCM
const short bitsPerSample = 16; // 16 bits in a short
int byteRate = sampleRate * bitsPerSample * channels / 8;
short blockAlign = (short)(bitsPerSample * channels / 8);
// === FILE INFO === //
writer.Write(Encoding.ASCII.GetBytes("RIFF"));
long sizePos = outFile.Position;
writer.Write(0); // size of file, will be written later
writer.Write(Encoding.ASCII.GetBytes("WAVE"));
writer.Write(Encoding.ASCII.GetBytes("fmt ")); // trailing space is required, not a typo
writer.Write(16); // length of format header
// === AUDIO FORMAT === //
writer.Write(pcmFormat);
writer.Write(channels);
writer.Write(sampleRate);
writer.Write(byteRate);
writer.Write(blockAlign);
writer.Write(bitsPerSample);
// === SAMPLE DATA === //
writer.Write(Encoding.ASCII.GetBytes("data"));
writer.Flush();
long dataPos = outFile.Position;
writer.Write(0); // temporary data size
foreach (var sample in samples)
{
foreach (var s in sample)
{
writer.Write(s);
}
}
writer.Flush();
// write the file size
writer.Seek((int)sizePos, System.IO.SeekOrigin.Begin);
writer.Write((int)(outFile.Length - 8)); // spec says to subtract 8 bytes from the file size
// write the data size
writer.Seek((int)dataPos, System.IO.SeekOrigin.Begin);
writer.Write((int)(outFile.Length - dataPos)); // size of the data only
writer.Flush();
}
private void ClearStoredDebugSamples()
{
lock (debugStoredSamples)
{
debugStoredSamples.Clear();
}
DebugWriteTimer = 0;
shouldWriteDebugFile = false;
}
}
}

View File

@@ -69,13 +69,13 @@ namespace Barotrauma.Steam
foreach (var contentPackage in contentPackages)
{
Steamworks.SteamServer.SetKey(
$"contentpackage{index}",
$"contentpackage{index}",
new ServerListContentPackageInfo(contentPackage).ToString());
index++;
}
return;
}
Steamworks.SteamServer.SetKey(key.Value.ToLowerInvariant(), value.ToString());
}

View File

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

View File

@@ -33,6 +33,7 @@
<Command name="togglekarmatestmode"/>
<Command name="respawnnow"/>
<Command name="traitorlist"/>
<Command name="setsalary"/>
</Preset>
<Preset
@@ -94,5 +95,6 @@
<Command name="togglecampaignteleport"/>
<Command name="respawnnow"/>
<Command name="traitorlist"/>
<Command name="setsalary"/>
</Preset>
</PermissionPresets>

View File

@@ -184,6 +184,20 @@ namespace Barotrauma
}
}
/// <summary>
/// Is some condition met (e.g. entity null, indetectable, outside level) that prevents anyone from detecting the target?
/// </summary>
public bool ShouldBeIgnored()
{
if (InDetectable) { return true; }
if (Entity == null) { return true; }
if (Level.Loaded != null && WorldPosition.Y > Level.Loaded.Size.Y)
{
return true;
}
return false;
}
public AITarget(Entity e, XElement element) : this(e)
{
SightRange = element.GetAttributeFloat("sightrange", 0.0f);

View File

@@ -216,7 +216,7 @@ namespace Barotrauma
private bool IsAttackingOwner(Character other) =>
PetBehavior != null && PetBehavior.Owner != null &&
!other.IsUnconscious && !other.IsArrested &&
!other.IsUnconscious && !other.IsHandcuffed &&
other.AIController is HumanAIController humanAI &&
humanAI.ObjectiveManager.CurrentObjective is AIObjectiveCombat combat &&
combat.Enemy != null && combat.Enemy == PetBehavior.Owner;
@@ -2694,13 +2694,8 @@ namespace Barotrauma
float maxModifier = 5;
foreach (AITarget aiTarget in AITarget.List)
{
if (aiTarget.InDetectable) { continue; }
if (aiTarget.Entity == null) { continue; }
if (aiTarget.ShouldBeIgnored()) { continue; }
if (ignoredTargets.Contains(aiTarget)) { continue; }
if (Level.Loaded != null && aiTarget.WorldPosition.Y > Level.Loaded.Size.Y)
{
continue;
}
if (aiTarget.Type == AITarget.TargetType.HumanOnly) { continue; }
if (!TargetOutposts)
{

View File

@@ -67,6 +67,12 @@ namespace Barotrauma
private readonly float reportProblemsInterval = 1.0f;
private float reportProblemsTimer;
/// <summary>
/// Affects how far the character can hear sounds created by AI targets with the tag ProvocativeToHumanAI.
/// Used as a multiplier on the sound range of the target, e.g. a value of 0.5 would mean a target with a sound range of 1000 would need to be within 500 units for this character to hear it.
/// Only affects the "fight intruders" objective, which makes the character go and inspect noises.
/// </summary>
public float Hearing { get; set; } = 1.0f;
/// <summary>
/// How far other characters can hear reports done by this character (e.g. reports for fires, intruders). Defaults to infinity.
@@ -339,39 +345,8 @@ namespace Barotrauma
enemyCheckTimer -= deltaTime;
if (enemyCheckTimer < 0)
{
CheckEnemies();
enemyCheckTimer = enemyCheckInterval * Rand.Range(0.75f, 1.25f);
if (!objectiveManager.IsCurrentObjective<AIObjectiveCombat>())
{
float closestDistance = 0;
Character closestEnemy = null;
foreach (Character c in Character.CharacterList)
{
if (c.Submarine != Character.Submarine) { continue; }
if (c.Removed || c.IsDead || c.IsIncapacitated) { continue; }
if (IsFriendly(c)) { continue; }
Vector2 toTarget = c.WorldPosition - WorldPosition;
float dist = toTarget.LengthSquared();
float maxDistance = Character.Submarine == null ? enemySpotDistanceOutside : enemySpotDistanceInside;
if (dist > maxDistance * maxDistance) { continue; }
if (EnemyAIController.IsLatchedToSomeoneElse(c, Character)) { continue; }
var head = Character.AnimController.GetLimb(LimbType.Head);
if (head == null) { continue; }
float rotation = head.body.TransformedRotation;
Vector2 forward = VectorExtensions.Forward(rotation);
float angle = MathHelper.ToDegrees(VectorExtensions.Angle(toTarget, forward));
if (angle > 70) { continue; }
if (!Character.CanSeeTarget(c)) { continue; }
if (dist < closestDistance || closestEnemy == null)
{
closestEnemy = c;
closestDistance = dist;
}
}
if (closestEnemy != null)
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Defensive, closestEnemy);
}
}
}
}
bool useInsideSteering = !isOutside || isBlocked || HasValidPath() || IsCloseEnoughToTarget(steeringBuffer);
@@ -586,6 +561,42 @@ namespace Barotrauma
ShipCommandManager?.Update(deltaTime);
}
private void CheckEnemies()
{
//already in combat, no need to check
if (objectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return; }
float closestDistance = 0;
Character closestEnemy = null;
foreach (Character c in Character.CharacterList)
{
if (c.Submarine != Character.Submarine) { continue; }
if (c.Removed || c.IsDead || c.IsIncapacitated) { continue; }
if (IsFriendly(c)) { continue; }
Vector2 toTarget = c.WorldPosition - WorldPosition;
float dist = toTarget.LengthSquared();
float maxDistance = Character.Submarine == null ? enemySpotDistanceOutside : enemySpotDistanceInside;
if (dist > maxDistance * maxDistance) { continue; }
if (EnemyAIController.IsLatchedToSomeoneElse(c, Character)) { continue; }
var head = Character.AnimController.GetLimb(LimbType.Head);
if (head == null) { continue; }
float rotation = head.body.TransformedRotation;
Vector2 forward = VectorExtensions.Forward(rotation);
float angle = MathHelper.ToDegrees(VectorExtensions.Angle(toTarget, forward));
if (angle > 70) { continue; }
if (!Character.CanSeeTarget(c)) { continue; }
if (dist < closestDistance || closestEnemy == null)
{
closestEnemy = c;
closestDistance = dist;
}
}
if (closestEnemy != null)
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Defensive, closestEnemy);
}
}
private void UnequipUnnecessaryItems()
{
if (Character.LockHands) { return; }
@@ -632,7 +643,9 @@ namespace Barotrauma
isCurrentObjectiveFindSafety ||
Character.AnimController.InWater ||
Character.AnimController.HeadInWater ||
Character.IsClimbing ||
Character.Submarine == null ||
Character.Submarine.Info.HasTag(SubmarineTag.Shuttle) ||
(!Character.IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID) && !Character.IsEscorted) ||
ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOnAlsoWhenInactive) ||
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) ||
@@ -845,8 +858,9 @@ namespace Barotrauma
{
// In the campaign mode, undocking happens after leaving the outpost, so we can't use that.
campaign.BeforeLevelLoading += Relocate;
campaign.OnSaveAndQuit += Relocate;
campaign.ItemsRelocatedToMainSub = true;
}
campaign.ItemsRelocatedToMainSub = true;
#if CLIENT
HintManager.OnItemMarkedForRelocation();
#endif
@@ -1011,6 +1025,7 @@ namespace Barotrauma
{
Order newOrder = null;
Hull targetHull = null;
// for now, escorted characters use the report system to get targets but do not speak. escort-character specific dialogue could be implemented
bool speak = Character.SpeechImpediment < 100 && !Character.IsEscorted;
if (Character.CurrentHull != null)
@@ -1024,7 +1039,7 @@ namespace Barotrauma
if (target.CurrentHull != hull || !target.Enabled) { continue; }
if (AIObjectiveFightIntruders.IsValidTarget(target, Character, false))
{
if (!target.IsArrested && AddTargets<AIObjectiveFightIntruders, Character>(Character, target) && newOrder == null)
if (!target.IsHandcuffed && AddTargets<AIObjectiveFightIntruders, Character>(Character, target) && newOrder == null)
{
var orderPrefab = OrderPrefab.Prefabs["reportintruders"];
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
@@ -1436,10 +1451,7 @@ namespace Barotrauma
{
return AIObjectiveCombat.CombatMode.Offensive;
}
return
humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() ||
humanAI.ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders) ?
AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive;
return humanAI.ObjectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>() ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive;
}
else
{
@@ -1479,28 +1491,36 @@ namespace Barotrauma
{
// The guards don't react to player's aggressions when there's an instigator around
isAttackerFightingEnemy = true;
return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (instigator.CombatAction != null ? instigator.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat);
return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : instigator.CombatAction?.WitnessReaction ?? AIObjectiveCombat.CombatMode.Retreat;
}
if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !eitherIsMentallyUnstable)
{
if (c.IsSecurity)
{
return attacker.CombatAction != null ? attacker.CombatAction.GuardReaction : AIObjectiveCombat.CombatMode.Offensive;
return attacker.CombatAction?.GuardReaction ?? AIObjectiveCombat.CombatMode.Offensive;
}
else
{
return attacker.CombatAction != null ? attacker.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat;
return attacker.CombatAction?.WitnessReaction ?? AIObjectiveCombat.CombatMode.Retreat;
}
}
else
{
if (humanAI.ObjectiveManager.GetLastActiveObjective<AIObjectiveCombat>()?.Enemy == attacker)
if (humanAI.ObjectiveManager.GetLastActiveObjective<AIObjectiveCombat>() is AIObjectiveCombat currentCombatObjective && currentCombatObjective.Enemy == attacker)
{
// Already targeting the attacker -> treat as a more serious threat.
cumulativeDamage *= 2;
currentCombatObjective.AllowHoldFire = false;
c.IsCriminal = true;
}
if (c.IsCriminal)
{
// Always react if the attacker has been misbehaving earlier.
cumulativeDamage = Math.Max(cumulativeDamage, minorDamageThreshold);
}
if (cumulativeDamage > majorDamageThreshold)
{
c.IsCriminal = true;
if (c.IsSecurity)
{
return AIObjectiveCombat.CombatMode.Offensive;
@@ -1544,7 +1564,7 @@ namespace Barotrauma
}
}
public void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character target, float delay = 0, Func<AIObjective, bool> abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false)
public void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character target, float delay = 0, Func<AIObjective, bool> abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false, bool speakWarnings = false)
{
if (mode == AIObjectiveCombat.CombatMode.None) { return; }
if (Character.IsDead || Character.IsIncapacitated || Character.Removed) { return; }
@@ -1579,6 +1599,7 @@ namespace Barotrauma
HoldPosition = Character.Info?.Job?.Prefab.Identifier == "watchman",
AbortCondition = abortCondition,
AllowHoldFire = allowHoldFire,
SpeakWarnings = speakWarnings
};
if (onAbort != null)
{
@@ -1777,7 +1798,7 @@ namespace Barotrauma
}
if (!otherCharacter.CanSeeTarget(character, seeThroughWindows: true)) { continue; }
if (!otherHumanAI.structureDamageAccumulator.ContainsKey(character)) { otherHumanAI.structureDamageAccumulator.Add(character, 0.0f); }
otherHumanAI.structureDamageAccumulator.TryAdd(character, 0.0f);
float prevAccumulatedDamage = otherHumanAI.structureDamageAccumulator[character];
otherHumanAI.structureDamageAccumulator[character] += MathHelper.Clamp(damageAmount, -MaxDamagePerFrame, MaxDamagePerFrame);
float accumulatedDamage = Math.Max(otherHumanAI.structureDamageAccumulator[character], maxAccumulatedDamage);
@@ -1789,27 +1810,36 @@ namespace Barotrauma
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss, Reputation.MaxReputationLossFromWallDamage);
}
if (accumulatedDamage <= WarningThreshold) { return; }
if (accumulatedDamage > WarningThreshold && prevAccumulatedDamage <= WarningThreshold &&
!someoneSpoke && !character.IsIncapacitated && character.Stun <= 0.0f)
if (!character.IsCriminal)
{
//if the damage is still fairly low, wait and see if the character keeps damaging the walls to the point where we need to intervene
if (accumulatedDamage < ArrestThreshold)
if (accumulatedDamage <= WarningThreshold) { return; }
if (accumulatedDamage > WarningThreshold && prevAccumulatedDamage <= WarningThreshold &&
!someoneSpoke && !character.IsIncapacitated && character.Stun <= 0.0f)
{
if (otherHumanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>())
//if the damage is still fairly low, wait and see if the character keeps damaging the walls to the point where we need to intervene
if (accumulatedDamage < ArrestThreshold)
{
(otherHumanAI.ObjectiveManager.CurrentObjective as AIObjectiveIdle)?.FaceTargetAndWait(character, 5.0f);
if (otherHumanAI.ObjectiveManager.CurrentObjective is AIObjectiveIdle idleObjective)
{
idleObjective.FaceTargetAndWait(character, 5.0f);
}
}
otherCharacter.Speak(TextManager.Get("dialogdamagewallswarning").Value, null, Rand.Range(0.5f, 1.0f), "damageoutpostwalls".ToIdentifier(), 10.0f);
someoneSpoke = true;
}
otherCharacter.Speak(TextManager.Get("dialogdamagewallswarning").Value, null, Rand.Range(0.5f, 1.0f), "damageoutpostwalls".ToIdentifier(), 10.0f);
someoneSpoke = true;
}
// React if we are security
if ((accumulatedDamage > ArrestThreshold && prevAccumulatedDamage <= ArrestThreshold) ||
if (character.IsCriminal ||
(accumulatedDamage > ArrestThreshold && prevAccumulatedDamage <= ArrestThreshold) ||
(accumulatedDamage > KillThreshold && prevAccumulatedDamage <= KillThreshold))
{
var combatMode = accumulatedDamage > KillThreshold ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Arrest;
if (combatMode == AIObjectiveCombat.CombatMode.Offensive)
{
character.IsCriminal = true;
}
if (!TriggerSecurity(otherHumanAI, combatMode))
{
// Else call the others
@@ -1830,17 +1860,18 @@ namespace Barotrauma
if (humanAI == null) { return false; }
if (!humanAI.Character.IsSecurity) { return false; }
if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; }
humanAI.AddCombatObjective(combatMode, character, delay: GetReactionTime(), allowHoldFire: true, onCompleted: () =>
{
//if the target is arrested successfully, reset the damage accumulator
foreach (Character anyCharacter in Character.CharacterList)
{
if (anyCharacter.AIController is HumanAIController anyAI)
humanAI.AddCombatObjective(combatMode, character, delay: GetReactionTime(),
onCompleted: () =>
{
//if the target is arrested successfully, reset the damage accumulator
foreach (Character anyCharacter in Character.CharacterList)
{
anyAI.structureDamageAccumulator?.Remove(character);
if (anyCharacter.AIController is HumanAIController anyAI)
{
anyAI.structureDamageAccumulator?.Remove(character);
}
}
}
});
});
return true;
}
}
@@ -1848,11 +1879,14 @@ namespace Barotrauma
public static void ItemTaken(Item item, Character thief)
{
if (item == null || thief == null || item.GetComponent<LevelResource>() != null) { return; }
bool someoneSpoke = false;
bool stolenItemsInside = item.OwnInventory?.FindAllItems(it => it.SpawnedInCurrentOutpost && !it.AllowStealing, recursive: true).Any() ?? false;
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing || stolenItemsInside) && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag(Tags.HandLockerItem))
if (item.Illegitimate && item.GetRootInventoryOwner() is Character itemOwner && itemOwner != thief && itemOwner.TeamID == thief.TeamID)
{
// The player attempts to use a bot as a mule or get them arrested -> just arrest the player instead.
thief.IsCriminal = true;
}
bool foundIllegitimateItems = item.Illegitimate || item.OwnInventory?.FindItem(it => it.Illegitimate, recursive: true) != null;
if (foundIllegitimateItems && thief.TeamID != CharacterTeamType.FriendlyNPC)
{
foreach (Character otherCharacter in Character.CharacterList)
{
@@ -1872,19 +1906,24 @@ namespace Barotrauma
if (item.HasTag(Tags.FireExtinguisher) && connectedHulls.Any(h => h.FireSources.Any())) { continue; }
if (item.HasTag(Tags.DivingGear) && connectedHulls.Any(h => h.ConnectedGaps.Any(g => AIObjectiveFixLeaks.IsValidTarget(g, thief)))) { continue; }
}
if (!someoneSpoke)
if (item.HasTag(Tags.Handcuffs) && thief.HasEquippedItem(item))
{
if (!item.StolenDuringRound)
{
ApplyStealingReputationLoss(item);
item.StolenDuringRound = true;
}
otherCharacter.Speak(TextManager.Get("dialogstealwarning").Value, null, Rand.Range(0.5f, 1.0f), "thief".ToIdentifier(), 10.0f);
someoneSpoke = true;
// Handcuffed -> don't react.
continue;
}
if (!item.StolenDuringRound)
{
item.StolenDuringRound = true;
ApplyStealingReputationLoss(item);
#if CLIENT
HintManager.OnStoleItem(thief, item);
#endif
}
if (!someoneSpoke)
{
otherCharacter.Speak(TextManager.Get("dialogstealwarning").Value, null, Rand.Range(0.5f, 1.0f), "thief".ToIdentifier(), 10.0f);
someoneSpoke = true;
}
// React if we are security
if (!TriggerSecurity(otherHumanAI))
{
@@ -1900,7 +1939,7 @@ namespace Barotrauma
}
}
}
else if (item.OwnInventory?.FindItem(it => it.SpawnedInCurrentOutpost && !item.AllowStealing, true) is { } foundItem)
else if (item.OwnInventory?.FindItem(it => it.Illegitimate, true) is { } foundItem)
{
ItemTaken(foundItem, thief);
}
@@ -1914,11 +1953,12 @@ namespace Barotrauma
{
findThieves.InspectEveryone();
}
bool isCriminal = thief.IsCriminal;
humanAI.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, thief, delay: GetReactionTime(),
abortCondition: obj => thief.Inventory.FindItem(it => it != null && it.StolenDuringRound, true) == null,
abortCondition: obj => !isCriminal && thief.Inventory.FindItem(it => it.Illegitimate, recursive: true) == null,
onAbort: () =>
{
if (item != null && !item.Removed && humanAI != null && !humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveGetItem>())
if (!item.Removed && !humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveGetItem>())
{
humanAI.ObjectiveManager.AddObjective(new AIObjectiveGetItem(humanAI.Character, item, humanAI.ObjectiveManager, equip: false)
{
@@ -1926,7 +1966,8 @@ namespace Barotrauma
});
}
},
allowHoldFire: true);
allowHoldFire: !isCriminal,
speakWarnings: !isCriminal);
return true;
}
}
@@ -2084,7 +2125,7 @@ namespace Barotrauma
}
bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreOxygen = HasDivingGear(character);
bool ignoreEnemies = ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() || ObjectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
bool ignoreEnemies = ObjectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>();
float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater: false, ignoreOxygen, ignoreFire, ignoreEnemies);
if (isCurrentHull)
{
@@ -2146,7 +2187,7 @@ namespace Barotrauma
{
if (!visibleHulls.Contains(c.CurrentHull)) { continue; }
}
if (IsActive(c) && !IsFriendly(character, c) && !c.IsArrested)
if (IsActive(c) && !IsFriendly(character, c) && !c.IsHandcuffed)
{
enemyCount++;
}

View File

@@ -492,14 +492,20 @@ namespace Barotrauma
var door = currentPath.CurrentNode.ConnectedDoor;
float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1));
float colliderHeight = collider.Height / 2 + collider.Radius;
float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y;
if (heightDiff < colliderHeight)
if (currentPath.CurrentNode.Stairs == null)
{
//the waypoint is between the top and bottom of the collider, no need to move vertically.
diff.Y = 0.0f;
float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y;
if (heightDiff < colliderHeight)
{
// Original comment:
//the waypoint is between the top and bottom of the collider, no need to move vertically.
// Note that the waypoint can be below collider too! This might be incorrect.
diff.Y = 0.0f;
}
}
if (currentPath.CurrentNode.Stairs != null)
else
{
// In stairs
bool isNextNodeInSameStairs = currentPath.NextNode?.Stairs == currentPath.CurrentNode.Stairs;
if (!isNextNodeInSameStairs)
{
@@ -883,7 +889,18 @@ namespace Barotrauma
//steer away from edges of the hull
bool wander = false;
bool inWater = character.AnimController.InWater;
var currentHull = character.CurrentHull;
Hull currentHull = character.CurrentHull;
// TODO: disabled for now, because seems to cause bots to walk towards walls/doors in some places. In some places it's because how the hulls are defined, but there is probably something else too, is it seems to happen also elsewhere.
// if (!inWater)
// {
// Vector2 colliderBottomPos = ConvertUnits.ToDisplayUnits(character.AnimController.GetColliderBottom());
// if (Hull.FindHull(colliderBottomPos, guess: currentHull, useWorldCoordinates: false) is Hull lowestHull)
// {
// // Use the hull found at the collider bottom, if found.
// // Makes difference in some rooms that have multiple hulls, of which the lowest hull where the feet are might not be the same as where the center position of the main collider is.
// currentHull = lowestHull;
// }
// }
if (currentHull != null && !inWater)
{
float roomWidth = currentHull.Rect.Width;

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