Merge pull request #14469 from FakeFishGames/master

Separatist jobgear icons
This commit is contained in:
dinhtuananhfakefish
2024-08-22 10:04:09 +03:00
committed by GitHub
265 changed files with 8151 additions and 2906 deletions

View File

@@ -72,9 +72,9 @@ body:
attributes: attributes:
label: Version label: Version
description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu. description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu.
options: options:
- v1.3.0.4 - v1.5.9.1 (Summer Update Hotfix 2)
- v1.4.3.0 (unstable) - v1.6.101.0 (unstable)
- Other - Other
validations: validations:
required: true required: true

View File

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

View File

@@ -558,14 +558,17 @@ namespace Barotrauma
if (GameMain.Client != null) { chatMessage += " " + TextManager.Get("DeathChatNotification"); } if (GameMain.Client != null) { chatMessage += " " + TextManager.Get("DeathChatNotification"); }
GameMain.NetworkMember.RespawnManager?.ShowRespawnPromptIfNeeded(); RespawnManager.ShowDeathPromptIfNeeded();
GameMain.NetworkMember.AddChatMessage(chatMessage.Value, ChatMessageType.Dead); GameMain.NetworkMember.AddChatMessage(chatMessage.Value, ChatMessageType.Dead);
GameMain.LightManager.LosEnabled = false; GameMain.LightManager.LosEnabled = false;
controlled = null; 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; 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.Params.CanInteract && character.SelectedCharacter != null && character.SelectedCharacter.Inventory != null)
{ {
if (character.SelectedCharacter.CanInventoryBeAccessed) if (character.SelectedCharacter.IsInventoryAccessibleTo(character))
{ {
character.SelectedCharacter.Inventory.Update(deltaTime, cam); 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.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.Locked = false;
character.SelectedCharacter.Inventory.CurrentLayout = CharacterInventory.Layout.Left; character.SelectedCharacter.Inventory.CurrentLayout = CharacterInventory.Layout.Left;
@@ -759,7 +759,7 @@ namespace Barotrauma
textPos.Y += largeTextSize.Y; textPos.Y += largeTextSize.Y;
} }
if (character.FocusedCharacter.CanBeDragged) if (character.FocusedCharacter.CanBeDraggedBy(character))
{ {
string text = character.CanEat ? "EatHint" : "GrabHint"; string text = character.CanEat ? "EatHint" : "GrabHint";
GUI.DrawString(spriteBatch, textPos, GetCachedHudText(text, InputType.Grab), GUI.DrawString(spriteBatch, textPos, GetCachedHudText(text, InputType.Grab),
@@ -767,11 +767,7 @@ namespace Barotrauma
textPos.Y += largeTextSize.Y; textPos.Y += largeTextSize.Y;
} }
if (!character.DisableHealthWindow && if (character.FocusedCharacter.CanBeHealedBy(character))
character.IsFriendly(character.FocusedCharacter) &&
character.FocusedCharacter.CharacterHealth.UseHealthWindow &&
character.CanInteractWith(character.FocusedCharacter, 160f, false) &&
!character.IsClimbing)
{ {
GUI.DrawString(spriteBatch, textPos, GetCachedHudText("HealHint", InputType.Health), GUI.DrawString(spriteBatch, textPos, GetCachedHudText("HealHint", InputType.Health),
GUIStyle.Green, Color.Black, 2, GUIStyle.SmallFont); 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); 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; float modifiedSkillLevel = skill.Level;
if (Character != null) if (Character != null)
@@ -525,6 +525,7 @@ namespace Barotrauma
ushort infoID = inc.ReadUInt16(); ushort infoID = inc.ReadUInt16();
string newName = inc.ReadString(); string newName = inc.ReadString();
string originalName = inc.ReadString(); string originalName = inc.ReadString();
bool renamingEnabled = inc.ReadBoolean();
int tagCount = inc.ReadByte(); int tagCount = inc.ReadByte();
HashSet<Identifier> tagSet = new HashSet<Identifier>(); HashSet<Identifier> tagSet = new HashSet<Identifier>();
for (int i = 0; i < tagCount; i++) for (int i = 0; i < tagCount; i++)
@@ -538,7 +539,8 @@ namespace Barotrauma
Color skinColor = inc.ReadColorR8G8B8(); Color skinColor = inc.ReadColorR8G8B8();
Color hairColor = inc.ReadColorR8G8B8(); Color hairColor = inc.ReadColorR8G8B8();
Color facialHairColor = inc.ReadColorR8G8B8(); Color facialHairColor = inc.ReadColorR8G8B8();
Identifier npcId = inc.ReadIdentifier(); Identifier npcId = inc.ReadIdentifier();
Identifier factionId = inc.ReadIdentifier(); Identifier factionId = inc.ReadIdentifier();
@@ -571,7 +573,8 @@ namespace Barotrauma
CharacterInfo ch = new CharacterInfo(speciesName, newName, originalName, jobPrefab, variant, npcIdentifier: npcId) CharacterInfo ch = new CharacterInfo(speciesName, newName, originalName, jobPrefab, variant, npcIdentifier: npcId)
{ {
ID = infoID, ID = infoID,
MinReputationToHire = (factionId, minReputationToHire) MinReputationToHire = (factionId, minReputationToHire),
RenamingEnabled = renamingEnabled
}; };
ch.RecreateHead(tagSet.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); ch.RecreateHead(tagSet.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex);
ch.Head.SkinColor = skinColor; ch.Head.SkinColor = skinColor;
@@ -582,6 +585,7 @@ namespace Barotrauma
ch.ExperiencePoints = inc.ReadInt32(); ch.ExperiencePoints = inc.ReadInt32();
ch.AdditionalTalentPoints = inc.ReadRangedInteger(0, MaxAdditionalTalentPoints); ch.AdditionalTalentPoints = inc.ReadRangedInteger(0, MaxAdditionalTalentPoints);
ch.PermanentlyDead = inc.ReadBoolean();
return ch; return ch;
} }

View File

@@ -357,6 +357,7 @@ namespace Barotrauma
case EventType.Control: case EventType.Control:
bool myCharacter = msg.ReadBoolean(); bool myCharacter = msg.ReadBoolean();
byte ownerID = msg.ReadByte(); byte ownerID = msg.ReadByte();
bool renamingEnabled = msg.ReadBoolean();
ResetNetState(); ResetNetState();
if (myCharacter) if (myCharacter)
{ {
@@ -385,6 +386,10 @@ namespace Barotrauma
} }
IsRemotePlayer = ownerID > 0; IsRemotePlayer = ownerID > 0;
} }
if (info != null)
{
info.RenamingEnabled = renamingEnabled;
}
break; break;
case EventType.Status: case EventType.Status:
ReadStatus(msg); ReadStatus(msg);
@@ -723,11 +728,19 @@ namespace Barotrauma
character.ReadStatus(inc); 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); CharacterInfo duplicateCharacterInfo = GameMain.GameSession.CrewManager.GetCharacterInfos().FirstOrDefault(c => c.ID == info.ID);
GameMain.GameSession.CrewManager.RemoveCharacterInfo(duplicateCharacterInfo); 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) if (GameMain.Client.SessionId == ownerId)

View File

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

View File

@@ -289,18 +289,19 @@ namespace Barotrauma
spriteAnimState.Add(decorativeSprite, new SpriteState()); spriteAnimState.Add(decorativeSprite, new SpriteState());
} }
TintMask = null; TintMask = null;
float sourceRectScale = ragdoll.RagdollParams.SourceRectScale;
foreach (var subElement in element.Elements()) foreach (var subElement in element.Elements())
{ {
switch (subElement.Name.ToString().ToLowerInvariant()) switch (subElement.Name.ToString().ToLowerInvariant())
{ {
case "sprite": 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; break;
case "damagedsprite": 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; break;
case "conditionalsprite": 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); ConditionalSprites.Add(conditionalSprite);
if (conditionalSprite.DeformableSprite != null) if (conditionalSprite.DeformableSprite != null)
{ {
@@ -310,7 +311,7 @@ namespace Barotrauma
} }
break; break;
case "deformablesprite": 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); var deformations = CreateDeformations(subElement);
Deformations.AddRange(deformations); Deformations.AddRange(deformations);
NonConditionalDeformations.AddRange(deformations); NonConditionalDeformations.AddRange(deformations);
@@ -339,7 +340,7 @@ namespace Barotrauma
ContentPath tintMaskPath = subElement.GetAttributeContentPath("texture"); ContentPath tintMaskPath = subElement.GetAttributeContentPath("texture");
if (!tintMaskPath.IsNullOrWhiteSpace()) 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); TintHighlightThreshold = subElement.GetAttributeFloat("highlightthreshold", 0.6f);
TintHighlightMultiplier = subElement.GetAttributeFloat("highlightmultiplier", 0.8f); TintHighlightMultiplier = subElement.GetAttributeFloat("highlightmultiplier", 0.8f);
} }
@@ -348,7 +349,7 @@ namespace Barotrauma
ContentPath huskMaskPath = subElement.GetAttributeContentPath("texture"); ContentPath huskMaskPath = subElement.GetAttributeContentPath("texture");
if (!huskMaskPath.IsNullOrWhiteSpace()) if (!huskMaskPath.IsNullOrWhiteSpace())
{ {
HuskMask = new Sprite(subElement, file: GetSpritePath(huskMaskPath)); HuskMask = new Sprite(subElement, file: GetSpritePath(huskMaskPath), sourceRectScale: sourceRectScale);
} }
break; break;
} }
@@ -700,31 +701,34 @@ namespace Barotrauma
if (spriteParams == null || Alpha <= 0) { return; } if (spriteParams == null || Alpha <= 0) { return; }
float burn = spriteParams.IgnoreTint ? 0 : burnOverLayStrength; float burn = spriteParams.IgnoreTint ? 0 : burnOverLayStrength;
float brightness = Math.Max(1.0f - burn, 0.2f); float brightness = Math.Max(1.0f - burn, 0.2f);
Color clr = spriteParams.Color; Color tintedColor = spriteParams.Color;
if (!spriteParams.IgnoreTint) if (!spriteParams.IgnoreTint)
{ {
clr = clr.Multiply(ragdoll.RagdollParams.Color); tintedColor = tintedColor.Multiply(ragdoll.RagdollParams.Color);
if (character.Info != null) 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) 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) 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); Color blankColor = new Color(brightness, brightness, brightness, 1);
if (deadTimer > 0) if (deadTimer > 0)
{ {
color = Color.Lerp(color, spriteParams.DeadColor, MathUtils.InverseLerp(0, spriteParams.DeadColorTime, deadTimer)); 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; color = overrideColor ?? color;
colorWithoutTint = overrideColor ?? colorWithoutTint;
blankColor = overrideColor ?? blankColor; blankColor = overrideColor ?? blankColor;
color *= Alpha; color *= Alpha;
blankColor *= Alpha; blankColor *= Alpha;
@@ -739,6 +743,7 @@ namespace Barotrauma
else if (severedFadeOutTimer > SeveredFadeOutTime - 1.0f) else if (severedFadeOutTimer > SeveredFadeOutTime - 1.0f)
{ {
color *= SeveredFadeOutTime - severedFadeOutTimer; color *= SeveredFadeOutTime - severedFadeOutTimer;
colorWithoutTint *= SeveredFadeOutTime - severedFadeOutTimer;
} }
} }
@@ -956,7 +961,7 @@ namespace Barotrauma
{ {
DamagedSprite.Draw(spriteBatch, DamagedSprite.Draw(spriteBatch,
new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), new Vector2(body.DrawPosition.X, -body.DrawPosition.Y),
color * damageOverlayStrength, activeSprite.Origin, colorWithoutTint * damageOverlayStrength, activeSprite.Origin,
-body.DrawRotation, -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 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;
using System.Linq; using System.Linq;
@@ -75,7 +75,7 @@ namespace Barotrauma
foreach (ItemComponent ic in Item.Components) foreach (ItemComponent ic in Item.Components)
{ {
if (ic is Holdable) { continue; } 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 && if (SerializableProperty.GetProperties<InGameEditable>(ic).Count == 0 &&
!SerializableProperty.GetProperties<ConditionallyEditable>(ic).Any(p => p.GetAttribute<ConditionallyEditable>().IsEditable(ic))) !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; 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) public void Draw(SpriteBatch spriteBatch, Vector2 drawPos, Vector2 parentPos, Color color)
{ {
if (CircuitBox.UI is not { } circuitBoxUi) { return; } 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 #nullable enable
using System;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
@@ -80,6 +81,18 @@ namespace Barotrauma
return true; 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) static void UpdateLabelColor(GUITextBox box)
{ {
bool found = TextManager.ContainsTag(box.Text); bool found = TextManager.ContainsTag(box.Text);
@@ -97,8 +110,8 @@ namespace Barotrauma
} }
} }
bodyTextBox.OnDeselected += (textBox, _) => UpdateLabelColor(textBox); bodyTextBox.OnDeselected += static (textBox, _) => UpdateLabelColor(textBox);
headerTextBox.OnDeselected += (textBox, _) => UpdateLabelColor(textBox); headerTextBox.OnDeselected += static (textBox, _) => UpdateLabelColor(textBox);
UpdateLabelColor(bodyTextBox); UpdateLabelColor(bodyTextBox);
UpdateLabelColor(headerTextBox); UpdateLabelColor(headerTextBox);

View File

@@ -87,6 +87,16 @@ namespace Barotrauma
SnapshotMoveAffectedNodes(); SnapshotMoveAffectedNodes();
startClick = cursorPos; 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> /// <summary>
/// Finds all connections and gathers them into a single list for easier iteration. /// Finds all connections and gathers them into a single list for easier iteration.
@@ -168,38 +178,36 @@ namespace Barotrauma
LastResizeAffectedNode = FindResizeBorderUnderCursor(lastNodesUnderCursor, cursorPos); 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; } 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)
{
continue;
}
return Option.Some((dir, node));
} }
return Option.None; return Option.Some((dir, node));
} }
/// <summary> /// <summary>
@@ -281,14 +289,14 @@ namespace Barotrauma
if (circuitBoxUi.Locked) { return; } if (circuitBoxUi.Locked) { return; }
bool isDragThresholdExceeded = Vector2.DistanceSquared(startClick, cursorPos) > dragTreshold * dragTreshold; bool isDragThresholdExceeded = Vector2.DistanceSquared(startClick, cursorPos) > dragTreshold * dragTreshold;
if (LastResizeAffectedNode.IsSome()) if (LastConnectorUnderCursor.IsSome())
{
IsResizing |= isDragThresholdExceeded;
}
else if (LastConnectorUnderCursor.IsSome())
{ {
IsWiring |= isDragThresholdExceeded; IsWiring |= isDragThresholdExceeded;
} }
else if (LastResizeAffectedNode.IsSome())
{
IsResizing |= isDragThresholdExceeded;
}
else else
{ {
IsDragging |= isDragThresholdExceeded; IsDragging |= isDragThresholdExceeded;

View File

@@ -1,4 +1,4 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
@@ -350,6 +350,10 @@ namespace Barotrauma
{ {
component.Sprite.Draw(spriteBatch, PlayerInput.MousePosition); 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) foreach (var c in CircuitBox.Components)
{ {
@@ -360,11 +364,11 @@ namespace Barotrauma
{ {
n.DrawHUD(spriteBatch, camera); n.DrawHUD(spriteBatch, camera);
} }
if (Locked) if (Locked)
{ {
LocalizedString lockedText = TextManager.Get("CircuitBoxLocked") LocalizedString lockedText = TextManager.Get("CircuitBoxLocked")
.Fallback(TextManager.Get("ConnectionLocked")); .Fallback(TextManager.Get("ConnectionLocked"), useDefaultLanguageIfFound: false);
Vector2 size = GUIStyle.LargeFont.MeasureString(lockedText); Vector2 size = GUIStyle.LargeFont.MeasureString(lockedText);
Vector2 pos = new Vector2(screenRect.Center.X - size.X / 2, screenRect.Top + screenRect.Height * 0.05f); 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 (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())) if (PlayerInput.MidButtonHeld() || (PlayerInput.IsAltDown() && PlayerInput.PrimaryMouseButtonHeld()))
@@ -629,6 +649,7 @@ namespace Barotrauma
if (PlayerInput.PrimaryMouseButtonClicked()) if (PlayerInput.PrimaryMouseButtonClicked())
{ {
bool selectedNode = false;
if (MouseSnapshotHandler.IsResizing && MouseSnapshotHandler.LastResizeAffectedNode.TryUnwrap(out var r)) if (MouseSnapshotHandler.IsResizing && MouseSnapshotHandler.LastResizeAffectedNode.TryUnwrap(out var r))
{ {
var (dir, node) = r; var (dir, node) = r;
@@ -647,7 +668,7 @@ namespace Barotrauma
} }
else if (!MouseSnapshotHandler.IsWiring) else if (!MouseSnapshotHandler.IsWiring)
{ {
TrySelectComponentsUnderCursor(); selectedNode = TrySelectComponentsUnderCursor();
} }
} }
@@ -658,8 +679,15 @@ namespace Barotrauma
CircuitBox.AddWire(one, two); 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; CircuitBox.HeldComponent = Option.None;
MouseSnapshotHandler.EndDragging(); MouseSnapshotHandler.EndDragging();
@@ -732,11 +760,17 @@ namespace Barotrauma
} }
} }
private void TrySelectComponentsUnderCursor() private bool TrySelectComponentsUnderCursor()
{ {
CircuitBoxNode? foundNode = GetTopmostNode(MouseSnapshotHandler.GetLastComponentsUnderCursor()); 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()); CircuitBox.SelectComponents(foundNode is null ? ImmutableArray<CircuitBoxNode>.Empty : ImmutableArray.Create(foundNode), !PlayerInput.IsShiftDown());
return foundNode is not null;
} }
private void OpenContextMenu() private void OpenContextMenu()
@@ -767,17 +801,26 @@ namespace Barotrauma
var editLabel = new ContextMenuOption(TextManager.Get("circuitboxeditlabel"), isEnabled: nodeOption is CircuitBoxLabelNode && !Locked, () => 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); 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, () => var addLabelOption = new ContextMenuOption(TextManager.Get("circuitboxaddlabel"), isEnabled: !Locked, () =>
{ {
CircuitBox.AddLabel(cursorPos); 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 // show component name in the header to better indicate what is about to be deleted
if (nodeOption is CircuitBoxComponent comp) if (nodeOption is CircuitBoxComponent comp)

View File

@@ -114,7 +114,7 @@ namespace Barotrauma
textBox.MaxTextLength = maxLength; textBox.MaxTextLength = maxLength;
textBox.OnKeyHit += (sender, key) => textBox.OnKeyHit += (sender, key) =>
{ {
if (key != Keys.Tab) if (key != Keys.Tab && key != Keys.LeftShift)
{ {
ResetAutoComplete(); ResetAutoComplete();
} }
@@ -181,7 +181,8 @@ namespace Barotrauma
if (PlayerInput.KeyHit(Keys.Tab) && !textBox.IsIMEActive) 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)) if (PlayerInput.KeyDown(Keys.LeftControl) || PlayerInput.KeyDown(Keys.RightControl))
@@ -634,6 +635,20 @@ namespace Barotrauma
{ {
NewMessage("Ready checks can only be commenced in multiplayer.", Color.Red); 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) => commands.Add(new Command("bindkey", "bindkey [key] [command]: Binds a key to a command.", (string[] args) =>
{ {
@@ -793,6 +808,7 @@ namespace Barotrauma
AssignRelayToServer("money", true); AssignRelayToServer("money", true);
AssignRelayToServer("showmoney", true); AssignRelayToServer("showmoney", true);
AssignRelayToServer("setskill", true); AssignRelayToServer("setskill", true);
AssignRelayToServer("setsalary", true);
AssignRelayToServer("readycheck", true); AssignRelayToServer("readycheck", true);
commands.Add(new Command("debugjobassignment", "", (string[] args) => { })); commands.Add(new Command("debugjobassignment", "", (string[] args) => { }));
AssignRelayToServer("debugjobassignment", true); AssignRelayToServer("debugjobassignment", true);
@@ -838,11 +854,8 @@ namespace Barotrauma
AssignOnExecute("teleportcharacter|teleport", (string[] args) => AssignOnExecute("teleportcharacter|teleport", (string[] args) =>
{ {
Character tpCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args, false); Vector2 cursorWorldPos = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition);
if (tpCharacter != null) TeleportCharacter(cursorWorldPos, Character.Controlled, args);
{
tpCharacter.TeleportTo(GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition));
}
}); });
AssignOnExecute("spawn|spawncharacter", (string[] args) => AssignOnExecute("spawn|spawncharacter", (string[] args) =>
@@ -1425,6 +1438,9 @@ namespace Barotrauma
AssignRelayToServer("water|editwater", false); AssignRelayToServer("water|editwater", false);
AssignRelayToServer("fire|editfire", 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.", 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, null,
@@ -2345,6 +2361,11 @@ namespace Barotrauma
})); }));
#if DEBUG #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) => commands.Add(new Command("listspamfilters", "Lists filters that are in the global spam filter.", (string[] args) =>
{ {
if (!SpamServerFilters.GlobalSpamFilter.TryUnwrap(out var filter)) if (!SpamServerFilters.GlobalSpamFilter.TryUnwrap(out var filter))
@@ -3093,12 +3114,12 @@ namespace Barotrauma
{ {
if (Screen.Selected == GameMain.GameScreen) 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; return;
} }
if (Screen.Selected == GameMain.SubEditorScreen) 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; return;
} }
} }

View File

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

View File

@@ -403,9 +403,9 @@ namespace Barotrauma
{ {
senderName = (message.Type == ChatMessageType.Private ? "[PM] " : "") + message.SenderName; 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, var msgHolder = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.0f), chatBox.Content.RectTransform, Anchor.TopCenter), style: null,

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?.HRManagerUI.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> removals = new Queue<GUIComponent>();
private static readonly Queue<GUIComponent> additions = 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. // 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. // 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> /// <summary>
/// Adds the component on the addition queue. /// Adds the component on the addition queue.
@@ -737,11 +737,11 @@ namespace Barotrauma
if (!component.Visible) { return; } if (!component.Visible) { return; }
if (component.UpdateOrder < 0) if (component.UpdateOrder < 0)
{ {
first.Add(component); firstAdditions.Add(component);
} }
else if (component.UpdateOrder > 0) else if (component.UpdateOrder > 0)
{ {
last.Add(component); lastAdditions.Add(component);
} }
else else
{ {
@@ -800,9 +800,9 @@ namespace Barotrauma
RemoveFromUpdateList(component); RemoveFromUpdateList(component);
} }
} }
ProcessHelperList(first); ProcessHelperList(firstAdditions);
ProcessAdditions(); ProcessAdditions();
ProcessHelperList(last); ProcessHelperList(lastAdditions);
ProcessRemovals(); ProcessRemovals();
} }
} }
@@ -897,7 +897,7 @@ namespace Barotrauma
public static IEnumerable<GUIComponent> GetAdditions() public static IEnumerable<GUIComponent> GetAdditions()
{ {
return additions; return additions.Union(firstAdditions).Union(lastAdditions);
} }
#endregion #endregion
@@ -2171,6 +2171,28 @@ namespace Barotrauma
return frame; 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) public static void NotifyPrompt(LocalizedString header, LocalizedString body)
{ {
GUIMessageBox msgBox = new GUIMessageBox(header, body, new[] { TextManager.Get("Ok") }, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); GUIMessageBox msgBox = new GUIMessageBox(header, body, new[] { TextManager.Get("Ok") }, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175));

View File

@@ -815,7 +815,8 @@ namespace Barotrauma
protected virtual void SetAlpha(float a) protected virtual void SetAlpha(float a)
{ {
color = new Color(color.R / 255.0f, color.G / 255.0f, color.B / 255.0f, 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) 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; 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)); 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); SetAlpha(0.0f);
CoroutineManager.StartCoroutine(LerpAlpha(1.0f, duration, false, wait)); 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) 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 }) 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); Scale, SpriteEffects, 0.0f);
} }

View File

@@ -9,7 +9,6 @@ namespace Barotrauma
{ {
public class GUIMessageBox : GUIFrame 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>(); public readonly static List<GUIComponent> MessageBoxes = new List<GUIComponent>();
private static int DefaultWidth private static int DefaultWidth
{ {

View File

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

View File

@@ -1,5 +1,6 @@
#nullable enable #nullable enable
using System;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -25,6 +26,11 @@ namespace Barotrauma
public delegate void OnValueChangedHandler(GUISelectionCarousel<T> carousel); public delegate void OnValueChangedHandler(GUISelectionCarousel<T> carousel);
public OnValueChangedHandler? OnValueChanged; 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; } public GUITextBlock TextBlock { get; private set; }
@@ -89,35 +95,9 @@ namespace Barotrauma
GUIStyle.Apply(TextBlock, "TextBlock", this); GUIStyle.Apply(TextBlock, "TextBlock", this);
RightButton = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), layoutGroup.RectTransform), style: "GUIButtonToggleRight"); RightButton = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), layoutGroup.RectTransform), style: "GUIButtonToggleRight");
GUIStyle.Apply(RightButton, "RightButton", this); GUIStyle.Apply(RightButton, "RightButton", this);
RightButton.OnClicked += (btn, userData) => RightButton.OnClicked += (_, _) => SelectNextValidElement();
{ LeftButton.OnClicked += (_, _) => SelectNextValidElement(directionLeft: true);
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;
};
if (newElements != null && newElements.Any()) if (newElements != null && newElements.Any())
{ {
@@ -140,9 +120,11 @@ namespace Barotrauma
SelectElement(null); SelectElement(null);
return; 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); 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) protected override void SetAlpha(float a)
{ {
// base.SetAlpha(a); textColor = new Color(TextColor, a);
textColor = new Color(TextColor.R / 255.0f, TextColor.G / 255.0f, TextColor.B / 255.0f, a); if (hoverTextColor.HasValue)
{
hoverTextColor = new Color(hoverTextColor.Value, a);
}
} }
/// <summary> /// <summary>

View File

@@ -8,12 +8,16 @@ using PlayerBalanceElement = Barotrauma.CampaignUI.PlayerBalanceElement;
namespace Barotrauma namespace Barotrauma
{ {
class CrewManagement /// <summary>
/// The "HR manager" UI, which is used to hire/fire characters and rename crewmates.
/// </summary>
class HRManagerUI
{ {
private CampaignMode campaign => campaignUI.Campaign; private CampaignMode campaign => campaignUI.Campaign;
private readonly CampaignUI campaignUI; private readonly CampaignUI campaignUI;
private readonly GUIComponent parentComponent; private readonly GUIComponent parentComponent;
private GUIComponent pendingAndCrewPanel;
private GUIListBox hireableList, pendingList, crewList; private GUIListBox hireableList, pendingList, crewList;
private GUIFrame characterPreviewFrame; private GUIFrame characterPreviewFrame;
private GUIDropDown sortingDropDown; private GUIDropDown sortingDropDown;
@@ -24,7 +28,21 @@ namespace Barotrauma
private PlayerBalanceElement? playerBalanceElement; private PlayerBalanceElement? playerBalanceElement;
private List<CharacterInfo> PendingHires => campaign.Map?.CurrentLocation?.HireManager?.PendingHires; private List<CharacterInfo> PendingHires => campaign.Map?.CurrentLocation?.HireManager?.PendingHires;
private bool HasPermission => CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageHires);
private bool wasReplacingPermanentlyDeadCharacter;
/// <summary>
/// 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.
/// </summary>
private static bool ReplacingPermanentlyDeadCharacter =>
GameMain.NetworkMember?.ServerSettings is { RespawnMode: RespawnMode.Permadeath, IronmanMode: false } &&
GameMain.Client?.CharacterInfo is { PermanentlyDead: true };
private bool hadPermissionToHire;
private static bool HasPermissionToHire => ReplacingPermanentlyDeadCharacter ?
GameMain.NetworkMember?.ServerSettings.ReplaceCostPercentage <= 0 || CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMoney) || CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageHires) :
CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageHires);
private Point resolutionWhenCreated; private Point resolutionWhenCreated;
@@ -40,7 +58,7 @@ namespace Barotrauma
SkillDesc SkillDesc
} }
public CrewManagement(CampaignUI campaignUI, GUIComponent parentComponent) public HRManagerUI(CampaignUI campaignUI, GUIComponent parentComponent)
{ {
this.campaignUI = campaignUI; this.campaignUI = campaignUI;
this.parentComponent = parentComponent; this.parentComponent = parentComponent;
@@ -53,27 +71,35 @@ namespace Barotrauma
(locationChangeInfo) => UpdateLocationView(locationChangeInfo.NewLocation, true, locationChangeInfo.PrevLocation)); (locationChangeInfo) => UpdateLocationView(locationChangeInfo.NewLocation, true, locationChangeInfo.PrevLocation));
Reputation.OnAnyReputationValueChanged.RegisterOverwriteExisting( Reputation.OnAnyReputationValueChanged.RegisterOverwriteExisting(
"CrewManagement.UpdateLocationView".ToIdentifier(), _ => needsHireableRefresh = true); "CrewManagement.UpdateLocationView".ToIdentifier(), _ => needsHireableRefresh = true);
hadPermissionToHire = HasPermissionToHire;
wasReplacingPermanentlyDeadCharacter = ReplacingPermanentlyDeadCharacter;
} }
public void RefreshPermissions() public void RefreshUI()
{ {
RefreshCrewFrames(hireableList); RefreshCrewFrames(hireableList);
RefreshCrewFrames(crewList); RefreshCrewFrames(crewList);
RefreshCrewFrames(pendingList); RefreshCrewFrames(pendingList);
if (clearAllButton != null) { clearAllButton.Enabled = HasPermission; } if (clearAllButton != null) { clearAllButton.Enabled = HasPermissionToHire; }
hadPermissionToHire = HasPermissionToHire;
wasReplacingPermanentlyDeadCharacter = ReplacingPermanentlyDeadCharacter;
} }
private void RefreshCrewFrames(GUIListBox listBox) private void RefreshCrewFrames(GUIListBox listBox)
{ {
if (listBox == null) { return; } if (listBox == null) { return; }
listBox.CanBeFocused = HasPermission; listBox.CanBeFocused = HasPermissionToHire;
foreach (GUIComponent child in listBox.Content.Children) foreach (GUIComponent child in listBox.Content.Children)
{ {
if (child.FindChild(c => c is GUIButton && c.UserData is CharacterInfo, true) is GUIButton buyButton) if (child.FindChild(c => c is GUIButton && c.UserData is CharacterInfo, true) is GUIButton buyButton)
{ {
CharacterInfo characterInfo = buyButton.UserData as CharacterInfo; CharacterInfo characterInfo = buyButton.UserData as CharacterInfo;
bool enoughReputationToHire = EnoughReputationToHire(characterInfo); buyButton.Enabled =
buyButton.Enabled = HasPermission && enoughReputationToHire; //"normal buying" is disabled when replacing a dead character
!ReplacingPermanentlyDeadCharacter &&
HasPermissionToHire &&
EnoughReputationToHire(characterInfo) && campaign.CanAffordNewCharacter(characterInfo);
foreach (GUITextBlock text in child.GetAllChildren<GUITextBlock>()) foreach (GUITextBlock text in child.GetAllChildren<GUITextBlock>())
{ {
text.TextColor = new Color(text.TextColor, buyButton.Enabled ? 1.0f : 0.6f); text.TextColor = new Color(text.TextColor, buyButton.Enabled ? 1.0f : 0.6f);
@@ -174,11 +200,13 @@ namespace Barotrauma
playerBalanceElement = CampaignUI.AddBalanceElement(pendingAndCrewMainGroup, new Vector2(1.0f, 0.75f / 14.0f)); playerBalanceElement = CampaignUI.AddBalanceElement(pendingAndCrewMainGroup, new Vector2(1.0f, 0.75f / 14.0f));
pendingAndCrewPanel = 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)
});
var pendingAndCrewGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), anchor: Anchor.Center, var 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) parent: pendingAndCrewPanel.RectTransform));
{
MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height)
}).RectTransform));
float height = 0.05f; float height = 0.05f;
new GUITextBlock(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), TextManager.Get("campaigncrew.pending"), font: GUIStyle.SubHeadingFont); new GUITextBlock(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), TextManager.Get("campaigncrew.pending"), font: GUIStyle.SubHeadingFont);
@@ -222,7 +250,7 @@ namespace Barotrauma
{ {
ClickSound = GUISoundType.Cart, ClickSound = GUISoundType.Cart,
ForceUpperCase = ForceUpperCase.Yes, ForceUpperCase = ForceUpperCase.Yes,
Enabled = HasPermission, Enabled = HasPermissionToHire,
OnClicked = (b, o) => RemoveAllPendingHires() OnClicked = (b, o) => RemoveAllPendingHires()
}; };
GUITextBlock.AutoScaleAndNormalize(validateHiresButton.TextBlock, clearAllButton.TextBlock); GUITextBlock.AutoScaleAndNormalize(validateHiresButton.TextBlock, clearAllButton.TextBlock);
@@ -277,7 +305,7 @@ namespace Barotrauma
{ {
foreach (CharacterInfo c in hireableCharacters) foreach (CharacterInfo c in hireableCharacters)
{ {
if (c == null) { continue; } if (c == null || PendingHires.Contains(c)) { continue; }
CreateCharacterFrame(c, hireableList); CreateCharacterFrame(c, hireableList);
} }
} }
@@ -289,8 +317,8 @@ namespace Barotrauma
{ {
HireManager hireManager = location.HireManager; HireManager hireManager = location.HireManager;
if (hireManager == null) { return; } if (hireManager == null) { return; }
int hireVal = hireManager.AvailableCharacters.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.GetIdentifier()); int newVal = availableHires.Aggregate(0, (curr, hire) => curr + hire.ID);
if (hireVal != newVal) if (hireVal != newVal)
{ {
location.HireManager.AvailableCharacters = availableHires; location.HireManager.AvailableCharacters = availableHires;
@@ -371,7 +399,7 @@ namespace Barotrauma
} }
} }
private void CreateCharacterFrame(CharacterInfo characterInfo, GUIListBox listBox) public GUIComponent CreateCharacterFrame(CharacterInfo characterInfo, GUIListBox listBox, bool hideSalary = false)
{ {
Skill skill = null; Skill skill = null;
Color? jobColor = null; Color? jobColor = null;
@@ -442,33 +470,41 @@ namespace Barotrauma
CanBeFocused = false CanBeFocused = false
}; };
} }
if (listBox != crewList) if (!hideSalary)
{ {
new GUITextBlock(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform), if (listBox != crewList)
TextManager.FormatCurrency(HireManager.GetSalaryFor(characterInfo)),
textAlignment: Alignment.Center)
{ {
CanBeFocused = false new GUITextBlock(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform),
}; TextManager.FormatCurrency(ReplacingPermanentlyDeadCharacter ? campaign.NewCharacterCost(characterInfo) : HireManager.GetSalaryFor(characterInfo)),
} textAlignment: Alignment.Center)
else {
{ CanBeFocused = false
// 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 }; }
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) if (listBox == hireableList)
{ {
var hireButton = new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementAddButton") var hireButton = new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementAddButton")
{ {
ToolTip = TextManager.Get("hirebutton"),
ClickSound = GUISoundType.Cart, ClickSound = GUISoundType.Cart,
UserData = characterInfo, UserData = characterInfo,
Enabled = CanHire(characterInfo), Enabled = CanHire(characterInfo) && !ReplacingPermanentlyDeadCharacter,
OnClicked = (b, o) => AddPendingHire(o as CharacterInfo) OnClicked = (b, o) => AddPendingHire(o as CharacterInfo)
}; };
hireButton.OnAddedToGUIUpdateList += (GUIComponent btn) => hireButton.OnAddedToGUIUpdateList += (GUIComponent btn) =>
{ {
if (ReplacingPermanentlyDeadCharacter)
{
return;
}
if (PendingHires.Count + campaign.CrewManager.GetCharacterInfos().Count() >= CrewManager.MaxCrewSize) if (PendingHires.Count + campaign.CrewManager.GetCharacterInfos().Count() >= CrewManager.MaxCrewSize)
{ {
if (btn.Enabled) if (btn.Enabled)
@@ -483,6 +519,41 @@ namespace Barotrauma
btn.Enabled = CanHire(characterInfo); btn.Enabled = CanHire(characterInfo);
} }
}; };
if (ReplacingPermanentlyDeadCharacter)
{
bool canHire = CanHire(characterInfo) && campaign.CanAffordNewCharacter(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, campaign.NewCharacterCost(characterInfo)))
{
return false;
}
gameClient.SendTakeOverBotRequest(characterInfo);
needsHireableRefresh = true;
campaign.ShowCampaignUI = false;
return true;
}
};
takeoverButton.OnAddedToGUIUpdateList += (GUIComponent btn) =>
{
bool canHireCurrently = ReplacingPermanentlyDeadCharacter && CanHire(characterInfo) && campaign.CanAffordNewCharacter(characterInfo);
btn.ToolTip = TextManager.Get(canHireCurrently ? "hireandtakecontrol" : "hireandtakecontroldisabled");
btn.Visible = GameMain.GameSession is { AllowHrManagerBotTakeover: true };
btn.Enabled = canHireCurrently;
};
}
} }
else if (listBox == pendingList) else if (listBox == pendingList)
{ {
@@ -501,7 +572,7 @@ namespace Barotrauma
{ {
UserData = characterInfo, UserData = characterInfo,
//can't fire if there's only one character in the crew //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) => OnClicked = (btn, obj) =>
{ {
var confirmDialog = new GUIMessageBox( var confirmDialog = new GUIMessageBox(
@@ -534,11 +605,13 @@ namespace Barotrauma
}; };
} }
bool CanHire(CharacterInfo characterInfo) bool CanHire(CharacterInfo thisCharacterInfo)
{ {
if (!HasPermission) { return false; } if (!HasPermissionToHire) { return false; }
return EnoughReputationToHire(characterInfo); return EnoughReputationToHire(thisCharacterInfo);
} }
return frame;
} }
private bool EnoughReputationToHire(CharacterInfo characterInfo) private bool EnoughReputationToHire(CharacterInfo characterInfo)
@@ -709,10 +782,10 @@ namespace Barotrauma
totalBlock.Text = TextManager.FormatCurrency(total); totalBlock.Text = TextManager.FormatCurrency(total);
bool enoughMoney = campaign == null || campaign.CanAfford(total); bool enoughMoney = campaign == null || campaign.CanAfford(total);
totalBlock.TextColor = enoughMoney ? Color.White : Color.Red; 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; } if (hires == null || hires.None()) { return false; }
@@ -750,11 +823,14 @@ namespace Barotrauma
{ {
UpdateLocationView(campaign.Map.CurrentLocation, true); UpdateLocationView(campaign.Map.CurrentLocation, true);
SelectCharacter(null, null, null); SelectCharacter(null, null, null);
var dialog = new GUIMessageBox( if (createNotification)
TextManager.Get("newcrewmembers"), {
TextManager.GetWithVariable("crewhiredmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.DisplayName), var dialog = new GUIMessageBox(
new LocalizedString[] { TextManager.Get("Ok") }); TextManager.Get("newcrewmembers"),
dialog.Buttons[0].OnClicked += dialog.Close; TextManager.GetWithVariable("crewhiredmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.DisplayName),
new LocalizedString[] { TextManager.Get("Ok") });
dialog.Buttons[0].OnClicked += dialog.Close;
}
} }
if (createNetworkEvent) if (createNetworkEvent)
@@ -767,7 +843,7 @@ namespace Barotrauma
private bool CreateRenamingComponent(GUIButton button, object userData) 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), var outerGlowFrame = new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), parentComponent.RectTransform, Anchor.Center),
style: "OuterGlow", color: Color.Black * 0.7f); style: "OuterGlow", color: Color.Black * 0.7f);
var frame = new GUIFrame(new RectTransform(new Vector2(0.33f, 0.4f), outerGlowFrame.RectTransform, anchor: Anchor.Center) var frame = new GUIFrame(new RectTransform(new Vector2(0.33f, 0.4f), outerGlowFrame.RectTransform, anchor: Anchor.Center)
@@ -876,6 +952,15 @@ namespace Barotrauma
playerBalanceElement = CampaignUI.UpdateBalanceElement(playerBalanceElement); playerBalanceElement = CampaignUI.UpdateBalanceElement(playerBalanceElement);
} }
// When showing this window to someone hiring a new character, the right side panels aren't needed
pendingAndCrewPanel.Visible = !ReplacingPermanentlyDeadCharacter;
if (hadPermissionToHire != HasPermissionToHire ||
wasReplacingPermanentlyDeadCharacter != ReplacingPermanentlyDeadCharacter)
{
RefreshUI();
}
if (needsHireableRefresh) if (needsHireableRefresh)
{ {
RefreshCrewFrames(hireableList); RefreshCrewFrames(hireableList);
@@ -949,7 +1034,7 @@ namespace Barotrauma
} }
} }
public void SetPendingHires(List<int> characterInfos, Location location) public void SetPendingHires(List<UInt16> characterInfos, Location location)
{ {
List<CharacterInfo> oldHires = PendingHires.ToList(); List<CharacterInfo> oldHires = PendingHires.ToList();
foreach (CharacterInfo pendingHire in oldHires) foreach (CharacterInfo pendingHire in oldHires)
@@ -957,9 +1042,9 @@ namespace Barotrauma
RemovePendingHire(pendingHire, createNetworkMessage: false); RemovePendingHire(pendingHire, createNetworkMessage: false);
} }
PendingHires.Clear(); 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) if (match != null)
{ {
AddPendingHire(match, createNetworkMessage: false); AddPendingHire(match, createNetworkMessage: false);
@@ -992,7 +1077,7 @@ namespace Barotrauma
msg.WriteUInt16((ushort)PendingHires.Count); msg.WriteUInt16((ushort)PendingHires.Count);
foreach (CharacterInfo pendingHire in PendingHires) foreach (CharacterInfo pendingHire in PendingHires)
{ {
msg.WriteInt32(pendingHire.GetIdentifierUsingOriginalName()); msg.WriteUInt16(pendingHire.ID);
} }
} }
@@ -1002,17 +1087,16 @@ namespace Barotrauma
msg.WriteBoolean(validRenaming); msg.WriteBoolean(validRenaming);
if (validRenaming) if (validRenaming)
{ {
int identifier = renameCharacter.info.GetIdentifierUsingOriginalName(); msg.WriteUInt16(renameCharacter.info.ID);
msg.WriteInt32(identifier);
msg.WriteString(renameCharacter.newName); 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(existingCrewMember);
} }
msg.WriteBoolean(firedCharacter != null); msg.WriteBoolean(firedCharacter != null);
if (firedCharacter != null) if (firedCharacter != null)
{ {
msg.WriteInt32(firedCharacter.GetIdentifier()); msg.WriteUInt16(firedCharacter.ID);
} }
GameMain.Client.ClientPeer?.Send(msg, DeliveryMethod.Reliable); GameMain.Client.ClientPeer?.Send(msg, DeliveryMethod.Reliable);

View File

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

View File

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

View File

@@ -320,7 +320,61 @@ namespace Barotrauma
var reputationButton = createTabButton(InfoFrameTab.Reputation, "reputation"); 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"); 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) if (GameMain.IsMultiplayer)
{ {
balanceText.ToolTip = TextManager.Get("bankdescription"); balanceText.ToolTip = TextManager.Get("bankdescription");
@@ -343,6 +397,13 @@ namespace Barotrauma
{ {
if (!e.Owner.IsNone()) { return; } if (!e.Owner.IsNone()) { return; }
SetBalanceText(balanceText, e.Wallet.Balance); 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); registeredEvents.Add(eventIdentifier);
@@ -350,6 +411,9 @@ namespace Barotrauma
{ {
text.Text = TextManager.GetWithVariable("bankbalanceformat", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", balance)); 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"); var submarineButton = createTabButton(InfoFrameTab.Submarine, "submarine");
@@ -1037,11 +1101,10 @@ namespace Barotrauma
{ {
int newRewardDistribution = RoundRewardDistribution(scroll, bar.Step); int newRewardDistribution = RoundRewardDistribution(scroll, bar.Step);
if (newRewardDistribution == targetWallet.RewardDistribution) { return false; } if (newRewardDistribution == targetWallet.RewardDistribution) { return false; }
SetRewardDistribution(character, newRewardDistribution); SetRewardDistribution(Option.Some(character), newRewardDistribution);
return true; return true;
} }
}; };
int RoundRewardDistribution(float scroll, float step) => (int)MathUtils.RoundTowardsClosest(scroll * 100, step * 100);
SetRewardText(targetWallet.RewardDistribution, rewardBlock); SetRewardText(targetWallet.RewardDistribution, rewardBlock);
@@ -1201,6 +1264,7 @@ namespace Barotrauma
{ {
moneyBlock.Text = TextManager.FormatCurrency(e.Info.Balance); moneyBlock.Text = TextManager.FormatCurrency(e.Info.Balance);
salarySlider.BarScrollValue = e.Info.RewardDistribution / 100f; salarySlider.BarScrollValue = e.Info.RewardDistribution / 100f;
SetRewardText(e.Info.RewardDistribution, rewardBlock);
} }
UpdateAllInputs(); UpdateAllInputs();
@@ -1311,20 +1375,29 @@ namespace Barotrauma
transfer.Write(msg); transfer.Write(msg);
GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); 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) private GUIComponent CreateClientInfoFrame(GUIFrame frame, Client client, Sprite permissionIcon = null)
{ {
GUIComponent paddedFrame; GUIComponent paddedFrame;

View File

@@ -1,4 +1,4 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -71,7 +71,9 @@ namespace Barotrauma
private HashSet<Identifier> selectedTalents = new HashSet<Identifier>(); private HashSet<Identifier> selectedTalents = new HashSet<Identifier>();
private readonly Queue<Identifier> showCaseClosureQueue = new(); private readonly Queue<Identifier> showCaseClosureQueue = new();
private GUITextBlock? nameBlock;
private GUIButton? renameButton;
private GUIListBox? skillListBox; private GUIListBox? skillListBox;
private GUITextBlock? talentPointText; private GUITextBlock? talentPointText;
private GUIProgressBar? experienceBar; private GUIProgressBar? experienceBar;
@@ -133,43 +135,65 @@ namespace Barotrauma
GUIFrame containerFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), characterLayout.RectTransform), style: null); 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)); GUILayoutGroup playerFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), containerFrame.RectTransform, Anchor.TopCenter));
GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false); GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false);
GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), skillLayout.RectTransform, Anchor.BottomRight), if (!GameMain.NetLobbyScreen.PermadeathMode)
text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew"), style: "GUIButtonSmall")
{ {
IgnoreLayoutGroups = false, GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), skillLayout.RectTransform, Anchor.BottomRight),
TextBlock = text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew"), style: "GUIButtonSmall")
{ {
AutoScaleHorizontal = true IgnoreLayoutGroups = false,
} TextBlock =
};
newCharacterBox.OnClicked = (button, o) =>
{
if (!GameMain.NetLobbyScreen.CampaignCharacterDiscarded)
{
GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(() =>
{ {
newCharacterBox.Text = TextManager.Get("settings"); AutoScaleHorizontal = true
if (TabMenu.PendingChangesFrame != null) }
{ };
NetLobbyScreen.CreateChangesPendingFrame(TabMenu.PendingChangesFrame);
}
OpenMenu(); newCharacterBox.OnClicked = (button, o) =>
});
return true;
}
OpenMenu();
return true;
void OpenMenu()
{ {
characterSettingsFrame!.Visible = true; if (!GameMain.NetLobbyScreen.CampaignCharacterDiscarded)
content.Visible = false; {
} 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;
}
};
}
else if (characterInfo != null)
{
renameButton = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), skillLayout.RectTransform, Anchor.BottomRight),
text: TextManager.Get("button.RenameCharacter"), style: "GUIButtonSmall")
{
Enabled = characterInfo.RenamingEnabled,
ToolTip = TextManager.Get("permadeath.rename.description"),
IgnoreLayoutGroups = false,
TextBlock =
{
AutoScaleHorizontal = true
},
OnClicked = (_, _) =>
{
CreateRenamePopup();
return true;
}
};
}
GUILayoutGroup characterCloseButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), characterLayout.RectTransform), childAnchor: Anchor.BottomCenter); 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? 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 +201,7 @@ namespace Barotrauma
OnClicked = (button, o) => OnClicked = (button, o) =>
{ {
GameMain.Client?.SendCharacterInfo(GameMain.Client.PendingName); GameMain.Client?.SendCharacterInfo(GameMain.Client.PendingName);
GameMain.NetLobbyScreen.CampaignCharacterDiscarded = false;
characterSettingsFrame.Visible = false; characterSettingsFrame.Visible = false;
content.Visible = true; content.Visible = true;
return true; return true;
@@ -184,6 +209,57 @@ namespace Barotrauma
}; };
} }
private void CreateRenamePopup()
{
GUIMessageBox renamePopup = new(
TextManager.Get("button.RenameCharacter"), TextManager.Get("permadeath.rename.description"),
new LocalizedString[] { TextManager.Get("Confirm"), TextManager.Get("Cancel") }, minSize: new Point(0, GUI.IntScale(230)));
GUITextBox newNameBox = new(new(Vector2.One, renamePopup.Content.RectTransform), "")
{
OnEnterPressed = (textBox, text) =>
{
textBox.Text = text.Trim();
return true;
}
};
renamePopup.Buttons[0].OnClicked += (_, _) =>
{
if (newNameBox.Text?.Trim() is string newName && newName != "")
{
if (characterInfo != null)
{
if (newNameBox.Text == characterInfo.Name)
{
renamePopup.Close();
return true;
}
if (GameMain.GameSession?.Campaign?.CampaignUI?.HRManagerUI is { } crewManagement)
{
crewManagement.RenameCharacter(characterInfo, newName);
if (nameBlock != null)
{
nameBlock.Text = newName;
}
if (renameButton != null)
{
renameButton.Enabled = false;
}
renamePopup.Close();
}
return true;
}
DebugConsole.ThrowError("Tried to rename character, but CharacterInfo completely missing!");
return true;
}
else
{
newNameBox.Flash();
return false;
}
};
renamePopup.Buttons[1].OnClicked += renamePopup.Close;
}
private void CreateStatPanel(GUIComponent parent, CharacterInfo info) private void CreateStatPanel(GUIComponent parent, CharacterInfo info)
{ {
Job job = info.Job; Job job = info.Job;
@@ -201,7 +277,7 @@ namespace Barotrauma
CanBeFocused = true CanBeFocused = true
}; };
GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), info.Name, font: GUIStyle.SubHeadingFont); nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), info.Name, font: GUIStyle.SubHeadingFont);
if (!info.OmitJobInMenus) if (!info.OmitJobInMenus)
{ {

View File

@@ -773,7 +773,7 @@ namespace Barotrauma
subItems ??= GetSubItems(); subItems ??= GetSubItems();
return subItems.Any(i => return subItems.Any(i =>
i.Prefab.SwappableItem != null && 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)) && (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))); Submarine.MainSub.IsEntityFoundOnThisSub(i, true) && category.ItemTags.Any(t => i.HasTag(t)));
} }
@@ -876,7 +876,7 @@ namespace Barotrauma
{ {
parent.Content.ClearChildren(); parent.Content.ClearChildren();
currentUpgradeCategory = category; 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) foreach (Item item in entitiesOnSub)
{ {

View File

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

View File

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

View File

@@ -522,6 +522,11 @@ namespace Barotrauma
} }
} }
if (GameMain.GameSession?.Campaign?.CampaignUI?.HRManagerUI is { } crewManagement)
{
crewManagement.RefreshUI();
}
return background; return background;
} }
@@ -532,6 +537,10 @@ namespace Barotrauma
crewList.RemoveChild(component); crewList.RemoveChild(component);
traitorButtons.RemoveAll(t => t.IsChildOf(component, recursive: true)); traitorButtons.RemoveAll(t => t.IsChildOf(component, recursive: true));
} }
if (GameMain.GameSession?.Campaign?.CampaignUI?.HRManagerUI is { } crewManagement)
{
crewManagement.RefreshUI();
}
} }
private static void SetCharacterComponentTooltip(GUIComponent characterComponent) private static void SetCharacterComponentTooltip(GUIComponent characterComponent)
@@ -678,12 +687,12 @@ namespace Barotrauma
/// <summary> /// <summary>
/// Adds the message to the single player chatbox. /// Adds the message to the single player chatbox.
/// </summary> /// </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); 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) if (!IsSinglePlayer)
{ {
@@ -692,9 +701,13 @@ namespace Barotrauma
} }
if (string.IsNullOrEmpty(text)) { return; } 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)); ChatBox.AddMessage(ChatMessage.Create(senderName, text, messageType, sender));
} }
@@ -708,9 +721,9 @@ namespace Barotrauma
} }
if (string.IsNullOrEmpty(message.Text)) { return; } 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); ChatBox.AddMessage(message);
} }
@@ -3688,6 +3701,9 @@ namespace Barotrauma
crewList.ClearChildren(); 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) public XElement Save(XElement parentElement)
{ {
var element = new XElement("crew"); var element = new XElement("crew");

View File

@@ -254,7 +254,7 @@ namespace Barotrauma
buttonText = TextManager.Get("map"); buttonText = TextManager.Get("map");
} }
else if (prevCampaignUIAutoOpenType != availableTransition && else if (prevCampaignUIAutoOpenType != availableTransition &&
(availableTransition == TransitionType.ProgressToNextEmptyLocation || availableTransition == TransitionType.ReturnToPreviousEmptyLocation)) availableTransition == TransitionType.ProgressToNextEmptyLocation)
{ {
HintManager.OnAvailableTransition(availableTransition); 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 //opening the campaign map pauses the game and prevents HintManager from running -> update it manually to get the hint to show up immediately
@@ -344,18 +344,6 @@ namespace Barotrauma
} }
} }
protected SubmarineInfo GetPredefinedStartOutpost()
{
if (Map?.CurrentLocation?.Type?.GetForcedOutpostGenerationParams() is OutpostGenerationParams parameters && !parameters.OutpostFilePath.IsNullOrEmpty())
{
return new SubmarineInfo(parameters.OutpostFilePath.Value)
{
OutpostGenerationParams = parameters
};
}
return null;
}
partial void NPCInteractProjSpecific(Character npc, Character interactor) partial void NPCInteractProjSpecific(Character npc, Character interactor)
{ {
if (npc == null || interactor == null) { return; } if (npc == null || interactor == null) { return; }
@@ -370,7 +358,7 @@ namespace Barotrauma
UpgradeManager.CreateUpgradeErrorMessage(TextManager.Get("Dialog.CantUpgrade").Value, IsSinglePlayer, npc); UpgradeManager.CreateUpgradeErrorMessage(TextManager.Get("Dialog.CantUpgrade").Value, IsSinglePlayer, npc);
return; return;
case InteractionType.Crew when GameMain.NetworkMember != null: case InteractionType.Crew when GameMain.NetworkMember != null:
CampaignUI.CrewManagement.SendCrewState(false); CampaignUI.HRManagerUI.SendCrewState(false);
goto default; goto default;
case InteractionType.MedicalClinic: case InteractionType.MedicalClinic:
CampaignUI.MedicalClinic.RequestLatestPending(); CampaignUI.MedicalClinic.RequestLatestPending();

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); campaignUIContainer = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: "InnerGlow", color: Color.Black);
CampaignUI = new CampaignUI(this, campaignUIContainer) CampaignUI = new CampaignUI(this, campaignUIContainer)
@@ -720,7 +720,7 @@ namespace Barotrauma
} }
else 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()) foreach (Item item in Item.ItemList.ToList())
@@ -906,6 +906,8 @@ namespace Barotrauma
public void ClientReadCrew(IReadMessage msg) public void ClientReadCrew(IReadMessage msg)
{ {
bool createNotification = msg.ReadBoolean();
ushort availableHireLength = msg.ReadUInt16(); ushort availableHireLength = msg.ReadUInt16();
List<CharacterInfo> availableHires = new List<CharacterInfo>(); List<CharacterInfo> availableHires = new List<CharacterInfo>();
for (int i = 0; i < availableHireLength; i++) for (int i = 0; i < availableHireLength; i++)
@@ -916,10 +918,10 @@ namespace Barotrauma
} }
ushort pendingHireLength = msg.ReadUInt16(); ushort pendingHireLength = msg.ReadUInt16();
List<int> pendingHires = new List<int>(); List<UInt16> pendingHires = new List<UInt16>();
for (int i = 0; i < pendingHireLength; i++) for (int i = 0; i < pendingHireLength; i++)
{ {
pendingHires.Add(msg.ReadInt32()); pendingHires.Add(msg.ReadUInt16());
} }
ushort hiredLength = msg.ReadUInt16(); ushort hiredLength = msg.ReadUInt16();
@@ -934,30 +936,49 @@ namespace Barotrauma
bool renameCrewMember = msg.ReadBoolean(); bool renameCrewMember = msg.ReadBoolean();
if (renameCrewMember) if (renameCrewMember)
{ {
int renamedIdentifier = msg.ReadInt32(); UInt16 renamedIdentifier = msg.ReadUInt16();
string newName = msg.ReadString(); 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); } if (renamedCharacter != null)
{
CrewManager.RenameCharacter(renamedCharacter, newName);
// Since renaming can only be done once in permadeath, we can safely set this to false to disable the renaming in the UI.
renamedCharacter.RenamingEnabled = false;
}
else
{
DebugConsole.ThrowError($"Could not find a character to rename with the ID {renamedIdentifier}.");
}
} }
bool fireCharacter = msg.ReadBoolean(); bool fireCharacter = msg.ReadBoolean();
if (fireCharacter) if (fireCharacter)
{ {
int firedIdentifier = msg.ReadInt32(); UInt16 firedIdentifier = msg.ReadUInt16();
CharacterInfo firedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.GetIdentifier() == firedIdentifier); 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 // 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 (firedCharacter != null) { CrewManager.FireCharacter(firedCharacter); }
} }
if (map?.CurrentLocation?.HireManager != null && CampaignUI?.CrewManagement != null && if (map?.CurrentLocation?.HireManager != null && CampaignUI?.HRManagerUI != null)
/*can't apply until we have the latest save file*/
!NetIdUtils.IdMoreRecent(pendingSaveID, LastSaveID))
{ {
CampaignUI.CrewManagement.SetHireables(map.CurrentLocation, availableHires); //can't apply until we have the latest save file
if (hiredCharacters.Any()) { CampaignUI.CrewManagement.ValidateHires(hiredCharacters, takeMoney: false); } if (!NetIdUtils.IdMoreRecent(pendingSaveID, LastSaveID))
CampaignUI.CrewManagement.SetPendingHires(pendingHires, map.CurrentLocation); {
if (renameCrewMember || fireCharacter) { CampaignUI.CrewManagement.UpdateCrew(); } CampaignUI.HRManagerUI.SetHireables(map.CurrentLocation, availableHires);
if (hiredCharacters.Any()) { CampaignUI.HRManagerUI.ValidateHires(hiredCharacters, takeMoney: false, createNotification: createNotification); }
CampaignUI.HRManagerUI.SetPendingHires(pendingHires, map.CurrentLocation);
if (renameCrewMember || fireCharacter) { CampaignUI.HRManagerUI.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) public void ClientReadMoney(IReadMessage inc)
@@ -979,6 +1000,7 @@ namespace Barotrauma
else else
{ {
Bank.Balance = info.Balance; Bank.Balance = info.Balance;
Bank.RewardDistribution = info.RewardDistribution;
TryInvokeEvent(Bank, transaction.ChangedData, info); TryInvokeEvent(Bank, transaction.ChangedData, info);
} }
} }
@@ -994,6 +1016,11 @@ namespace Barotrauma
public override bool TryPurchase(Client client, int price) public override bool TryPurchase(Client client, int price)
{ {
if (price == 0)
{
return true;
}
if (!AllowedToManageCampaign(ClientPermissions.ManageMoney)) if (!AllowedToManageCampaign(ClientPermissions.ManageMoney))
{ {
return PersonalWallet.TryDeduct(price); return PersonalWallet.TryDeduct(price);

View File

@@ -1,6 +1,8 @@
using System; using Barotrauma.Abilities;
using Barotrauma.Networking;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using System;
namespace Barotrauma namespace Barotrauma
{ {
@@ -43,13 +45,19 @@ namespace Barotrauma
private GUIButton crewListButton, commandButton, tabMenuButton; private GUIButton crewListButton, commandButton, tabMenuButton;
private GUIImage talentPointNotification; private GUIImage talentPointNotification;
private GUIComponent respawnInfoFrame, respawnButtonContainer; private GUIComponent deathChoiceInfoFrame, deathChoiceButtonContainer;
private GUITextBlock respawnInfoText; private GUITextBlock respawnInfoText;
private GUITickBox respawnTickBox; private GUITickBox deathChoiceTickBox;
private GUIButton takeOverBotButton;
private GUIButton hrManagerButton;
public DeathPrompt DeathPrompt;
private GUIImage eventLogNotification; private GUIImage eventLogNotification;
private Point prevTopLeftButtonsResolution; private Point prevTopLeftButtonsResolution;
public bool AllowHrManagerBotTakeover => GameMain.NetworkMember?.ServerSettings is { RespawnMode: RespawnMode.Permadeath, IronmanMode: false }
&& Level.IsLoadedFriendlyOutpost;
private void CreateTopLeftButtons() private void CreateTopLeftButtons()
{ {
@@ -96,30 +104,63 @@ namespace Barotrauma
talentPointNotification = CreateNotificationIcon(tabMenuButton); talentPointNotification = CreateNotificationIcon(tabMenuButton);
eventLogNotification = CreateNotificationIcon(tabMenuButton); eventLogNotification = CreateNotificationIcon(tabMenuButton);
respawnInfoFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 1.0f), parent: topLeftButtonGroup.RectTransform) // The visibility of the following contents of deathChoiceInfoFrame is controlled by SetRespawnInfo()
{ MaxSize = new Point(HUDLayoutSettings.ButtonAreaTop.Width / 3, int.MaxValue) }, style: null)
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 Visible = false
}; };
respawnInfoText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), respawnInfoFrame.RectTransform), "", wrap: true); respawnInfoText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), deathChoiceInfoFrame.RectTransform), "", wrap: true);
respawnButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), respawnInfoFrame.RectTransform, Anchor.CenterRight), isHorizontal: true, childAnchor: Anchor.CenterLeft) deathChoiceButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), deathChoiceInfoFrame.RectTransform, Anchor.CenterRight), isHorizontal: true, childAnchor: Anchor.CenterLeft)
{ {
AbsoluteSpacing = HUDLayoutSettings.Padding, AbsoluteSpacing = HUDLayoutSettings.Padding,
Stretch = true, Stretch = true,
Visible = false 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( OnClicked = (btn, userdata) =>
"respawnquestionprompt", "[percentage]",
(Math.Round(Networking.RespawnManager.SkillLossPercentageOnImmediateRespawn).ToString())),
OnSelected = (tickbox) =>
{ {
GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: !tickbox.Selected); DeathPrompt.CreateTakeOverBotPanel();
return true; 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); prevTopLeftButtonsResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
} }
@@ -150,6 +191,8 @@ namespace Barotrauma
GameMain.NetLobbyScreen.CharacterAppearanceCustomizationMenu?.AddToGUIUpdateList(); GameMain.NetLobbyScreen.CharacterAppearanceCustomizationMenu?.AddToGUIUpdateList();
GameMain.NetLobbyScreen?.JobSelectionFrame?.AddToGUIUpdateList(); GameMain.NetLobbyScreen?.JobSelectionFrame?.AddToGUIUpdateList();
} }
DeathPrompt?.AddToGUIUpdateList();
} }
public static GUIImage CreateNotificationIcon(GUIComponent parent, bool offset = true) public static GUIImage CreateNotificationIcon(GUIComponent parent, bool offset = true)
@@ -230,16 +273,67 @@ namespace Barotrauma
HintManager.Update(); HintManager.Update();
ObjectiveManager.VideoPlayer.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; } 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.Text = text;
respawnInfoText.TextColor = textColor; 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) public void Draw(SpriteBatch spriteBatch)

View File

@@ -1,4 +1,4 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -81,6 +81,22 @@ namespace Barotrauma
public void Update(float deltaTime) 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; DateTimeOffset now = DateTimeOffset.Now;
UpdateQueue(afflictionRequests, now, onTimeout: static callback => { callback(new AfflictionRequest(RequestResult.Timeout, ImmutableArray<NetAffliction>.Empty)); }); 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)); }); 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 (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; return QuickUseAction.PutToEquippedItem;
} }
@@ -843,13 +843,14 @@ namespace Barotrauma
else if (character.HeldItems.FirstOrDefault(i => else if (character.HeldItems.FirstOrDefault(i =>
i.OwnInventory != null && i.OwnInventory != null &&
i.OwnInventory.Container.DrawInventory && 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) (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 (allowEquip)
{ {
if (!character.HasEquippedItem(item)) 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 //put the item in a hand slot if that hand is free
if ((item.AllowedSlots.Contains(InvSlotType.RightHand) && character.Inventory.GetItemInLimbSlot(InvSlotType.RightHand) == null) || if ((item.AllowedSlots.Contains(InvSlotType.RightHand) && character.Inventory.GetItemInLimbSlot(InvSlotType.RightHand) == null) ||

View File

@@ -79,7 +79,8 @@ namespace Barotrauma.Items.Components
set; 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; } 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.")] [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)); var sortedItems = itemsPerSlot
foreach (var items in 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; int firstFreeSlot = -1;
for (int i = 0; i < Inventory.Capacity; i++) for (int i = 0; i < Inventory.Capacity; i++)
@@ -591,7 +598,8 @@ namespace Barotrauma.Items.Components
contained.Item.Scale, contained.Item.Scale,
spriteEffects, spriteEffects,
depth: containedSpriteDepth); 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>()) 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) partial void SetLightSourceState(bool enabled, float brightness)
{ {
if (Light == null) { return; } if (Light == null) { return; }
if (item.HiddenInGame) { enabled = false; } if (item.IsHidden) { enabled = false; }
Light.Enabled = enabled; Light.Enabled = enabled;
lightColorMultiplier = brightness; lightColorMultiplier = brightness;
if (enabled) if (enabled)

View File

@@ -429,7 +429,7 @@ namespace Barotrauma.Items.Components
{ {
if (it?.Submarine == null) { return false; } if (it?.Submarine == null) { return false; }
if (item.Submarine == null || !item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true)) { 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; } if (it.GetComponent<Pickable>() == null) { return false; }
var holdable = it.GetComponent<Holdable>(); var holdable = it.GetComponent<Holdable>();
@@ -470,10 +470,10 @@ namespace Barotrauma.Items.Components
scissorComponent = new GUIScissorComponent(new RectTransform(Vector2.One, submarineContainer.RectTransform, Anchor.Center)); 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 }; 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); 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); electricalFrame = CreateMiniMap(item.Submarine, miniMapContainer, new MiniMapSettings(createHullElements: false), electricalPointsOfInterest, out electricalMapComponents);
Dictionary<MiniMapGUIComponent, GUIComponent> electricChildren = new Dictionary<MiniMapGUIComponent, GUIComponent>(); Dictionary<MiniMapGUIComponent, GUIComponent> electricChildren = new Dictionary<MiniMapGUIComponent, GUIComponent>();
@@ -566,7 +566,7 @@ namespace Barotrauma.Items.Components
displayedSubs.Add(item.Submarine); displayedSubs.Add(item.Submarine);
displayedSubs.AddRange(item.Submarine.DockedTo.Where(s => s.TeamID == item.Submarine.TeamID)); 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); BakeSubmarine(item.Submarine, parentRect);
elementSize = GuiFrame.Rect.Size; elementSize = GuiFrame.Rect.Size;
@@ -763,7 +763,7 @@ namespace Barotrauma.Items.Components
worldBorders.Location += item.Submarine.WorldPosition.ToPoint(); worldBorders.Location += item.Submarine.WorldPosition.ToPoint();
foreach (Gap gap in Gap.GapList) 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); RectangleF entityRect = ScaleRectToUI(gap, miniMapFrame.Rect, worldBorders);
Vector2 scale = new Vector2(entityRect.Size.X / spriteSize.X, entityRect.Size.Y / spriteSize.Y) * 2.0f; 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)) if (DisplayAsSameItem(it.Prefab, searchedPrefab))
{ {
// ignore items on players and hidden inventories // 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) if (it.FindParentInventory(inventory => inventory is ItemInventory { Owner: Item { ParentInventory: null } }) is ItemInventory parent)
{ {
@@ -1112,7 +1112,7 @@ namespace Barotrauma.Items.Components
if (ShowHullIntegrity) if (ShowHullIntegrity)
{ {
float amount = 1f + hullData.LinkedHulls.Count; 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)); 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 (linkedEntity is Hull linkedHull)
{ {
if (linkedHulls.Contains(linkedHull) || linkedHull.HiddenInGame) { continue; } if (linkedHulls.Contains(linkedHull) || linkedHull.IsHidden) { continue; }
linkedHulls.Add(linkedHull); linkedHulls.Add(linkedHull);
GetLinkedHulls(linkedHull, linkedHulls); GetLinkedHulls(linkedHull, linkedHulls);
} }
@@ -1737,7 +1737,7 @@ namespace Barotrauma.Items.Components
bool IsPartofSub(MapEntity entity) 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); return sub.IsEntityFoundOnThisSub(entity, true);
} }

View File

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

View File

@@ -60,7 +60,7 @@ namespace Barotrauma.Items.Components
public override bool ShouldDrawHUD(Character character) 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 (!HasRequiredItems(character, false) || character.SelectedItem != item) { return false; }
if (character.IsTraitor && item.ConditionPercentage > MinSabotageCondition) { return true; } if (character.IsTraitor && item.ConditionPercentage > MinSabotageCondition) { return true; }
if (item.ConditionPercentageRelativeToDefaultMaxCondition < RepairThreshold) { return true; } if (item.ConditionPercentageRelativeToDefaultMaxCondition < RepairThreshold) { return true; }
@@ -224,7 +224,7 @@ namespace Barotrauma.Items.Components
partial void UpdateProjSpecific(float deltaTime) partial void UpdateProjSpecific(float deltaTime)
{ {
if (item.HiddenInGame) { return; } if (item.IsHidden) { return; }
if (FakeBrokenTimer > 0.0f) if (FakeBrokenTimer > 0.0f)
{ {
item.FakeBroken = true; item.FakeBroken = true;
@@ -397,6 +397,12 @@ namespace Barotrauma.Items.Components
GUI.DrawString(spriteBatch, GUI.DrawString(spriteBatch,
new Vector2(item.DrawPosition.X, -item.DrawPosition.Y + 20), "Condition: " + (int)item.Condition + "/" + (int)item.MaxCondition, new Vector2(item.DrawPosition.X, -item.DrawPosition.Y + 20), "Condition: " + (int)item.Condition + "/" + (int)item.MaxCondition,
GUIStyle.Orange); 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)); 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) public void ResizeNode(CircuitBoxNode node, CircuitBoxResizeDirection dir, Vector2 amount)
{ {
if (Locked) { return; } if (Locked) { return; }
@@ -528,6 +539,12 @@ namespace Barotrauma.Items.Components
_ => node.Position _ => node.Position
}; };
} }
foreach (var labelOverride in data.LabelOverrides)
{
RenameConnectionLabelsInternal(labelOverride.Type, labelOverride.Override.ToDictionary());
}
wasInitializedByServer = true; wasInitializedByServer = true;
break; break;
} }
@@ -556,6 +573,12 @@ namespace Barotrauma.Items.Components
ResizeLabelInternal(data.ID, data.Position, data.Size); ResizeLabelInternal(data.ID, data.Position, data.Size);
break; break;
} }
case CircuitBoxOpcode.RenameConnections:
{
var data = INetSerializableStruct.Read<CircuitBoxRenameConnectionLabelsEvent>(msg);
RenameConnectionLabelsInternal(data.Type, data.Override.ToDictionary());
break;
}
default: default:
throw new ArgumentOutOfRangeException(nameof(header), header, "This opcode cannot be handled using entity events"); 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; } if (wire.HiddenInGame && Screen.Selected == GameMain.GameScreen) { continue; }
Connection recipient = wire.OtherConnection(this); Connection recipient = wire.OtherConnection(this);
LocalizedString label = recipient == null ? "" : recipient.item.Name + $" ({recipient.DisplayName})"; LocalizedString label;
if (wire.Locked) { label += "\n" + TextManager.Get("ConnectionLocked"); } if (wire.Item.IsLayerHidden)
{
label = TextManager.Get("ConnectionLocked");
}
else
{
label = recipient == null ? "" : recipient.item.Name + $" ({recipient.DisplayName})";
if (wire.Locked) { label += "\n" + TextManager.Get("ConnectionLocked"); }
}
DrawWire(spriteBatch, wire, position, wirePosition, equippedWire, panel, label); DrawWire(spriteBatch, wire, position, wirePosition, equippedWire, panel, label);
wirePosition.Y += wireInterval; wirePosition.Y += wireInterval;
@@ -494,7 +503,7 @@ namespace Barotrauma.Items.Components
ConnectionPanel.HighlightedWire = wire; ConnectionPanel.HighlightedWire = wire;
bool allowRewiring = GameMain.NetworkMember?.ServerSettings == null || GameMain.NetworkMember.ServerSettings.AllowRewiring || panel.AlwaysAllowRewiring; 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 //start dragging the wire
if (PlayerInput.PrimaryMouseButtonHeld()) { DraggingConnected = wire; } if (PlayerInput.PrimaryMouseButtonHeld()) { DraggingConnected = wire; }

View File

@@ -283,12 +283,12 @@ namespace Barotrauma.Items.Components
texts.Add(CharacterHUD.GetCachedHudText("PlayHint", InputType.Use)); texts.Add(CharacterHUD.GetCachedHudText("PlayHint", InputType.Use));
textColors.Add(GUIStyle.Green); 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)); texts.Add(CharacterHUD.GetCachedHudText("HealHint", InputType.Health));
textColors.Add(GUIStyle.Green); textColors.Add(GUIStyle.Green);
} }
if (target.CanBeDragged) if (target.CanBeDraggedBy(Character.Controlled))
{ {
texts.Add(CharacterHUD.GetCachedHudText("GrabHint", InputType.Grab)); texts.Add(CharacterHUD.GetCachedHudText("GrabHint", InputType.Grab));
textColors.Add(GUIStyle.Green); textColors.Add(GUIStyle.Green);

View File

@@ -196,7 +196,7 @@ namespace Barotrauma.Items.Components
Vector2 particlePos = GetRelativeFiringPosition(); Vector2 particlePos = GetRelativeFiringPosition();
foreach (ParticleEmitter emitter in particleEmitters) 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) if (crosshairSprite != null)
{ {
Vector2 itemPos = cam.WorldToScreen(new Vector2(item.WorldRect.X + transformedBarrelPos.X, item.WorldRect.Y - transformedBarrelPos.Y)); 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; Vector2 mouseDiff = itemPos - PlayerInput.MousePosition;
crosshairPos = new Vector2( crosshairPos = new Vector2(
@@ -268,7 +268,7 @@ namespace Barotrauma.Items.Components
foreach (ParticleEmitter emitter in particleEmitterCharges) foreach (ParticleEmitter emitter in particleEmitterCharges)
{ {
// color is currently not connected to ammo type, should be updated when ammo is changed // 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) if (chargeSoundChannel == null || !chargeSoundChannel.IsPlaying)
@@ -339,7 +339,7 @@ namespace Barotrauma.Items.Components
if (crosshairSprite != null) if (crosshairSprite != null)
{ {
Vector2 itemPos = cam.WorldToScreen(item.WorldPosition); 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; Vector2 mouseDiff = itemPos - PlayerInput.MousePosition;
crosshairPos = new Vector2( crosshairPos = new Vector2(
@@ -372,7 +372,7 @@ namespace Barotrauma.Items.Components
recoilOffset = RecoilDistance; 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) 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, railSprite?.Draw(spriteBatch,
drawPos, drawPos,
overrideColor ?? item.SpriteColor, overrideColor ?? item.SpriteColor,
rotation + MathHelper.PiOver2, item.Scale, Rotation + MathHelper.PiOver2, item.Scale,
SpriteEffects.None, item.SpriteDepth + (railSprite.Depth - item.Sprite.Depth)); SpriteEffects.None, item.SpriteDepth + (railSprite.Depth - item.Sprite.Depth));
barrelSprite?.Draw(spriteBatch, barrelSprite?.Draw(spriteBatch,
drawPos - GetRecoilOffset() * item.Scale, drawPos - GetRecoilOffset() * item.Scale,
overrideColor ?? item.SpriteColor, overrideColor ?? item.SpriteColor,
rotation + MathHelper.PiOver2, item.Scale, Rotation + MathHelper.PiOver2, item.Scale,
SpriteEffects.None, item.SpriteDepth + (barrelSprite.Depth - item.Sprite.Depth)); SpriteEffects.None, item.SpriteDepth + (barrelSprite.Depth - item.Sprite.Depth));
float chargeRatio = currentChargeTime / MaxChargeTime; float chargeRatio = currentChargeTime / MaxChargeTime;
@@ -402,9 +402,9 @@ namespace Barotrauma.Items.Components
foreach ((Sprite chargeSprite, Vector2 position) in chargeSprites) foreach ((Sprite chargeSprite, Vector2 position) in chargeSprites)
{ {
chargeSprite?.Draw(spriteBatch, 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, item.SpriteColor,
rotation + MathHelper.PiOver2, item.Scale, Rotation + MathHelper.PiOver2, item.Scale,
SpriteEffects.None, item.SpriteDepth + (chargeSprite.Depth - item.Sprite.Depth)); SpriteEffects.None, item.SpriteDepth + (chargeSprite.Depth - item.Sprite.Depth));
} }
@@ -427,9 +427,9 @@ namespace Barotrauma.Items.Components
float newPositionOffset = barrelPositionModifier * SpinningBarrelDistance; float newPositionOffset = barrelPositionModifier * SpinningBarrelDistance;
spinningBarrel.Draw(spriteBatch, 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), Color.Lerp(overrideColor ?? item.SpriteColor, newColorModifier, 0.8f),
rotation + MathHelper.PiOver2, item.Scale, Rotation + MathHelper.PiOver2, item.Scale,
SpriteEffects.None, newDepth); SpriteEffects.None, newDepth);
} }
} }
@@ -475,9 +475,9 @@ namespace Barotrauma.Items.Components
{ {
spriteBatch.DrawLine(drawPos, drawPos + center * circleRadius, GUIStyle.Green, thickness: lineThickness); 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 else
{ {
@@ -510,7 +510,12 @@ namespace Barotrauma.Items.Components
}; };
widget.MouseHeld += (deltaTime) => 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(); UpdateBarrel();
MapEntity.DisableSelect = true; MapEntity.DisableSelect = true;
}; };
@@ -554,7 +559,12 @@ namespace Barotrauma.Items.Components
}; };
widget.MouseHeld += (deltaTime) => 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(); UpdateBarrel();
MapEntity.DisableSelect = true; MapEntity.DisableSelect = true;
}; };
@@ -580,10 +590,44 @@ namespace Barotrauma.Items.Components
void UpdateBarrel() 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() public Vector2 GetDrawPos()
{ {
Vector2 drawPos = new Vector2(item.Rect.X + transformedBarrelPos.X, item.Rect.Y - transformedBarrelPos.Y); 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; } if (projectileID == 0) { return; }
//ID ushort.MaxValue = launched without a projectile //ID ushort.MaxValue = launched without a projectile
if (projectileID == ushort.MaxValue) if (projectileID == LaunchWithoutProjectileId)
{ {
Launch(null, user); 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‖"; toolTip = $"‖color:{colorStr}‖{name}‖color:end‖";
if (item.GetComponent<Quality>() != null) if (item.GetComponent<Quality>() != null)
@@ -478,10 +478,11 @@ namespace Barotrauma
{ {
int row = (int)Math.Floor((double)i / slotsPerRow); int row = (int)Math.Floor((double)i / slotsPerRow);
int slotsPerThisRow = Math.Min(slotsPerRow, capacity - row * slotsPerRow); int slotsPerThisRow = Math.Min(slotsPerRow, capacity - row * slotsPerRow);
int slotNumberOnThisRow = i - row * slotsPerRow;
int rowWidth = (int)(rectSize.X * slotsPerThisRow + spacing.X * (slotsPerThisRow - 1)); int rowWidth = (int)(rectSize.X * slotsPerThisRow + spacing.X * (slotsPerThisRow - 1));
slotRect.X = (int)(center.X) - rowWidth / 2; 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); slotRect.Y = (int)(topLeft.Y + (rectSize.Y + spacing.Y) * row);
visualSlots[i] = new VisualSlot(slotRect); visualSlots[i] = new VisualSlot(slotRect);
@@ -1185,6 +1186,7 @@ namespace Barotrauma
{ {
DraggingItems.RemoveAll(it => !Character.Controlled.CanInteractWith(it)); DraggingItems.RemoveAll(it => !Character.Controlled.CanInteractWith(it));
} }
if (DraggingItems.Any() && PlayerInput.PrimaryMouseButtonReleased()) if (DraggingItems.Any() && PlayerInput.PrimaryMouseButtonReleased())
{ {
Character.Controlled.ClearInputs(); Character.Controlled.ClearInputs();
@@ -1193,198 +1195,234 @@ namespace Barotrauma
if (!DetermineMouseOnInventory(ignoreDraggedItem: true) && if (!DetermineMouseOnInventory(ignoreDraggedItem: true) &&
(CharacterHealth.OpenHealthWindow != null || mouseOnPortrait)) (CharacterHealth.OpenHealthWindow != null || mouseOnPortrait))
{ {
bool dropSuccessful = false; if (TryPortraitAndHealthDrop(mouseOnPortrait))
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)
{
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; return;
} }
} }
if (selectedSlot == null) if (selectedSlot == null)
{ {
if (DraggingItemToWorld && HandleOutsideInventoryDrop();
Character.Controlled.FocusedItem is { OwnInventory: { } inventory } item && item.GetComponent<ItemContainer>() is { } container && }
container.HasRequiredItems(Character.Controlled, addMessage: false) && else if (!DraggingItems.Any(it => selectedSlot.ParentInventory.slots[selectedSlot.SlotIndex].Contains(it)))
container.AllowDragAndDrop && {
inventory.CanBePut(DraggingItems.FirstOrDefault())) 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; if (indices != null && inventory.visualSlots != null)
foreach (Item it in DraggingItems)
{ {
bool success = Character.Controlled.FocusedItem.OwnInventory.TryPutItem(it, Character.Controlled); foreach (int i in indices)
if (!success) { break; }
anySuccess |= success;
}
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)); inventory.visualSlots[i]?.ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.4f);
} }
} }
break;
SoundPlayer.PlayUISound(GUISoundType.DropItem); }
bool removed = false; }
if (Screen.Selected is SubEditorScreen editor) 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()); DraggingItems.ForEachMod(it => it.Remove());
removed = true; removed = true;
} }
else else
{ {
if (editor.WiringMode) DraggingItems.ForEachMod(it => it.Drop(Character.Controlled));
{
DraggingItems.ForEachMod(it => it.Remove());
removed = true;
}
else
{
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
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)))
{ {
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); SoundPlayer.PlayUISound(GUISoundType.PickItem);
} }
else else
{ {
bool anySuccess = false; if (selectedInventory.visualSlots != null){ selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f); }
bool allowCombine = true; SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
//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);
}
} }
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)) highlightedSubInventorySlots.Add(new SlotReference(
{ parentItem.ParentInventory, parentItem.ParentInventory.visualSlots[i],
selectedSlot = null; 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) private static bool CanSelectSlot(SlotReference selectedSlot)
@@ -1504,6 +1542,31 @@ namespace Barotrauma
} }
if (DraggingItems.Any()) 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())) if (DraggingSlot == null || (!DraggingSlot.MouseOn()))
{ {
@@ -1521,10 +1584,8 @@ namespace Barotrauma
if ((GUI.MouseOn == null || mouseOnHealthInterface) && selectedSlot == null) if ((GUI.MouseOn == null || mouseOnHealthInterface) && selectedSlot == null)
{ {
var shadowSprite = GUIStyle.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0]; var shadowSprite = GUIStyle.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0];
LocalizedString toolTip = mouseOnHealthInterface ? TextManager.Get("QuickUseAction.UseTreatment") :
Character.Controlled.FocusedItem != null ? (LocalizedString toolTip, Color toolTipColor) = GetDragLabelTextAndColor(mouseOnHealthInterface);
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");
Vector2 nameSize = GUIStyle.Font.MeasureString(DraggingItems.First().Name); Vector2 nameSize = GUIStyle.Font.MeasureString(DraggingItems.First().Name);
Vector2 toolTipSize = GUIStyle.SmallFont.MeasureString(toolTip); 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(nameSize.X * textOffset, -iconSize / 2), DraggingItems.First().Name, Color.White);
GUI.DrawString(spriteBatch, textPos + new Vector2(toolTipSize.X * textOffset, 0), toolTip, 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); 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; bool useDragDropGive = IsValidTargetForDragDropGive(Character.Controlled, Character.Controlled.FocusedCharacter);
slotRect.Location += selectedSlot.Slot.DrawOffset.ToPoint();
if (selectedSlot.TooltipNeedsRefresh()) Color toolTipColor = Color.LightGreen;
LocalizedString toolTip;
if (mouseOnHealthInterface)
{ {
selectedSlot.RefreshTooltip(); toolTip = TextManager.Get("QuickUseAction.UseTreatment");
} }
else if (Character.Controlled.FocusedItem != null)
if (!slotIconTooltip.IsNullOrEmpty())
{ {
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 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); DrawSideIcon(deconstructOrder.SymbolSprite, Direction.Right, TextManager.Get("tooltip.markedfordeconstruction"), GUIStyle.Red, out bool mouseOn);
if (mouseOn) { availableContextualOrder = (item, Tags.DontDeconstructThis); } 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 _); 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) 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) if (editing)
{ {
@@ -424,7 +424,7 @@ namespace Barotrauma
textureScale: Vector2.One * Scale, textureScale: Vector2.One * Scale,
depth: d); depth: d);
} }
DrawDecorativeSprites(spriteBatch, DrawPosition, flippedX && Prefab.CanSpriteFlipX, flippedY && Prefab.CanSpriteFlipY, rotation: 0, depth); DrawDecorativeSprites(spriteBatch, DrawPosition, flippedX && Prefab.CanSpriteFlipX, flippedY && Prefab.CanSpriteFlipY, rotation: 0, depth, overrideColor);
} }
} }
else 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); 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) else if (body.Enabled)
@@ -456,30 +456,50 @@ namespace Barotrauma
//don't draw the item on hands if it's also being worn //don't draw the item on hands if it's also being worn
if (GetComponent<Wearable>() is { IsActive: true }) { return; } if (GetComponent<Wearable>() is { IsActive: true }) { return; }
if (!back) { return; } if (!back) { return; }
float depthStep = 0.000001f;
if (holdable.Picker.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == this) if (holdable.Picker.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == this)
{ {
Limb holdLimb = holdable.Picker.AnimController.GetLimb(LimbType.RightArm); depth = GetHeldItemDepth(LimbType.RightHand, holdable, depth);
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); }
}
}
} }
else if (holdable.Picker.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == this) else if (holdable.Picker.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == this)
{ {
Limb holdLimb = holdable.Picker.AnimController.GetLimb(LimbType.LeftArm); depth = GetHeldItemDepth(LimbType.LeftHand, holdable, depth);
}
static float GetHeldItemDepth(LimbType limb, Holdable holdable, float depth)
{
if (holdable?.Picker?.AnimController == null) { return 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) 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) 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); Vector2 origin = GetSpriteOrigin(activeSprite);
@@ -489,7 +509,7 @@ namespace Barotrauma
float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f); float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f);
body.Draw(spriteBatch, fadeInBrokenSprite.Sprite, color * fadeInBrokenSpriteAlpha, d, Scale); 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) foreach (var upgrade in Upgrades)
@@ -617,11 +637,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) 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; } if (!spriteAnimState[decorativeSprite].IsActive) { continue; }
Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier,

View File

@@ -215,7 +215,9 @@ namespace Barotrauma
} }
else 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 particlesPerSec = Math.Max(open * rect.Width * particleAmountMultiplier, 10.0f);
float emitInterval = 1.0f / particlesPerSec; float emitInterval = 1.0f / particlesPerSec;

View File

@@ -210,7 +210,9 @@ namespace Barotrauma
{ {
bool primaryMouseButtonHeld = PlayerInput.PrimaryMouseButtonHeld(); bool primaryMouseButtonHeld = PlayerInput.PrimaryMouseButtonHeld();
bool secondaryMouseButtonHeld = PlayerInput.SecondaryMouseButtonHeld(); 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); Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition);
Hull hull = FindHull(position); Hull hull = FindHull(position);
@@ -218,29 +220,67 @@ namespace Barotrauma
if (hull == null || hull.IdFreed) { return; } if (hull == null || hull.IdFreed) { return; }
if (EditWater) if (EditWater)
{ {
const float waterIncrement = 100000.0f;
if (primaryMouseButtonHeld) if (primaryMouseButtonHeld)
{ {
ShowHulls = true; SetWaterVolume(hull.WaterVolume + waterIncrement * deltaTime);
hull.WaterVolume += 100000.0f * deltaTime;
hull.networkUpdatePending = true;
hull.serverUpdateDelay = 0.5f;
} }
else if (secondaryMouseButtonHeld) 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.networkUpdatePending = true;
hull.serverUpdateDelay = 0.5f; hull.serverUpdateDelay = 0.5f;
} }
} }
else if (EditFire) else if (EditFire)
{ {
bool networkUpdate = false;
if (primaryMouseButtonHeld) if (primaryMouseButtonHeld)
{ {
new FireSource(position, hull, isNetworkMessage: true); 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.networkUpdatePending = true;
hull.serverUpdateDelay = 0.5f; hull.serverUpdateDelay = 0.5f;
} }
} }
} }

View File

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

View File

@@ -1115,13 +1115,23 @@ namespace Barotrauma
float subCrushDepth = SubmarineInfo.GetSubCrushDepth(SubmarineSelection.CurrentOrPendingSubmarine(), ref pendingSubInfo); float subCrushDepth = SubmarineInfo.GetSubCrushDepth(SubmarineSelection.CurrentOrPendingSubmarine(), ref pendingSubInfo);
string crushDepthWarningIconStyle = null; 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++; iconCount++;
crushDepthWarningIconStyle = "CrushDepthWarningHighIcon"; crushDepthWarningIconStyle = "CrushDepthWarningHighIcon";
tooltip = "crushdepthwarninghigh"; 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++; iconCount++;
crushDepthWarningIconStyle = "CrushDepthWarningLowIcon"; crushDepthWarningIconStyle = "CrushDepthWarningLowIcon";

View File

@@ -382,7 +382,10 @@ namespace Barotrauma
if (!HasBody && !ShowStructures) { return; } if (!HasBody && !ShowStructures) { return; }
if (HasBody && !ShowWalls) { 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; 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 public override bool SelectableInEditor
{ {
get { return !IsHidden(); } get { return ShouldDrawIcon(); }
} }
public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true)
{ {
if (!editing && (!GameMain.DebugDraw || Screen.Selected.Cam.Zoom < 0.1f)) { return; } if (!editing && (!GameMain.DebugDraw || Screen.Selected.Cam.Zoom < 0.1f)) { return; }
if (IsHidden()) { return; } if (!ShouldDrawIcon()) { return; }
Vector2 drawPos = Position; Vector2 drawPos = Position;
if (Submarine != null) { drawPos += Submarine.DrawPosition; } if (Submarine != null) { drawPos += Submarine.DrawPosition; }
@@ -59,8 +59,10 @@ namespace Barotrauma
Color.White); Color.White);
} }
Sprite sprite = iconSprites[SpawnType.ToString()];
Sprite sprite2 = null; 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) if (spawnType == SpawnType.Human && AssignedJob?.Icon != null)
{ {
sprite = iconSprites["Path"]; sprite = iconSprites["Path"];
@@ -87,9 +89,12 @@ namespace Barotrauma
sprite = iconSprites["Ladder"]; sprite = iconSprites["Ladder"];
} }
float spriteScale = iconSize / (float)sprite.SourceRect.Width; if (sprite != null)
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); 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) if (spawnType == SpawnType.Human && AssignedJob?.Icon != null)
{ {
@@ -160,22 +165,22 @@ namespace Barotrauma
public override bool IsMouseOn(Vector2 position) public override bool IsMouseOn(Vector2 position)
{ {
if (IsHidden()) { return false; } if (!ShouldDrawIcon()) { return false; }
float dist = Vector2.DistanceSquared(position, WorldPosition); float dist = Vector2.DistanceSquared(position, WorldPosition);
float radius = (SpawnType == SpawnType.Path ? WaypointSize : SpawnPointSize) * 0.6f; float radius = (SpawnType == SpawnType.Path ? WaypointSize : SpawnPointSize) * 0.6f;
return dist < radius * radius; 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) if (spawnType == SpawnType.Path)
{ {
return (!GameMain.DebugDraw && !ShowWayPoints); return GameMain.DebugDraw || ShowWayPoints;
} }
else else
{ {
return (!GameMain.DebugDraw && !ShowSpawnPoints); return GameMain.DebugDraw || ShowSpawnPoints;
} }
} }

View File

@@ -30,6 +30,7 @@ namespace Barotrauma.Networking
txt = msg.ReadString(); txt = msg.ReadString();
string senderName = msg.ReadString(); string senderName = msg.ReadString();
Entity sender = null;
Character senderCharacter = null; Character senderCharacter = null;
Client senderClient = null; Client senderClient = null;
bool hasSenderClient = msg.ReadBoolean(); bool hasSenderClient = msg.ReadBoolean();
@@ -40,13 +41,14 @@ namespace Barotrauma.Networking
=> c.SessionOrAccountIdMatches(userId)); => c.SessionOrAccountIdMatches(userId));
if (senderClient != null) { senderName = senderClient.Name; } if (senderClient != null) { senderName = senderClient.Name; }
} }
bool hasSenderCharacter = msg.ReadBoolean(); bool hasSender = msg.ReadBoolean();
if (hasSenderCharacter) if (hasSender)
{ {
senderCharacter = Entity.FindEntityByID(msg.ReadUInt16()) as Character; sender = Entity.FindEntityByID(msg.ReadUInt16());
if (senderCharacter != null) 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); GameMain.Client.ServerSettings.ServerLog?.WriteLine(txt, messageType);
break; break;
default: 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)) if (type == ChatMessageType.Radio && CanUseRadio(senderCharacter, out WifiComponent radio))
{ {
Signal s = new Signal(txt, sender: senderCharacter, source: radio.Item); 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 IsClient => true;
public override bool IsServer => false; public override bool IsServer => false;
#if DEBUG
public float DebugServerVoipAmplitude;
#endif
public override Voting Voting { get; } public override Voting Voting { get; }
@@ -112,6 +116,8 @@ namespace Barotrauma.Networking
//has the client been given a character to control this round //has the client been given a character to control this round
public bool HasSpawned; public bool HasSpawned;
public float EndRoundTimeRemaining { get; private set; }
public LocalizedString TraitorFirstObjective; public LocalizedString TraitorFirstObjective;
public TraitorEventPrefab TraitorMission = null; public TraitorEventPrefab TraitorMission = null;
@@ -198,20 +204,6 @@ namespace Barotrauma.Networking
CanBeFocused = false 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 = new ChatBox(inGameHUD, isSinglePlayer: false);
chatBox.OnEnterMessage += EnterChatMessage; chatBox.OnEnterMessage += EnterChatMessage;
chatBox.InputBox.OnTextChanged += TypingChatMessage; chatBox.InputBox.OnTextChanged += TypingChatMessage;
@@ -250,6 +242,19 @@ namespace Barotrauma.Networking
} }
}; };
ShowLogButton.TextBlock.AutoScaleHorizontal = true; 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; GameMain.DebugDraw = false;
Hull.EditFire = false; Hull.EditFire = false;
@@ -674,6 +679,11 @@ namespace Barotrauma.Networking
VoipClient.Read(inc); VoipClient.Read(inc);
break; 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: case ServerPacketHeader.QUERY_STARTGAME:
DebugConsole.Log("Received QUERY_STARTGAME packet."); DebugConsole.Log("Received QUERY_STARTGAME packet.");
string subName = inc.ReadString(); string subName = inc.ReadString();
@@ -1327,7 +1337,7 @@ namespace Barotrauma.Networking
if (GameMain.GameSession?.GameMode is CampaignMode campaign) if (GameMain.GameSession?.GameMode is CampaignMode campaign)
{ {
campaign.CampaignUI?.UpgradeStore?.RequestRefresh(); campaign.CampaignUI?.UpgradeStore?.RequestRefresh();
campaign.CampaignUI?.CrewManagement?.RefreshPermissions(); campaign.CampaignUI?.HRManagerUI?.RefreshUI();
} }
} }
@@ -1382,6 +1392,7 @@ namespace Barotrauma.Networking
ServerSettings.AllowRewiring = inc.ReadBoolean(); ServerSettings.AllowRewiring = inc.ReadBoolean();
ServerSettings.AllowImmediateItemDelivery = inc.ReadBoolean(); ServerSettings.AllowImmediateItemDelivery = inc.ReadBoolean();
ServerSettings.AllowFriendlyFire = inc.ReadBoolean(); ServerSettings.AllowFriendlyFire = inc.ReadBoolean();
ServerSettings.AllowDragAndDropGive = inc.ReadBoolean();
ServerSettings.LockAllDefaultWires = inc.ReadBoolean(); ServerSettings.LockAllDefaultWires = inc.ReadBoolean();
ServerSettings.AllowLinkingWifiToChat = inc.ReadBoolean(); ServerSettings.AllowLinkingWifiToChat = inc.ReadBoolean();
ServerSettings.MaximumMoneyTransferRequest = inc.ReadInt32(); ServerSettings.MaximumMoneyTransferRequest = inc.ReadInt32();
@@ -1541,7 +1552,7 @@ namespace Barotrauma.Networking
} }
else else
{ {
GameMain.GameSession.StartRound(levelData, mirrorLevel); GameMain.GameSession.StartRound(levelData, mirrorLevel, startOutpost: campaign?.GetPredefinedStartOutpost());
} }
isOutpost = levelData.Type == LevelData.LevelType.Outpost; isOutpost = levelData.Type == LevelData.LevelType.Outpost;
} }
@@ -1812,6 +1823,8 @@ namespace Barotrauma.Networking
GameStarted = inc.ReadBoolean(); GameStarted = inc.ReadBoolean();
bool allowSpectating = inc.ReadBoolean(); bool allowSpectating = inc.ReadBoolean();
bool permadeathMode = inc.ReadBoolean();
bool ironmanMode = inc.ReadBoolean();
ReadPermissions(inc); ReadPermissions(inc);
@@ -1819,8 +1832,17 @@ namespace Barotrauma.Networking
{ {
if (Screen.Selected != GameMain.GameScreen) if (Screen.Selected != GameMain.GameScreen)
{ {
new GUIMessageBox(TextManager.Get("PleaseWait"), TextManager.Get(allowSpectating ? "RoundRunningSpectateEnabled" : "RoundRunningSpectateDisabled")); LocalizedString message;
if (Screen.Selected is not ModDownloadScreen) { GameMain.NetLobbyScreen.Select(); } 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(); }
} }
} }
} }
@@ -1935,7 +1957,7 @@ namespace Barotrauma.Networking
if (GameMain.GameSession?.GameMode is CampaignMode campaign) if (GameMain.GameSession?.GameMode is CampaignMode campaign)
{ {
campaign.CampaignUI?.UpgradeStore?.RequestRefresh(); campaign.CampaignUI?.UpgradeStore?.RequestRefresh();
campaign.CampaignUI?.CrewManagement?.RefreshPermissions(); campaign.CampaignUI?.HRManagerUI?.RefreshUI();
} }
} }
} }
@@ -2103,6 +2125,8 @@ namespace Barotrauma.Networking
float sendingTime = inc.ReadSingle() - 0.0f;//TODO: reimplement inc.SenderConnection.RemoteTimeOffset; float sendingTime = inc.ReadSingle() - 0.0f;//TODO: reimplement inc.SenderConnection.RemoteTimeOffset;
EndRoundTimeRemaining = inc.ReadSingle();
SegmentTableReader<ServerNetSegment>.Read(inc, SegmentTableReader<ServerNetSegment>.Read(inc,
segmentDataReader: (segment, inc) => segmentDataReader: (segment, inc) =>
{ {
@@ -2373,7 +2397,16 @@ namespace Barotrauma.Networking
WaitForNextRoundRespawn = waitForNextRoundRespawn; WaitForNextRoundRespawn = waitForNextRoundRespawn;
IWriteMessage msg = new WriteOnlyMessage(); IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ClientPacketHeader.READY_TO_SPAWN); 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); ClientPeer?.Send(msg, DeliveryMethod.Reliable);
} }
@@ -2704,16 +2737,19 @@ namespace Barotrauma.Networking
public override void AddChatMessage(ChatMessage message) public override void AddChatMessage(ChatMessage message)
{ {
if (string.IsNullOrEmpty(message.Text)) { 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()) if (message.Text.IsNullOrEmpty())
{ {
message.Sender.ShowTextlessSpeechBubble(2.0f, message.Color); sender.ShowTextlessSpeechBubble(2.0f, message.Color);
} }
else else
{ {
message.Sender.ShowSpeechBubble(message.Color, message.Text); sender.ShowSpeechBubble(message.Color, message.Text);
if (!sender.IsBot)
{
sender.TextChatVolume = 1f;
}
} }
} }
GameMain.NetLobbyScreen.NewChatMessage(message); GameMain.NetLobbyScreen.NewChatMessage(message);
@@ -3197,19 +3233,19 @@ namespace Barotrauma.Networking
{ {
LocalizedString respawnText = string.Empty; LocalizedString respawnText = string.Empty;
Color textColor = Color.White; Color textColor = Color.White;
bool canChooseRespawn = bool hideRespawnButtons = false;
GameMain.GameSession.GameMode is CampaignMode &&
Character.Controlled == null && if (EndRoundTimeRemaining > 0)
Level.Loaded?.Type != LevelData.LevelType.Outpost && {
(characterInfo == null || HasSpawned); respawnText = TextManager.GetWithVariable("endinground", "[time]", ToolBox.SecondsToReadableTime(EndRoundTimeRemaining))
.Fallback(ToolBox.SecondsToReadableTime(EndRoundTimeRemaining), useDefaultLanguageIfFound: false);
}
if (RespawnManager.CurrentState == RespawnManager.State.Waiting) if (RespawnManager.CurrentState == RespawnManager.State.Waiting)
{ {
if (RespawnManager.RespawnCountdownStarted) if (RespawnManager.RespawnCountdownStarted)
{ {
float timeLeft = (float)(RespawnManager.RespawnTime - DateTime.Now).TotalSeconds; float timeLeft = (float)(RespawnManager.RespawnTime - DateTime.Now).TotalSeconds;
respawnText = TextManager.GetWithVariable( respawnText = TextManager.GetWithVariable("RespawningIn", "[time]", ToolBox.SecondsToReadableTime(timeLeft));
RespawnManager.UsingShuttle && !RespawnManager.ForceSpawnInMainSub ?
"RespawnShuttleDispatching" : "RespawningIn", "[time]", ToolBox.SecondsToReadableTime(timeLeft));
} }
else if (RespawnManager.PendingRespawnCount > 0) else if (RespawnManager.PendingRespawnCount > 0)
{ {
@@ -3232,12 +3268,12 @@ namespace Barotrauma.Networking
//textScale = 1.0f + phase * 0.5f; //textScale = 1.0f + phase * 0.5f;
textColor = Color.Lerp(GUIStyle.Red, Color.White, 1.0f - phase); textColor = Color.Lerp(GUIStyle.Red, Color.White, 1.0f - phase);
} }
canChooseRespawn = false; hideRespawnButtons = true;
} }
GameMain.GameSession?.SetRespawnInfo( GameMain.GameSession.SetRespawnInfo(
visible: !respawnText.IsNullOrEmpty() || canChooseRespawn, text: respawnText.Value, textColor: textColor, text: respawnText.Value, textColor: textColor,
buttonsVisible: canChooseRespawn, waitForNextRoundRespawn: (WaitForNextRoundRespawn ?? true)); waitForNextRoundRespawn: (WaitForNextRoundRespawn ?? true), hideButtons: hideRespawnButtons);
} }
if (!ShowNetStats) { return; } if (!ShowNetStats) { return; }

View File

@@ -1,5 +1,4 @@
using System; using System;
using System.Linq;
namespace Barotrauma.Networking namespace Barotrauma.Networking
{ {
@@ -23,6 +22,14 @@ namespace Barotrauma.Networking
get; private set; get; private set;
} }
public static void ShowDeathPromptIfNeeded(float delay = 1.0f)
{
if (UseDeathPrompt)
{
DeathPrompt.Create(delay);
}
}
partial void UpdateTransportingProjSpecific(float deltaTime) partial void UpdateTransportingProjSpecific(float deltaTime)
{ {
if (GameMain.Client?.Character == null || GameMain.Client.Character.Submarine != RespawnShuttle) { return; } 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) public void ClientEventRead(IReadMessage msg, float sendingTime)
{ {
bool respawnPromptPending = false; bool respawnPromptPending = false;
var newState = (State)msg.ReadRangedInteger(0, Enum.GetNames(typeof(State)).Length); var newState = (State)msg.ReadRangedInteger(0, Enum.GetNames(typeof(State)).Length);
ForceSpawnInMainSub = false;
switch (newState) switch (newState)
{ {
case State.Transporting: case State.Transporting:
@@ -122,7 +67,6 @@ namespace Barotrauma.Networking
RequiredRespawnCount = msg.ReadUInt16(); RequiredRespawnCount = msg.ReadUInt16();
respawnPromptPending = msg.ReadBoolean(); respawnPromptPending = msg.ReadBoolean();
RespawnCountdownStarted = msg.ReadBoolean(); RespawnCountdownStarted = msg.ReadBoolean();
ForceSpawnInMainSub = msg.ReadBoolean();
ResetShuttle(); ResetShuttle();
float newRespawnTime = msg.ReadSingle(); float newRespawnTime = msg.ReadSingle();
RespawnTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, milliseconds: (int)(newRespawnTime * 1000.0f)); RespawnTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, milliseconds: (int)(newRespawnTime * 1000.0f));
@@ -136,7 +80,7 @@ namespace Barotrauma.Networking
if (respawnPromptPending) if (respawnPromptPending)
{ {
GameMain.Client.HasSpawned = true; GameMain.Client.HasSpawned = true;
ShowRespawnPromptIfNeeded(delay: 1.0f); DeathPrompt.Create(delay: 1.0f);
} }
msg.ReadPadBits(); msg.ReadPadBits();

View File

@@ -468,11 +468,28 @@ namespace Barotrauma.Networking
}; };
AssignGUIComponent(nameof(SaveServerLogs), saveLogsBox); 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 // 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, Stretch = true,
RelativeSpacing = 0.05f RelativeSpacing = 0.05f
@@ -683,7 +700,7 @@ namespace Barotrauma.Networking
// antigriefing // 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, AutoHideScrollBar = true,
UseGridLayout = 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), var allowFriendlyFire = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform),
TextManager.Get("ServerSettingsAllowFriendlyFire")); TextManager.Get("ServerSettingsAllowFriendlyFire"));
AssignGUIComponent(nameof(AllowFriendlyFire), allowFriendlyFire); 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), var killableNPCs = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform),
TextManager.Get("ServerSettingsKillableNPCs")); TextManager.Get("ServerSettingsKillableNPCs"));

View File

@@ -1,19 +1,10 @@
using Concentus.Enums; using Concentus.Enums;
using Concentus.Structs; using Concentus.Structs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Barotrauma.Networking namespace Barotrauma.Networking
{ {
static partial class VoipConfig 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() public static OpusEncoder CreateEncoder()
{ {
var encoder = new OpusEncoder(FREQUENCY, 1, OpusApplication.OPUS_APPLICATION_VOIP); var encoder = new OpusEncoder(FREQUENCY, 1, OpusApplication.OPUS_APPLICATION_VOIP);
@@ -22,10 +13,5 @@ namespace Barotrauma.Networking
encoder.SignalType = OpusSignal.OPUS_SIGNAL_VOICE; encoder.SignalType = OpusSignal.OPUS_SIGNAL_VOICE;
return encoder; 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 MouseState latestMouseState; //the absolute latest state, do NOT use for player interaction
static KeyboardState keyboardState, oldKeyboardState; static KeyboardState keyboardState, oldKeyboardState;
static double timeSinceClick; static double timeSincePrimaryClick;
static Point lastClickPosition; static Point lastPrimaryClickPosition;
static double timeSinceSecondaryClick;
static Point lastSecondaryClickPosition;
const float DoubleClickDelay = 0.4f; const float DoubleClickDelay = 0.4f;
public static float MaxDoubleClickDistance 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); } 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 allowInput;
static bool wasWindowActive; static bool wasWindowActive;
@@ -406,7 +410,12 @@ namespace Barotrauma
public static bool DoubleClicked() public static bool DoubleClicked()
{ {
return AllowInput && doubleClicked; return AllowInput && primaryDoubleClicked;
}
public static bool SecondaryDoubleClicked()
{
return AllowInput && secondaryDoubleClicked;
} }
public static bool KeyHit(InputType inputType) public static bool KeyHit(InputType inputType)
@@ -466,7 +475,8 @@ namespace Barotrauma
public static void Update(double deltaTime) public static void Update(double deltaTime)
{ {
timeSinceClick += deltaTime; timeSincePrimaryClick += deltaTime;
timeSinceSecondaryClick += deltaTime;
if (!GameMain.WindowActive) if (!GameMain.WindowActive)
{ {
@@ -495,11 +505,33 @@ namespace Barotrauma
MouseSpeedPerSecond = MouseSpeed / (float)deltaTime; MouseSpeedPerSecond = MouseSpeed / (float)deltaTime;
// Split into two to not accept drag & drop releasing as part of a double-click // Split into two to not accept drag & drop releasing as part of a double-click
doubleClicked = false; primaryDoubleClicked = false;
if (PrimaryMouseButtonClicked()) 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) if (timeSinceClick < DoubleClickDelay && dist < MaxDoubleClickDistance)
{ {
doubleClicked = true; doubleClicked = true;
@@ -513,11 +545,8 @@ namespace Barotrauma
{ {
timeSinceClick = 0.0; timeSinceClick = 0.0;
} }
}
return doubleClicked;
if (PrimaryMouseButtonDown())
{
lastClickPosition = mouseState.Position;
} }
} }

View File

@@ -39,7 +39,7 @@ namespace Barotrauma
public CampaignMode Campaign { get; } public CampaignMode Campaign { get; }
public CrewManagement CrewManagement { get; set; } public HRManagerUI HRManagerUI { get; set; }
public Store Store { get; private set; } public Store Store { get; private set; }
@@ -102,7 +102,7 @@ namespace Barotrauma
var crewTab = new GUIFrame(new RectTransform(Vector2.One, container.RectTransform), color: Color.Black * 0.9f); var crewTab = new GUIFrame(new RectTransform(Vector2.One, container.RectTransform), color: Color.Black * 0.9f);
tabs[(int)CampaignMode.InteractionType.Crew] = crewTab; tabs[(int)CampaignMode.InteractionType.Crew] = crewTab;
CrewManagement = new CrewManagement(this, crewTab); HRManagerUI = new HRManagerUI(this, crewTab);
// store tab ------------------------------------------------------------------------- // store tab -------------------------------------------------------------------------
@@ -204,7 +204,7 @@ namespace Barotrauma
submarineSelection?.Update(); submarineSelection?.Update();
break; break;
case CampaignMode.InteractionType.Crew: case CampaignMode.InteractionType.Crew:
CrewManagement?.Update(); HRManagerUI?.Update();
break; break;
case CampaignMode.InteractionType.Store: case CampaignMode.InteractionType.Store:
Store?.Update(deltaTime); Store?.Update(deltaTime);
@@ -598,8 +598,8 @@ namespace Barotrauma
Store.SelectStore(npc); Store.SelectStore(npc);
break; break;
case CampaignMode.InteractionType.Crew: case CampaignMode.InteractionType.Crew:
CrewManagement.UpdateCrew(); HRManagerUI.UpdateCrew();
CrewManagement.UpdateHireables(); HRManagerUI.UpdateHireables();
break; break;
case CampaignMode.InteractionType.PurchaseSub: case CampaignMode.InteractionType.PurchaseSub:
submarineSelection ??= new SubmarineSelection(false, () => Campaign.ShowCampaignUI = false, tabs[(int)CampaignMode.InteractionType.PurchaseSub].RectTransform); submarineSelection ??= new SubmarineSelection(false, () => Campaign.ShowCampaignUI = false, tabs[(int)CampaignMode.InteractionType.PurchaseSub].RectTransform);

View File

@@ -781,7 +781,7 @@ namespace Barotrauma.CharacterEditor
// Lightmaps // Lightmaps
if (GameMain.LightManager.LightingEnabled && Character.Controlled != null) 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.RenderLightMap(graphics, spriteBatch, cam);
GameMain.LightManager.UpdateObstructVision(graphics, spriteBatch, cam, Character.Controlled.CursorWorldPosition); 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 Barotrauma.Lights;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
@@ -177,10 +177,15 @@ namespace Barotrauma
Stopwatch sw = new Stopwatch(); Stopwatch sw = new Stopwatch();
sw.Start(); sw.Start();
GameMain.LightManager.ObstructVision = if (Character.Controlled != null &&
Character.Controlled != null && (Character.Controlled.ViewTarget == Character.Controlled || Character.Controlled.ViewTarget == null))
Character.Controlled.ObstructVision && {
(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); 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); currentLevelData = LevelData.CreateRandom(seedBox.Text, generationParams: selectedParams);
editorContainer.ClearChildren(); editorContainer.ClearChildren();
SortLevelObjectsList(currentLevelData); 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; return true;
}; };
@@ -996,7 +996,7 @@ namespace Barotrauma
{ {
foreach (Item item in Item.ItemList) 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>()) foreach (var light in item.GetComponents<Items.Components.LightComponent>())
{ {
light.Update((float)deltaTime, Cam); light.Update((float)deltaTime, Cam);

View File

@@ -63,6 +63,9 @@ namespace Barotrauma
private GUITickBox spectateBox; private GUITickBox spectateBox;
public bool Spectating => spectateBox is { Selected: true, Visible: true }; 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 GUILayoutGroup playerInfoContent;
private GUIComponent changesPendingText; private GUIComponent changesPendingText;
private bool createPendingChangesText = true; private bool createPendingChangesText = true;
@@ -87,7 +90,14 @@ namespace Barotrauma
private GUIFrame characterInfoFrame; private GUIFrame characterInfoFrame;
private GUIFrame appearanceFrame; 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>(); private readonly List<GUIComponent> campaignDisabledElements = new List<GUIComponent>();
public CharacterInfo.AppearanceCustomizationMenu CharacterAppearanceCustomizationMenu { get; set; } public CharacterInfo.AppearanceCustomizationMenu CharacterAppearanceCustomizationMenu { get; set; }
@@ -191,7 +201,7 @@ namespace Barotrauma
public bool UsingShuttle public bool UsingShuttle
{ {
get { return shuttleTickBox.Selected; } get { return shuttleTickBox.Selected && !PermadeathMode; }
set { shuttleTickBox.Selected = value; } 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) }, var respawnModeHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true };
TextManager.Get("ServerSettingsAllowRespawning")) 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"), respawnModeSelection.AddElement(respawnMode, TextManager.Get($"respawnmode.{respawnMode}"), TextManager.Get($"respawnmode.{respawnMode}.tooltip"));
OnSelected = (tickbox) => }
{
GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); respawnModeSelection.ElementSelectionCondition += (value) => value != RespawnMode.Permadeath || SelectedMode == GameModePreset.MultiPlayerCampaign;
RefreshEnabledElements(); respawnModeSelection.OnValueChanged += (_) => GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
return true; AssignComponentToServerSetting(respawnModeSelection, nameof(ServerSettings.RespawnMode));
}
};
AssignComponentToServerSetting(respawnBox, nameof(ServerSettings.AllowRespawn));
clientDisabledElements.Add(respawnBox);
GUILayoutGroup shuttleHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), settingsContent.RectTransform), isHorizontal: true) 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")) shuttleTickBox = new GUITickBox(new RectTransform(Vector2.One, shuttleHolder.RectTransform), TextManager.Get("RespawnShuttle"))
{ {
ToolTip = TextManager.Get("RespawnShuttleExplanation"), ToolTip = TextManager.Get("RespawnShuttleExplanation"),
Selected = true, Selected = !PermadeathMode,
OnSelected = (GUITickBox box) => OnSelected = (GUITickBox box) =>
{ {
GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
@@ -985,7 +993,7 @@ namespace Barotrauma
} }
}; };
AssignComponentToServerSetting(shuttleTickBox, nameof(ServerSettings.UseRespawnShuttle)); AssignComponentToServerSetting(shuttleTickBox, nameof(ServerSettings.UseRespawnShuttle));
respawnSettingsElements.Add(shuttleTickBox); midRoundRespawnSettings.Add(shuttleTickBox);
shuttleTickBox.TextBlock.RectTransform.SizeChanged += () => shuttleTickBox.TextBlock.RectTransform.SizeChanged += () =>
{ {
@@ -1008,9 +1016,9 @@ namespace Barotrauma
}; };
ShuttleList.ListBox.RectTransform.MinSize = new Point(250, 0); ShuttleList.ListBox.RectTransform.MinSize = new Point(250, 0);
shuttleHolder.RectTransform.MinSize = new Point(0, ShuttleList.RectTransform.Children.Max(c => c.MinSize.Y)); 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)); range: new Vector2(10.0f, 600.0f));
LocalizedString intervalLabel = respawnIntervalSliderLabel.Text; LocalizedString intervalLabel = respawnIntervalSliderLabel.Text;
respawnIntervalSlider.StepValue = 10.0f; respawnIntervalSlider.StepValue = 10.0f;
@@ -1026,7 +1034,6 @@ namespace Barotrauma
return true; return true;
}; };
respawnIntervalSlider.OnMoved(respawnIntervalSlider, respawnIntervalSlider.BarScroll); respawnIntervalSlider.OnMoved(respawnIntervalSlider, respawnIntervalSlider.BarScroll);
respawnSettingsElements.AddRange(respawnIntervalElement.GetAllChildren());
AssignComponentToServerSetting(respawnIntervalSlider, nameof(ServerSettings.RespawnInterval)); AssignComponentToServerSetting(respawnIntervalSlider, nameof(ServerSettings.RespawnInterval));
var minRespawnElement = CreateLabeledSlider(settingsContent, "ServerSettingsMinRespawn", "", "ServerSettingsMinRespawnToolTip", out var minRespawnSlider, out var minRespawnSliderLabel, var minRespawnElement = CreateLabeledSlider(settingsContent, "ServerSettingsMinRespawn", "", "ServerSettingsMinRespawnToolTip", out var minRespawnSlider, out var minRespawnSliderLabel,
@@ -1043,7 +1050,7 @@ namespace Barotrauma
return true; return true;
}; };
minRespawnSlider.OnMoved(minRespawnSlider, minRespawnSlider.BarScroll); minRespawnSlider.OnMoved(minRespawnSlider, minRespawnSlider.BarScroll);
respawnSettingsElements.AddRange(minRespawnElement.GetAllChildren()); midRoundRespawnSettings.AddRange(minRespawnElement.GetAllChildren());
AssignComponentToServerSetting(minRespawnSlider, nameof(ServerSettings.MinRespawnRatio)); AssignComponentToServerSetting(minRespawnSlider, nameof(ServerSettings.MinRespawnRatio));
var respawnDurationElement = CreateLabeledSlider(settingsContent, "ServerSettingsRespawnDuration", "", "ServerSettingsRespawnDurationTooltip", out var respawnDurationSlider, out var respawnDurationSliderLabel, 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); return value <= 0.0f ? 1.0f : (value - scrollBar.Range.X) / (scrollBar.Range.Y - scrollBar.Range.X);
}; };
respawnDurationSlider.OnMoved(respawnDurationSlider, respawnDurationSlider.BarScroll); respawnDurationSlider.OnMoved(respawnDurationSlider, respawnDurationSlider.BarScroll);
respawnSettingsElements.AddRange(respawnDurationElement.GetAllChildren()); midRoundRespawnSettings.AddRange(respawnDurationElement.GetAllChildren());
AssignComponentToServerSetting(respawnDurationSlider, nameof(ServerSettings.MaxTransportTime)); AssignComponentToServerSetting(respawnDurationSlider, nameof(ServerSettings.MaxTransportTime));
var skillLossElement = CreateLabeledSlider(settingsContent, "ServerSettingsSkillLossPercentageOnDeath", "", "ServerSettingsSkillLossPercentageOnDeathToolTip", var skillLossElement = CreateLabeledSlider(settingsContent, "ServerSettingsSkillLossPercentageOnDeath", "", "ServerSettingsSkillLossPercentageOnDeathToolTip",
@@ -1085,7 +1092,8 @@ namespace Barotrauma
GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
return true; return true;
}; };
respawnSettingsElements.AddRange(skillLossElement.GetAllChildren()); permadeathDisabledRespawnSettings.AddRange(skillLossElement.GetAllChildren());
clientDisabledElements.AddRange(skillLossElement.GetAllChildren());
AssignComponentToServerSetting(skillLossSlider, nameof(ServerSettings.SkillLossPercentageOnDeath)); AssignComponentToServerSetting(skillLossSlider, nameof(ServerSettings.SkillLossPercentageOnDeath));
skillLossSlider.OnMoved(skillLossSlider, skillLossSlider.BarScroll); skillLossSlider.OnMoved(skillLossSlider, skillLossSlider.BarScroll);
@@ -1103,11 +1111,71 @@ namespace Barotrauma
GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
return true; return true;
}; };
respawnSettingsElements.AddRange(skillLossImmediateRespawnElement.GetAllChildren()); midRoundRespawnSettings.AddRange(skillLossImmediateRespawnElement.GetAllChildren());
permadeathDisabledRespawnSettings.AddRange(skillLossImmediateRespawnElement.GetAllChildren());
AssignComponentToServerSetting(skillLossImmediateRespawnSlider, nameof(ServerSettings.SkillLossPercentageOnImmediateRespawn)); AssignComponentToServerSetting(skillLossImmediateRespawnSlider, nameof(ServerSettings.SkillLossPercentageOnImmediateRespawn));
skillLossImmediateRespawnSlider.OnMoved(skillLossImmediateRespawnSlider, skillLossImmediateRespawnSlider.BarScroll); skillLossImmediateRespawnSlider.OnMoved(skillLossImmediateRespawnSlider, skillLossImmediateRespawnSlider.BarScroll);
foreach (var respawnElement in respawnSettingsElements) var newCharacterCostSliderElement = CreateLabeledSlider(settingsContent,
"ServerSettings.ReplaceCostPercentage", "", "ServerSettings.ReplaceCostPercentage.tooltip",
out var newCharacterCostSlider, out var newCharacterCostSliderLabel,
range: new Vector2(0, 200), step: 10f);
newCharacterCostSlider.StepValue = 10f;
newCharacterCostSlider.OnMoved = (GUIScrollBar scrollBar, float _) =>
{
GUITextBlock textBlock = scrollBar.UserData as GUITextBlock;
int currentMultiplier = (int)Math.Round(scrollBar.BarScrollValue);
if (currentMultiplier < 1)
{
textBlock.Text = TextManager.Get("ServerSettings.ReplaceCostPercentage.Free");
}
else
{
textBlock.Text = TextManager.GetWithVariable("percentageformat", "[value]", currentMultiplier.ToString());
}
return true;
};
newCharacterCostSlider.OnReleased = (GUIScrollBar scrollBar, float barScroll) =>
{
GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
return true;
};
clientDisabledElements.AddRange(newCharacterCostSliderElement.GetAllChildren());
permadeathEnabledRespawnSettings.AddRange(newCharacterCostSliderElement.GetAllChildren());
ironmanDisabledRespawnSettings.AddRange(newCharacterCostSliderElement.GetAllChildren());
AssignComponentToServerSetting(newCharacterCostSlider, nameof(ServerSettings.ReplaceCostPercentage));
newCharacterCostSlider.OnMoved(newCharacterCostSlider, newCharacterCostSlider.BarScroll); // initialize
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)) if (!clientDisabledElements.Contains(respawnElement))
{ {
@@ -1650,19 +1718,31 @@ namespace Barotrauma
bool campaignStarted = CampaignFrame.Visible; bool campaignStarted = CampaignFrame.Visible;
bool gameStarted = client != null && client.GameStarted; 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) foreach (var element in clientDisabledElements)
{ {
element.Enabled = manageSettings; element.Enabled = manageSettings;
} }
// Then disable elements depending on other conditions
traitorElements.ForEach(e => e.Enabled &= settings.TraitorProbability > 0); traitorElements.ForEach(e => e.Enabled &= settings.TraitorProbability > 0);
SetTraitorDangerIndicators(settings.TraitorDangerLevel); 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 //go through the individual elements that are only enabled in a specific context
shuttleTickBox.Enabled &= !gameStarted;
if (ShuttleList != null) 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) if (SubList != null)
{ {
@@ -1672,7 +1752,6 @@ namespace Barotrauma
{ {
ModeList.Enabled = !gameStarted && (settings.AllowModeVoting || HasPermission(ClientPermissions.SelectMode)); ModeList.Enabled = !gameStarted && (settings.AllowModeVoting || HasPermission(ClientPermissions.SelectMode));
} }
shuttleTickBox.Enabled &= !gameStarted;
RefreshStartButtonVisibility(); RefreshStartButtonVisibility();
@@ -1750,6 +1829,10 @@ namespace Barotrauma
private void UpdatePlayerFrame(CharacterInfo characterInfo, bool allowEditing, GUIComponent parent, bool createPendingText = true) private void UpdatePlayerFrame(CharacterInfo characterInfo, bool allowEditing, GUIComponent parent, bool createPendingText = true)
{ {
if (GameMain.Client == null) { return; } if (GameMain.Client == null) { return; }
// When permanently dead and still characterless, spectating is the only option
spectateBox.Enabled = !PermanentlyDead;
createPendingChangesText = createPendingText; createPendingChangesText = createPendingText;
if (characterInfo == null || CampaignCharacterDiscarded) if (characterInfo == null || CampaignCharacterDiscarded)
{ {
@@ -1780,41 +1863,58 @@ namespace Barotrauma
MaxTextLength = Client.MaxNameLength, MaxTextLength = Client.MaxNameLength,
OverflowClip = true OverflowClip = true
}; };
CharacterNameBox.OnEnterPressed += (tb, text) => { CharacterNameBox.Deselect(); return true; }; if (!allowEditing ||
CharacterNameBox.OnDeselected += (tb, key) => (PermanentlyDead && !characterInfo.RenamingEnabled))
{ {
if (GameMain.Client == null) { return; } CharacterNameBox.Readonly = true;
string newName = Client.SanitizeName(tb.Text); CharacterNameBox.Enabled = false;
if (newName == GameMain.Client.Name) return; }
if (string.IsNullOrWhiteSpace(newName)) else
{
CharacterNameBox.OnEnterPressed += (tb, text) =>
{ {
tb.Text = GameMain.Client.Name; CharacterNameBox.Deselect();
} return true;
else };
CharacterNameBox.OnDeselected += (tb, key) =>
{ {
if (isGameRunning) if (GameMain.Client == null)
{ {
GameMain.Client.PendingName = tb.Text; return;
TabMenu.PendingChanges = true; }
if (createPendingText)
{ string newName = Client.SanitizeName(tb.Text);
CreateChangesPendingText(); if (newName == GameMain.Client.Name) { return; }
} if (string.IsNullOrWhiteSpace(newName))
{
tb.Text = GameMain.Client.Name;
} }
else 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 //spacing
new GUIFrame(new RectTransform(new Vector2(1.0f, 0.006f), parent.RectTransform), style: null); 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) GUILayoutGroup characterInfoTabs = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), parent.RectTransform), isHorizontal: true)
{ {
@@ -1892,37 +1992,70 @@ namespace Barotrauma
{ {
characterInfo.CreateIcon(new RectTransform(new Vector2(1.0f, 0.16f), parent.RectTransform, Anchor.TopCenter)); 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, new GUITextBlock(
SelectedColor = Color.Transparent new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform),
}; TextManager.Get("deceased"),
textAlignment: Alignment.Center, font: GUIStyle.LargeFont);
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()) 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); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), characterInfo.Job.Name, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont, wrap: true)
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()), HoverColor = Color.Transparent,
textColor, SelectedColor = Color.Transparent
font: GUIStyle.SmallFont); };
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 // Spacing
new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), parent.RectTransform), style: null); 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, // Button to create new character
OnClicked = (btn, userdata) => 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); TryDiscardCampaignCharacter(() =>
}); {
return true; UpdatePlayerFrame(null, true, parent);
} });
}; return true;
}
};
}
} }
TeamPreferenceListBox = null; TeamPreferenceListBox = null;
@@ -2095,14 +2228,20 @@ namespace Barotrauma
{ {
if (GameMain.Client == null) { return; } if (GameMain.Client == null) { return; }
spectateBox.Selected = spectate; spectateBox.Selected = spectate;
if (spectate) if (spectate)
{ {
playerInfoContent.ClearChildren();
GameMain.Client.CharacterInfo?.Remove(); GameMain.Client.CharacterInfo?.Remove();
GameMain.Client.CharacterInfo = null; GameMain.Client.CharacterInfo = null;
GameMain.Client.Character?.Remove(); // TODO: The following lines are ancient, unexplained, and they cause a client spectating because of permadeath
GameMain.Client.Character = null; // 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), new GUITextBlock(new RectTransform(Vector2.One, playerInfoContent.RectTransform, Anchor.Center),
TextManager.Get("PlayingAsSpectator"), TextManager.Get("PlayingAsSpectator"),
textAlignment: Alignment.Center); textAlignment: Alignment.Center);
@@ -2118,6 +2257,10 @@ namespace Barotrauma
// Server owner is allowed to spectate regardless of the server settings // Server owner is allowed to spectate regardless of the server settings
if (GameMain.Client != null && GameMain.Client.IsServerOwner) { return; } 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 // Show the player config menu if spectating is not allowed
if (spectateBox.Selected && !allowSpectating) { spectateBox.Selected = false; } if (spectateBox.Selected && !allowSpectating) { spectateBox.Selected = false; }
@@ -3609,6 +3752,7 @@ namespace Barotrauma
GameMain.GameSession = null; GameMain.GameSession = null;
} }
respawnModeSelection.Refresh(); // not all respawn modes are compatible with all game modes
RefreshGameModeContent(); RefreshGameModeContent();
RefreshEnabledElements(); RefreshEnabledElements();
} }

View File

@@ -27,38 +27,8 @@ namespace Barotrauma
get => Submarine.MainSub; get => Submarine.MainSub;
set => Submarine.MainSub = value; set => Submarine.MainSub = value;
} }
private enum LayerVisibility
{
Visible,
Invisible
}
private enum LayerLinkage private readonly record struct LayerData(bool IsVisible = true, bool IsGrouped = false);
{
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;
}
}
public enum Mode public enum Mode
{ {
@@ -1105,11 +1075,22 @@ namespace Barotrauma
GameSession gameSession = new GameSession(backedUpSubInfo, "", GameModePreset.TestMode, CampaignSettings.Empty, null); GameSession gameSession = new GameSession(backedUpSubInfo, "", GameModePreset.TestMode, CampaignSettings.Empty, null);
gameSession.StartRound(null, false); gameSession.StartRound(null, false);
(gameSession.GameMode as TestGameMode).OnRoundEnd = () =>
foreach ((string layerName, LayerData layerData) in Layers)
{ {
Submarine.Unload(); Identifier identifier = layerName.ToIdentifier();
GameMain.SubEditorScreen.Select(); bool enabled = layerData.IsVisible;
}; MainSub.SetLayerEnabled(identifier, enabled);
}
if (gameSession.GameMode is TestGameMode testGameMode)
{
testGameMode.OnRoundEnd = () =>
{
Submarine.Unload();
GameMain.SubEditorScreen.Select();
};
}
return true; return true;
} }
@@ -1469,6 +1450,7 @@ namespace Barotrauma
{ {
var subInfo = new SubmarineInfo(); var subInfo = new SubmarineInfo();
MainSub = new Submarine(subInfo, showErrorMessages: false); MainSub = new Submarine(subInfo, showErrorMessages: false);
ReconstructLayers();
} }
MainSub.UpdateTransform(interpolate: false); MainSub.UpdateTransform(interpolate: false);
@@ -1504,7 +1486,10 @@ namespace Barotrauma
} }
ImageManager.OnEditorSelected(); ImageManager.OnEditorSelected();
ReconstructLayers(); if (Layers.None())
{
ReconstructLayers();
}
} }
public override void OnFileDropped(string filePath, string extension) public override void OnFileDropped(string filePath, string extension)
@@ -1661,7 +1646,6 @@ namespace Barotrauma
}); });
ClearFilter(); ClearFilter();
ClearLayers();
} }
private void CreateDummyCharacter() private void CreateDummyCharacter()
@@ -2165,32 +2149,32 @@ namespace Barotrauma
if (Layers.Any()) if (Layers.Any())
{ {
var layerVisibilityGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.01f), leftColumn.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); 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); 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), var layerVisibilityDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), layerVisibilityGroup.RectTransform), text: visibleLayersString, selectMultiple: true);
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 (var layer in Layers)
foreach (string layerName in Layers.Keys)
{ {
string layerName = layer.Key;
layerVisibilityDropDown.AddItem(TextManager.Capitalize(layerName), layerName); layerVisibilityDropDown.AddItem(TextManager.Capitalize(layerName), layerName);
if (MainSub?.Info == null) { continue; } if (visibleLayers.Contains(layer))
if (!MainSub.Info.LayersHiddenByDefault.Contains(layerName.ToIdentifier()))
{ {
layerVisibilityDropDown.SelectItem(layerName); layerVisibilityDropDown.SelectItem(layerName);
} }
} }
layerVisibilityDropDown.OnSelected += (_, __) => layerVisibilityDropDown.OnSelected += (button, obj) =>
{ {
if (MainSub.Info == null) { return false; } string layerName = (string)obj;
MainSub.Info.LayersHiddenByDefault.Clear(); bool isVisible = layerVisibilityDropDown.SelectedDataMultiple.Contains(obj);
foreach (string layerName in Layers.Keys) if (isVisible)
{ {
//selected as visible = not hidden MainSub.Info.LayersHiddenByDefault.Remove(layerName.ToIdentifier());
if (layerVisibilityDropDown.SelectedDataMultiple.Any(o => o as string == layerName))
{
continue;
}
MainSub.Info.LayersHiddenByDefault.Add(layerName.ToIdentifier());
} }
else
{
MainSub.Info.LayersHiddenByDefault.Add(layerName.ToIdentifier());
}
UpdateLayerPanel();
layerVisibilityDropDown.Text = ToolBox.LimitString(layerVisibilityDropDown.Text.Value, layerVisibilityDropDown.Font, layerVisibilityDropDown.Rect.Width); layerVisibilityDropDown.Text = ToolBox.LimitString(layerVisibilityDropDown.Text.Value, layerVisibilityDropDown.Font, layerVisibilityDropDown.Rect.Width);
return true; return true;
}; };
@@ -2508,6 +2492,15 @@ namespace Barotrauma
return true; 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")) new GUITickBox(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), TextManager.Get("allowdisconnectedwires"))
{ {
Selected = MainSub?.Info?.BeaconStationInfo?.AllowDisconnectedWires ?? true, Selected = MainSub?.Info?.BeaconStationInfo?.AllowDisconnectedWires ?? true,
@@ -3932,15 +3925,14 @@ namespace Barotrauma
MapEntity.HighlightedEntities.ToList() : MapEntity.HighlightedEntities.ToList() :
new List<MapEntity>(MapEntity.SelectedList); new List<MapEntity>(MapEntity.SelectedList);
Item target = null; bool allowOpening = false;
var targetItem = (targets.Count == 1 ? targets.Single() : null) as Item;
var single = targets.Count == 1 ? targets.Single() : null; // Do not offer the ability to open the inventory if the inventory should never be drawn
if (single is Item item && item.Components.Any(static ic => ic is not ConnectionPanel && ic is not Repairable && ic.GuiFrame != null)) allowOpening = targetItem is not null && targetItem.Components.Any(static ic =>
{ ic is not ConnectionPanel &&
// Do not offer the ability to open the inventory if the inventory should never be drawn ic is not Repairable &&
var containers = item.GetComponents<ItemContainer>(); ic is not ItemContainer { DrawInventory: false } &&
if (containers.Any(static c => c.DrawInventory) || item.GetComponent<CircuitBox>() is not null) { target = item; } ic.GuiFrame != null);
}
bool hasTargets = targets.Count > 0; bool hasTargets = targets.Count > 0;
@@ -3984,7 +3976,6 @@ namespace Barotrauma
} }
else else
{ {
List<ContextMenuOption> availableLayers = new List<ContextMenuOption> List<ContextMenuOption> availableLayers = new List<ContextMenuOption>
{ {
new ContextMenuOption("editor.layer.nolayer", true, onSelected: () => { MoveToLayer(null, targets); }) new ContextMenuOption("editor.layer.nolayer", true, onSelected: () => { MoveToLayer(null, targets); })
@@ -3992,7 +3983,8 @@ namespace Barotrauma
availableLayers.AddRange(Layers.Select(layer => new ContextMenuOption(layer.Key, true, onSelected: () => { MoveToLayer(layer.Key, targets); }))); availableLayers.AddRange(Layers.Select(layer => new ContextMenuOption(layer.Key, true, onSelected: () => { MoveToLayer(layer.Key, targets); })));
List<ContextMenuOption> availableLayerOptions = new List<ContextMenuOption> 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.createlayer", isEnabled: hasTargets, onSelected: () => { CreateNewLayer(null, targets); }),
new ContextMenuOption("editor.layer.selectall", isEnabled: hasTargets, onSelected: () => new ContextMenuOption("editor.layer.selectall", isEnabled: hasTargets, onSelected: () =>
{ {
@@ -4006,7 +3998,7 @@ namespace Barotrauma
availableLayerOptions.AddRange(Layers.Select(layer => new ContextMenuOption(layer.Key, true, onSelected: () => { MoveToLayer(layer.Key, targets); }))); availableLayerOptions.AddRange(Layers.Select(layer => new ContextMenuOption(layer.Key, true, onSelected: () => { MoveToLayer(layer.Key, targets); })));
GUIContextMenu.CreateContextMenu( 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.cut", isEnabled: hasTargets, onSelected: () => MapEntity.Cut(targets)),
new ContextMenuOption("editor.copytoclipboard", isEnabled: hasTargets, onSelected: () => MapEntity.Copy(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))), new ContextMenuOption("editor.paste", isEnabled: MapEntity.CopiedList.Any(), onSelected: () => MapEntity.Paste(cam.ScreenToWorld(PlayerInput.MousePosition))),
@@ -4061,13 +4053,13 @@ namespace Barotrauma
MoveToLayer(name, content); MoveToLayer(name, content);
} }
Layers.Add(name, LayerData.Default); Layers.Add(name, new LayerData());
UpdateLayerPanel(); UpdateLayerPanel();
} }
private void RenameLayer(string original, string newName) 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)) foreach (MapEntity entity in MapEntity.MapEntityList.Where(entity => entity.Layer == original))
{ {
@@ -4076,7 +4068,7 @@ namespace Barotrauma
if (!string.IsNullOrWhiteSpace(newName)) if (!string.IsNullOrWhiteSpace(newName))
{ {
Layers.TryAdd(newName, LayerData.Default); Layers.TryAdd(newName, originalData);
} }
UpdateLayerPanel(); UpdateLayerPanel();
} }
@@ -4088,7 +4080,7 @@ namespace Barotrauma
{ {
if (!string.IsNullOrWhiteSpace(entity.Layer)) if (!string.IsNullOrWhiteSpace(entity.Layer))
{ {
Layers.TryAdd(entity.Layer, LayerData.Default); Layers.TryAdd(entity.Layer, new LayerData(!entity.IsLayerHidden));
} }
} }
UpdateLayerPanel(); UpdateLayerPanel();
@@ -4099,6 +4091,18 @@ namespace Barotrauma
Layers.Clear(); Layers.Clear();
UpdateLayerPanel(); 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) private void PasteAssembly(string text = null, Vector2? pos = null)
{ {
@@ -4492,39 +4496,39 @@ namespace Barotrauma
} }
/// <summary> /// <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> /// </summary>
/// <param name="itemContainer">The item we want to open</param> /// <param name="item">The item we want to open</param>
private void OpenItem(Item itemContainer) 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 // 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); TeleportDummyCharacter(oldItemPosition);
// Override this so we can be sure the container opens // 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; } if (container != null) { container.KeepOpenWhenEquipped = true; }
// We accept any slots except "Any" since that would take priority // We accept any slots except "Any" since that would take priority
List<InvSlotType> allowedSlots = new List<InvSlotType>(); List<InvSlotType> allowedSlots = new List<InvSlotType>();
itemContainer.AllowedSlots.ForEach(type => item.AllowedSlots.ForEach(type =>
{ {
if (type != InvSlotType.Any) { allowedSlots.Add(type); } if (type != InvSlotType.Any) { allowedSlots.Add(type); }
}); });
// Try to place the item in the dummy character's inventory // Try to place the item in the dummy character's inventory
bool success = dummyCharacter.Inventory.TryPutItem(itemContainer, dummyCharacter, allowedSlots); bool success = dummyCharacter.Inventory.TryPutItem(item, dummyCharacter, allowedSlots);
if (success) { OpenedItem = itemContainer; } if (success) { OpenedItem = item; }
else { return; } else { return; }
} }
MapEntity.SelectedList.Clear(); MapEntity.SelectedList.Clear();
MapEntity.FilteredSelectedList.Clear(); MapEntity.FilteredSelectedList.Clear();
MapEntity.SelectEntity(itemContainer); MapEntity.SelectEntity(item);
dummyCharacter.SelectedItem = itemContainer; dummyCharacter.SelectedItem = item;
FilterEntities(entityFilterBox.Text); FilterEntities(entityFilterBox.Text);
MapEntity.StopSelection(); MapEntity.StopSelection();
} }
@@ -5176,7 +5180,7 @@ namespace Barotrauma
}; };
new GUIButton(new RectTransform(new Vector2(0.6f, 1f), buttonHeaders.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes }; 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") GUIFrame parent = new GUIFrame(new RectTransform(new Vector2(1f, 0.1f), layerList.Content.RectTransform), style: "ListBoxElement")
{ {
@@ -5188,7 +5192,7 @@ namespace Barotrauma
GUILayoutGroup layerVisibilityLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.25f, 1f), layerGroup.RectTransform), childAnchor: Anchor.Center); 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) GUITickBox layerVisibleButton = new GUITickBox(new RectTransform(Vector2.One, layerVisibilityLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), string.Empty)
{ {
Selected = visibility == LayerVisibility.Visible, Selected = isVisible,
OnSelected = box => OnSelected = box =>
{ {
if (!Layers.TryGetValue(layer, out LayerData data)) if (!Layers.TryGetValue(layer, out LayerData data))
@@ -5196,12 +5200,15 @@ namespace Barotrauma
UpdateLayerPanel(); UpdateLayerPanel();
return false; return false;
} }
//hiding a layer automatically deselects it (can't edit a hidden layer)
if (!box.Selected && layerList.SelectedData as string == 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; return true;
} }
}; };
@@ -5209,7 +5216,7 @@ namespace Barotrauma
GUILayoutGroup layerChainLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.15f, 1f), layerGroup.RectTransform), childAnchor: Anchor.Center); 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) GUITickBox layerChainButton = new GUITickBox(new RectTransform(Vector2.One, layerChainLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), string.Empty)
{ {
Selected = linkage == LayerLinkage.Linked, Selected = isGrouped,
OnSelected = box => OnSelected = box =>
{ {
if (!Layers.TryGetValue(layer, out LayerData data)) if (!Layers.TryGetValue(layer, out LayerData data))
@@ -5218,7 +5225,7 @@ namespace Barotrauma
return false; return false;
} }
Layers[layer] = new LayerData(data.Visible, box.Selected ? LayerLinkage.Linked : LayerLinkage.Unlinked); Layers[layer] = data with { IsGrouped = box.Selected };
return true; return true;
} }
}; };
@@ -5249,7 +5256,6 @@ namespace Barotrauma
btn.ToolTip = originalBtnText; btn.ToolTip = originalBtnText;
} }
} }
} }
public void UpdateUndoHistoryPanel() public void UpdateUndoHistoryPanel()
@@ -6303,11 +6309,11 @@ namespace Barotrauma
if (!Layers.TryGetValue(entity.Layer, out LayerData data)) 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 true;
} }
return data.Visible == LayerVisibility.Visible; return data.IsVisible;
} }
public static bool IsLayerLinked(MapEntity entity) public static bool IsLayerLinked(MapEntity entity)
@@ -6316,11 +6322,11 @@ namespace Barotrauma
if (!Layers.TryGetValue(entity.Layer, out LayerData data)) 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 true;
} }
return data.Linkage == LayerLinkage.Linked; return data.IsGrouped;
} }
public static ImmutableHashSet<MapEntity> GetEntitiesInSameLayer(MapEntity entity) 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) 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)) : base(style, new RectTransform(Vector2.One, parent))
{ {
this.elementHeight = (int)(elementHeight * GUI.Scale); elementHeight = (int)(elementHeight * GUI.Scale);
var tickBoxStyle = GUIStyle.GetComponentStyle("GUITickBox"); var tickBoxStyle = GUIStyle.GetComponentStyle("GUITickBox");
var textBoxStyle = GUIStyle.GetComponentStyle("GUITextBox"); var textBoxStyle = GUIStyle.GetComponentStyle("GUITextBox");
var numberInputStyle = GUIStyle.GetComponentStyle("GUINumberInput"); var numberInputStyle = GUIStyle.GetComponentStyle("GUINumberInput");
@@ -343,7 +343,50 @@ namespace Barotrauma
Color = Color.Black 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 //scale the size of this component and the layout group to fit the children
Recalculate(); Recalculate();

View File

@@ -137,22 +137,10 @@ namespace Barotrauma.Sounds
{ {
for (int i = 0; i < length; i++) 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 int FillStreamBuffer(int samplePos, short[] buffer);
public abstract float GetAmplitudeAtPlaybackPos(int playbackPos); public abstract float GetAmplitudeAtPlaybackPos(int playbackPos);

View File

@@ -107,7 +107,7 @@ namespace Barotrauma.Sounds
float finalGain = gain * GameSettings.CurrentConfig.Audio.VoiceChatVolume * client.VoiceVolume; float finalGain = gain * GameSettings.CurrentConfig.Audio.VoiceChatVolume * client.VoiceVolume;
for (int i = 0; i < readSamples; i++) 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? 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); 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); currentLobby?.SetData("EosEndpoint", puids[0].Value);
} }
DebugConsole.Log("Lobby updated!"); DebugConsole.Log("Lobby updated!");
} }
private static void SetServerListInfo(Identifier key, object value) private static void SetServerListInfo(Identifier key, object value)
{ {
switch (value) switch (value)
@@ -115,7 +115,7 @@ namespace Barotrauma.Steam
.JoinEscaped(',')); .JoinEscaped(','));
return; return;
} }
currentLobby?.SetData(key.Value.ToLowerInvariant(), value.ToString()); currentLobby?.SetData(key.Value.ToLowerInvariant(), value.ToString());
} }

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>1.4.6.0</Version> <Version>1.5.9.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
@@ -64,12 +64,14 @@
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(Configuration)'!='Debug'"> <ItemGroup Condition="'$(Configuration)'!='Debug'">
<ProjectReference Include="..\..\Libraries\Concentus\CSharp\Concentus\Concentus.NetStandard.csproj" AdditionalProperties="Configuration=Release" />
<ProjectReference Include="..\..\Libraries\Facepunch.Steamworks\Facepunch.Steamworks.Posix.csproj" AdditionalProperties="Configuration=Release" /> <ProjectReference Include="..\..\Libraries\Facepunch.Steamworks\Facepunch.Steamworks.Posix.csproj" AdditionalProperties="Configuration=Release" />
<ProjectReference Include="..\..\Libraries\Farseer Physics Engine 3.5\Farseer.NetStandard.csproj" AdditionalProperties="Configuration=Release" /> <ProjectReference Include="..\..\Libraries\Farseer Physics Engine 3.5\Farseer.NetStandard.csproj" AdditionalProperties="Configuration=Release" />
<ProjectReference Include="..\..\Libraries\Hyper.ComponentModel\Hyper.ComponentModel.NetStandard.csproj" AdditionalProperties="Configuration=Release" /> <ProjectReference Include="..\..\Libraries\Hyper.ComponentModel\Hyper.ComponentModel.NetStandard.csproj" AdditionalProperties="Configuration=Release" />
<ProjectReference Include="..\..\Libraries\Lidgren.Network\Lidgren.NetStandard.csproj" AdditionalProperties="Configuration=Release" /> <ProjectReference Include="..\..\Libraries\Lidgren.Network\Lidgren.NetStandard.csproj" AdditionalProperties="Configuration=Release" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(Configuration)'=='Debug'"> <ItemGroup Condition="'$(Configuration)'=='Debug'">
<ProjectReference Include="..\..\Libraries\Concentus\CSharp\Concentus\Concentus.NetStandard.csproj" AdditionalProperties="Configuration=Debug" />
<ProjectReference Include="..\..\Libraries\Facepunch.Steamworks\Facepunch.Steamworks.Posix.csproj" AdditionalProperties="Configuration=Debug" /> <ProjectReference Include="..\..\Libraries\Facepunch.Steamworks\Facepunch.Steamworks.Posix.csproj" AdditionalProperties="Configuration=Debug" />
<ProjectReference Include="..\..\Libraries\Farseer Physics Engine 3.5\Farseer.NetStandard.csproj" AdditionalProperties="Configuration=Debug" /> <ProjectReference Include="..\..\Libraries\Farseer Physics Engine 3.5\Farseer.NetStandard.csproj" AdditionalProperties="Configuration=Debug" />
<ProjectReference Include="..\..\Libraries\Hyper.ComponentModel\Hyper.ComponentModel.NetStandard.csproj" AdditionalProperties="Configuration=Debug" /> <ProjectReference Include="..\..\Libraries\Hyper.ComponentModel\Hyper.ComponentModel.NetStandard.csproj" AdditionalProperties="Configuration=Debug" />

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>1.4.6.0</Version> <Version>1.5.9.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
@@ -70,12 +70,14 @@
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(Configuration)'!='Debug'"> <ItemGroup Condition="'$(Configuration)'!='Debug'">
<ProjectReference Include="..\..\Libraries\Concentus\CSharp\Concentus\Concentus.NetStandard.csproj" AdditionalProperties="Configuration=Release" />
<ProjectReference Include="..\..\Libraries\Facepunch.Steamworks\Facepunch.Steamworks.Posix.csproj" AdditionalProperties="Configuration=Release" /> <ProjectReference Include="..\..\Libraries\Facepunch.Steamworks\Facepunch.Steamworks.Posix.csproj" AdditionalProperties="Configuration=Release" />
<ProjectReference Include="..\..\Libraries\Farseer Physics Engine 3.5\Farseer.NetStandard.csproj" AdditionalProperties="Configuration=Release" /> <ProjectReference Include="..\..\Libraries\Farseer Physics Engine 3.5\Farseer.NetStandard.csproj" AdditionalProperties="Configuration=Release" />
<ProjectReference Include="..\..\Libraries\Hyper.ComponentModel\Hyper.ComponentModel.NetStandard.csproj" AdditionalProperties="Configuration=Release" /> <ProjectReference Include="..\..\Libraries\Hyper.ComponentModel\Hyper.ComponentModel.NetStandard.csproj" AdditionalProperties="Configuration=Release" />
<ProjectReference Include="..\..\Libraries\Lidgren.Network\Lidgren.NetStandard.csproj" AdditionalProperties="Configuration=Release" /> <ProjectReference Include="..\..\Libraries\Lidgren.Network\Lidgren.NetStandard.csproj" AdditionalProperties="Configuration=Release" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(Configuration)'=='Debug'"> <ItemGroup Condition="'$(Configuration)'=='Debug'">
<ProjectReference Include="..\..\Libraries\Concentus\CSharp\Concentus\Concentus.NetStandard.csproj" AdditionalProperties="Configuration=Debug" />
<ProjectReference Include="..\..\Libraries\Facepunch.Steamworks\Facepunch.Steamworks.Posix.csproj" AdditionalProperties="Configuration=Debug" /> <ProjectReference Include="..\..\Libraries\Facepunch.Steamworks\Facepunch.Steamworks.Posix.csproj" AdditionalProperties="Configuration=Debug" />
<ProjectReference Include="..\..\Libraries\Farseer Physics Engine 3.5\Farseer.NetStandard.csproj" AdditionalProperties="Configuration=Debug" /> <ProjectReference Include="..\..\Libraries\Farseer Physics Engine 3.5\Farseer.NetStandard.csproj" AdditionalProperties="Configuration=Debug" />
<ProjectReference Include="..\..\Libraries\Hyper.ComponentModel\Hyper.ComponentModel.NetStandard.csproj" AdditionalProperties="Configuration=Debug" /> <ProjectReference Include="..\..\Libraries\Hyper.ComponentModel\Hyper.ComponentModel.NetStandard.csproj" AdditionalProperties="Configuration=Debug" />

View File

@@ -1,4 +1,5 @@
using Barotrauma.Networking; using Barotrauma.Networking;
using System.Linq;
using System.Xml.Linq; using System.Xml.Linq;
namespace Barotrauma 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)) if (HasAbilityFlag(AbilityFlags.RetainExperienceForNewCharacter))
{ {
var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this); var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this);

View File

@@ -56,6 +56,7 @@ namespace Barotrauma
msg.WriteUInt16(ID); msg.WriteUInt16(ID);
msg.WriteString(Name); msg.WriteString(Name);
msg.WriteString(OriginalName); msg.WriteString(OriginalName);
msg.WriteBoolean(RenamingEnabled);
msg.WriteByte((byte)Head.Preset.TagSet.Count); msg.WriteByte((byte)Head.Preset.TagSet.Count);
foreach (Identifier tag in Head.Preset.TagSet) foreach (Identifier tag in Head.Preset.TagSet)
{ {
@@ -96,6 +97,7 @@ namespace Barotrauma
msg.WriteInt32(ExperiencePoints); msg.WriteInt32(ExperiencePoints);
msg.WriteRangedInteger(AdditionalTalentPoints, 0, MaxAdditionalTalentPoints); msg.WriteRangedInteger(AdditionalTalentPoints, 0, MaxAdditionalTalentPoints);
msg.WriteBoolean(PermanentlyDead);
} }
} }
} }

View File

@@ -459,6 +459,7 @@ namespace Barotrauma
Client owner = controlEventData.Owner; Client owner = controlEventData.Owner;
msg.WriteBoolean(owner == c && owner.Character == this); msg.WriteBoolean(owner == c && owner.Character == this);
msg.WriteByte(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.SessionId : (byte)0); msg.WriteByte(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.SessionId : (byte)0);
msg.WriteBoolean(info is { RenamingEnabled: true });
break; break;
case CharacterStatusEventData statusEventData: case CharacterStatusEventData statusEventData:
WriteStatus(msg, statusEventData.ForceAfflictionData); WriteStatus(msg, statusEventData.ForceAfflictionData);

View File

@@ -963,7 +963,7 @@ namespace Barotrauma
} }
client.Muted = true; client.Muted = true;
GameMain.Server.SendDirectChatMessage(TextManager.Get("MutedByServer").Value, client, ChatMessageType.MessageBox); GameMain.Server.SendDirectChatMessage(TextManager.Get("MutedByServer").Value, client, ChatMessageType.MessageBox);
}, },
() => () =>
{ {
if (GameMain.Server == null) return null; if (GameMain.Server == null) return null;
@@ -1599,6 +1599,19 @@ namespace Barotrauma
NewMessage("Disabled RequireClientNameMatch"); 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 #endif
AssignOnClientRequestExecute( AssignOnClientRequestExecute(
@@ -1761,11 +1774,7 @@ namespace Barotrauma
"teleportcharacter|teleport", "teleportcharacter|teleport",
(Client client, Vector2 cursorWorldPos, string[] args) => (Client client, Vector2 cursorWorldPos, string[] args) =>
{ {
Character tpCharacter = (args.Length == 0) ? client.Character : FindMatchingCharacter(args, false); TeleportCharacter(cursorWorldPos, client.Character, args);
if (tpCharacter != null)
{
tpCharacter.TeleportTo(cursorWorldPos);
}
} }
); );
@@ -1922,6 +1931,17 @@ namespace Barotrauma
foreach (Client c in GameMain.Server.ConnectedClients) foreach (Client c in GameMain.Server.ConnectedClients)
{ {
if (c.Character != revivedCharacter) { continue; } 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 //clients stop controlling the character when it dies, force control back
GameMain.Server.SetClientCharacter(c, revivedCharacter); GameMain.Server.SetClientCharacter(c, revivedCharacter);
@@ -2545,6 +2565,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) => commands.Add(new Command("readycheck", "Commence a ready check.", (string[] args) =>
{ {
if (Screen.Selected == GameMain.GameScreen && GameMain.NetworkMember != null) if (Screen.Selected == GameMain.GameScreen && GameMain.NetworkMember != null)
@@ -2607,7 +2712,7 @@ namespace Barotrauma
})); }));
#endif #endif
} }
public static void ServerRead(IReadMessage inc, Client sender) public static void ServerRead(IReadMessage inc, Client sender)
{ {
string consoleCommand = inc.ReadString(); string consoleCommand = inc.ReadString();

View File

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

View File

@@ -123,6 +123,12 @@ namespace Barotrauma
public bool IsDuplicate(CharacterCampaignData other) 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; return AccountId == other.AccountId && other.ClientAddress == ClientAddress;
} }
@@ -133,6 +139,13 @@ namespace Barotrauma
WalletData = null; 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) public void SpawnInventoryItems(Character character, Inventory inventory)
{ {
if (character == null) if (character == null)
@@ -158,7 +171,7 @@ namespace Barotrauma
public void ApplyWalletData(Character character) 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() public XElement Save()
@@ -167,7 +180,6 @@ namespace Barotrauma
new XAttribute("name", Name), new XAttribute("name", Name),
new XAttribute("address", ClientAddress), new XAttribute("address", ClientAddress),
new XAttribute("accountid", AccountId.TryUnwrap(out var accountId) ? accountId.StringRepresentation : "")); new XAttribute("accountid", AccountId.TryUnwrap(out var accountId) ? accountId.StringRepresentation : ""));
CharacterInfo?.Save(element); CharacterInfo?.Save(element);
if (itemData != null) { element.Add(itemData); } if (itemData != null) { element.Add(itemData); }
if (healthData != null) { element.Add(healthData); } if (healthData != null) { element.Add(healthData); }

View File

@@ -16,6 +16,12 @@ namespace Barotrauma
private readonly HashSet<NetWalletTransaction> transactions = new HashSet<NetWalletTransaction>(); private readonly HashSet<NetWalletTransaction> transactions = new HashSet<NetWalletTransaction>();
private const float clientCheckInterval = 10; private const float clientCheckInterval = 10;
private float clientCheckTimer = clientCheckInterval; 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) public override Wallet GetWallet(Client client = null)
{ {
@@ -295,6 +301,12 @@ namespace Barotrauma
MoveDiscardedCharacterBalancesToBank(); MoveDiscardedCharacterBalancesToBank();
characterData.ForEach(cd => cd.HasSpawned = false); 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(); SavePets();
@@ -380,6 +392,9 @@ namespace Barotrauma
GameMain.GameSession.EventManager.RegisterEventHistory(); 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); 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; ForceMapUI = false;
@@ -513,6 +528,9 @@ namespace Barotrauma
Map?.Radiation?.UpdateRadiation(deltaTime); Map?.Radiation?.UpdateRadiation(deltaTime);
base.Update(deltaTime); base.Update(deltaTime);
MedicalClinic?.Update(deltaTime);
if (Level.Loaded != null) if (Level.Loaded != null)
{ {
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection) if (Level.Loaded.Type == LevelData.LevelType.LocationConnection)
@@ -1165,51 +1183,73 @@ namespace Barotrauma
if (!AllowedToManageWallets(sender)) { return; } if (!AllowedToManageWallets(sender)) { return; }
Character targetCharacter = Character.CharacterList.FirstOrDefault(c => c.ID == update.Target); if (update.Target.TryUnwrap(out ushort id))
targetCharacter?.Wallet.SetRewardDistribution(update.NewRewardDistribution); {
GameServer.Log($"{sender.Name} changed the salary of {targetCharacter?.Name ?? "the bank"} to {update.NewRewardDistribution}%.", ServerLog.MessageType.Money); 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) public void ServerReadCrew(IReadMessage msg, Client sender)
{ {
int[] pendingHires = null; UInt16[] pendingHires = null;
bool updatePending = msg.ReadBoolean(); bool updatePending = msg.ReadBoolean();
if (updatePending) if (updatePending)
{ {
ushort pendingHireLength = msg.ReadUInt16(); ushort pendingHireLength = msg.ReadUInt16();
pendingHires = new int[pendingHireLength]; pendingHires = new UInt16[pendingHireLength];
for (int i = 0; i < pendingHireLength; i++) for (int i = 0; i < pendingHireLength; i++)
{ {
pendingHires[i] = msg.ReadInt32(); pendingHires[i] = msg.ReadUInt16();
} }
} }
bool validateHires = msg.ReadBoolean(); bool validateHires = msg.ReadBoolean();
bool renameCharacter = msg.ReadBoolean(); bool renameCharacter = msg.ReadBoolean();
int renamedIdentifier = -1; UInt16 renamedIdentifier = 0;
string newName = null; string newName = null;
bool existingCrewMember = false; bool existingCrewMember = false;
if (renameCharacter) if (renameCharacter)
{ {
renamedIdentifier = msg.ReadInt32(); renamedIdentifier = msg.ReadUInt16();
newName = msg.ReadString(); newName = msg.ReadString();
existingCrewMember = msg.ReadBoolean(); existingCrewMember = msg.ReadBoolean();
if (!GameMain.Server.IsNameValid(sender, newName))
{
renameCharacter = false;
}
} }
bool fireCharacter = msg.ReadBoolean(); bool fireCharacter = msg.ReadBoolean();
int firedIdentifier = -1; int firedIdentifier = -1;
if (fireCharacter) { firedIdentifier = msg.ReadInt32(); } if (fireCharacter) { firedIdentifier = msg.ReadUInt16(); }
Location location = map?.CurrentLocation; Location location = map?.CurrentLocation;
CharacterInfo firedCharacter = null; CharacterInfo firedCharacter = null;
(ushort id, string newName) appliedRename = (Entity.NullEntityID, string.Empty);
if (location != null && AllowedToManageCampaign(sender, ClientPermissions.ManageHires)) if (location != null)
{ {
if (fireCharacter) if (fireCharacter && AllowedToManageCampaign(sender, ClientPermissions.ManageHires))
{ {
firedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.GetIdentifier() == firedIdentifier); firedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.ID == firedIdentifier);
if (firedCharacter != null && (firedCharacter.Character?.IsBot ?? true)) if (firedCharacter != null && (firedCharacter.Character?.IsBot ?? true))
{ {
CrewManager.FireCharacter(firedCharacter); CrewManager.FireCharacter(firedCharacter);
@@ -1223,29 +1263,45 @@ namespace Barotrauma
if (renameCharacter) if (renameCharacter)
{ {
CharacterInfo characterInfo = null; CharacterInfo characterInfo = null;
if (existingCrewMember && CrewManager != null) if (AllowedToManageCampaign(sender, ClientPermissions.ManageHires))
{ {
characterInfo = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.GetIdentifierUsingOriginalName() == renamedIdentifier); if (existingCrewMember && CrewManager != null)
{
characterInfo = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.ID == renamedIdentifier);
}
else if (!existingCrewMember && location.HireManager != null)
{
characterInfo = location.HireManager.AvailableCharacters.FirstOrDefault(info => info.ID == renamedIdentifier);
}
} }
else if(!existingCrewMember && location.HireManager != null) if (characterInfo == null && renamedIdentifier == sender.CharacterInfo?.ID)
{ {
characterInfo = location.HireManager.AvailableCharacters.FirstOrDefault(info => info.GetIdentifierUsingOriginalName() == renamedIdentifier); characterInfo = sender.CharacterInfo;
} }
if (characterInfo != null &&
if (characterInfo != null && (characterInfo.Character?.IsBot ?? true)) (characterInfo.Character == null || characterInfo.Character is { IsBot: true } || (characterInfo.RenamingEnabled && characterInfo == sender.CharacterInfo)))
{ {
GameServer.Log($"{sender.Name} renamed the character \"{characterInfo.Name}\" as \"{newName}\".", ServerLog.MessageType.ServerMessage);
if (existingCrewMember) if (existingCrewMember)
{ {
CrewManager.RenameCharacter(characterInfo, newName); CrewManager.RenameCharacter(characterInfo, newName);
if (characterInfo == sender.CharacterInfo)
{
//renaming is only allowed once
characterInfo.RenamingEnabled = false;
}
} }
else else
{ {
location.HireManager.RenameCharacter(characterInfo, newName); location.HireManager.RenameCharacter(characterInfo, newName);
} }
appliedRename = (characterInfo.ID, newName);
} }
else else
{ {
DebugConsole.ThrowError($"Tried to rename an invalid character ({renamedIdentifier})"); string errorMsg = $"Tried to rename an invalid character ({renamedIdentifier}, {characterInfo?.Name ?? "null"})";
DebugConsole.ThrowError(errorMsg);
GameMain.Server?.SendConsoleMessage(errorMsg, sender, Color.Red);
} }
} }
@@ -1262,9 +1318,9 @@ namespace Barotrauma
if (updatePending) if (updatePending)
{ {
List<CharacterInfo> pendingHireInfos = new List<CharacterInfo>(); 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) if (match == null)
{ {
DebugConsole.ThrowError($"Tried to add a character that doesn't exist ({identifier}) to pending hires"); DebugConsole.ThrowError($"Tried to add a character that doesn't exist ({identifier}) to pending hires");
@@ -1293,7 +1349,7 @@ namespace Barotrauma
// bounce back // bounce back
if (renameCharacter && existingCrewMember) if (renameCharacter && existingCrewMember)
{ {
SendCrewState((renamedIdentifier, newName), firedCharacter); SendCrewState(appliedRename, firedCharacter);
} }
else else
{ {
@@ -1311,7 +1367,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 /// 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. /// their available hires are different from the server.
/// </remarks> /// </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> availableHires = new List<CharacterInfo>();
List<CharacterInfo> pendingHires = new List<CharacterInfo>(); List<CharacterInfo> pendingHires = new List<CharacterInfo>();
@@ -1327,6 +1383,8 @@ namespace Barotrauma
IWriteMessage msg = new WriteOnlyMessage(); IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ServerPacketHeader.CREW); msg.WriteByte((byte)ServerPacketHeader.CREW);
msg.WriteBoolean(createNotification);
msg.WriteUInt16((ushort)availableHires.Count); msg.WriteUInt16((ushort)availableHires.Count);
foreach (CharacterInfo hire in availableHires) foreach (CharacterInfo hire in availableHires)
{ {
@@ -1337,7 +1395,7 @@ namespace Barotrauma
msg.WriteUInt16((ushort)pendingHires.Count); msg.WriteUInt16((ushort)pendingHires.Count);
foreach (CharacterInfo pendingHire in pendingHires) foreach (CharacterInfo pendingHire in pendingHires)
{ {
msg.WriteInt32(pendingHire.GetIdentifierUsingOriginalName()); msg.WriteUInt16(pendingHire.ID);
} }
var hiredCharacters = CrewManager.GetCharacterInfos().Where(ci => ci.IsNewHire); var hiredCharacters = CrewManager.GetCharacterInfos().Where(ci => ci.IsNewHire);
@@ -1348,16 +1406,16 @@ namespace Barotrauma
msg.WriteInt32(info.Salary); 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); msg.WriteBoolean(validRenaming);
if (validRenaming) if (validRenaming)
{ {
msg.WriteInt32(renamedCrewMember.id); msg.WriteUInt16(renamedCrewMember.id);
msg.WriteString(renamedCrewMember.newName); msg.WriteString(renamedCrewMember.newName);
} }
msg.WriteBoolean(firedCharacter != null); 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); GameMain.Server.ServerPeer.Send(msg, client.Connection, DeliveryMethod.Reliable);
} }
@@ -1369,6 +1427,8 @@ namespace Barotrauma
//(can happen e.g. if someone starts a vote to buy something and then disconnects) //(can happen e.g. if someone starts a vote to buy something and then disconnects)
if (client != null && !GameMain.Server.ConnectedClients.Contains(client)) { return false; } if (client != null && !GameMain.Server.ConnectedClients.Contains(client)) { return false; }
if (price == 0) { return true; }
Wallet wallet = GetWallet(client); Wallet wallet = GetWallet(client);
if (!AllowedToManageWallets(client)) if (!AllowedToManageWallets(client))
{ {
@@ -1475,5 +1535,57 @@ namespace Barotrauma
lastSaveID++; lastSaveID++;
DebugConsole.Log("Campaign saved, save ID " + 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;
using System.Collections.Generic; using System.Collections.Generic;
@@ -22,6 +22,36 @@ namespace Barotrauma
private readonly List<AfflictionSubscriber> afflictionSubscribers = new(); 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) public void ServerRead(IReadMessage inc, Client sender)
{ {
NetworkHeader header = (NetworkHeader)inc.ReadByte(); NetworkHeader header = (NetworkHeader)inc.ReadByte();
@@ -141,7 +171,7 @@ namespace Barotrauma
if (foundInfo is { Character.CharacterHealth: { } health }) if (foundInfo is { Character.CharacterHealth: { } health })
{ {
pendingAfflictions = GetAllAfflictions(health); pendingAfflictions = GetAllAfflictions(health);
infoId = foundInfo.GetIdentifierUsingOriginalName(); infoId = foundInfo.ID;
} }
INetSerializableStruct writeCrewMember = new NetCrewMember INetSerializableStruct writeCrewMember = new NetCrewMember

View File

@@ -18,10 +18,13 @@ namespace Barotrauma.Items.Components
private bool needsServerInitialization; private bool needsServerInitialization;
/// <summary> /// <summary>
/// When in multiplayer and the circuit box is loaded from the players inventory, /// When in multiplayer and the circuit box are loaded from the player inventory,
/// We only load the components from XML on server side since only the server has access to CharacterCampaignData /// We only load the components from XML on the server side
/// and then send a network event syncing the loaded properties. But circuit box properties are too complex to /// since only the server has access to CharacterCampaignData
/// sync using the existing syncing logic so we instead send the state using <see cref="CircuitBoxInitializeStateFromServerEvent"/>. /// 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> /// </summary>
public void MarkServerRequiredInitialization() public void MarkServerRequiredInitialization()
=> needsServerInitialization = true; => needsServerInitialization = true;
@@ -280,6 +283,15 @@ namespace Barotrauma.Items.Components
CreateServerEvent(data with { Size = Vector2.Max(data.Size, CircuitBoxLabelNode.MinSize) }); CreateServerEvent(data with { Size = Vector2.Max(data.Size, CircuitBoxLabelNode.MinSize) });
break; break;
} }
case CircuitBoxOpcode.RenameConnections:
{
var data = INetSerializableStruct.Read<CircuitBoxRenameConnectionLabelsEvent>(msg);
if (!CanAccessAndUnlocked(c)) { break; }
RenameConnectionLabelsInternal(data.Type, data.Override.ToDictionary());
CreateServerEvent(data);
break;
}
default: default:
throw new ArgumentOutOfRangeException(nameof(header), header, "This opcode cannot be handled using entity events"); 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(), Components: Components.Select(EventFromComponent).ToImmutableArray(),
Wires: Wires.Select(EventFromWire).ToImmutableArray(), Wires: Wires.Select(EventFromWire).ToImmutableArray(),
Labels: Labels.Select(EventFromLabel).ToImmutableArray(), Labels: Labels.Select(EventFromLabel).ToImmutableArray(),
LabelOverrides: InputOutputNodes.Select(EventFromLabelOverride).ToImmutableArray(),
InputPos: inputPos, InputPos: inputPos,
OutputPos: outputPos); OutputPos: outputPos);
@@ -347,6 +360,9 @@ namespace Barotrauma.Items.Components
static CircuitBoxServerAddLabelEvent EventFromLabel(CircuitBoxLabelNode label) static CircuitBoxServerAddLabelEvent EventFromLabel(CircuitBoxLabelNode label)
=> new(label.ID, label.Position, label.Size, label.Color, label.HeaderText, label.BodyText); => 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 // 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 //existing wire not in the list of new wires -> disconnect it
if (!wires[i].Contains(existingWire)) 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 //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 " + 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 //go through new wires
for (int i = 0; i < Connections.Count; i++) 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) 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>[]>(); 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]; 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); SharedRead(msg, receivedItemIdsFromClient, out bool readyToApply);
if (!readyToApply) { return; } if (!readyToApply) { return; }
if (c == null || c.Character == null) { return; } if (sender == null || sender.Character == null) { return; }
bool accessible = c.Character.CanAccessInventory(this); if (!IsInventoryAccessible())
if (this is CharacterInventory characterInventory && accessible)
{ {
if (Owner == null || Owner is not Character ownerCharacter) CreateCorrectiveNetworkEvent();
{
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();
}
}
}
return; 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<Item> prevItems = new List<Item>(AllItems.Distinct());
List<Inventory> prevItemInventories = new List<Inventory>() { this }; 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 //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,
Item itemInventory => itemInventory.FindParentInventory(inventory => inventory is CharacterInventory) as CharacterInventory, //you would fail to pick up the item because it gets unequipped before checking whether you can access the cabinet.
Character character => character.Inventory, var itemAccessibility = GetItemAccessibility();
_ => null
}; HandleRemovedItems();
if (previousCharacterInventory != null && previousCharacterInventory != c.Character?.Inventory) HandleAddedItems();
{
GameMain.Server?.KarmaManager.OnItemTakenFromPlayer(previousCharacterInventory, c, droppedItem);
}
if (droppedItem.body != null && prevOwner != null)
{
droppedItem.body.SetTransform(prevOwner.SimPosition, 0.0f);
}
}
}
foreach (ushort id in receivedItemIdsFromClient[i]) EnsureItemsInBothHands(sender.Character);
{
Item newItem = id == 0 ? null : Entity.FindEntityByID(id) as Item;
prevItemInventories.Add(newItem?.ParentInventory);
}
}
for (int i = 0; i < capacity; i++) receivedItemIds.Remove(sender);
{
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);
CreateNetworkEvent(); CreateNetworkEvent();
foreach (Inventory prevInventory in prevItemInventories.Distinct()) foreach (Inventory prevInventory in prevItemInventories.Distinct())
@@ -165,43 +54,211 @@ namespace Barotrauma
if (prevInventory != this) { prevInventory?.CreateNetworkEvent(); } 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; } // create a network event to correct the client's inventory state.
if (!prevItems.Contains(item)) // 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)); foreach (ushort itemId in receivedItemIdsFromClient[i])
string amountText = amount > 1 ? $"x{amount} " : string.Empty;
if (Owner == c.Character)
{ {
HumanAIController.ItemTaken(item, c.Character); if (Entity.FindEntityByID(itemId) is not Item item) { continue; }
GameServer.Log($"{GameServer.CharacterLogName(c.Character)} picked up {amountText}{item.Name}", ServerLog.MessageType.Inventory); 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 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);
} }
} }
#endregion
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);
}
} }
private void EnsureItemsInBothHands(Character character) private void EnsureItemsInBothHands(Character character)
{ {
if (this is not CharacterInventory charInv) { return; } if (this is not CharacterInventory charInv) { return; }

View File

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

View File

@@ -1,4 +1,5 @@
using System; using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -8,6 +9,8 @@ namespace Barotrauma.Networking
{ {
public bool VoiceEnabled = true; public bool VoiceEnabled = true;
public VoipServerDecoder VoipServerDecoder;
public UInt16 LastRecvClientListUpdate public UInt16 LastRecvClientListUpdate
= NetIdUtils.GetIdOlderThan(GameMain.Server.LastClientListUpdateID); = NetIdUtils.GetIdOlderThan(GameMain.Server.LastClientListUpdateID);
@@ -15,7 +18,7 @@ namespace Barotrauma.Networking
= NetIdUtils.GetIdOlderThan(GameMain.Server.ServerSettings.LastUpdateIdForFlag[ServerSettings.NetFlags.Properties]); = NetIdUtils.GetIdOlderThan(GameMain.Server.ServerSettings.LastUpdateIdForFlag[ServerSettings.NetFlags.Properties]);
public UInt16 LastRecvServerSettingsUpdate public UInt16 LastRecvServerSettingsUpdate
= NetIdUtils.GetIdOlderThan(GameMain.Server.ServerSettings.LastUpdateIdForFlag[ServerSettings.NetFlags.Properties]); = NetIdUtils.GetIdOlderThan(GameMain.Server.ServerSettings.LastUpdateIdForFlag[ServerSettings.NetFlags.Properties]);
public UInt16 LastRecvLobbyUpdate public UInt16 LastRecvLobbyUpdate
= NetIdUtils.GetIdOlderThan(GameMain.NetLobbyScreen.LastUpdateID); = NetIdUtils.GetIdOlderThan(GameMain.NetLobbyScreen.LastUpdateID);
@@ -129,6 +132,7 @@ namespace Barotrauma.Networking
JobPreferences = new List<JobVariant>(); JobPreferences = new List<JobVariant>();
VoipQueue = new VoipQueue(SessionId, true, true); VoipQueue = new VoipQueue(SessionId, true, true);
VoipServerDecoder = new VoipServerDecoder(VoipQueue, this);
GameMain.Server.VoipServer.RegisterQueue(VoipQueue); GameMain.Server.VoipServer.RegisterQueue(VoipQueue);
//initialize to infinity, gets set to a proper value when initializing midround syncing //initialize to infinity, gets set to a proper value when initializing midround syncing
@@ -277,5 +281,56 @@ namespace Barotrauma.Networking
{ {
return Permissions.HasFlag(permission); 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;
}
if (botCharacter.Info != null)
{
botCharacter.Info.RenamingEnabled = true; // Grant one opportunity to rename a taken over bot
}
// Now that the old permanently killed character will be replaced, we can fully discard it
var mpCampaign = GameMain.GameSession?.Campaign as MultiPlayerCampaign;
mpCampaign?.DiscardClientCharacterData(this);
GameMain.Server.SetClientCharacter(this, botCharacter);
if (mpCampaign?.SetClientCharacterData(this) is CharacterCampaignData characterData)
{
//the bot has spawned, but the new CharacterCampaignData technically hasn't, because we just created it
characterData.HasSpawned = true;
}
SpectateOnly = false;
return true;
}
} }
} }

View File

@@ -58,7 +58,10 @@ namespace Barotrauma.Networking
private bool wasReadyToStartAutomatically; private bool wasReadyToStartAutomatically;
private bool autoRestartTimerRunning; 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> /// <summary>
/// Chat messages that get sent to the owner of the server when the owner is determined /// Chat messages that get sent to the owner of the server when the owner is determined
@@ -354,6 +357,10 @@ namespace Barotrauma.Networking
if (ServerSettings.VoiceChatEnabled) if (ServerSettings.VoiceChatEnabled)
{ {
VoipServer.SendToClients(connectedClients); VoipServer.SendToClients(connectedClients);
foreach (var c in connectedClients)
{
c.VoipServerDecoder.DebugUpdate(deltaTime);
}
} }
if (GameStarted) if (GameStarted)
@@ -361,6 +368,7 @@ namespace Barotrauma.Networking
RespawnManager?.Update(deltaTime); RespawnManager?.Update(deltaTime);
entityEventManager.Update(connectedClients); entityEventManager.Update(connectedClients);
bool permadeathMode = ServerSettings.RespawnMode == RespawnMode.Permadeath;
//go through the characters backwards to give rejoining clients control of the latest created character //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--) for (int i = Character.CharacterList.Count - 1; i >= 0; i--)
@@ -371,13 +379,15 @@ namespace Barotrauma.Networking
Client owner = connectedClients.Find(c => (c.Character == null || c.Character == character) && character.IsClientOwner(c)); Client owner = connectedClients.Find(c => (c.Character == null || c.Character == character) && character.IsClientOwner(c));
bool canOwnerTakeControl = bool canOwnerTakeControl =
owner != null && owner.InGame && !owner.NeedsMidRoundSync && 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 (!character.IsDead)
{ {
character.KillDisconnectedTimer += deltaTime; character.KillDisconnectedTimer += deltaTime;
character.SetStun(1.0f); 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); character.Kill(CauseOfDeathType.Disconnected, null);
continue; continue;
@@ -402,8 +412,10 @@ namespace Barotrauma.Networking
Voting.Update(deltaTime); Voting.Update(deltaTime);
bool isCrewDead = bool isCrewDown =
connectedClients.All(c => !c.UsingFreeCam && (c.Character == null || c.Character.IsDead || c.Character.IsIncapacitated)); 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; bool subAtLevelEnd = false;
if (Submarine.MainSub != null && GameMain.GameSession.GameMode is not PvPMode) if (Submarine.MainSub != null && GameMain.GameSession.GameMode is not PvPMode)
@@ -432,45 +444,58 @@ namespace Barotrauma.Networking
} }
} }
float endRoundDelay = 1.0f; EndRoundDelay = 1.0f;
if (ServerSettings.AutoRestart && isCrewDead) if (permadeathMode && isCrewDown)
{ {
endRoundDelay = 5.0f; if (EndRoundTimer <= 0.0f)
endRoundTimer += deltaTime; {
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) else if (subAtLevelEnd && GameMain.GameSession?.GameMode is not CampaignMode)
{ {
endRoundDelay = 5.0f; EndRoundDelay = 5.0f;
endRoundTimer += deltaTime; EndRoundTimer += deltaTime;
} }
else if (isCrewDead && (RespawnManager == null || !RespawnManager.CanRespawnAgain)) else if (isCrewDown && (RespawnManager == null || !RespawnManager.CanRespawnAgain))
{ {
#if !DEBUG #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; EndRoundDelay = 120.0f;
endRoundTimer += deltaTime; EndRoundTimer += deltaTime;
#endif #endif
} }
else if (isCrewDead && (GameMain.GameSession?.GameMode is CampaignMode)) else if (isCrewDown && (GameMain.GameSession?.GameMode is CampaignMode))
{ {
#if !DEBUG #if !DEBUG
endRoundDelay = 2.0f; EndRoundDelay = isSomeoneIncapacitatedNotDead ? 120.0f : 2.0f;
endRoundTimer += deltaTime; EndRoundTimer += deltaTime;
#endif #endif
} }
else 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) else if (subAtLevelEnd)
{ {
@@ -478,11 +503,11 @@ namespace Barotrauma.Networking
} }
else if (RespawnManager == null) 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 else
{ {
Log("Ending round (no living players left)", ServerLog.MessageType.ServerMessage); Log("Ending round (no players left standing)", ServerLog.MessageType.ServerMessage);
} }
EndGame(wasSaved: false); EndGame(wasSaved: false);
return; return;
@@ -824,7 +849,7 @@ namespace Barotrauma.Networking
#endif #endif
return; return;
} }
connectedClient.VoipQueue.Read(inc); VoipServer.Read(inc, connectedClient);
} }
break; break;
case ClientPacketHeader.SERVER_SETTINGS: case ClientPacketHeader.SERVER_SETTINGS:
@@ -842,6 +867,9 @@ namespace Barotrauma.Networking
case ClientPacketHeader.REWARD_DISTRIBUTION: case ClientPacketHeader.REWARD_DISTRIBUTION:
ReadRewardDistributionMessage(inc, connectedClient); ReadRewardDistributionMessage(inc, connectedClient);
break; break;
case ClientPacketHeader.RESET_REWARD_DISTRIBUTION:
ResetRewardDistribution(connectedClient);
break;
case ClientPacketHeader.MEDICAL: case ClientPacketHeader.MEDICAL:
ReadMedicalMessage(inc, connectedClient); ReadMedicalMessage(inc, connectedClient);
break; break;
@@ -854,6 +882,9 @@ namespace Barotrauma.Networking
case ClientPacketHeader.READY_TO_SPAWN: case ClientPacketHeader.READY_TO_SPAWN:
ReadReadyToSpawnMessage(inc, connectedClient); ReadReadyToSpawnMessage(inc, connectedClient);
break; break;
case ClientPacketHeader.TAKEOVERBOT:
ReadTakeOverBotMessage(inc, connectedClient);
break;
case ClientPacketHeader.FILE_REQUEST: case ClientPacketHeader.FILE_REQUEST:
if (ServerSettings.AllowFileTransfers) if (ServerSettings.AllowFileTransfers)
{ {
@@ -1307,6 +1338,14 @@ namespace Barotrauma.Networking
mpCampaign.ServerReadRewardDistribution(inc, sender); 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) private void ReadMedicalMessage(IReadMessage inc, Client sender)
{ {
@@ -1342,6 +1381,91 @@ 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 (ServerSettings.ReplaceCostPercentage <= 0 ||
CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMoney) ||
CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageHires))
{
if (campaign.TryHireCharacter(campaign.CurrentLocation, hireableCharacter, takeMoney: true, sender, buyingNewCharacter: true))
{
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
{
SendConsoleMessage($"Could not hire the bot {hireableCharacter.Name}. No permission to manage money or hires.", sender, Color.Red);
DebugConsole.ThrowError($"Client {sender.Name} failed to hire the bot {hireableCharacter.Name}. No permission to manage money or hires.");
}
}
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;
}
// No longer show the hired character in the HR list of current hires
campaign.CrewManager.RemoveCharacterInfo(botInfo);
newCharacter.TeamID = CharacterTeamType.Team1;
campaign.CrewManager.InitializeCharacter(newCharacter, mainSubSpawnpoint, spawnWaypoint);
client.TryTakeOverBot(newCharacter);
});
}
private void ClientReadServerCommand(IReadMessage inc) private void ClientReadServerCommand(IReadMessage inc)
{ {
Client sender = ConnectedClients.Find(x => x.Connection == inc.Sender); Client sender = ConnectedClients.Find(x => x.Connection == inc.Sender);
@@ -1450,9 +1574,8 @@ namespace Barotrauma.Networking
if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save) if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save)
{ {
mpCampaign.SavePlayers(); mpCampaign.SavePlayers();
mpCampaign.HandleSaveAndQuit();
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine);
mpCampaign.UpdateStoreStock();
GameMain.GameSession?.EventManager?.RegisterEventHistory(registerFinishedOnly: true);
SaveUtil.SaveGame(GameMain.GameSession.SavePath); SaveUtil.SaveGame(GameMain.GameSession.SavePath);
} }
else else
@@ -1686,6 +1809,8 @@ namespace Barotrauma.Networking
outmsg.WriteBoolean(GameStarted); outmsg.WriteBoolean(GameStarted);
outmsg.WriteBoolean(ServerSettings.AllowSpectating); outmsg.WriteBoolean(ServerSettings.AllowSpectating);
outmsg.WriteBoolean(ServerSettings.RespawnMode == RespawnMode.Permadeath);
outmsg.WriteBoolean(ServerSettings.IronmanMode);
c.WritePermissions(outmsg); c.WritePermissions(outmsg);
} }
@@ -1776,6 +1901,7 @@ namespace Barotrauma.Networking
IWriteMessage outmsg = new WriteOnlyMessage(); IWriteMessage outmsg = new WriteOnlyMessage();
outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_INGAME); outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_INGAME);
outmsg.WriteSingle((float)NetTime.Now); outmsg.WriteSingle((float)NetTime.Now);
outmsg.WriteSingle(EndRoundTimeRemaining);
using (var segmentTable = SegmentTableWriter<ServerNetSegment>.StartWriting(outmsg)) using (var segmentTable = SegmentTableWriter<ServerNetSegment>.StartWriting(outmsg))
{ {
@@ -1858,7 +1984,8 @@ namespace Barotrauma.Networking
{ {
outmsg = new WriteOnlyMessage(); outmsg = new WriteOnlyMessage();
outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_INGAME); 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)) using (var segmentTable = SegmentTableWriter<ServerNetSegment>.StartWriting(outmsg))
{ {
@@ -2291,7 +2418,7 @@ namespace Barotrauma.Networking
} }
SendStartMessage(roundStartSeed, campaign.NextLevel.Seed, GameMain.GameSession, connectedClients, includesFinalize: false); SendStartMessage(roundStartSeed, campaign.NextLevel.Seed, GameMain.GameSession, connectedClients, includesFinalize: false);
GameMain.GameSession.StartRound(campaign.NextLevel, mirrorLevel: campaign.MirrorLevel); GameMain.GameSession.StartRound(campaign.NextLevel, startOutpost: campaign.GetPredefinedStartOutpost(), mirrorLevel: campaign.MirrorLevel);
SubmarineSwitchLoad = false; SubmarineSwitchLoad = false;
campaign.AssignClientCharacterInfos(connectedClients); campaign.AssignClientCharacterInfos(connectedClients);
Log("Game mode: " + selectedMode.Name.Value, ServerLog.MessageType.ServerMessage); Log("Game mode: " + selectedMode.Name.Value, ServerLog.MessageType.ServerMessage);
@@ -2320,17 +2447,18 @@ namespace Barotrauma.Networking
yield return CoroutineStatus.Failure; 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; 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); RespawnManager = new RespawnManager(this, ServerSettings.UseRespawnShuttle && !isOutpost ? selectedShuttle : null);
} }
if (campaign != null) if (campaign != null)
{ {
campaign.CargoManager.CreatePurchasedItems(); 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(); Level.Loaded?.SpawnNPCs();
@@ -2371,6 +2499,8 @@ namespace Barotrauma.Networking
} }
//always allow the server owner to spectate even if it's disallowed in server settings //always allow the server owner to spectate even if it's disallowed in server settings
teamClients.RemoveAll(c => c.Connection == OwnerConnection && c.SpectateOnly); 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; } //if (!teamClients.Any() && n > 0) { continue; }
@@ -2439,6 +2569,7 @@ namespace Barotrauma.Networking
wp.Submarine == Level.Loaded.StartOutpost && wp.Submarine == Level.Loaded.StartOutpost &&
wp.CurrentHull?.OutpostModuleTags != null && wp.CurrentHull?.OutpostModuleTags != null &&
wp.CurrentHull.OutpostModuleTags.Contains("airlock".ToIdentifier())); wp.CurrentHull.OutpostModuleTags.Contains("airlock".ToIdentifier()));
while (spawnWaypoints.Count > characterInfos.Count) while (spawnWaypoints.Count > characterInfos.Count)
{ {
spawnWaypoints.RemoveAt(Rand.Int(spawnWaypoints.Count)); spawnWaypoints.RemoveAt(Rand.Int(spawnWaypoints.Count));
@@ -2486,13 +2617,17 @@ namespace Barotrauma.Networking
characterData.ApplyWalletData(spawnedCharacter); characterData.ApplyWalletData(spawnedCharacter);
spawnedCharacter.GiveIdCardTags(mainSubWaypoints[i]); spawnedCharacter.GiveIdCardTags(mainSubWaypoints[i]);
spawnedCharacter.LoadTalents(); spawnedCharacter.LoadTalents();
characterData.HasSpawned = true; characterData.HasSpawned = true;
} }
if (GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign && spawnedCharacter.Info != null) if (GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign && spawnedCharacter.Info != null)
{ {
spawnedCharacter.Info.SetExperience(Math.Max(spawnedCharacter.Info.ExperiencePoints, mpCampaign.GetSavedExperiencePoints(teamClients[i]))); spawnedCharacter.Info.SetExperience(Math.Max(spawnedCharacter.Info.ExperiencePoints, mpCampaign.GetSavedExperiencePoints(teamClients[i])));
mpCampaign.ClearSavedExperiencePoints(teamClients[i]); mpCampaign.ClearSavedExperiencePoints(teamClients[i]);
if (spawnedCharacter.Info.LastRewardDistribution.TryUnwrap(out int salary))
{
spawnedCharacter.Wallet.SetRewardDistribution(salary);
}
} }
spawnedCharacter.SetOwnerClient(teamClients[i]); spawnedCharacter.SetOwnerClient(teamClients[i]);
@@ -2584,11 +2719,12 @@ namespace Barotrauma.Networking
msg.WriteInt32(seed); msg.WriteInt32(seed);
msg.WriteIdentifier(gameSession.GameMode.Preset.Identifier); msg.WriteIdentifier(gameSession.GameMode.Preset.Identifier);
bool missionAllowRespawn = GameMain.GameSession.GameMode is not MissionMode missionMode || !missionMode.Missions.Any(m => !m.AllowRespawn); bool missionAllowRespawn = GameMain.GameSession.GameMode is not MissionMode missionMode || !missionMode.Missions.Any(m => !m.AllowRespawn);
msg.WriteBoolean(ServerSettings.AllowRespawn && missionAllowRespawn); msg.WriteBoolean(ServerSettings.RespawnMode != RespawnMode.BetweenRounds && missionAllowRespawn);
msg.WriteBoolean(ServerSettings.AllowDisguises); msg.WriteBoolean(ServerSettings.AllowDisguises);
msg.WriteBoolean(ServerSettings.AllowRewiring); msg.WriteBoolean(ServerSettings.AllowRewiring);
msg.WriteBoolean(ServerSettings.AllowImmediateItemDelivery); msg.WriteBoolean(ServerSettings.AllowImmediateItemDelivery);
msg.WriteBoolean(ServerSettings.AllowFriendlyFire); msg.WriteBoolean(ServerSettings.AllowFriendlyFire);
msg.WriteBoolean(ServerSettings.AllowDragAndDropGive);
msg.WriteBoolean(ServerSettings.LockAllDefaultWires); msg.WriteBoolean(ServerSettings.LockAllDefaultWires);
msg.WriteBoolean(ServerSettings.AllowLinkingWifiToChat); msg.WriteBoolean(ServerSettings.AllowLinkingWifiToChat);
msg.WriteInt32(ServerSettings.MaximumMoneyTransferRequest); msg.WriteInt32(ServerSettings.MaximumMoneyTransferRequest);
@@ -2696,7 +2832,7 @@ namespace Barotrauma.Networking
GameMain.GameSession.CrewManager?.ServerWriteActiveOrders(msg); 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) if (GameStarted)
{ {
@@ -2712,14 +2848,14 @@ namespace Barotrauma.Networking
} }
string endMessage = TextManager.FormatServerMessage("RoundSummaryRoundHasEnded"); string endMessage = TextManager.FormatServerMessage("RoundSummaryRoundHasEnded");
List<Mission> missions = GameMain.GameSession.Missions.ToList(); missions ??= GameMain.GameSession.Missions.ToList();
if (GameMain.GameSession is { IsRunning: true }) if (GameMain.GameSession is { IsRunning: true })
{ {
GameMain.GameSession.EndRound(endMessage); GameMain.GameSession.EndRound(endMessage);
} }
TraitorManager.TraitorResults? traitorResults = traitorManager?.GetEndResults() ?? null; TraitorManager.TraitorResults? traitorResults = traitorManager?.GetEndResults() ?? null;
endRoundTimer = 0.0f; EndRoundTimer = 0.0f;
if (ServerSettings.AutoRestart) if (ServerSettings.AutoRestart)
{ {
@@ -2755,7 +2891,7 @@ namespace Barotrauma.Networking
msg.WriteByte((byte)transitionType); msg.WriteByte((byte)transitionType);
msg.WriteBoolean(wasSaved); msg.WriteBoolean(wasSaved);
msg.WriteString(endMessage); msg.WriteString(endMessage);
msg.WriteByte((byte)missions.Count); msg.WriteByte((byte)missions.Count());
foreach (Mission mission in missions) foreach (Mission mission in missions)
{ {
msg.WriteBoolean(mission.Completed); msg.WriteBoolean(mission.Completed);
@@ -2799,6 +2935,12 @@ namespace Barotrauma.Networking
{ {
logMsg = message.TextWithSender; logMsg = message.TextWithSender;
} }
if (message.Sender is Character sender)
{
sender.TextChatVolume = 1f;
}
Log(logMsg, ServerLog.MessageType.Chat); Log(logMsg, ServerLog.MessageType.Chat);
} }
@@ -2863,7 +3005,7 @@ namespace Barotrauma.Networking
} }
} }
private bool IsNameValid(Client c, string newName) public bool IsNameValid(Client c, string newName)
{ {
newName = Client.SanitizeName(newName); newName = Client.SanitizeName(newName);
@@ -2887,13 +3029,20 @@ namespace Barotrauma.Networking
} }
} }
Client nameTaken = ConnectedClients.Find(c2 => c != c2 && Homoglyphs.Compare(c2.Name.ToLower(), newName.ToLower())); Client nameTakenByClient = ConnectedClients.Find(c2 => c != c2 && Homoglyphs.Compare(c2.Name.ToLower(), newName.ToLower()));
if (nameTaken != null) if (nameTakenByClient != null)
{ {
SendDirectChatMessage($"ServerMessage.NameChangeFailedClientTooSimilar~[newname]={newName}~[takenname]={nameTaken.Name}", c, ChatMessageType.ServerMessageBox); SendDirectChatMessage($"ServerMessage.NameChangeFailedClientTooSimilar~[newname]={newName}~[takenname]={nameTakenByClient.Name}", c, ChatMessageType.ServerMessageBox);
return false; return false;
} }
Character nameTakenByCharacter =
GameSession.GetSessionCrewCharacters(CharacterType.Both).FirstOrDefault(c2 => c2 != c.Character && Homoglyphs.Compare(c2.Name.ToLower(), newName.ToLower()));
if (nameTakenByCharacter != null)
{
SendDirectChatMessage($"ServerMessage.NameChangeFailedClientTooSimilar~[newname]={newName}~[takenname]={nameTakenByCharacter.Name}", c, ChatMessageType.ServerMessageBox);
return false;
}
return true; return true;
} }
@@ -3318,24 +3467,24 @@ namespace Barotrauma.Networking
public void SendOrderChatMessage(OrderChatMessage message) 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 //check which clients can receive the message and apply distance effects
foreach (Client client in ConnectedClients) 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 //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); SendDirectChatMessage(new OrderChatMessage(message.Order, message.Text, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder), client);
} }
if (!string.IsNullOrWhiteSpace(message.Text)) if (!string.IsNullOrWhiteSpace(message.Text))
{ {
AddChatMessage(new OrderChatMessage(message.Order, message.Text, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder)); 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 //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); senderRadio.TransmitSignal(s, sentFromChat: true);
} }
} }
@@ -3636,6 +3785,7 @@ namespace Barotrauma.Networking
newCharacter.SetOwnerClient(client); newCharacter.SetOwnerClient(client);
newCharacter.Enabled = true; newCharacter.Enabled = true;
client.Character = newCharacter; client.Character = newCharacter;
client.CharacterInfo = newCharacter.Info;
CreateEntityEvent(newCharacter, new Character.ControlEventData(client)); CreateEntityEvent(newCharacter, new Character.ControlEventData(client));
} }
} }
@@ -3679,6 +3829,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 = new CharacterInfo(CharacterPrefab.HumanSpeciesName, newName);
sender.CharacterInfo.RecreateHead( sender.CharacterInfo.RecreateHead(
@@ -3841,7 +3999,7 @@ namespace Barotrauma.Networking
foreach (Client c in unassigned) foreach (Client c in unassigned)
{ {
//find all jobs that are still available //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 //all jobs taken, give a random job
if (remainingJobs.Count == 0) if (remainingJobs.Count == 0)
@@ -3884,9 +4042,15 @@ namespace Barotrauma.Networking
public void AssignBotJobs(List<CharacterInfo> bots, CharacterTeamType teamID) 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>(); 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); assignedPlayerCount.Add(jp, 0);
} }
@@ -3905,53 +4069,55 @@ namespace Barotrauma.Networking
} }
List<CharacterInfo> unassignedBots = new List<CharacterInfo>(bots); List<CharacterInfo> unassignedBots = new List<CharacterInfo>(bots);
while (unassignedBots.Count > 0)
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
{ {
canAssign = false; //if there's any jobs left that must be included in the crew, assign those
foreach (WayPoint spawnPoint in spawnPoints) var jobsBelowMinNumber = shuffledPrefabs.Where(jp => assignedPlayerCount[jp] < jp.MinNumber);
if (jobsBelowMinNumber.Any())
{ {
if (unassignedBots.Count == 0) { break; } AssignJob(unassignedBots[0], jobsBelowMinNumber.GetRandomUnsynced());
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;
} }
} 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.ToList())
foreach (CharacterInfo c in unassignedBots)
{ {
//find all jobs that are still available //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 //all jobs taken, give a random job
if (remainingJobs.None()) 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..."); 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); AssignJob(c, shuffledPrefabs.GetRandomUnsynced());
assignedPlayerCount[c.Job.Prefab]++;
} }
else //some jobs still left, choose one of them by random else
{ {
var job = remainingJobs.GetRandomUnsynced(); //some jobs still left, choose one of them by random (preferring ones there's the least of in the crew)
var variant = Rand.Range(0, job.Variants); var selectedJob = remainingJobs.GetRandomByWeight(jp => 1.0f / Math.Max(assignedPlayerCount[jp], 0.01f), Rand.RandSync.Unsynced);
c.Job = new Job(job, Rand.RandSync.Unsynced, variant); AssignJob(c, selectedJob);
assignedPlayerCount[c.Job.Prefab]++;
} }
} }
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) private Client FindClientWithJobPreference(List<Client> clients, JobPrefab job, bool forceAssign = false)

View File

@@ -458,15 +458,35 @@ namespace Barotrauma.Networking
RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationFailed)); 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 if (authenticators is null
|| !packet.AuthTicket.TryUnwrap(out var authTicket) || !packet.AuthTicket.TryUnwrap(out var authTicket)
|| !authenticators.TryGetValue(authTicket.Kind, out var authenticator)) || !authenticators.TryGetValue(authTicket.Kind, out var authenticator))
{ {
#if DEBUG #if DEBUG
DebugConsole.NewMessage($"Debug server accepts unauthenticated connections", Microsoft.Xna.Framework.Color.Yellow); DebugConsole.NewMessage("Debug server accepts unauthenticated connections", Microsoft.Xna.Framework.Color.Yellow);
acceptClient(new AccountInfo(packet.AccountId)); acceptClient(new AccountInfo(new UnauthenticatedAccountId(packet.Name)));
#else #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 #endif
return; return;
} }

View File

@@ -15,6 +15,8 @@ namespace Barotrauma.Networking
private int pendingRespawnCount, requiredRespawnCount; private int pendingRespawnCount, requiredRespawnCount;
private int prevPendingRespawnCount, prevRequiredRespawnCount; private int prevPendingRespawnCount, prevRequiredRespawnCount;
public bool IsShuttleInsideLevel => RespawnShuttle != null && RespawnShuttle.WorldPosition.Y < Level.Loaded.Size.Y;
private IEnumerable<Client> GetClientsToRespawn() private IEnumerable<Client> GetClientsToRespawn()
{ {
MultiPlayerCampaign campaign = GameMain.GameSession.GameMode as MultiPlayerCampaign; MultiPlayerCampaign campaign = GameMain.GameSession.GameMode as MultiPlayerCampaign;
@@ -24,18 +26,27 @@ namespace Barotrauma.Networking
if (c.SpectateOnly && (GameMain.Server.ServerSettings.AllowSpectating || GameMain.Server.OwnerConnection == c.Connection)) { continue; } if (c.SpectateOnly && (GameMain.Server.ServerSettings.AllowSpectating || GameMain.Server.OwnerConnection == c.Connection)) { continue; }
if (c.Character != null && !c.Character.IsDead) { 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); 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 && if (matchingData != null && matchingData.HasSpawned &&
Character.CharacterList.Any(c => c.Info == matchingData.CharacterInfo && !c.IsDead)) Character.CharacterList.Any(c => c.Info == matchingData.CharacterInfo && !c.IsDead))
{ {
continue; 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) 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; } if (!c.WaitForNextRoundRespawn.HasValue || c.WaitForNextRoundRespawn.Value) { continue; }
} }
} }
@@ -44,9 +55,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.InGame) { return false; }
if (c.SpectateOnly && (GameMain.Server.ServerSettings.AllowSpectating || GameMain.Server.OwnerConnection == c.Connection)) { return false; } if (c.SpectateOnly && (GameMain.Server.ServerSettings.AllowSpectating || GameMain.Server.OwnerConnection == c.Connection)) { return false; }
@@ -55,7 +66,9 @@ namespace Barotrauma.Networking
var matchingData = campaign.GetClientCharacterData(c); var matchingData = campaign.GetClientCharacterData(c);
if (matchingData != null && matchingData.HasSpawned) 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; return false;
} }
@@ -180,22 +193,27 @@ namespace Barotrauma.Networking
shuttleSteering.TargetVelocity = Vector2.Zero; shuttleSteering.TargetVelocity = Vector2.Zero;
} }
GameServer.Log("Dispatching the respawn shuttle.", ServerLog.MessageType.Spawning);
Vector2 spawnPos = FindSpawnPos(); Vector2 spawnPos = FindSpawnPos();
RespawnCharacters(spawnPos); RespawnCharacters(spawnPos, out bool anyCharacterSpawnedInShuttle);
if (anyCharacterSpawnedInShuttle)
CoroutineManager.StopCoroutines("forcepos");
if (spawnPos.Y > Level.Loaded.Size.Y)
{ {
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 else
{ {
RespawnShuttle.SetPosition(spawnPos); GameServer.Log("Respawning everyone in main sub.", ServerLog.MessageType.Spawning);
RespawnShuttle.Velocity = Vector2.Zero;
RespawnShuttle.NeutralizeBallast();
RespawnShuttle.EnableMaintainPosition();
} }
} }
else else
@@ -204,7 +222,7 @@ namespace Barotrauma.Networking
GameServer.Log("Respawning everyone in main sub.", ServerLog.MessageType.Spawning); GameServer.Log("Respawning everyone in main sub.", ServerLog.MessageType.Spawning);
GameMain.Server.CreateEntityEvent(this); GameMain.Server.CreateEntityEvent(this);
RespawnCharacters(null); RespawnCharacters(shuttlePos: null, out _);
} }
} }
@@ -244,7 +262,7 @@ namespace Barotrauma.Networking
} }
} }
if (RespawnShuttle.WorldPosition.Y > Level.Loaded.Size.Y || DateTime.Now > despawnTime) if (!IsShuttleInsideLevel || DateTime.Now > despawnTime)
{ {
CoroutineManager.StopCoroutines("forcepos"); CoroutineManager.StopCoroutines("forcepos");
@@ -289,7 +307,10 @@ namespace Barotrauma.Networking
if (DateTime.Now > ReturnTime) 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; CurrentState = State.Returning;
GameMain.Server.CreateEntityEvent(this); GameMain.Server.CreateEntityEvent(this);
@@ -311,8 +332,8 @@ namespace Barotrauma.Networking
} }
return shuttleEmptyTimer > 1.0f; return shuttleEmptyTimer > 1.0f;
} }
partial void RespawnCharactersProjSpecific(Vector2? shuttlePos) private void RespawnCharacters(Vector2? shuttlePos, out bool anyCharacterSpawnedInShuttle)
{ {
respawnedCharacters.Clear(); respawnedCharacters.Clear();
@@ -337,7 +358,7 @@ namespace Barotrauma.Networking
//all characters are in Team 1 in game modes/missions with only one team. //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 //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; 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(); List<CharacterInfo> characterInfos = clients.Select(c => c.CharacterInfo).ToList();
@@ -378,6 +399,8 @@ namespace Barotrauma.Networking
var cargoSp = WayPoint.WayPointList.Find(wp => wp.Submarine == respawnSub && wp.SpawnType == SpawnType.Cargo); var cargoSp = WayPoint.WayPointList.Find(wp => wp.Submarine == respawnSub && wp.SpawnType == SpawnType.Cargo);
anyCharacterSpawnedInShuttle = false;
for (int i = 0; i < characterInfos.Count; i++) for (int i = 0; i < characterInfos.Count; i++)
{ {
bool bot = i >= clients.Count; bool bot = i >= clients.Count;
@@ -412,10 +435,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); var character = Character.Create(characterInfos[i], (forceSpawnInMainSub ? mainSubSpawnPoints[i] : shuttleSpawnPoints[i]).WorldPosition, characterInfos[i].Name, isRemotePlayer: !bot, hasAi: bot);
characterCampaignData?.ApplyWalletData(character); characterCampaignData?.ApplyWalletData(character);
character.TeamID = CharacterTeamType.Team1; character.TeamID = CharacterTeamType.Team1;
character.LoadTalents(); character.LoadTalents();
if (characterInfos[i].LastRewardDistribution.TryUnwrap(out int salary))
{
character.Wallet.SetRewardDistribution(salary);
}
respawnedCharacters.Add(character); respawnedCharacters.Add(character);
@@ -446,7 +478,7 @@ namespace Barotrauma.Networking
$"Respawning {GameServer.ClientLogName(clients[i])} ({clients[i].Connection.Endpoint}) as {characterInfos[i].Job.Name}", ServerLog.MessageType.Spawning); $"Respawning {GameServer.ClientLogName(clients[i])} ({clients[i].Connection.Endpoint}) as {characterInfos[i].Job.Name}", ServerLog.MessageType.Spawning);
} }
if (RespawnShuttle != null) if (RespawnShuttle != null && anyCharacterSpawnedInShuttle)
{ {
List<Item> newRespawnItems = new List<Item>(); List<Item> newRespawnItems = new List<Item>();
Vector2 pos = cargoSp?.Position ?? character.Position; Vector2 pos = cargoSp?.Position ?? character.Position;
@@ -567,9 +599,7 @@ namespace Barotrauma.Networking
foreach (Skill skill in characterInfo.Job.GetSkills()) foreach (Skill skill in characterInfo.Job.GetSkills())
{ {
var skillPrefab = characterInfo.Job.Prefab.Skills.Find(s => skill.Identifier == s.Identifier); skill.Level = GetReducedSkill(characterInfo, skill, skillLossPercentage);
if (skillPrefab == null || skill.Level < skillPrefab.LevelRange.End) { continue; }
skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.End, skillLossPercentage / 100.0f);
} }
} }
@@ -585,14 +615,10 @@ namespace Barotrauma.Networking
msg.WriteSingle((float)(ReturnTime - DateTime.Now).TotalSeconds); msg.WriteSingle((float)(ReturnTime - DateTime.Now).TotalSeconds);
break; break;
case State.Waiting: 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)pendingRespawnCount);
msg.WriteUInt16((ushort)requiredRespawnCount); msg.WriteUInt16((ushort)requiredRespawnCount);
msg.WriteBoolean(IsRespawnPromptPendingForClient(c)); msg.WriteBoolean(IsRespawnDecisionPendingForClient(c));
msg.WriteBoolean(RespawnCountdownStarted); msg.WriteBoolean(RespawnCountdownStarted);
msg.WriteBoolean(forceSpawnInMainSub);
msg.WriteSingle((float)(RespawnTime - DateTime.Now).TotalSeconds); msg.WriteSingle((float)(RespawnTime - DateTime.Now).TotalSeconds);
break; break;
case State.Returning: case State.Returning:

View File

@@ -123,5 +123,21 @@ namespace Barotrauma.Networking
return garbleAmount < 1.0f; return garbleAmount < 1.0f;
} }
} }
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

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

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>1.4.6.0</Version> <Version>1.5.9.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
@@ -70,6 +70,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(Configuration)'!='Debug'"> <ItemGroup Condition="'$(Configuration)'!='Debug'">
<ProjectReference Include="..\..\Libraries\Concentus\CSharp\Concentus\Concentus.NetStandard.csproj" AdditionalProperties="Configuration=Release" />
<ProjectReference Include="..\..\Libraries\Facepunch.Steamworks\Facepunch.Steamworks.Win64.csproj" AdditionalProperties="Configuration=Release" /> <ProjectReference Include="..\..\Libraries\Facepunch.Steamworks\Facepunch.Steamworks.Win64.csproj" AdditionalProperties="Configuration=Release" />
<ProjectReference Include="..\..\Libraries\Farseer Physics Engine 3.5\Farseer.NetStandard.csproj" AdditionalProperties="Configuration=Release" /> <ProjectReference Include="..\..\Libraries\Farseer Physics Engine 3.5\Farseer.NetStandard.csproj" AdditionalProperties="Configuration=Release" />
<ProjectReference Include="..\..\Libraries\Hyper.ComponentModel\Hyper.ComponentModel.NetStandard.csproj" AdditionalProperties="Configuration=Release" /> <ProjectReference Include="..\..\Libraries\Hyper.ComponentModel\Hyper.ComponentModel.NetStandard.csproj" AdditionalProperties="Configuration=Release" />
@@ -77,6 +78,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(Configuration)'=='Debug'"> <ItemGroup Condition="'$(Configuration)'=='Debug'">
<ProjectReference Include="..\..\Libraries\Concentus\CSharp\Concentus\Concentus.NetStandard.csproj" AdditionalProperties="Configuration=Debug" />
<ProjectReference Include="..\..\Libraries\Facepunch.Steamworks\Facepunch.Steamworks.Win64.csproj" AdditionalProperties="Configuration=Debug" /> <ProjectReference Include="..\..\Libraries\Facepunch.Steamworks\Facepunch.Steamworks.Win64.csproj" AdditionalProperties="Configuration=Debug" />
<ProjectReference Include="..\..\Libraries\Farseer Physics Engine 3.5\Farseer.NetStandard.csproj" AdditionalProperties="Configuration=Debug" /> <ProjectReference Include="..\..\Libraries\Farseer Physics Engine 3.5\Farseer.NetStandard.csproj" AdditionalProperties="Configuration=Debug" />
<ProjectReference Include="..\..\Libraries\Hyper.ComponentModel\Hyper.ComponentModel.NetStandard.csproj" AdditionalProperties="Configuration=Debug" /> <ProjectReference Include="..\..\Libraries\Hyper.ComponentModel\Hyper.ComponentModel.NetStandard.csproj" AdditionalProperties="Configuration=Debug" />

View File

@@ -33,6 +33,7 @@
<Command name="togglekarmatestmode"/> <Command name="togglekarmatestmode"/>
<Command name="respawnnow"/> <Command name="respawnnow"/>
<Command name="traitorlist"/> <Command name="traitorlist"/>
<Command name="setsalary"/>
</Preset> </Preset>
<Preset <Preset
@@ -94,5 +95,6 @@
<Command name="togglecampaignteleport"/> <Command name="togglecampaignteleport"/>
<Command name="respawnnow"/> <Command name="respawnnow"/>
<Command name="traitorlist"/> <Command name="traitorlist"/>
<Command name="setsalary"/>
</Preset> </Preset>
</PermissionPresets> </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) public AITarget(Entity e, XElement element) : this(e)
{ {
SightRange = element.GetAttributeFloat("sightrange", 0.0f); SightRange = element.GetAttributeFloat("sightrange", 0.0f);

View File

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

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