Build 0.21.6.0

This commit is contained in:
Markus Isberg
2023-01-31 18:01:29 +02:00
parent 697ec52120
commit 25fa5a9552
145 changed files with 2317 additions and 1145 deletions
@@ -154,9 +154,12 @@ namespace Barotrauma
} }
if (LosFadeIn && clampedTimer / PanDuration > 0.8f) if (LosFadeIn && clampedTimer / PanDuration > 0.8f)
{ {
GameMain.LightManager.LosAlpha = ((clampedTimer / PanDuration) - 0.8f) * 5.0f; if (!GameMain.DevMode)
{
GameMain.LightManager.LosEnabled = true;
GameMain.LightManager.LosAlpha = ((clampedTimer / PanDuration) - 0.8f) * 5.0f;
}
Lights.LightManager.ViewTarget = prevControlled ?? (targetEntity as Entity); Lights.LightManager.ViewTarget = prevControlled ?? (targetEntity as Entity);
GameMain.LightManager.LosEnabled = true;
} }
#endif #endif
timer += CoroutineManager.DeltaTime; timer += CoroutineManager.DeltaTime;
@@ -170,8 +173,11 @@ namespace Barotrauma
#if CLIENT #if CLIENT
GUI.ScreenOverlayColor = Color.TransparentBlack; GUI.ScreenOverlayColor = Color.TransparentBlack;
GameMain.LightManager.LosEnabled = true; if (!GameMain.DevMode)
GameMain.LightManager.LosAlpha = 1f; {
GameMain.LightManager.LosEnabled = true;
GameMain.LightManager.LosAlpha = 1f;
}
#endif #endif
if (prevControlled != null && !prevControlled.Removed) if (prevControlled != null && !prevControlled.Removed)
@@ -724,6 +724,8 @@ namespace Barotrauma
} }
bossHealthBar.TopHealthBar.BarSize = bossHealthBar.SideHealthBar.BarSize = health; bossHealthBar.TopHealthBar.BarSize = bossHealthBar.SideHealthBar.BarSize = health;
Color color = bossHealthBar.Character.CharacterHealth.GetAfflictionStrength("poison") > 0 || bossHealthBar.Character.CharacterHealth.GetAfflictionStrength("paralysis") > 0 ? GUIStyle.HealthBarColorPoisoned : GUIStyle.Red;
bossHealthBar.TopHealthBar.Color = bossHealthBar.SideHealthBar.Color = color;
if (bossHealthBar.Character.Removed || !bossHealthBar.Character.Enabled) if (bossHealthBar.Character.Removed || !bossHealthBar.Character.Enabled)
{ {
@@ -2071,6 +2071,8 @@ namespace Barotrauma
foreach (var periodicEffect in newPeriodicEffects) foreach (var periodicEffect in newPeriodicEffects)
{ {
if (!existingAffliction.Prefab.PeriodicEffects.Contains(periodicEffect.effect)) { continue; } if (!existingAffliction.Prefab.PeriodicEffects.Contains(periodicEffect.effect)) { continue; }
if (existingAffliction.Strength < periodicEffect.effect.MinStrength) { continue; }
if (periodicEffect.effect.MaxStrength > 0 && existingAffliction.Strength > periodicEffect.effect.MaxStrength) { continue; }
//timer has wrapped around, apply the effect //timer has wrapped around, apply the effect
if (periodicEffect.timer - existingAffliction.PeriodicEffectTimers[periodicEffect.effect] > periodicEffect.effect.MinInterval / 2) if (periodicEffect.timer - existingAffliction.PeriodicEffectTimers[periodicEffect.effect] > periodicEffect.effect.MinInterval / 2)
{ {
@@ -0,0 +1,22 @@
#nullable enable
using System;
namespace Barotrauma
{
internal static class HealingCooldown
{
public static float NormalizedCooldown => MathF.Min((float) (DateTimeOffset.UtcNow - OnCooldownUntil).TotalSeconds / CooldownDuration, 0f);
public static bool IsOnCooldown => DateTimeOffset.UtcNow < OnCooldownUntil;
private static DateTimeOffset OnCooldownUntil = DateTimeOffset.MinValue;
private const float CooldownDuration = 0.5f;
public static readonly Identifier MedicalItemTag = new Identifier("medical");
public static void PutOnCooldown()
{
OnCooldownUntil = DateTimeOffset.UtcNow.AddSeconds(CooldownDuration);
}
}
}
@@ -8,6 +8,7 @@ using Microsoft.Xna.Framework.Graphics;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Barotrauma.IO; using Barotrauma.IO;
using Barotrauma.Utils;
using System.Linq; using System.Linq;
using System.Xml.Linq; using System.Xml.Linq;
using SpriteParams = Barotrauma.RagdollParams.SpriteParams; using SpriteParams = Barotrauma.RagdollParams.SpriteParams;
@@ -260,9 +261,7 @@ namespace Barotrauma
{ {
if (enableHuskSprite) if (enableHuskSprite)
{ {
List<WearableSprite> otherWearablesWithHusk = new List<WearableSprite>() { HuskSprite }; OtherWearables.Insert(0, HuskSprite);
otherWearablesWithHusk.AddRange(OtherWearables);
OtherWearables = otherWearablesWithHusk;
UpdateWearableTypesToHide(); UpdateWearableTypesToHide();
} }
else else
@@ -546,7 +545,7 @@ namespace Barotrauma
{ {
foreach (var affliction in result.Afflictions) foreach (var affliction in result.Afflictions)
{ {
if (affliction is AfflictionBleeding) if (affliction is AfflictionBleeding bleeding && bleeding.Prefab.DamageParticles)
{ {
bleedingDamage += affliction.GetVitalityDecrease(null); bleedingDamage += affliction.GetVitalityDecrease(null);
} }
@@ -555,7 +554,7 @@ namespace Barotrauma
float damage = 0; float damage = 0;
foreach (var affliction in result.Afflictions) foreach (var affliction in result.Afflictions)
{ {
if (affliction.Prefab.AfflictionType == "damage") if (affliction.Prefab.DamageParticles && affliction.Prefab.AfflictionType == "damage")
{ {
damage += affliction.GetVitalityDecrease(null); damage += affliction.GetVitalityDecrease(null);
} }
@@ -732,7 +731,7 @@ namespace Barotrauma
bool hideLimb = Hide || bool hideLimb = Hide ||
OtherWearables.Any(w => w.HideLimb) || OtherWearables.Any(w => w.HideLimb) ||
wearingItems.Any(w => w != null && w.HideLimb); WearingItems.Any(w => w.HideLimb);
bool drawHuskSprite = HuskSprite != null && !wearableTypesToHide.Contains(WearableType.Husk); bool drawHuskSprite = HuskSprite != null && !wearableTypesToHide.Contains(WearableType.Husk);
@@ -828,7 +827,7 @@ namespace Barotrauma
LightSource.LightSpriteEffect = (dir == Direction.Right) ? SpriteEffects.None : SpriteEffects.FlipVertically; LightSource.LightSpriteEffect = (dir == Direction.Right) ? SpriteEffects.None : SpriteEffects.FlipVertically;
} }
float step = depthStep; float step = depthStep;
WearableSprite onlyDrawable = wearingItems.Find(w => w.HideOtherWearables); WearableSprite onlyDrawable = WearingItems.Find(w => w.HideOtherWearables);
if (Params.MirrorHorizontally) if (Params.MirrorHorizontally)
{ {
spriteEffect = spriteEffect == SpriteEffects.None ? SpriteEffects.FlipHorizontally : SpriteEffects.None; spriteEffect = spriteEffect == SpriteEffects.None ? SpriteEffects.FlipHorizontally : SpriteEffects.None;
@@ -965,31 +964,28 @@ namespace Barotrauma
public void UpdateWearableTypesToHide() public void UpdateWearableTypesToHide()
{ {
alphaClipEffectParams?.Clear();
wearableTypeHidingSprites.Clear(); wearableTypeHidingSprites.Clear();
if (WearingItems != null && WearingItems.Count > 0)
void addWearablesFrom(IReadOnlyList<WearableSprite> wearableSprites)
{ {
if (wearableSprites.Count <= 0) { return; }
wearableTypeHidingSprites.AddRange( wearableTypeHidingSprites.AddRange(
WearingItems.FindAll(w => w.HideWearablesOfType != null && w.HideWearablesOfType.Count > 0)); wearableSprites.Where(w => w.HideWearablesOfType.Count > 0));
}
if (OtherWearables != null && OtherWearables.Count > 0)
{
wearableTypeHidingSprites.AddRange(
OtherWearables.FindAll(w => w.HideWearablesOfType != null && w.HideWearablesOfType.Count > 0));
} }
addWearablesFrom(WearingItems);
addWearablesFrom(OtherWearables);
wearableTypesToHide.Clear(); wearableTypesToHide.Clear();
if (wearableTypeHidingSprites.Count > 0)
if (wearableTypeHidingSprites.Count <= 0) { return; }
foreach (WearableSprite sprite in wearableTypeHidingSprites)
{ {
foreach (WearableSprite sprite in wearableTypeHidingSprites) wearableTypesToHide.UnionWith(sprite.HideWearablesOfType);
{
foreach (WearableType type in sprite.HideWearablesOfType)
{
if (!wearableTypesToHide.Contains(type))
{
wearableTypesToHide.Add(type);
}
}
}
} }
} }
@@ -1071,7 +1067,13 @@ namespace Barotrauma
} }
} }
private void DrawWearable(WearableSprite wearable, float depthStep, SpriteBatch spriteBatch, Color color, float alpha, SpriteEffects spriteEffect) private (
Color FinalColor,
Vector2 Origin,
float Rotation,
float Scale,
float Depth)
CalculateDrawParameters(WearableSprite wearable, float depthStep, Color color, float alpha)
{ {
var sprite = ActiveSprite; var sprite = ActiveSprite;
if (wearable.InheritSourceRect) if (wearable.InheritSourceRect)
@@ -1163,27 +1165,118 @@ namespace Barotrauma
float finalAlpha = alpha * wearableColor.A; float finalAlpha = alpha * wearableColor.A;
Color finalColor = color.Multiply(wearableColor); Color finalColor = color.Multiply(wearableColor);
finalColor = new Color(finalColor.R, finalColor.G, finalColor.B, (byte)finalAlpha); finalColor = new Color(finalColor.R, finalColor.G, finalColor.B, (byte)finalAlpha);
wearable.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), finalColor, origin, rotation, scale, spriteEffect, depth);
return (finalColor, origin, rotation, scale, depth);
} }
private WearableSprite GetWearableSprite(WearableType type)//, bool random = false) private static Effect alphaClipEffect;
private Dictionary<WearableSprite, Dictionary<string, object>> alphaClipEffectParams;
private void ApplyAlphaClip(SpriteBatch spriteBatch, WearableSprite wearable, WearableSprite alphaClipper, SpriteEffects spriteEffect)
{
SpriteRecorder.Command makeCommand(WearableSprite w)
{
var (_, origin, rotation, scale, _)
= CalculateDrawParameters(w, 0f, Color.White, 0f);
var command = SpriteRecorder.Command.FromTransform(
texture: w.Sprite.Texture,
pos: new Vector2(body.DrawPosition.X, -body.DrawPosition.Y),
srcRect: w.Sprite.SourceRect,
color: Color.White,
rotation: rotation,
origin: origin,
scale: new Vector2(scale, scale),
effects: spriteEffect,
depth: 0f,
index: 0);
return command;
}
void spacesFromCommand(WearableSprite w, SpriteRecorder.Command command, out CoordinateSpace2D textureSpace, out CoordinateSpace2D worldSpace)
{
var (topLeft, bottomLeft, topRight) = spriteEffect switch
{
SpriteEffects.None
=> (command.VertexTL, command.VertexBL, command.VertexTR),
SpriteEffects.FlipHorizontally | SpriteEffects.FlipVertically
=> (command.VertexBR, command.VertexTR, command.VertexBL),
SpriteEffects.FlipHorizontally
=> (command.VertexTR, command.VertexBR, command.VertexTL),
SpriteEffects.FlipVertically
=> (command.VertexBL, command.VertexTL, command.VertexBR)
};
textureSpace = new CoordinateSpace2D
{
Origin = topLeft.TextureCoordinate,
I = topRight.TextureCoordinate - topLeft.TextureCoordinate,
J = bottomLeft.TextureCoordinate - topLeft.TextureCoordinate
};
worldSpace = new CoordinateSpace2D
{
Origin = topLeft.Position.DiscardZ(),
I = topRight.Position.DiscardZ() - topLeft.Position.DiscardZ(),
J = bottomLeft.Position.DiscardZ() - topLeft.Position.DiscardZ()
};
}
var wearableCommand = makeCommand(wearable);
var clipperCommand = makeCommand(alphaClipper);
spacesFromCommand(wearable, wearableCommand, out var wearableTextureSpace, out var wearableWorldSpace);
spacesFromCommand(alphaClipper, clipperCommand, out var clipperTextureSpace, out var clipperWorldSpace);
var wearableUvToClipperUv =
wearableTextureSpace.CanonicalToLocal
* wearableWorldSpace.LocalToCanonical
* clipperWorldSpace.CanonicalToLocal
* clipperTextureSpace.LocalToCanonical;
alphaClipEffect ??= EffectLoader.Load("Effects/wearableclip");
alphaClipEffectParams ??= new Dictionary<WearableSprite, Dictionary<string, object>>();
if (!alphaClipEffectParams.ContainsKey(wearable)) { alphaClipEffectParams.Add(wearable, new Dictionary<string, object>()); }
var paramsToPass = new SpriteBatch.EffectWithParams
{
Effect = alphaClipEffect,
Params = alphaClipEffectParams[wearable]
};
paramsToPass.Params["wearableUvToClipperUv"] = wearableUvToClipperUv;
paramsToPass.Params["clipperTexelSize"] = 2f / alphaClipper.Sprite.Texture.Width;
paramsToPass.Params["aCutoff"] = 2f / 255f;
paramsToPass.Params["xTexture"] = wearable.Sprite.Texture;
paramsToPass.Params["xStencil"] = alphaClipper.Sprite.Texture;
spriteBatch.SwapEffect(paramsToPass);
}
private void DrawWearable(WearableSprite wearable, float depthStep, SpriteBatch spriteBatch, Color color, float alpha, SpriteEffects spriteEffect)
{
var (finalColor, origin, rotation, scale, depth)
= CalculateDrawParameters(wearable, depthStep, color, alpha);
var prevEffect = spriteBatch.GetCurrentEffect();
var alphaClipper = WearingItems.Find(w => w.AlphaClipOtherWearables);
bool shouldApplyAlphaClip = alphaClipper != null && wearable != alphaClipper;
if (shouldApplyAlphaClip)
{
ApplyAlphaClip(spriteBatch, wearable, alphaClipper, spriteEffect);
}
wearable.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), finalColor, origin, rotation, scale, spriteEffect, depth);
if (shouldApplyAlphaClip)
{
spriteBatch.SwapEffect(effect: prevEffect);
}
}
private WearableSprite GetWearableSprite(WearableType type)
{ {
var info = character.Info; var info = character.Info;
if (info == null) { return null; } if (info == null) { return null; }
ContentXElement element; ContentXElement element = info.FilterElements(info.Wearables, info.Head.Preset.TagSet, type)?.FirstOrDefault();
/*if (random) return element != null ? new WearableSprite(element.GetChildElement("sprite"), type) : null;
{
element = info.FilterElements(info.Wearables, info.Head.Preset.TagSet)?.GetRandom(Rand.RandSync.ClientOnly);
}
else
{*/
element = info.FilterElements(info.Wearables, info.Head.Preset.TagSet, type)?.FirstOrDefault();
//}
if (element != null)
{
return new WearableSprite(element.GetChildElement("sprite"), type);
}
return null;
} }
partial void RemoveProjSpecific() partial void RemoveProjSpecific()
@@ -1206,8 +1299,8 @@ namespace Barotrauma
LightSource?.Remove(); LightSource?.Remove();
LightSource = null; LightSource = null;
OtherWearables?.ForEach(w => w.Sprite.Remove()); OtherWearables.ForEach(w => w.Sprite.Remove());
OtherWearables = null; OtherWearables.Clear();
HuskSprite?.Sprite.Remove(); HuskSprite?.Sprite.Remove();
HuskSprite = null; HuskSprite = null;
@@ -92,7 +92,7 @@ namespace Barotrauma
public Option<ContentPackageId> UgcId = Option<ContentPackageId>.None(); public Option<ContentPackageId> UgcId = Option<ContentPackageId>.None();
public Option<DateTime> InstallTime = Option<DateTime>.None(); public Option<SerializableDateTime> InstallTime = Option<SerializableDateTime>.None();
public bool HasFile(File file) public bool HasFile(File file)
=> Files.Any(f => => Files.Any(f =>
@@ -120,7 +120,7 @@ namespace Barotrauma
public void DiscardHashAndInstallTime() public void DiscardHashAndInstallTime()
{ {
ExpectedHash = null; ExpectedHash = null;
InstallTime = Option<DateTime>.None(); InstallTime = Option<SerializableDateTime>.None();
} }
public static string IncrementModVersion(string modVersion) public static string IncrementModVersion(string modVersion)
@@ -159,8 +159,8 @@ namespace Barotrauma
addRootAttribute("gameversion", GameMain.Version); addRootAttribute("gameversion", GameMain.Version);
if (AltNames.Any()) { addRootAttribute("altnames", string.Join(",", AltNames)); } if (AltNames.Any()) { addRootAttribute("altnames", string.Join(",", AltNames)); }
if (ExpectedHash != null) { addRootAttribute("expectedhash", ExpectedHash.StringRepresentation); } if (ExpectedHash != null) { addRootAttribute("expectedhash", ExpectedHash.StringRepresentation); }
if (InstallTime.TryUnwrap(out var installTime)) { addRootAttribute("installtime", ToolBox.Epoch.FromDateTime(installTime)); } if (InstallTime.TryUnwrap(out var installTime)) { addRootAttribute("installtime", installTime); }
files.ForEach(f => rootElement.Add(f.ToXElement())); files.ForEach(f => rootElement.Add(f.ToXElement()));
doc.Add(rootElement); doc.Add(rootElement);
@@ -49,7 +49,7 @@ namespace Barotrauma
&& ugcId is SteamWorkshopId workshopId && ugcId is SteamWorkshopId workshopId
&& item.Id == workshopId.Value && item.Id == workshopId.Value
&& p.InstallTime.TryUnwrap(out var installTime) && p.InstallTime.TryUnwrap(out var installTime)
&& item.LatestUpdateTime <= installTime)) && item.LatestUpdateTime <= installTime.ToUtcValue()))
.ToArray(); .ToArray();
if (!needInstalling.Any()) { return Enumerable.Empty<Steamworks.Ugc.Item>(); } if (!needInstalling.Any()) { return Enumerable.Empty<Steamworks.Ugc.Item>(); }
@@ -1128,6 +1128,28 @@ namespace Barotrauma
}); });
AssignRelayToServer("debugdraw", false); AssignRelayToServer("debugdraw", false);
AssignOnExecute("devmode", (string[] args) =>
{
if (args.None() || !bool.TryParse(args[0], out bool state))
{
state = !GameMain.DevMode;
}
GameMain.DevMode = state;
if (GameMain.DevMode)
{
GameMain.LightManager.LightingEnabled = false;
GameMain.LightManager.LosEnabled = false;
}
else
{
GameMain.LightManager.LightingEnabled = true;
GameMain.LightManager.LosEnabled = true;
GameMain.LightManager.LosAlpha = 1f;
}
NewMessage("Dev mode " + (GameMain.DevMode ? "enabled" : "disabled"), Color.White);
});
AssignRelayToServer("devmode", false);
AssignOnExecute("debugdrawlocalization", (string[] args) => AssignOnExecute("debugdrawlocalization", (string[] args) =>
{ {
if (args.None() || !bool.TryParse(args[0], out bool state)) if (args.None() || !bool.TryParse(args[0], out bool state))
@@ -1229,12 +1251,14 @@ namespace Barotrauma
HumanAIController.debugai = !HumanAIController.debugai; HumanAIController.debugai = !HumanAIController.debugai;
if (HumanAIController.debugai) if (HumanAIController.debugai)
{ {
GameMain.DevMode = true;
GameMain.DebugDraw = true; GameMain.DebugDraw = true;
GameMain.LightManager.LightingEnabled = false; GameMain.LightManager.LightingEnabled = false;
GameMain.LightManager.LosEnabled = false; GameMain.LightManager.LosEnabled = false;
} }
else else
{ {
GameMain.DevMode = false;
GameMain.DebugDraw = false; GameMain.DebugDraw = false;
GameMain.LightManager.LightingEnabled = true; GameMain.LightManager.LightingEnabled = true;
GameMain.LightManager.LosEnabled = true; GameMain.LightManager.LosEnabled = true;
@@ -8,7 +8,8 @@ partial class CheckObjectiveAction : BinaryOptionAction
public enum CheckType public enum CheckType
{ {
Added, Added,
Completed Completed,
Incomplete
} }
[Serialize(CheckType.Completed, IsPropertySaveable.Yes)] [Serialize(CheckType.Completed, IsPropertySaveable.Yes)]
@@ -30,8 +31,13 @@ partial class CheckObjectiveAction : BinaryOptionAction
{ {
CheckType.Added => true, CheckType.Added => true,
CheckType.Completed => segment.IsCompleted, CheckType.Completed => segment.IsCompleted,
CheckType.Incomplete => !segment.IsCompleted,
_ => false _ => false
}; };
} }
else if (Type == CheckType.Incomplete)
{
success = true;
}
} }
} }
@@ -1,5 +1,6 @@
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace Barotrauma; namespace Barotrauma;
@@ -13,11 +14,22 @@ partial class UIHighlightAction : EventAction
bool useCircularFlash = false; bool useCircularFlash = false;
if (Id != ElementId.None) if (Id != ElementId.None)
{ {
FindAndFlashComponents(c => Equals(Id, c.UserData)); var predicate = (GUIComponent c) => c is not null && Equals(Id, c.UserData);
if (!FindAndFlashAddedComponents(predicate))
{
if (predicate(GUIMessageBox.VisibleBox))
{
Flash(GUIMessageBox.VisibleBox);
}
else
{
FindAndFlashMessageBoxComponents(predicate);
}
}
} }
else if (!EntityIdentifier.IsEmpty) else if (!EntityIdentifier.IsEmpty)
{ {
FindAndFlashComponents(c => FindAndFlashAddedComponents(c =>
c.UserData is MapEntityPrefab mep && mep.Identifier == EntityIdentifier || c.UserData is MapEntity me && me.Prefab.Identifier == EntityIdentifier); c.UserData is MapEntityPrefab mep && mep.Identifier == EntityIdentifier || c.UserData is MapEntity me && me.Prefab.Identifier == EntityIdentifier);
} }
else if (!OrderIdentifier.IsEmpty) else if (!OrderIdentifier.IsEmpty)
@@ -26,26 +38,26 @@ partial class UIHighlightAction : EventAction
bool foundMinimapNode = false; bool foundMinimapNode = false;
if (!OrderTargetTag.IsEmpty) if (!OrderTargetTag.IsEmpty)
{ {
foundMinimapNode = FindAndFlashComponents(c => foundMinimapNode = FindAndFlashAddedComponents(c =>
c.UserData is CrewManager.MinimapNodeData nodeData && nodeData.Order is Order order && c.UserData is CrewManager.MinimapNodeData nodeData && nodeData.Order is Order order &&
order.Identifier == OrderIdentifier && order.Option == OrderOption && order.TargetEntity is Item item && item.HasTag(OrderTargetTag)); order.Identifier == OrderIdentifier && order.Option == OrderOption && order.TargetEntity is Item item && item.HasTag(OrderTargetTag));
} }
if (!foundMinimapNode) if (!foundMinimapNode)
{ {
FindAndFlashComponents(c => c.UserData is Order order && order.Identifier == OrderIdentifier && order.Option == OrderOption, FindAndFlashAddedComponents(c => c.UserData is Order order && order.Identifier == OrderIdentifier && order.Option == OrderOption,
c => c.UserData is Order order && order.Identifier == OrderIdentifier, c => c.UserData is Order order && order.Identifier == OrderIdentifier,
c => Equals(OrderCategory, c.UserData)); c => Equals(OrderCategory, c.UserData));
} }
} }
bool FindAndFlashComponents(params Func<GUIComponent, bool>[] predicates) bool FindAndFlashComponents(IEnumerable<GUIComponent> components, params Func<GUIComponent, bool>[] predicates)
{ {
foreach (var predicate in predicates) foreach (var predicate in predicates)
{ {
if (HighlightMultiple) if (HighlightMultiple)
{ {
bool found = false; bool found = false;
foreach (var component in GUI.GetAdditions()) foreach (var component in components)
{ {
if (predicate(component)) if (predicate(component))
{ {
@@ -55,7 +67,7 @@ partial class UIHighlightAction : EventAction
}; };
return found; return found;
} }
else if (GUI.GetAdditions().FirstOrDefault(predicate) is GUIComponent component) else if (components.FirstOrDefault(predicate) is GUIComponent component)
{ {
Flash(component); Flash(component);
return true; return true;
@@ -64,6 +76,10 @@ partial class UIHighlightAction : EventAction
return false; return false;
} }
bool FindAndFlashAddedComponents(params Func<GUIComponent, bool>[] predicates) => FindAndFlashComponents(GUI.GetAdditions(), predicates);
bool FindAndFlashMessageBoxComponents(params Func<GUIComponent, bool>[] predicates) => FindAndFlashComponents(GUIMessageBox.VisibleBox?.GetAllChildren() ?? Enumerable.Empty<GUIComponent>(), predicates);
void Flash(GUIComponent component) void Flash(GUIComponent component)
{ {
if (component.FlashTimer <= 0.0f) if (component.FlashTimer <= 0.0f)
@@ -1143,14 +1143,13 @@ namespace Barotrauma
bool wrap = element.GetAttributeBool("wrap", true); bool wrap = element.GetAttributeBool("wrap", true);
Alignment alignment = Alignment alignment =
element.GetAttributeEnum("alignment", text.Contains('\n') ? Alignment.Left : Alignment.Center); element.GetAttributeEnum("alignment", text.Contains('\n') ? Alignment.Left : Alignment.Center);
GUIFont font; if (!GUIStyle.Fonts.TryGetValue(element.GetAttributeIdentifier("font", "Font"), out GUIFont font))
if (!GUIStyle.Fonts.TryGetValue(element.GetAttributeIdentifier("font", "Font"), out font))
{ {
font = GUIStyle.Font; font = GUIStyle.Font;
} }
var textBlock = new GUITextBlock(RectTransform.Load(element, parent), var textBlock = new GUITextBlock(RectTransform.Load(element, parent),
text, color, font, alignment, wrap: wrap, style: style) RichString.Rich(text), color, font, alignment, wrap: wrap, style: style)
{ {
TextScale = scale TextScale = scale
}; };
@@ -236,7 +236,8 @@ namespace Barotrauma
new GUIButton(new RectTransform(new Vector2(0.3f, 0.5f), buttonContainer.RectTransform, Anchor.Center), new GUIButton(new RectTransform(new Vector2(0.3f, 0.5f), buttonContainer.RectTransform, Anchor.Center),
style: "UIToggleButton") style: "UIToggleButton")
{ {
OnClicked = Close OnClicked = Close,
UserData = UIHighlightAction.ElementId.MessageBoxCloseButton
} }
}; };
InputType? closeInput = null; InputType? closeInput = null;
@@ -140,6 +140,7 @@ namespace Barotrauma
public readonly static GUIColor HealthBarColorLow = new GUIColor("HealthBarColorLow"); public readonly static GUIColor HealthBarColorLow = new GUIColor("HealthBarColorLow");
public readonly static GUIColor HealthBarColorMedium = new GUIColor("HealthBarColorMedium"); public readonly static GUIColor HealthBarColorMedium = new GUIColor("HealthBarColorMedium");
public readonly static GUIColor HealthBarColorHigh = new GUIColor("HealthBarColorHigh"); public readonly static GUIColor HealthBarColorHigh = new GUIColor("HealthBarColorHigh");
public readonly static GUIColor HealthBarColorPoisoned = new GUIColor("HealthBarColorPoisoned");
public static Point ItemFrameMargin public static Point ItemFrameMargin
{ {
@@ -615,7 +615,7 @@ namespace Barotrauma
listBackground.SetCrop(true); listBackground.SetCrop(true);
GUIFont font = GUIStyle.Font; GUIFont font = GUIStyle.Font;
info.CreateSpecsWindow(specsFrame, font); info.CreateSpecsWindow(specsFrame, font, includeCrushDepth: true);
descriptionTextBlock.Text = info.Description; descriptionTextBlock.Text = info.Description;
descriptionTextBlock.CalculateHeightFromText(); descriptionTextBlock.CalculateHeightFromText();
} }
@@ -1779,7 +1779,10 @@ namespace Barotrauma
{ {
CurrentSelectMode = GUIListBox.SelectMode.None CurrentSelectMode = GUIListBox.SelectMode.None
}; };
sub.Info.CreateSpecsWindow(specsListBox, GUIStyle.Font, includeTitle: false, includeClass: false, includeDescription: true); sub.Info.CreateSpecsWindow(specsListBox, GUIStyle.Font,
includeTitle: false,
includeClass: false,
includeDescription: true);
} }
} }
@@ -1171,7 +1171,7 @@ namespace Barotrauma
materialCostList.Visible = false; materialCostList.Visible = false;
materialCostList.UserData = UpgradeStoreUserData.MaterialCostList; materialCostList.UserData = UpgradeStoreUserData.MaterialCostList;
var priceText = new GUITextBlock(rectT(0.2f, 1f, buyButtonLayout), formattedPrice) var priceText = new GUITextBlock(rectT(0.2f, 1f, buyButtonLayout), formattedPrice, textAlignment: Alignment.Right)
{ {
UserData = UpgradeStoreUserData.PriceLabel, UserData = UpgradeStoreUserData.PriceLabel,
//prices on swappable items are always visible, upgrade prices are enabled in UpdateUpgradeEntry for purchasable upgrades //prices on swappable items are always visible, upgrade prices are enabled in UpdateUpgradeEntry for purchasable upgrades
@@ -23,9 +23,13 @@ namespace Barotrauma
{ {
class GameMain : Game class GameMain : Game
{ {
public static bool ShowFPS = false; public static bool ShowFPS;
public static bool ShowPerf = false; public static bool ShowPerf;
public static bool DebugDraw; public static bool DebugDraw;
/// <summary>
/// Doesn't automatically enable los or bot AI or do anything like that. Probably not fully implemented.
/// </summary>
public static bool DevMode;
public static bool IsSingleplayer => NetworkMember == null; public static bool IsSingleplayer => NetworkMember == null;
public static bool IsMultiplayer => NetworkMember != null; public static bool IsMultiplayer => NetworkMember != null;
@@ -398,7 +402,7 @@ namespace Barotrauma
TextureLoader.Init(GraphicsDevice); TextureLoader.Init(GraphicsDevice);
//do this here because we need it for the loading screen //do this here because we need it for the loading screen
WaterRenderer.Instance = new WaterRenderer(base.GraphicsDevice, Content); WaterRenderer.Instance = new WaterRenderer(base.GraphicsDevice);
Quad.Init(GraphicsDevice); Quad.Init(GraphicsDevice);
@@ -512,10 +516,10 @@ namespace Barotrauma
TitleScreen.LoadState = 75.0f; TitleScreen.LoadState = 75.0f;
yield return CoroutineStatus.Running; yield return CoroutineStatus.Running;
GameScreen = new GameScreen(GraphicsDeviceManager.GraphicsDevice, Content); GameScreen = new GameScreen(GraphicsDeviceManager.GraphicsDevice);
ParticleManager = new ParticleManager(GameScreen.Cam); ParticleManager = new ParticleManager(GameScreen.Cam);
LightManager = new Lights.LightManager(base.GraphicsDevice, Content); LightManager = new Lights.LightManager(base.GraphicsDevice);
TitleScreen.LoadState = 80.0f; TitleScreen.LoadState = 80.0f;
yield return CoroutineStatus.Running; yield return CoroutineStatus.Running;
@@ -788,7 +788,6 @@ namespace Barotrauma
{ {
return; return;
} }
if (ws != null) if (ws != null)
{ {
hull = Hull.FindHull(ws.WorldPosition); hull = Hull.FindHull(ws.WorldPosition);
@@ -802,7 +801,6 @@ namespace Barotrauma
hull = Hull.FindHull(se.WorldPosition); hull = Hull.FindHull(se.WorldPosition);
} }
} }
if (IsSinglePlayer) if (IsSinglePlayer)
{ {
order.OrderGiver?.Speak(order.GetChatMessage("", hull?.DisplayName?.Value, givingOrderToSelf: character == order.OrderGiver, isNewOrder: isNewOrder), ChatMessageType.Order); order.OrderGiver?.Speak(order.GetChatMessage("", hull?.DisplayName?.Value, givingOrderToSelf: character == order.OrderGiver, isNewOrder: isNewOrder), ChatMessageType.Order);
@@ -817,13 +815,13 @@ namespace Barotrauma
{ {
//can't issue an order if no characters are available //can't issue an order if no characters are available
if (character == null) { return; } if (character == null) { return; }
var orderGiver = order?.OrderGiver; var orderGiver = order?.OrderGiver;
if (IsSinglePlayer) if (IsSinglePlayer)
{ {
character.SetOrder(order, isNewOrder, speak: orderGiver != character); bool isGivingOrderToSelf = orderGiver == character;
string message = order?.GetChatMessage(character.Name, orderGiver?.CurrentHull?.DisplayName?.Value, givingOrderToSelf: character == orderGiver, orderOption: order?.Option ?? Identifier.Empty, isNewOrder: isNewOrder); character.SetOrder(order, isNewOrder, speak: !isGivingOrderToSelf);
orderGiver?.Speak(message); string message = order?.GetChatMessage(character.Name, orderGiver?.CurrentHull?.DisplayName?.Value, isGivingOrderToSelf, orderOption: order?.Option ?? Identifier.Empty, isNewOrder: isNewOrder);
orderGiver?.Speak(message);
} }
else if (orderGiver != null) else if (orderGiver != null)
{ {
@@ -965,7 +965,7 @@ namespace Barotrauma
break; break;
case QuickUseAction.PutToEquippedItem: case QuickUseAction.PutToEquippedItem:
//order by the condition of the contained item to prefer putting into the item with the emptiest ammo/battery/tank //order by the condition of the contained item to prefer putting into the item with the emptiest ammo/battery/tank
foreach (Item heldItem in character.HeldItems.OrderBy(it => it.GetComponent<ItemContainer>()?.GetContainedIndicatorState() ?? 0.0f)) foreach (Item heldItem in character.HeldItems.OrderByDescending(heldItem => GetContainPriority(item, heldItem)))
{ {
if (heldItem.OwnInventory == null) { continue; } if (heldItem.OwnInventory == null) { continue; }
//don't allow swapping if we're moving items into an item with 1 slot holding a stack of items //don't allow swapping if we're moving items into an item with 1 slot holding a stack of items
@@ -986,6 +986,22 @@ namespace Barotrauma
} }
} }
break; break;
static float GetContainPriority(Item item, Item containerItem)
{
var container = containerItem.GetComponent<ItemContainer>();
if (container == null) { return 0.0f; }
for (int i = 0; i < container.Inventory.Capacity; i++)
{
var containedItems = container.Inventory.GetItemsAt(i);
if (containedItems.Any() && container.Inventory.CanBePutInSlot(item, i))
{
//if there's a stack in the contained item that we can add the item to, prefer that
return 10.0f;
}
}
return -container.GetContainedIndicatorState();
}
} }
if (success) if (success)
@@ -216,16 +216,19 @@ namespace Barotrauma.Items.Components
if (brokenSprite == null || !IsBroken) if (brokenSprite == null || !IsBroken)
{ {
spriteBatch.Draw(doorSprite.Texture, pos, if (doorSprite?.Texture != null)
getSourceRect(doorSprite, openState, IsHorizontal), {
color, 0.0f, doorSprite.Origin, item.Scale, item.SpriteEffects, doorSprite.Depth); spriteBatch.Draw(doorSprite.Texture, pos,
getSourceRect(doorSprite, openState, IsHorizontal),
color, 0.0f, doorSprite.Origin, item.Scale, item.SpriteEffects, doorSprite.Depth);
}
} }
float maxCondition = item.Repairables.Any() ? float maxCondition = item.Repairables.Any() ?
item.Repairables.Min(r => r.RepairThreshold) / 100.0f * item.MaxCondition : item.Repairables.Min(r => r.RepairThreshold) / 100.0f * item.MaxCondition :
item.MaxCondition; item.MaxCondition;
float healthRatio = item.Health / maxCondition; float healthRatio = item.Health / maxCondition;
if (brokenSprite != null && healthRatio < 1.0f) if (brokenSprite?.Texture != null && healthRatio < 1.0f)
{ {
Vector2 scale = scaleBrokenSprite ? new Vector2(1.0f - healthRatio) : Vector2.One; Vector2 scale = scaleBrokenSprite ? new Vector2(1.0f - healthRatio) : Vector2.One;
if (IsHorizontal) { scale.X = 1; } else { scale.Y = 1; } if (IsHorizontal) { scale.X = 1; } else { scale.Y = 1; }
@@ -285,34 +288,45 @@ namespace Barotrauma.Items.Components
//sent by the server, or reverting it back to its old state if no msg from server was received //sent by the server, or reverting it back to its old state if no msg from server was received
PredictedState = open; PredictedState = open;
resetPredictionTimer = CorrectionDelay; resetPredictionTimer = CorrectionDelay;
if (stateChanged) PlaySound(forcedOpen ? ActionType.OnPicked : ActionType.OnUse); if (stateChanged && !IsBroken)
{
PlayInteractionSound();
}
} }
else else
{ {
isOpen = open; isOpen = open;
if (!isNetworkMessage || open != PredictedState) if (!isNetworkMessage || open != PredictedState)
{ {
StopPicking(null); StopPicking(null);
ActionType actionType = ActionType.OnUse; if (!IsBroken)
if (forcedOpen)
{ {
actionType = ActionType.OnPicked; PlayInteractionSound();
} }
else
{
if (open && HasSoundsOfType[(int)ActionType.OnOpen])
{
actionType = ActionType.OnOpen;
}
else if (!open && HasSoundsOfType[(int)ActionType.OnClose])
{
actionType = ActionType.OnClose;
}
}
PlaySound(actionType);
if (isOpen) { stuck = MathHelper.Clamp(stuck - StuckReductionOnOpen, 0.0f, 100.0f); } if (isOpen) { stuck = MathHelper.Clamp(stuck - StuckReductionOnOpen, 0.0f, 100.0f); }
} }
} }
void PlayInteractionSound()
{
ActionType actionType = ActionType.OnUse;
if (forcedOpen)
{
actionType = ActionType.OnPicked;
}
else
{
if (open && HasSoundsOfType[(int)ActionType.OnOpen])
{
actionType = ActionType.OnOpen;
}
else if (!open && HasSoundsOfType[(int)ActionType.OnClose])
{
actionType = ActionType.OnClose;
}
}
PlaySound(actionType);
}
} }
public override void ClientEventRead(IReadMessage msg, float sendingTime) public override void ClientEventRead(IReadMessage msg, float sendingTime)
@@ -78,14 +78,21 @@ namespace Barotrauma.Items.Components
public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1)
{ {
if (Light.LightSprite != null && (item.body == null || item.body.Enabled) && lightBrightness > 0.0f && IsOn && Light.Enabled) if (Light?.LightSprite == null) { return; }
if ((item.body == null || item.body.Enabled) && lightBrightness > 0.0f && IsOn && Light.Enabled)
{ {
Vector2 origin = Light.LightSprite.Origin; Vector2 origin = Light.LightSprite.Origin;
if ((Light.LightSpriteEffect & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally) { origin.X = Light.LightSprite.SourceRect.Width - origin.X; } if ((Light.LightSpriteEffect & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally) { origin.X = Light.LightSprite.SourceRect.Width - origin.X; }
if ((Light.LightSpriteEffect & SpriteEffects.FlipVertically) == SpriteEffects.FlipVertically) { origin.Y = Light.LightSprite.SourceRect.Height - origin.Y; } if ((Light.LightSpriteEffect & SpriteEffects.FlipVertically) == SpriteEffects.FlipVertically) { origin.Y = Light.LightSprite.SourceRect.Height - origin.Y; }
Vector2 drawPos = item.body?.DrawPosition ?? item.DrawPosition; Vector2 drawPos = item.body?.DrawPosition ?? item.DrawPosition;
Light.LightSprite.Draw(spriteBatch, new Vector2(drawPos.X, -drawPos.Y), lightColor * lightBrightness, origin, -Light.Rotation, item.Scale, Light.LightSpriteEffect, itemDepth - 0.0001f);
Color color = lightColor;
if (Light.OverrideLightSpriteAlpha.HasValue)
{
color = new Color(lightColor, Light.OverrideLightSpriteAlpha.Value);
}
Light.LightSprite.Draw(spriteBatch, new Vector2(drawPos.X, -drawPos.Y), color * lightBrightness, origin, -Light.Rotation, item.Scale, Light.LightSpriteEffect, itemDepth - 0.0001f);
} }
} }
@@ -413,7 +413,7 @@ namespace Barotrauma.Items.Components
var wire = it.GetComponent<Wire>(); var wire = it.GetComponent<Wire>();
if (wire != null && wire.Connections.Any(c => c != null)) { return false; } if (wire != null && wire.Connections.Any(c => c != null)) { return false; }
if (it.Container?.GetComponent<ItemContainer>() is { DrawInventory: false }) { return false; } if (it.Container?.GetComponent<ItemContainer>() is { DrawInventory: false } or { AllowAccess: false }) { return false; }
if (it.HasTag("traitormissionitem")) { return false; } if (it.HasTag("traitormissionitem")) { return false; }
@@ -72,7 +72,9 @@ namespace Barotrauma.Items.Components
public override bool RecreateGUIOnResolutionChange => true; public override bool RecreateGUIOnResolutionChange => true;
public bool TriggerInfographic { get; set; } public bool TriggerInfographic { get; set; }
public bool IsInfographicVisible => infographic != null && infographic.Visible;
partial void InitProjSpecific(ContentXElement element) partial void InitProjSpecific(ContentXElement element)
{ {
CreateGUI(); CreateGUI();
@@ -108,6 +110,9 @@ namespace Barotrauma.Items.Components
{ AbsoluteOffset = GUIStyle.ItemFrameOffset }, { AbsoluteOffset = GUIStyle.ItemFrameOffset },
isHorizontal: true) isHorizontal: true)
{ {
CanBeFocused = true,
HoverCursor = CursorState.Default,
AlwaysOverrideCursor = true,
RelativeSpacing = 0.012f, RelativeSpacing = 0.012f,
Stretch = true Stretch = true
}; };
@@ -675,7 +680,7 @@ namespace Barotrauma.Items.Components
} }
} }
if (TriggerInfographic) if (GuiFrame is not null && GuiFrame.Visible && TriggerInfographic)
{ {
CreateInfrographic(); CreateInfrographic();
TriggerInfographic = false; TriggerInfographic = false;
@@ -851,8 +856,9 @@ namespace Barotrauma.Items.Components
{ {
AbsoluteOffset = new Point(0, -50).Multiply(GUI.Scale) AbsoluteOffset = new Point(0, -50).Multiply(GUI.Scale)
}; };
new GUIButton(closeButtonRt, TextManager.Get("close")) new GUIButton(closeButtonRt, TextManager.Get("closeinfographic"))
{ {
UserData = UIHighlightAction.ElementId.CloseButton,
OnClicked = (_, _) => OnClicked = (_, _) =>
{ {
CloseInfographic(Character.Controlled); CloseInfographic(Character.Controlled);
@@ -871,6 +877,7 @@ namespace Barotrauma.Items.Components
string style = arrowStyle == InfographicArrowStyle.Straight ? "InfographicArrow" : "InfographicArrowCurved"; string style = arrowStyle == InfographicArrowStyle.Straight ? "InfographicArrow" : "InfographicArrowCurved";
return new GUIImage(rt, style) return new GUIImage(rt, style)
{ {
CanBeFocused = false,
Rotation = MathHelper.ToRadians(rotationDegrees), Rotation = MathHelper.ToRadians(rotationDegrees),
SpriteEffects = spriteEffects SpriteEffects = spriteEffects
}; };
@@ -329,6 +329,7 @@ namespace Barotrauma.Items.Components
partial void UpdateSignalsProjSpecific() partial void UpdateSignalsProjSpecific()
{ {
if (signals == null) { return; }
for (int i = 0; i < signals.Length && i < uiElements.Count; i++) for (int i = 0; i < signals.Length && i < uiElements.Count; i++)
{ {
if (uiElements[i] is GUITextBox tb) if (uiElements[i] is GUITextBox tb)
@@ -9,7 +9,7 @@ namespace Barotrauma.Items.Components
{ {
private static void GetDamageModifierText(ref LocalizedString description, DamageModifier damageModifier, Identifier afflictionIdentifier) private static void GetDamageModifierText(ref LocalizedString description, DamageModifier damageModifier, Identifier afflictionIdentifier)
{ {
int roundedValue = (int)Math.Round((1 - damageModifier.DamageMultiplier * damageModifier.ProbabilityMultiplier) * 100); int roundedValue = (int)Math.Round((1 - Math.Min(damageModifier.DamageMultiplier, damageModifier.ProbabilityMultiplier)) * 100);
if (roundedValue == 0) { return; } if (roundedValue == 0) { return; }
string colorStr = XMLExtensions.ToStringHex(GUIStyle.Green); string colorStr = XMLExtensions.ToStringHex(GUIStyle.Green);
@@ -18,7 +18,7 @@ namespace Barotrauma.Items.Components
TextManager.Get($"afflictiontype.{afflictionIdentifier}").Fallback(afflictionIdentifier.Value); TextManager.Get($"afflictiontype.{afflictionIdentifier}").Fallback(afflictionIdentifier.Value);
if (!description.IsNullOrWhiteSpace()) { description += '\n'; } if (!description.IsNullOrWhiteSpace()) { description += '\n'; }
description += $" ‖color:{colorStr}‖{roundedValue.ToString("-0;+#")}%‖color:end‖ {afflictionName}"; description += $" ‖color:{colorStr}‖{roundedValue:-0;+#}%‖color:end‖ {afflictionName}";
} }
public override void AddTooltipInfo(ref LocalizedString name, ref LocalizedString description) public override void AddTooltipInfo(ref LocalizedString name, ref LocalizedString description)
@@ -36,7 +36,6 @@ namespace Barotrauma.Items.Components
{ {
continue; continue;
} }
foreach (Identifier afflictionIdentifier in damageModifier.ParsedAfflictionIdentifiers) foreach (Identifier afflictionIdentifier in damageModifier.ParsedAfflictionIdentifiers)
{ {
GetDamageModifierText(ref description, damageModifier, afflictionIdentifier); GetDamageModifierText(ref description, damageModifier, afflictionIdentifier);
@@ -1713,6 +1713,15 @@ namespace Barotrauma
GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, Color.White); GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, Color.White);
} }
} }
if (HealingCooldown.IsOnCooldown && item.HasTag(HealingCooldown.MedicalItemTag))
{
RectangleF cdRect = rect;
// shrink the rect from top to bottom depending on HealingCooldown.NormalizedCooldown
cdRect.Height *= HealingCooldown.NormalizedCooldown;
cdRect.Y += rect.Height;
GUI.DrawFilledRectangle(spriteBatch, cdRect, Color.White * 0.5f);
}
} }
if (inventory != null && if (inventory != null &&
@@ -203,7 +203,7 @@ namespace Barotrauma
} }
} }
partial void InitProjSpecific() public void InitSpriteStates()
{ {
Prefab.Sprite?.EnsureLazyLoaded(); Prefab.Sprite?.EnsureLazyLoaded();
Prefab.InventoryIcon?.EnsureLazyLoaded(); Prefab.InventoryIcon?.EnsureLazyLoaded();
@@ -211,7 +211,6 @@ namespace Barotrauma
{ {
brokenSprite.Sprite.EnsureLazyLoaded(); brokenSprite.Sprite.EnsureLazyLoaded();
} }
foreach (var decorativeSprite in Prefab.DecorativeSprites) foreach (var decorativeSprite in Prefab.DecorativeSprites)
{ {
decorativeSprite.Sprite.EnsureLazyLoaded(); decorativeSprite.Sprite.EnsureLazyLoaded();
@@ -221,6 +220,11 @@ namespace Barotrauma
UpdateSpriteStates(0.0f); UpdateSpriteStates(0.0f);
} }
partial void InitProjSpecific()
{
InitSpriteStates();
}
private Rectangle? cachedVisibleExtents; private Rectangle? cachedVisibleExtents;
public void ResetCachedVisibleSize() public void ResetCachedVisibleSize()
@@ -1409,7 +1413,7 @@ namespace Barotrauma
if (targetComponent == null) if (targetComponent == null)
{ {
ApplyStatusEffects(actionType, 1.0f, targetCharacter, targetLimb, useTarget, true, worldPosition: worldPosition); ApplyStatusEffects(actionType, 1.0f, targetCharacter, targetLimb, useTarget, isNetworkEvent: true, worldPosition: worldPosition);
} }
else else
{ {
@@ -236,6 +236,16 @@ namespace Barotrauma
DecorativeSprites = decorativeSprites.ToImmutableArray(); DecorativeSprites = decorativeSprites.ToImmutableArray();
ContainedSprites = containedSprites.ToImmutableArray(); ContainedSprites = containedSprites.ToImmutableArray();
DecorativeSpriteGroups = decorativeSpriteGroups.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary(); DecorativeSpriteGroups = decorativeSpriteGroups.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary();
#if CLIENT
foreach (Item item in Item.ItemList)
{
if (item.Prefab == this)
{
item.InitSpriteStates();
}
}
#endif
} }
public bool CanCharacterBuy() public bool CanCharacterBuy()
@@ -1,5 +1,4 @@
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -65,15 +64,9 @@ namespace Barotrauma
public Texture2D WaterTexture { get; } public Texture2D WaterTexture { get; }
public WaterRenderer(GraphicsDevice graphicsDevice, ContentManager content) public WaterRenderer(GraphicsDevice graphicsDevice)
{ {
#if WINDOWS WaterEffect = EffectLoader.Load("Effects/watershader");
WaterEffect = content.Load<Effect>("Effects/watershader");
#endif
#if LINUX || OSX
WaterEffect = content.Load<Effect>("Effects/watershader_opengl");
#endif
WaterTexture = TextureLoader.FromFile("Content/Effects/waterbump.png"); WaterTexture = TextureLoader.FromFile("Content/Effects/waterbump.png");
WaterEffect.Parameters["xWaterBumpMap"].SetValue(WaterTexture); WaterEffect.Parameters["xWaterBumpMap"].SetValue(WaterTexture);
@@ -1,6 +1,5 @@
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System; using System;
@@ -73,12 +72,14 @@ namespace Barotrauma.Lights
private int recalculationCount; private int recalculationCount;
private float time;
public IEnumerable<LightSource> Lights public IEnumerable<LightSource> Lights
{ {
get { return lights; } get { return lights; }
} }
public LightManager(GraphicsDevice graphics, ContentManager content) public LightManager(GraphicsDevice graphics)
{ {
lights = new List<LightSource>(100); lights = new List<LightSource>(100);
@@ -96,13 +97,8 @@ namespace Barotrauma.Lights
{ {
CreateRenderTargets(graphics); CreateRenderTargets(graphics);
#if WINDOWS LosEffect = EffectLoader.Load("Effects/losshader");
LosEffect = content.Load<Effect>("Effects/losshader"); SolidColorEffect = EffectLoader.Load("Effects/solidcolor");
SolidColorEffect = content.Load<Effect>("Effects/solidcolor");
#else
LosEffect = content.Load<Effect>("Effects/losshader_opengl");
SolidColorEffect = content.Load<Effect>("Effects/solidcolor_opengl");
#endif
if (lightEffect == null) if (lightEffect == null)
{ {
@@ -171,10 +167,12 @@ namespace Barotrauma.Lights
public void Update(float deltaTime) public void Update(float deltaTime)
{ {
//wrap around if the timer gets very large, otherwise we'd start running into floating point accuracy issues
time = (time + deltaTime) % 100000.0f;
foreach (LightSource light in activeLights) foreach (LightSource light in activeLights)
{ {
if (!light.Enabled) { continue; } if (!light.Enabled) { continue; }
light.Update(deltaTime); light.Update(time);
} }
} }
@@ -200,8 +200,6 @@ namespace Barotrauma.Lights
private static Texture2D lightTexture; private static Texture2D lightTexture;
private float blinkTimer, flickerState, pulseState;
private VertexPositionColorTexture[] vertices; private VertexPositionColorTexture[] vertices;
private short[] indices; private short[] indices;
@@ -486,12 +484,12 @@ namespace Barotrauma.Lights
if (addLight) { GameMain.LightManager.AddLight(this); } if (addLight) { GameMain.LightManager.AddLight(this); }
} }
public void Update(float deltaTime) public void Update(float time)
{ {
float brightness = 1.0f; float brightness = 1.0f;
if (lightSourceParams.BlinkFrequency > 0.0f) if (lightSourceParams.BlinkFrequency > 0.0f)
{ {
blinkTimer = (blinkTimer + deltaTime * lightSourceParams.BlinkFrequency) % 1.0f; float blinkTimer = (time * lightSourceParams.BlinkFrequency) % 1.0f;
if (blinkTimer > 0.5f) if (blinkTimer > 0.5f)
{ {
CurrentBrightness = 0.0f; CurrentBrightness = 0.0f;
@@ -500,14 +498,13 @@ namespace Barotrauma.Lights
} }
if (lightSourceParams.PulseFrequency > 0.0f && lightSourceParams.PulseAmount > 0.0f) if (lightSourceParams.PulseFrequency > 0.0f && lightSourceParams.PulseAmount > 0.0f)
{ {
pulseState = (pulseState + deltaTime * lightSourceParams.PulseFrequency) % 1.0f; float pulseState = (time * lightSourceParams.PulseFrequency) % 1.0f;
//oscillate between 0-1 //oscillate between 0-1
brightness *= 1.0f - (float)(Math.Sin(pulseState * MathHelper.TwoPi) + 1.0f) / 2.0f * lightSourceParams.PulseAmount; brightness *= 1.0f - (float)(Math.Sin(pulseState * MathHelper.TwoPi) + 1.0f) / 2.0f * lightSourceParams.PulseAmount;
} }
if (lightSourceParams.Flicker > 0.0f) if (lightSourceParams.Flicker > 0.0f && lightSourceParams.FlickerSpeed > 0.0f)
{ {
flickerState += deltaTime * lightSourceParams.FlickerSpeed; float flickerState = (time * lightSourceParams.FlickerSpeed) % 255;
flickerState %= 255;
brightness *= 1.0f - PerlinNoise.GetPerlin(flickerState, flickerState * 0.5f) * lightSourceParams.Flicker; brightness *= 1.0f - PerlinNoise.GetPerlin(flickerState, flickerState * 0.5f) * lightSourceParams.Flicker;
} }
CurrentBrightness = brightness; CurrentBrightness = brightness;
@@ -68,7 +68,7 @@ namespace Barotrauma
private (Rectangle targetArea, RichString tip)? tooltip; private (Rectangle targetArea, RichString tip)? tooltip;
private (SubmarineInfo pendingSub, float realWorldCrushDepth) pendingSubInfo; private SubmarineInfo.PendingSubInfo pendingSubInfo;
private RichString beaconStationActiveText, beaconStationInactiveText; private RichString beaconStationActiveText, beaconStationInactiveText;
@@ -936,39 +936,8 @@ namespace Barotrauma
if (connection.LevelData.HasHuntingGrounds) { iconCount++; } if (connection.LevelData.HasHuntingGrounds) { iconCount++; }
if (connection.Locked) { iconCount++; } if (connection.Locked) { iconCount++; }
string tooltip = null; string tooltip = null;
float subCrushDepth = Level.DefaultRealWorldCrushDepth;
var currentOrPendingSub = SubmarineSelection.CurrentOrPendingSubmarine();
if (Submarine.MainSub != null && Submarine.MainSub.Info == currentOrPendingSub)
{
subCrushDepth = Submarine.MainSub.RealWorldCrushDepth;
}
else if (currentOrPendingSub != null)
{
if (pendingSubInfo.pendingSub != currentOrPendingSub)
{
// Store the real world crush depth for the pending sub so that we don't have to calculate it again every time
pendingSubInfo = (currentOrPendingSub, currentOrPendingSub.GetRealWorldCrushDepth());
}
subCrushDepth = pendingSubInfo.realWorldCrushDepth;
}
if (GameMain.GameSession?.Campaign?.UpgradeManager != null)
{
var hullUpgradePrefab = UpgradePrefab.Find("increasewallhealth".ToIdentifier());
if (hullUpgradePrefab != null)
{
int pendingLevel = GameMain.GameSession.Campaign.UpgradeManager.GetUpgradeLevel(hullUpgradePrefab, hullUpgradePrefab.UpgradeCategories.First());
int currentLevel = GameMain.GameSession.Campaign.UpgradeManager.GetRealUpgradeLevel(hullUpgradePrefab, hullUpgradePrefab.UpgradeCategories.First());
if (pendingLevel > currentLevel)
{
string updateValueStr = hullUpgradePrefab.SourceElement?.GetChildElement("Structure")?.GetAttributeString("crushdepth", null);
if (!string.IsNullOrEmpty(updateValueStr))
{
subCrushDepth = PropertyReference.CalculateUpgrade(subCrushDepth, pendingLevel - currentLevel, updateValueStr);
}
}
}
}
float subCrushDepth = SubmarineInfo.GetSubCrushDepth(SubmarineSelection.CurrentOrPendingSubmarine(), ref pendingSubInfo);
string crushDepthWarningIconStyle = null; string crushDepthWarningIconStyle = null;
if (connection.LevelData.InitialDepth * Physics.DisplayToRealWorldRatio > subCrushDepth) if (connection.LevelData.InitialDepth * Physics.DisplayToRealWorldRatio > subCrushDepth)
{ {
@@ -1125,6 +1094,14 @@ namespace Barotrauma
} }
} }
/// <summary>
/// Resets <see cref="pendingSubInfo"/> and forces crush depth to be calculated again for icon displaying purposes
/// </summary>
public void ResetPendingSub()
{
pendingSubInfo = new SubmarineInfo.PendingSubInfo();
}
partial void RemoveProjSpecific() partial void RemoveProjSpecific()
{ {
noiseOverlay?.Remove(); noiseOverlay?.Remove();
@@ -89,7 +89,11 @@ namespace Barotrauma
CreateSpecsWindow(descriptionBox, font, includeDescription: true); CreateSpecsWindow(descriptionBox, font, includeDescription: true);
} }
public void CreateSpecsWindow(GUIListBox parent, GUIFont font, bool includeTitle = true, bool includeClass = true, bool includeDescription = false) public void CreateSpecsWindow(GUIListBox parent, GUIFont font,
bool includeTitle = true,
bool includeClass = true,
bool includeDescription = false,
bool includeCrushDepth = false)
{ {
float leftPanelWidth = 0.6f; float leftPanelWidth = 0.6f;
float rightPanelWidth = 0.4f / leftPanelWidth; float rightPanelWidth = 0.4f / leftPanelWidth;
@@ -155,6 +159,22 @@ namespace Barotrauma
{ CanBeFocused = false }; { CanBeFocused = false };
cargoCapacityText.RectTransform.MinSize = new Point(0, cargoCapacityText.Children.First().Rect.Height); cargoCapacityText.RectTransform.MinSize = new Point(0, cargoCapacityText.Children.First().Rect.Height);
if (includeCrushDepth)
{
var crushDepthText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), parent.Content.RectTransform),
TextManager.Get("CrushDepth"), textAlignment: Alignment.TopLeft, font: font, wrap: true)
{
CanBeFocused = false
};
new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), crushDepthText.RectTransform, Anchor.TopRight, Pivot.TopLeft),
TextManager.GetWithVariable("meterformat", "[meters]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetSubCrushDepth())),
textAlignment: Alignment.TopLeft, font: font, wrap: true)
{
CanBeFocused = false
};
crushDepthText.RectTransform.MinSize = new Point(0, crushDepthText.Children.First().Rect.Height);
}
if (RecommendedCrewSizeMax > 0) if (RecommendedCrewSizeMax > 0)
{ {
var crewSizeText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), parent.Content.RectTransform), var crewSizeText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), parent.Content.RectTransform),
@@ -227,5 +247,57 @@ namespace Barotrauma
GUITextBlock.AutoScaleAndNormalize(parent.Content.GetAllChildren<GUITextBlock>().Where(c => c != submarineNameText && c != descBlock)); GUITextBlock.AutoScaleAndNormalize(parent.Content.GetAllChildren<GUITextBlock>().Where(c => c != submarineNameText && c != descBlock));
parent.ForceLayoutRecalculation(); parent.ForceLayoutRecalculation();
} }
public readonly record struct PendingSubInfo(SubmarineInfo PendingSub = null, bool StructuresDefineRealWorldCrushDepth = false, float RealWorldCrushDepth = Level.DefaultRealWorldCrushDepth);
private float GetSubCrushDepth()
{
var pendingSubInfo = new PendingSubInfo();
return GetSubCrushDepth(this, ref pendingSubInfo);
}
public static float GetSubCrushDepth(SubmarineInfo subInfo, ref PendingSubInfo pendingSubInfo)
{
float subCrushDepth = Level.DefaultRealWorldCrushDepth;
if (Submarine.MainSub != null && Submarine.MainSub.Info == subInfo)
{
subCrushDepth = Submarine.MainSub.RealWorldCrushDepth;
}
else if (subInfo != null)
{
if (pendingSubInfo.PendingSub != subInfo)
{
// Store the real world crush depth for the pending sub so that we don't have to calculate it again every time
pendingSubInfo = new PendingSubInfo(subInfo, subInfo.IsCrushDepthDefinedInStructures(out float realWorldCrushDepth), realWorldCrushDepth);
}
subCrushDepth = pendingSubInfo.RealWorldCrushDepth;
}
if (GameMain.GameSession?.Campaign?.UpgradeManager != null && UpgradePrefab.Find("increasewallhealth".ToIdentifier()) is UpgradePrefab hullUpgradePrefab)
{
int pendingLevel = GameMain.GameSession.Campaign.UpgradeManager.GetUpgradeLevel(hullUpgradePrefab, hullUpgradePrefab.UpgradeCategories.First(), info: subInfo);
// If there is a sub switch pending, unless its structures have crush depth defined in their elements,
// calculate the value based on the default crush depth and pending upgrade level
int currentLevel = 0;
if (pendingSubInfo.PendingSub is null || pendingSubInfo.StructuresDefineRealWorldCrushDepth)
{
currentLevel = GameMain.GameSession.Campaign.UpgradeManager.GetRealUpgradeLevelForSub(hullUpgradePrefab, hullUpgradePrefab.UpgradeCategories.First(), subInfo);
}
if (pendingLevel > currentLevel)
{
string updateValueStr = hullUpgradePrefab.SourceElement?.GetChildElement("Structure")?.GetAttributeString("crushdepth", null);
if (!string.IsNullOrEmpty(updateValueStr))
{
if (currentLevel > 0)
{
// If the current level is greater than 0, reset the crush depth value back to the base value before calculating the upgrade
int upgradePercentage = UpgradePrefab.ParsePercentage(updateValueStr, Identifier.Empty, suppressWarnings: true);
subCrushDepth /= (1f + (upgradePercentage / 100f * currentLevel));
}
subCrushDepth = PropertyReference.CalculateUpgrade(subCrushDepth, pendingLevel, updateValueStr);
}
}
}
return subCrushDepth;
}
} }
} }
@@ -45,7 +45,7 @@ namespace Barotrauma
} }
} }
private struct Door private readonly struct Door
{ {
public readonly Rectangle Rect; public readonly Rectangle Rect;
@@ -153,7 +153,9 @@ namespace Barotrauma
ScrollBarVisible = false, ScrollBarVisible = false,
Spacing = GUI.IntScale(5) Spacing = GUI.IntScale(5)
}; };
subInfo.CreateSpecsWindow(specsContainer, GUIStyle.Font, includeTitle: false, includeDescription: true); subInfo.CreateSpecsWindow(specsContainer, GUIStyle.Font,
includeTitle: false,
includeDescription: true);
int width = specsContainer.Rect.Width; int width = specsContainer.Rect.Width;
void recalculateSpecsContainerHeight() void recalculateSpecsContainerHeight()
{ {
@@ -191,6 +193,7 @@ namespace Barotrauma
TaskPool.Add(nameof(GeneratePreviewMeshes), GeneratePreviewMeshes(), _ => TaskPool.Add(nameof(GeneratePreviewMeshes), GeneratePreviewMeshes(), _ =>
{ {
if (isDisposed) { return; }
// Reset the camera's position on the main thread, // Reset the camera's position on the main thread,
// because the Camera class is not thread-safe and // because the Camera class is not thread-safe and
// it's possible for its state to not get updated // it's possible for its state to not get updated
@@ -12,7 +12,7 @@ namespace Barotrauma.Networking
string name, string name,
Either<Address, AccountId> addressOrAccountId, Either<Address, AccountId> addressOrAccountId,
string reason, string reason,
DateTime? expiration) Option<SerializableDateTime> expiration)
{ {
this.Name = name; this.Name = name;
this.AddressOrAccountId = addressOrAccountId; this.AddressOrAccountId = addressOrAccountId;
@@ -94,8 +94,9 @@ namespace Barotrauma.Networking
topArea.ForceLayoutRecalculation(); topArea.ForceLayoutRecalculation();
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedPlayerFrame.RectTransform), new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedPlayerFrame.RectTransform),
bannedPlayer.ExpirationTime == null ? bannedPlayer.ExpirationTime.TryUnwrap(out var expirationTime)
TextManager.Get("BanPermanent") : TextManager.GetWithVariable("BanExpires", "[time]", bannedPlayer.ExpirationTime.Value.ToString()), ? TextManager.GetWithVariable("BanExpires", "[time]", expirationTime.ToLocalUserString())
: TextManager.Get("BanPermanent"),
font: GUIStyle.SmallFont); font: GUIStyle.SmallFont);
LocalizedString reason = TextManager.GetServerMessage(bannedPlayer.Reason).Fallback(bannedPlayer.Reason); LocalizedString reason = TextManager.GetServerMessage(bannedPlayer.Reason).Fallback(bannedPlayer.Reason);
@@ -149,11 +150,11 @@ namespace Barotrauma.Networking
bool includesExpiration = incMsg.ReadBoolean(); bool includesExpiration = incMsg.ReadBoolean();
incMsg.ReadPadBits(); incMsg.ReadPadBits();
DateTime? expiration = null; Option<SerializableDateTime> expiration = Option<SerializableDateTime>.None();
if (includesExpiration) if (includesExpiration)
{ {
double hoursFromNow = incMsg.ReadDouble(); double hoursFromNow = incMsg.ReadDouble();
expiration = DateTime.Now + TimeSpan.FromHours(hoursFromNow); expiration = Option<SerializableDateTime>.Some(SerializableDateTime.LocalNow + TimeSpan.FromHours(hoursFromNow));
} }
string reason = incMsg.ReadString(); string reason = incMsg.ReadString();
@@ -26,8 +26,8 @@ namespace Barotrauma.Networking
if (type != ChatMessageType.Order) if (type != ChatMessageType.Order)
{ {
changeType = (PlayerConnectionChangeType)msg.ReadByte(); changeType = (PlayerConnectionChangeType)msg.ReadByte();
txt = msg.ReadString();
} }
txt = msg.ReadString();
string senderName = msg.ReadString(); string senderName = msg.ReadString();
Character senderCharacter = null; Character senderCharacter = null;
@@ -87,11 +87,6 @@ namespace Barotrauma.Networking
targetRoom = senderCharacter?.CurrentHull?.DisplayName?.Value; targetRoom = senderCharacter?.CurrentHull?.DisplayName?.Value;
} }
txt = orderPrefab.GetChatMessage(orderMessageInfo.TargetCharacter?.Name, targetRoom,
givingOrderToSelf: orderMessageInfo.TargetCharacter == senderCharacter,
orderOption: orderOption,
isNewOrder: orderMessageInfo.IsNewOrder);
if (GameMain.Client.GameStarted && Screen.Selected == GameMain.GameScreen) if (GameMain.Client.GameStarted && Screen.Selected == GameMain.GameScreen)
{ {
Order order = null; Order order = null;
@@ -82,6 +82,11 @@ namespace Barotrauma.Networking
Initialization = ConnectionInitialization.SteamTicketAndVersion Initialization = ConnectionInitialization.SteamTicketAndVersion
}; };
if (steamAuthTicket is { Canceled: true })
{
throw new InvalidOperationException("ReadConnectionInitializationStep failed: Steam auth ticket has been cancelled.");
}
ClientSteamTicketAndVersionPacket body = new ClientSteamTicketAndVersionPacket ClientSteamTicketAndVersionPacket body = new ClientSteamTicketAndVersionPacket
{ {
Name = GameMain.Client.Name, Name = GameMain.Client.Name,
@@ -120,6 +120,7 @@ namespace Barotrauma.Networking
client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]);
client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters; client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters;
client.RadioNoise = 0.0f;
if (messageType == ChatMessageType.Radio) if (messageType == ChatMessageType.Radio)
{ {
client.VoipSound.SetRange(radio.Range * RangeNear * speechImpedimentMultiplier * rangeMultiplier, radio.Range * speechImpedimentMultiplier * rangeMultiplier); client.VoipSound.SetRange(radio.Range * RangeNear * speechImpedimentMultiplier * rangeMultiplier, radio.Range * speechImpedimentMultiplier * rangeMultiplier);
@@ -131,7 +132,6 @@ namespace Barotrauma.Networking
} }
else else
{ {
client.VoipSound.SetRange(ChatMessage.SpeakRange * RangeNear * speechImpedimentMultiplier * rangeMultiplier, ChatMessage.SpeakRange * speechImpedimentMultiplier * rangeMultiplier); client.VoipSound.SetRange(ChatMessage.SpeakRange * RangeNear * speechImpedimentMultiplier * rangeMultiplier, ChatMessage.SpeakRange * speechImpedimentMultiplier * rangeMultiplier);
} }
client.VoipSound.UseMuffleFilter = client.VoipSound.UseMuffleFilter =
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Barotrauma.IO; using Barotrauma.IO;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Xml.Linq; using System.Xml.Linq;
@@ -58,7 +59,7 @@ namespace Barotrauma
var saveFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), saveList.Content.RectTransform) { MinSize = new Point(0, 45) }, style: "ListBoxElement") var saveFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), saveList.Content.RectTransform) { MinSize = new Point(0, 45) }, style: "ListBoxElement")
{ {
UserData = saveInfo.FilePath UserData = saveInfo
}; };
var nameText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform), Path.GetFileNameWithoutExtension(saveInfo.FilePath), var nameText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform), Path.GetFileNameWithoutExtension(saveInfo.FilePath),
@@ -87,10 +88,9 @@ namespace Barotrauma
}; };
string saveTimeStr = string.Empty; string saveTimeStr = string.Empty;
if (saveInfo.SaveTime > 0) if (saveInfo.SaveTime.TryUnwrap(out var time))
{ {
DateTime time = ToolBox.Epoch.ToDateTime(saveInfo.SaveTime); saveTimeStr = time.ToLocalUserString();
saveTimeStr = time.ToString();
} }
new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), saveFrame.RectTransform), new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), saveFrame.RectTransform),
text: saveTimeStr, textAlignment: Alignment.Right, font: GUIStyle.SmallFont) text: saveTimeStr, textAlignment: Alignment.Right, font: GUIStyle.SmallFont)
@@ -102,6 +102,26 @@ namespace Barotrauma
return saveFrame; return saveFrame;
} }
protected void SortSaveList()
{
saveList.Content.RectTransform.SortChildren((c1, c2) =>
{
if (c1.GUIComponent.UserData is not CampaignMode.SaveInfo file1
|| c2.GUIComponent.UserData is not CampaignMode.SaveInfo file2)
{
return 0;
}
if (!file1.SaveTime.TryUnwrap(out var file1WriteTime)
|| !file2.SaveTime.TryUnwrap(out var file2WriteTime))
{
return 0;
}
return file2WriteTime.CompareTo(file1WriteTime);
});
}
public struct CampaignSettingElements public struct CampaignSettingElements
{ {
public SettingValue<bool> TutorialEnabled; public SettingValue<bool> TutorialEnabled;
@@ -367,5 +387,25 @@ namespace Barotrauma
return inputContainer; return inputContainer;
} }
} }
public abstract void UpdateLoadMenu(IEnumerable<CampaignMode.SaveInfo> saveFiles = null);
protected bool DeleteSave(GUIButton button, object obj)
{
if (obj is not CampaignMode.SaveInfo saveInfo) { return false; }
var header = TextManager.Get("deletedialoglabel");
var body = TextManager.GetWithVariable("deletedialogquestion", "[file]", Path.GetFileNameWithoutExtension(saveInfo.FilePath));
EventEditorScreen.AskForConfirmation(header, body, () =>
{
SaveUtil.DeleteSave(saveInfo.FilePath);
prevSaveFiles?.RemoveAll(s => s.FilePath == saveInfo.FilePath);
UpdateLoadMenu(prevSaveFiles.ToList());
return true;
});
return true;
}
} }
} }
@@ -192,7 +192,7 @@ namespace Barotrauma
yield return CoroutineStatus.Success; yield return CoroutineStatus.Success;
} }
public void UpdateLoadMenu(IEnumerable<CampaignMode.SaveInfo> saveFiles = null) public override void UpdateLoadMenu(IEnumerable<CampaignMode.SaveInfo> saveFiles = null)
{ {
prevSaveFiles?.Clear(); prevSaveFiles?.Clear();
prevSaveFiles = null; prevSaveFiles = null;
@@ -220,37 +220,16 @@ namespace Barotrauma
CreateSaveElement(saveInfo); CreateSaveElement(saveInfo);
} }
saveList.Content.RectTransform.SortChildren((c1, c2) => SortSaveList();
{
string file1 = c1.GUIComponent.UserData as string;
string file2 = c2.GUIComponent.UserData as string;
DateTime file1WriteTime = DateTime.MinValue;
DateTime file2WriteTime = DateTime.MinValue;
try
{
file1WriteTime = File.GetLastWriteTime(file1);
}
catch
{
//do nothing - DateTime.MinValue will be used and the element will get sorted at the bottom of the list
};
try
{
file2WriteTime = File.GetLastWriteTime(file2);
}
catch
{
//do nothing - DateTime.MinValue will be used and the element will get sorted at the bottom of the list
};
return file2WriteTime.CompareTo(file1WriteTime);
});
loadGameButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.12f), loadGameContainer.RectTransform, Anchor.BottomRight), TextManager.Get("LoadButton")) loadGameButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.12f), loadGameContainer.RectTransform, Anchor.BottomRight), TextManager.Get("LoadButton"))
{ {
OnClicked = (btn, obj) => OnClicked = (btn, obj) =>
{ {
if (string.IsNullOrWhiteSpace(saveList.SelectedData as string)) { return false; } if (saveList.SelectedData is not CampaignMode.SaveInfo saveInfo) { return false; }
LoadGame?.Invoke(saveList.SelectedData as string); if (string.IsNullOrWhiteSpace(saveInfo.FilePath)) { return false; }
LoadGame?.Invoke(saveInfo.FilePath);
CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup");
return true; return true;
}, },
@@ -264,37 +243,20 @@ namespace Barotrauma
}; };
} }
private bool SelectSaveFile(GUIComponent component, object obj) private bool SelectSaveFile(GUIComponent component, object obj)
{ {
string fileName = (string)obj; if (obj is not CampaignMode.SaveInfo saveInfo) { return true; }
string fileName = saveInfo.FilePath;
loadGameButton.Enabled = true; loadGameButton.Enabled = true;
deleteMpSaveButton.Visible = deleteMpSaveButton.Enabled = GameMain.Client.IsServerOwner; deleteMpSaveButton.Visible = deleteMpSaveButton.Enabled = GameMain.Client.IsServerOwner;
deleteMpSaveButton.Enabled = GameMain.GameSession?.SavePath != fileName; deleteMpSaveButton.Enabled = GameMain.GameSession?.SavePath != fileName;
if (deleteMpSaveButton.Visible) if (deleteMpSaveButton.Visible)
{ {
deleteMpSaveButton.UserData = obj as string; deleteMpSaveButton.UserData = saveInfo;
} }
return true; return true;
} }
private bool DeleteSave(GUIButton button, object obj)
{
string saveFile = obj as string;
if (obj == null) { return false; }
var header = TextManager.Get("deletedialoglabel");
var body = TextManager.GetWithVariable("deletedialogquestion", "[file]", Path.GetFileNameWithoutExtension(saveFile));
EventEditorScreen.AskForConfirmation(header, body, () =>
{
SaveUtil.DeleteSave(saveFile);
prevSaveFiles?.RemoveAll(s => s.FilePath == saveFile);
UpdateLoadMenu(prevSaveFiles.ToList());
return true;
});
return true;
}
} }
} }
@@ -581,7 +581,7 @@ namespace Barotrauma
} }
} }
public void UpdateLoadMenu(IEnumerable<CampaignMode.SaveInfo> saveFiles = null) public override void UpdateLoadMenu(IEnumerable<CampaignMode.SaveInfo> saveFiles = null)
{ {
prevSaveFiles?.Clear(); prevSaveFiles?.Clear();
prevSaveFiles = null; prevSaveFiles = null;
@@ -649,46 +649,27 @@ namespace Barotrauma
} }
} }
saveList.Content.RectTransform.SortChildren((c1, c2) => SortSaveList();
{
string file1 = c1.GUIComponent.UserData as string;
string file2 = c2.GUIComponent.UserData as string;
DateTime file1WriteTime = DateTime.MinValue;
DateTime file2WriteTime = DateTime.MinValue;
try
{
file1WriteTime = File.GetLastWriteTime(file1);
}
catch
{
//do nothing - DateTime.MinValue will be used and the element will get sorted at the bottom of the list
};
try
{
file2WriteTime = File.GetLastWriteTime(file2);
}
catch
{
//do nothing - DateTime.MinValue will be used and the element will get sorted at the bottom of the list
};
return file2WriteTime.CompareTo(file1WriteTime);
});
loadGameButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.12f), loadGameContainer.RectTransform, Anchor.BottomRight), TextManager.Get("LoadButton")) loadGameButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.12f), loadGameContainer.RectTransform, Anchor.BottomRight), TextManager.Get("LoadButton"))
{ {
OnClicked = (btn, obj) => OnClicked = (btn, obj) =>
{ {
if (string.IsNullOrWhiteSpace(saveList.SelectedData as string)) { return false; } if (saveList.SelectedData is not CampaignMode.SaveInfo saveInfo) { return false; }
LoadGame?.Invoke(saveList.SelectedData as string); if (string.IsNullOrWhiteSpace(saveInfo.FilePath)) { return false; }
LoadGame?.Invoke(saveInfo.FilePath);
return true; return true;
}, },
Enabled = false Enabled = false
}; };
} }
private bool SelectSaveFile(GUIComponent component, object obj) private bool SelectSaveFile(GUIComponent component, object obj)
{ {
string fileName = (string)obj; if (obj is not CampaignMode.SaveInfo saveInfo) { return true; }
string fileName = saveInfo.FilePath;
XDocument doc = SaveUtil.LoadGameSessionDoc(fileName); XDocument doc = SaveUtil.LoadGameSessionDoc(fileName);
if (doc?.Root == null) if (doc?.Root == null)
@@ -701,72 +682,55 @@ namespace Barotrauma
RemoveSaveFrame(); RemoveSaveFrame();
string subName = doc.Root.GetAttributeString("submarine", ""); string subName = saveInfo.SubmarineName;
string saveTime = doc.Root.GetAttributeString("savetime", "unknown"); LocalizedString saveTime = saveInfo.SaveTime
DateTime? time = null; .Select(t => (LocalizedString)t.ToLocalUserString())
if (long.TryParse(saveTime, out long unixTime)) .Fallback(TextManager.Get("Unknown"));
{
time = ToolBox.Epoch.ToDateTime(unixTime);
saveTime = time.ToString();
}
string mapseed = doc.Root.GetAttributeString("mapseed", "unknown"); string mapseed = doc.Root.GetAttributeString("mapseed", "unknown");
var saveFileFrame = new GUIFrame(new RectTransform(new Vector2(0.45f, 0.6f), loadGameContainer.RectTransform, Anchor.TopRight) var saveFileFrame = new GUIFrame(
{ new RectTransform(new Vector2(0.45f, 0.6f), loadGameContainer.RectTransform, Anchor.TopRight)
RelativeOffset = new Vector2(0.0f, 0.1f) {
}, style: "InnerFrame") RelativeOffset = new Vector2(0.0f, 0.1f)
}, style: "InnerFrame")
{ {
UserData = "savefileframe" UserData = "savefileframe"
}; };
var titleText = new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.2f), saveFileFrame.RectTransform, Anchor.TopCenter) var titleText = new GUITextBlock(
{ new RectTransform(new Vector2(0.9f, 0.2f), saveFileFrame.RectTransform, Anchor.TopCenter)
RelativeOffset = new Vector2(0, 0.05f) {
}, RelativeOffset = new Vector2(0, 0.05f)
},
Path.GetFileNameWithoutExtension(fileName), font: GUIStyle.LargeFont, textAlignment: Alignment.Center); Path.GetFileNameWithoutExtension(fileName), font: GUIStyle.LargeFont, textAlignment: Alignment.Center);
titleText.Text = ToolBox.LimitString(titleText.Text, titleText.Font, titleText.Rect.Width); titleText.Text = ToolBox.LimitString(titleText.Text, titleText.Font, titleText.Rect.Width);
var layoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.5f), saveFileFrame.RectTransform, Anchor.Center) var layoutGroup = new GUILayoutGroup(
{ new RectTransform(new Vector2(0.8f, 0.5f), saveFileFrame.RectTransform, Anchor.Center)
RelativeOffset = new Vector2(0, 0.1f) {
}); RelativeOffset = new Vector2(0, 0.1f)
});
new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("Submarine")} : {subName}", font: GUIStyle.SmallFont); new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform),
new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("LastSaved")} : {saveTime}", font: GUIStyle.SmallFont); $"{TextManager.Get("Submarine")} : {subName}", font: GUIStyle.SmallFont);
new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("MapSeed")} : {mapseed}", font: GUIStyle.SmallFont); new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform),
$"{TextManager.Get("LastSaved")} : {saveTime}", font: GUIStyle.SmallFont);
new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform),
$"{TextManager.Get("MapSeed")} : {mapseed}", font: GUIStyle.SmallFont);
new GUIButton(new RectTransform(new Vector2(0.4f, 0.15f), saveFileFrame.RectTransform, Anchor.BottomCenter) new GUIButton(new RectTransform(new Vector2(0.4f, 0.15f), saveFileFrame.RectTransform, Anchor.BottomCenter)
{ {
RelativeOffset = new Vector2(0, 0.1f) RelativeOffset = new Vector2(0, 0.1f)
}, TextManager.Get("Delete"), style: "GUIButtonSmall") }, TextManager.Get("Delete"), style: "GUIButtonSmall")
{ {
UserData = fileName, UserData = saveInfo,
OnClicked = DeleteSave OnClicked = DeleteSave
}; };
return true; return true;
} }
private bool DeleteSave(GUIButton button, object obj)
{
string saveFile = obj as string;
if (obj == null) { return false; }
LocalizedString header = TextManager.Get("deletedialoglabel");
LocalizedString body = TextManager.GetWithVariable("deletedialogquestion", "[file]", Path.GetFileNameWithoutExtension(saveFile));
EventEditorScreen.AskForConfirmation(header, body, () =>
{
SaveUtil.DeleteSave(saveFile);
prevSaveFiles?.RemoveAll(s => s.FilePath == saveFile);
UpdateLoadMenu(prevSaveFiles.ToList());
return true;
});
return true;
}
private void RemoveSaveFrame() private void RemoveSaveFrame()
{ {
GUIComponent prevFrame = null; GUIComponent prevFrame = null;
@@ -551,6 +551,7 @@ namespace Barotrauma
submarineSelection.RefreshSubmarineDisplay(true, setTransferOptionToTrue: true); submarineSelection.RefreshSubmarineDisplay(true, setTransferOptionToTrue: true);
break; break;
case CampaignMode.InteractionType.Map: case CampaignMode.InteractionType.Map:
GameMain.GameSession?.Map?.ResetPendingSub();
//refresh mission rewards (may have been changed by e.g. a pending submarine switch) //refresh mission rewards (may have been changed by e.g. a pending submarine switch)
foreach (GUITextBlock rewardText in missionRewardTexts) foreach (GUITextBlock rewardText in missionRewardTexts)
{ {
@@ -25,14 +25,11 @@ namespace Barotrauma.CharacterEditor
{ {
get get
{ {
if (cam == null) cam ??= new Camera()
{ {
cam = new Camera() MinZoom = 0.1f,
{ MaxZoom = 5.0f
MinZoom = 0.1f, };
MaxZoom = 5.0f
};
}
return cam; return cam;
} }
} }
@@ -262,7 +259,10 @@ namespace Barotrauma.CharacterEditor
#endif #endif
} }
GameMain.Instance.ResolutionChanged -= OnResolutionChanged; GameMain.Instance.ResolutionChanged -= OnResolutionChanged;
GameMain.LightManager.LightingEnabled = true; if (!GameMain.DevMode)
{
GameMain.LightManager.LightingEnabled = true;
}
ClearWidgets(); ClearWidgets();
ClearSelection(); ClearSelection();
} }
@@ -280,6 +280,7 @@ namespace Barotrauma.CharacterEditor
#region Main methods #region Main methods
public override void AddToGUIUpdateList() public override void AddToGUIUpdateList()
{ {
if (rightArea == null || leftArea == null) { return; }
rightArea.AddToGUIUpdateList(); rightArea.AddToGUIUpdateList();
leftArea.AddToGUIUpdateList(); leftArea.AddToGUIUpdateList();
@@ -778,7 +779,7 @@ namespace Barotrauma.CharacterEditor
scaledMouseSpeed = PlayerInput.MouseSpeedPerSecond * (float)deltaTime; scaledMouseSpeed = PlayerInput.MouseSpeedPerSecond * (float)deltaTime;
Cam.UpdateTransform(true); Cam.UpdateTransform(true);
Submarine.CullEntities(Cam); Submarine.CullEntities(Cam);
Submarine.MainSub.UpdateTransform(); Submarine.MainSub?.UpdateTransform();
// Lightmaps // Lightmaps
if (GameMain.LightManager.LightingEnabled) if (GameMain.LightManager.LightingEnabled)
@@ -1570,10 +1571,7 @@ namespace Barotrauma.CharacterEditor
{ {
wayPoint = WayPoint.GetRandom(spawnType: SpawnType.Human, sub: Submarine.MainSub); wayPoint = WayPoint.GetRandom(spawnType: SpawnType.Human, sub: Submarine.MainSub);
} }
if (wayPoint == null) wayPoint ??= WayPoint.GetRandom(sub: Submarine.MainSub);
{
wayPoint = WayPoint.GetRandom(sub: Submarine.MainSub);
}
spawnPosition = wayPoint.WorldPosition; spawnPosition = wayPoint.WorldPosition;
} }
@@ -3998,7 +3996,7 @@ namespace Barotrauma.CharacterEditor
}; };
}).Draw(spriteBatch, deltaTime); }).Draw(spriteBatch, deltaTime);
} }
else else if (groundedParams != null)
{ {
GetAnimationWidget("HeadPosition", color, Color.Black, initMethod: w => GetAnimationWidget("HeadPosition", color, Color.Black, initMethod: w =>
{ {
@@ -4107,7 +4105,7 @@ namespace Barotrauma.CharacterEditor
}; };
}).Draw(spriteBatch, deltaTime); }).Draw(spriteBatch, deltaTime);
} }
else else if (groundedParams != null)
{ {
GetAnimationWidget("TorsoPosition", color, Color.Black, initMethod: w => GetAnimationWidget("TorsoPosition", color, Color.Black, initMethod: w =>
{ {
@@ -1,6 +1,5 @@
using Barotrauma.Extensions; using Barotrauma.Extensions;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using System; using System;
using System.Diagnostics; using System.Diagnostics;
@@ -27,7 +26,7 @@ namespace Barotrauma
public Effect ThresholdTintEffect { get; private set; } public Effect ThresholdTintEffect { get; private set; }
public Effect BlueprintEffect { get; set; } public Effect BlueprintEffect { get; set; }
public GameScreen(GraphicsDevice graphics, ContentManager content) public GameScreen(GraphicsDevice graphics)
{ {
cam = new Camera(); cam = new Camera();
cam.Translate(new Vector2(-10.0f, 50.0f)); cam.Translate(new Vector2(-10.0f, 50.0f));
@@ -38,20 +37,13 @@ namespace Barotrauma
CreateRenderTargets(graphics); CreateRenderTargets(graphics);
}; };
Effect LoadEffect(string path)
=> content.Load<Effect>(path
#if LINUX || OSX
+"_opengl"
#endif
);
//var blurEffect = LoadEffect("Effects/blurshader"); //var blurEffect = LoadEffect("Effects/blurshader");
damageEffect = LoadEffect("Effects/damageshader"); damageEffect = EffectLoader.Load("Effects/damageshader");
PostProcessEffect = LoadEffect("Effects/postprocess"); PostProcessEffect = EffectLoader.Load("Effects/postprocess");
GradientEffect = LoadEffect("Effects/gradientshader"); GradientEffect = EffectLoader.Load("Effects/gradientshader");
GrainEffect = LoadEffect("Effects/grainshader"); GrainEffect = EffectLoader.Load("Effects/grainshader");
ThresholdTintEffect = LoadEffect("Effects/thresholdtint"); ThresholdTintEffect = EffectLoader.Load("Effects/thresholdtint");
BlueprintEffect = LoadEffect("Effects/blueprintshader"); BlueprintEffect = EffectLoader.Load("Effects/blueprintshader");
damageStencil = TextureLoader.FromFile("Content/Map/walldamage.png"); damageStencil = TextureLoader.FromFile("Content/Map/walldamage.png");
damageEffect.Parameters["xStencil"].SetValue(damageStencil); damageEffect.Parameters["xStencil"].SetValue(damageStencil);
@@ -994,7 +994,7 @@ namespace Barotrauma
|| item.IsDownloadPending || item.IsDownloadPending
|| (item.InstallTime.TryGetValue(out var workshopInstallTime) || (item.InstallTime.TryGetValue(out var workshopInstallTime)
&& pkg.InstallTime.TryUnwrap(out var localInstallTime) && pkg.InstallTime.TryUnwrap(out var localInstallTime)
&& localInstallTime < workshopInstallTime))); && localInstallTime.ToUtcValue() < workshopInstallTime)));
modUpdateStatus = (DateTime.Now + ModUpdateInterval, count); modUpdateStatus = (DateTime.Now + ModUpdateInterval, count);
} }
@@ -1,6 +1,5 @@
#nullable enable #nullable enable
using System.Linq; using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
@@ -34,7 +33,7 @@ namespace Barotrauma
{ {
BlueprintEffect.Dispose(); BlueprintEffect.Dispose();
GameMain.Instance.Content.Unload(); GameMain.Instance.Content.Unload();
BlueprintEffect = GameMain.Instance.Content.Load<Effect>("Effects/blueprintshader_opengl"); BlueprintEffect = EffectLoader.Load("Effects/blueprintshader");
GameMain.GameScreen.BlueprintEffect = BlueprintEffect; GameMain.GameScreen.BlueprintEffect = BlueprintEffect;
return true; return true;
} }
@@ -9,7 +9,7 @@ using System.Diagnostics;
namespace Barotrauma namespace Barotrauma
{ {
class SerializableEntityEditor : GUIComponent sealed class SerializableEntityEditor : GUIComponent
{ {
private readonly int elementHeight; private readonly int elementHeight;
private readonly GUILayoutGroup layoutGroup; private readonly GUILayoutGroup layoutGroup;
@@ -399,10 +399,6 @@ namespace Barotrauma
{ {
propertyField = CreateBoolField(entity, property, boolVal, displayName, toolTip); propertyField = CreateBoolField(entity, property, boolVal, displayName, toolTip);
} }
else if (value is string stringVal)
{
propertyField = CreateStringField(entity, property, stringVal, displayName, toolTip);
}
else if (value.GetType().IsEnum) else if (value.GetType().IsEnum)
{ {
if (value.GetType().IsDefined(typeof(FlagsAttribute), inherit: false)) if (value.GetType().IsDefined(typeof(FlagsAttribute), inherit: false))
@@ -450,6 +446,10 @@ namespace Barotrauma
{ {
propertyField = CreateStringArrayField(entity, property, a, displayName, toolTip); propertyField = CreateStringArrayField(entity, property, a, displayName, toolTip);
} }
else if (value is string or Identifier)
{
propertyField = CreateStringField(entity, property, value.ToString(), displayName, toolTip);
}
return propertyField; return propertyField;
} }
@@ -696,7 +696,7 @@ namespace Barotrauma
propertyBox.OnEnterPressed += (box, text) => OnApply(box); propertyBox.OnEnterPressed += (box, text) => OnApply(box);
refresh += () => refresh += () =>
{ {
if (!propertyBox.Selected) { propertyBox.Text = (string)property.GetValue(entity); } if (!propertyBox.Selected) { propertyBox.Text = property.GetValue(entity).ToString(); }
}; };
bool OnApply(GUITextBox textBox) bool OnApply(GUITextBox textBox)
@@ -714,7 +714,7 @@ namespace Barotrauma
if (SetPropertyValue(property, entity, textBox.Text)) if (SetPropertyValue(property, entity, textBox.Text))
{ {
TrySendNetworkUpdate(entity, property); TrySendNetworkUpdate(entity, property);
textBox.Text = (string) property.GetValue(entity); textBox.Text = property.GetValue(entity).ToString();
textBox.Flash(GUIStyle.Green, flashDuration: 1f); textBox.Flash(GUIStyle.Green, flashDuration: 1f);
} }
//restore the entities that were selected before applying //restore the entities that were selected before applying
@@ -42,12 +42,7 @@ namespace Barotrauma
{ {
if (effect == null) if (effect == null)
{ {
#if WINDOWS effect = EffectLoader.Load("Effects/deformshader");
effect = GameMain.Instance.Content.Load<Effect>("Effects/deformshader");
#endif
#if LINUX || OSX
effect = GameMain.Instance.Content.Load<Effect>("Effects/deformshader_opengl");
#endif
} }
Invert = invert; Invert = invert;
@@ -202,6 +202,7 @@ namespace Barotrauma.Steam
ContentPackageManager.EnabledPackages.Core!, ContentPackageManager.EnabledPackages.Core!,
(p) => { }, (p) => { },
heightScale: 1.0f / 13.0f); heightScale: 1.0f / 13.0f);
enabledCoreDropdown.AllowNonText = true;
Label(topLeft, "", GUIStyle.SubHeadingFont, heightScale: 1.0f); Label(topLeft, "", GUIStyle.SubHeadingFont, heightScale: 1.0f);
topRight.ChildAnchor = Anchor.CenterLeft; topRight.ChildAnchor = Anchor.CenterLeft;
@@ -535,34 +536,119 @@ namespace Barotrauma.Steam
bulkUpdateButton.Enabled = false; bulkUpdateButton.Enabled = false;
bulkUpdateButton.ToolTip = ""; bulkUpdateButton.ToolTip = "";
ContentPackageManager.UpdateContentPackageList(); ContentPackageManager.UpdateContentPackageList();
var corePackages = ContentPackageManager.CorePackages.ToArray();
var currentCore = ContentPackageManager.EnabledPackages.Core!;
SwapDropdownValues<CorePackage>(enabledCoreDropdown, SwapDropdownValues<CorePackage>(enabledCoreDropdown,
(p) => p.Name, (p) => p.Name,
ContentPackageManager.CorePackages.ToArray(), corePackages,
ContentPackageManager.EnabledPackages.Core!, currentCore,
(p) => (p) =>
{ {
// Manually set dropdown text because
// adding buttons to the elements breaks
// this part of the dropdown code
enabledCoreDropdown.Text = p.Name;
enabledCoreDropdown.ButtonTextColor = enabledCoreDropdown.ButtonTextColor =
p.HasAnyErrors p.HasAnyErrors
? GUIStyle.Red ? GUIStyle.Red
: GUIStyle.TextColorNormal; : GUIStyle.TextColorNormal;
}); });
enabledCoreDropdown.ListBox.Content.Children
.OfType<GUITextBlock>() void addButtonForMod(ContentPackage mod, GUILayoutGroup parent)
.ForEach(tb =>
CreateModErrorInfo(
(tb.UserData as ContentPackage)!,
tb,
tb));
void addRegularModToList(RegularPackage mod, GUIListBox list)
{ {
var modFrame = new GUIFrame(new RectTransform((1.0f, 0.08f), list.Content.RectTransform), if (ContentPackageManager.LocalPackages.Contains(mod))
{
var editButton = new GUIButton(new RectTransform(Vector2.One, parent.RectTransform, scaleBasis: ScaleBasis.Smallest), "",
style: "WorkshopMenu.EditButton")
{
OnClicked = (button, o) =>
{
ToolBox.OpenFileWithShell(mod.Dir);
return false;
},
ToolTip = TextManager.Get("OpenLocalModInExplorer")
};
}
else if (ContentPackageManager.WorkshopPackages.Contains(mod))
{
var infoButton = new GUIButton(
new RectTransform(Vector2.One, parent.RectTransform, scaleBasis: ScaleBasis.Smallest), "",
style: null)
{
CanBeSelected = false,
OnClicked = (button, o) =>
{
PrepareToShowModInfo(mod);
return false;
}
};
if (!SteamManager.IsInitialized)
{
infoButton.Enabled = false;
}
TaskPool.AddIfNotFound(
$"DetermineUpdateRequired{mod.UgcId}",
mod.IsUpToDate(),
t =>
{
if (!t.TryGetResult(out bool isUpToDate)) { return; }
if (isUpToDate) { return; }
infoButton.CanBeSelected = true;
infoButton.ApplyStyle(GUIStyle.ComponentStyles["WorkshopMenu.InfoButtonUpdate"]);
infoButton.ToolTip = TextManager.Get("ViewModDetailsUpdateAvailable");
bulkUpdateButton.Enabled = true;
bulkUpdateButton.ToolTip = TextManager.Get("ModUpdatesAvailable");
});
}
}
GUILayoutGroup createBaseModListUi(ContentPackage mod, GUIListBox listBox, float height)
{
var modFrame = new GUIFrame(new RectTransform((1.0f, height), listBox.Content.RectTransform),
style: "ListBoxElement") style: "ListBoxElement")
{ {
UserData = mod UserData = mod
}; };
var frameContent = new GUILayoutGroup(new RectTransform((0.95f, 0.9f), modFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft)
{
Stretch = true,
RelativeSpacing = 0.02f
};
var modNameScissor = new GUIScissorComponent(new RectTransform((0.8f, 1.0f), frameContent.RectTransform))
{
CanBeFocused = false
};
var modName = new GUITextBlock(new RectTransform(Vector2.One, modNameScissor.Content.RectTransform),
text: mod.Name)
{
CanBeFocused = false
};
CreateModErrorInfo(mod, modFrame, modName);
addButtonForMod(mod, frameContent);
return frameContent;
}
foreach (var element in enabledCoreDropdown.ListBox.Content.Children.ToArray())
{
enabledCoreDropdown.ListBox.RemoveChild(element);
if (element.UserData is not ContentPackage mod) { continue; }
createBaseModListUi(mod, enabledCoreDropdown.ListBox, 0.24f);
}
enabledCoreDropdown.Select(corePackages.IndexOf(currentCore));
void addRegularModToList(RegularPackage mod, GUIListBox list)
{
var frameContent = createBaseModListUi(mod, list, 0.08f);
var modFrame = frameContent.Parent;
var contextMenuHandler = new GUICustomComponent(new RectTransform(Vector2.Zero, modFrame.RectTransform), var contextMenuHandler = new GUICustomComponent(new RectTransform(Vector2.Zero, modFrame.RectTransform),
onUpdate: (f, component) => onUpdate: (f, component) =>
{ {
@@ -639,76 +725,13 @@ namespace Barotrauma.Steam
contextMenuOptions.ToArray()); contextMenuOptions.ToArray());
} }
}); });
var frameContent = new GUILayoutGroup(new RectTransform((0.95f, 0.9f), modFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft)
{
Stretch = true,
RelativeSpacing = 0.02f
};
var dragIndicator = new GUIButton(new RectTransform((0.5f, 0.5f), frameContent.RectTransform, scaleBasis: ScaleBasis.BothHeight), var dragIndicator = new GUIButton(new RectTransform((0.5f, 0.5f), frameContent.RectTransform, scaleBasis: ScaleBasis.BothHeight),
style: "GUIDragIndicator") style: "GUIDragIndicator")
{ {
CanBeFocused = false CanBeFocused = false
}; };
dragIndicator.RectTransform.SetAsFirstChild();
var modNameScissor = new GUIScissorComponent(new RectTransform((0.8f, 1.0f), frameContent.RectTransform))
{
CanBeFocused = false
};
var modName = new GUITextBlock(new RectTransform(Vector2.One, modNameScissor.Content.RectTransform),
text: mod.Name)
{
CanBeFocused = false
};
CreateModErrorInfo(mod, modFrame, modName);
if (ContentPackageManager.LocalPackages.Contains(mod))
{
var editButton = new GUIButton(new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "",
style: "WorkshopMenu.EditButton")
{
OnClicked = (button, o) =>
{
ToolBox.OpenFileWithShell(mod.Dir);
return false;
},
ToolTip = TextManager.Get("OpenLocalModInExplorer")
};
}
else if (ContentPackageManager.WorkshopPackages.Contains(mod))
{
var infoButton = new GUIButton(
new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "",
style: null)
{
CanBeSelected = false,
OnClicked = (button, o) =>
{
PrepareToShowModInfo(mod);
return false;
}
};
if (!SteamManager.IsInitialized)
{
infoButton.Enabled = false;
}
TaskPool.AddIfNotFound(
$"DetermineUpdateRequired{mod.UgcId}",
mod.IsUpToDate(),
t =>
{
if (!t.TryGetResult(out bool isUpToDate)) { return; }
if (!isUpToDate)
{
infoButton.CanBeSelected = true;
infoButton.ApplyStyle(GUIStyle.ComponentStyles["WorkshopMenu.InfoButtonUpdate"]);
infoButton.ToolTip = TextManager.Get("ViewModDetailsUpdateAvailable");
bulkUpdateButton.Enabled = true;
bulkUpdateButton.ToolTip = TextManager.Get("ModUpdatesAvailable");
}
});
}
} }
void addRegularModsToList(IEnumerable<RegularPackage> mods, GUIListBox list) void addRegularModsToList(IEnumerable<RegularPackage> mods, GUIListBox list)
@@ -729,7 +752,7 @@ namespace Barotrauma.Steam
.Where(p => ContentPackageManager.RegularPackages.Contains(p))) .Where(p => ContentPackageManager.RegularPackages.Contains(p)))
.ToArray(); .ToArray();
var disabledMods = ContentPackageManager.RegularPackages.Where(p => !enabledMods.Contains(p)); var disabledMods = ContentPackageManager.RegularPackages.Where(p => !enabledMods.Contains(p));
addRegularModsToList(enabledMods, enabledRegularModsList); addRegularModsToList(enabledMods, enabledRegularModsList);
if (refreshDisabled) { addRegularModsToList(disabledMods, disabledRegularModsList); } if (refreshDisabled) { addRegularModsToList(disabledMods, disabledRegularModsList); }
@@ -747,7 +770,7 @@ namespace Barotrauma.Steam
var mod = child.UserData as RegularPackage; var mod = child.UserData as RegularPackage;
if (mod is null || !ContentPackageManager.WorkshopPackages.Contains(mod)) { continue; } if (mod is null || !ContentPackageManager.WorkshopPackages.Contains(mod)) { continue; }
if (!mod.UgcId.TryUnwrap(out var ugcId)) { continue; } if (!mod.UgcId.TryUnwrap(out var ugcId)) { continue; }
if (!(ugcId is SteamWorkshopId workshopId)) { continue; } if (ugcId is not SteamWorkshopId workshopId) { continue; }
var btn = child.GetChild<GUILayoutGroup>()?.GetAllChildren<GUIButton>().Last(); var btn = child.GetChild<GUILayoutGroup>()?.GetAllChildren<GUIButton>().Last();
if (btn is null) { continue; } if (btn is null) { continue; }
@@ -68,10 +68,13 @@ namespace Barotrauma.Steam
} }
protected static void SwapDropdownValues<T>( protected static void SwapDropdownValues<T>(
GUIDropDown dropdown, Func<T, LocalizedString> textFunc, IReadOnlyList<T> values, T currentValue, GUIDropDown dropdown,
Func<T, LocalizedString> textFunc,
IReadOnlyList<T> values,
T currentValue,
Action<T> setter) Action<T> setter)
{ {
if (dropdown.ListBox.Content.Children.Any(c => !(c.UserData is T))) if (dropdown.ListBox.Content.Children.Any(c => c.UserData is not T))
{ {
throw new Exception("SwapValues must preserve the type of the dropdown's userdata"); throw new Exception("SwapValues must preserve the type of the dropdown's userdata");
} }
@@ -0,0 +1,12 @@
using Microsoft.Xna.Framework.Graphics;
namespace Barotrauma;
static class EffectLoader
{
public static Effect Load(string path)
=> GameMain.Instance.Content.Load<Effect>(path
#if LINUX || OSX
+"_opengl"
#endif
);
}
@@ -9,7 +9,7 @@ namespace Barotrauma
{ {
sealed class SpriteRecorder : ISpriteBatch, IDisposable sealed class SpriteRecorder : ISpriteBatch, IDisposable
{ {
private readonly record struct Command( public readonly record struct Command(
Texture2D Texture, Texture2D Texture,
VertexPositionColorTexture VertexBL, VertexPositionColorTexture VertexBL,
VertexPositionColorTexture VertexBR, VertexPositionColorTexture VertexBR,
@@ -346,7 +346,7 @@ namespace Barotrauma
} }
recordedBuffers.Clear(); recordedBuffers.Clear();
commandList.Clear(); commandList.Clear();
indexBuffer.Dispose(); indexBuffer = null; indexBuffer?.Dispose(); indexBuffer = null;
ReadyToRender = false; ReadyToRender = false;
} }
} }
@@ -6,3 +6,5 @@
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Potential Code Quality Issues", "RECS0026:Possible unassigned object created by 'new'", Justification = "<Pending>", Scope = "member", Target = "~M:Barotrauma.GameSettings.CreateSettingsFrame")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Potential Code Quality Issues", "RECS0026:Possible unassigned object created by 'new'", Justification = "<Pending>", Scope = "member", Target = "~M:Barotrauma.GameSettings.CreateSettingsFrame")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Potential Code Quality Issues", "IDE0047")]
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>0.21.1.0</Version> <Version>0.21.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
+1 -1
View File
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>0.21.1.0</Version> <Version>0.21.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
@@ -79,3 +79,8 @@
/processorParam:DebugMode=Auto /processorParam:DebugMode=Auto
/build:blueprintshader.fx /build:blueprintshader.fx
#begin wearableclip.fx
/importer:EffectImporter
/processor:EffectProcessor
/processorParam:DebugMode=Auto
/build:wearableclip.fx
@@ -78,3 +78,9 @@
/processor:EffectProcessor /processor:EffectProcessor
/processorParam:DebugMode=Auto /processorParam:DebugMode=Auto
/build:thresholdtint_opengl.fx /build:thresholdtint_opengl.fx
#begin wearableclip_opengl.fx
/importer:EffectImporter
/processor:EffectProcessor
/processorParam:DebugMode=Auto
/build:wearableclip_opengl.fx
@@ -0,0 +1,42 @@
Texture2D xTexture;
sampler TextureSampler : register (s0) = sampler_state { Texture = <xTexture>; };
Texture2D xStencil;
sampler StencilSampler = sampler_state { Texture = <xStencil>; };
float aCutoff;
float4x4 wearableUvToClipperUv;
float clipperTexelSize;
float stencilSample(float2 texCoord, float2 offset)
{
return xStencil.Sample(
StencilSampler,
mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy + offset).a;
}
float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0
{
float4 c = xTexture.Sample(TextureSampler, texCoord) * color;
float minStencil = stencilSample(texCoord, float2(0,0));
minStencil = min(minStencil, stencilSample(texCoord, float2(-clipperTexelSize,0)));
minStencil = min(minStencil, stencilSample(texCoord, float2(clipperTexelSize,0)));
minStencil = min(minStencil, stencilSample(texCoord, float2(0,-clipperTexelSize)));
minStencil = min(minStencil, stencilSample(texCoord, float2(0,clipperTexelSize)));
float aDiff = minStencil - aCutoff;
clip(aDiff);
return c;
}
technique StencilShader
{
pass Pass1
{
PixelShader = compile ps_4_0_level_9_1 main();
}
}
@@ -0,0 +1,42 @@
Texture2D xTexture;
sampler TextureSampler : register (s0) = sampler_state { Texture = <xTexture>; };
Texture2D xStencil;
sampler StencilSampler = sampler_state { Texture = <xStencil>; };
float aCutoff;
float4x4 wearableUvToClipperUv;
float clipperTexelSize;
float stencilSample(float2 texCoord, float2 offset)
{
return tex2D(
StencilSampler,
mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy + offset).a;
}
float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0
{
float4 c = tex2D(TextureSampler, texCoord) * color;
float minStencil = stencilSample(texCoord, float2(0,0));
minStencil = min(minStencil, stencilSample(texCoord, float2(-clipperTexelSize,0)));
minStencil = min(minStencil, stencilSample(texCoord, float2(clipperTexelSize,0)));
minStencil = min(minStencil, stencilSample(texCoord, float2(0,-clipperTexelSize)));
minStencil = min(minStencil, stencilSample(texCoord, float2(0,clipperTexelSize)));
float aDiff = minStencil - aCutoff;
clip(aDiff);
return c;
}
technique StencilShader
{
pass Pass1
{
PixelShader = compile ps_2_0 main();
}
}
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>0.21.1.0</Version> <Version>0.21.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>0.21.1.0</Version> <Version>0.21.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
+1 -1
View File
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>0.21.1.0</Version> <Version>0.21.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
@@ -14,6 +14,13 @@ namespace Barotrauma
/// </summary> /// </summary>
public bool Discarded; public bool Discarded;
public void ApplyDeathEffects()
{
RespawnManager.ReduceCharacterSkills(this);
RemoveSavedStatValuesOnDeath();
CauseOfDeath = null;
}
partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel) partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel)
{ {
if (Character == null || Character.Removed) { return; } if (Character == null || Character.Removed) { return; }
@@ -0,0 +1,51 @@
#nullable enable
using System;
using System.Collections.Generic;
using Barotrauma.Networking;
namespace Barotrauma
{
internal static class HealingCooldown
{
private static readonly Dictionary<Client, DateTimeOffset> HealingCooldowns = new();
// Little bit less than client's 0.5 second cooldown to account for latency
private const float CooldownDuration = 0.4f;
public static bool IsOnCooldown(Client client)
{
RemoveExpiredCooldowns();
return HealingCooldowns.ContainsKey(client);
}
public static void SetCooldown(Client client)
{
RemoveExpiredCooldowns();
DateTimeOffset newCooldown = DateTimeOffset.UtcNow.AddSeconds(CooldownDuration);
HealingCooldowns[client] = newCooldown;
}
private static void RemoveExpiredCooldowns()
{
HashSet<Client>? expiredCooldowns = null;
DateTimeOffset now = DateTimeOffset.UtcNow;
foreach (var (client, cooldown) in HealingCooldowns)
{
if (now < cooldown) { continue; }
expiredCooldowns ??= new HashSet<Client>();
expiredCooldowns.Add(client);
}
if (expiredCooldowns is null) { return; }
foreach (Client expiredCooldown in expiredCooldowns)
{
HealingCooldowns.Remove(expiredCooldown);
}
}
}
}
@@ -109,15 +109,22 @@ namespace Barotrauma
return AccountId == other.AccountId && other.ClientAddress == ClientAddress; return AccountId == other.AccountId && other.ClientAddress == ClientAddress;
} }
public void Reset()
{
itemData = null;
healthData = null;
WalletData = null;
}
public void SpawnInventoryItems(Character character, Inventory inventory) public void SpawnInventoryItems(Character character, Inventory inventory)
{ {
if (character == null) if (character == null)
{ {
throw new System.InvalidOperationException($"Failed to spawn inventory items. Character was null."); throw new InvalidOperationException($"Failed to spawn inventory items. Character was null.");
} }
if (itemData == null) if (itemData == null)
{ {
throw new System.InvalidOperationException($"Failed to spawn inventory items for the character \"{character.Name}\". No saved inventory data."); throw new InvalidOperationException($"Failed to spawn inventory items for the character \"{character.Name}\". No saved inventory data.");
} }
character.SpawnInventoryItems(inventory, itemData.FromPackage(null)); character.SpawnInventoryItems(inventory, itemData.FromPackage(null));
} }
@@ -240,9 +240,7 @@ namespace Barotrauma
//reduce skills if the character has died //reduce skills if the character has died
if (characterInfo.CauseOfDeath != null && characterInfo.CauseOfDeath.Type != CauseOfDeathType.Disconnected) if (characterInfo.CauseOfDeath != null && characterInfo.CauseOfDeath.Type != CauseOfDeathType.Disconnected)
{ {
RespawnManager.ReduceCharacterSkills(characterInfo); characterInfo.ApplyDeathEffects();
characterInfo.RemoveSavedStatValuesOnDeath();
characterInfo.CauseOfDeath = null;
} }
c.CharacterInfo = characterInfo; c.CharacterInfo = characterInfo;
SetClientCharacterData(c); SetClientCharacterData(c);
@@ -254,13 +252,21 @@ namespace Barotrauma
{ {
if (data.HasSpawned && !GameMain.Server.ConnectedClients.Any(c => data.MatchesClient(c))) if (data.HasSpawned && !GameMain.Server.ConnectedClients.Any(c => data.MatchesClient(c)))
{ {
var character = Character.CharacterList.Find(c => c.Info == data.CharacterInfo && !c.IsHusk); var character = Character.CharacterList.Find(c => c.Info == data.CharacterInfo && !c.IsHusk);
if (character != null && (!character.IsDead || character.CauseOfDeath?.Type == CauseOfDeathType.Disconnected)) if (character != null &&
(!character.IsDead || character.CauseOfDeath?.Type == CauseOfDeathType.Disconnected))
{ {
//character still alive (or killed by Disconnect) -> save it as-is
characterData.RemoveAll(cd => cd.IsDuplicate(data)); characterData.RemoveAll(cd => cd.IsDuplicate(data));
data.Refresh(character); data.Refresh(character);
characterData.Add(data); characterData.Add(data);
} }
else
{
//character dead or removed -> reduce skills, remove items, health data, etc
data.CharacterInfo.ApplyDeathEffects();
data.Reset();
}
} }
} }
@@ -1,4 +1,5 @@
using Barotrauma.Items.Components; using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking; using Barotrauma.Networking;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System.Collections.Generic; using System.Collections.Generic;
@@ -19,7 +20,7 @@ namespace Barotrauma
bool accessible = c.Character.CanAccessInventory(this); bool accessible = c.Character.CanAccessInventory(this);
if (this is CharacterInventory characterInventory && accessible) if (this is CharacterInventory characterInventory && accessible)
{ {
if (Owner == null || !(Owner is Character ownerCharacter)) if (Owner == null || Owner is not Character ownerCharacter)
{ {
accessible = false; accessible = false;
} }
@@ -39,7 +40,7 @@ namespace Barotrauma
{ {
foreach (ushort id in newItemIDs[i]) foreach (ushort id in newItemIDs[i])
{ {
if (!(Entity.FindEntityByID(id) is Item item)) { continue; } if (Entity.FindEntityByID(id) is not Item item) { continue; }
item.PositionUpdateInterval = 0.0f; item.PositionUpdateInterval = 0.0f;
if (item.ParentInventory != null && item.ParentInventory != this) if (item.ParentInventory != null && item.ParentInventory != this)
{ {
@@ -94,7 +95,15 @@ namespace Barotrauma
{ {
foreach (ushort id in newItemIDs[i]) foreach (ushort id in newItemIDs[i])
{ {
if (!(Entity.FindEntityByID(id) is Item item) || slots[i].Contains(item)) { continue; } 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())
{
DebugConsole.AddWarning($"Client {c.Name} tried to pick up a non-pickable item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"})");
continue;
}
if (GameMain.Server != null) if (GameMain.Server != null)
{ {
@@ -105,7 +114,7 @@ namespace Barotrauma
(c.Character == null || item.PreviousParentInventory == null || !c.Character.CanAccessInventory(item.PreviousParentInventory))) (c.Character == null || item.PreviousParentInventory == null || !c.Character.CanAccessInventory(item.PreviousParentInventory)))
{ {
#if DEBUG || UNSTABLE #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); DebugConsole.NewMessage($"Client {c.Name} failed to pick up item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"}). No access.", Color.Yellow);
#endif #endif
if (item.body != null && !c.PendingPositionUpdates.Contains(item)) if (item.body != null && !c.PendingPositionUpdates.Contains(item))
{ {
@@ -153,25 +153,27 @@ namespace Barotrauma
(components[containerIndex] as ItemContainer).Inventory.ServerEventRead(msg, c); (components[containerIndex] as ItemContainer).Inventory.ServerEventRead(msg, c);
break; break;
case EventType.Treatment: case EventType.Treatment:
if (c.Character == null || !c.Character.CanInteractWith(this)) return; if (c.Character == null || !c.Character.CanInteractWith(this)) { return; }
UInt16 characterID = msg.ReadUInt16(); UInt16 characterID = msg.ReadUInt16();
byte limbIndex = msg.ReadByte(); byte limbIndex = msg.ReadByte();
Character targetCharacter = FindEntityByID(characterID) as Character; if (HealingCooldown.IsOnCooldown(c)) { return; }
if (targetCharacter == null) break; if (FindEntityByID(characterID) is not Character targetCharacter) { break; }
if (targetCharacter != c.Character && c.Character.SelectedCharacter != targetCharacter) break; if (targetCharacter != c.Character && c.Character.SelectedCharacter != targetCharacter) { break; }
HealingCooldown.SetCooldown(c);
Limb targetLimb = limbIndex < targetCharacter.AnimController.Limbs.Length ? targetCharacter.AnimController.Limbs[limbIndex] : null; Limb targetLimb = limbIndex < targetCharacter.AnimController.Limbs.Length ? targetCharacter.AnimController.Limbs[limbIndex] : null;
if (ContainedItems == null || ContainedItems.All(i => i == null)) if (ContainedItems == null || ContainedItems.All(static i => i == null))
{ {
GameServer.Log(GameServer.CharacterLogName(c.Character) + " used item " + Name, ServerLog.MessageType.ItemInteraction); GameServer.Log($"{GameServer.CharacterLogName(c.Character)} used item {Name}", ServerLog.MessageType.ItemInteraction);
} }
else else
{ {
GameServer.Log( GameServer.Log(
GameServer.CharacterLogName(c.Character) + " used item " + Name + " (contained items: " + string.Join(", ", ContainedItems.Select(i => i.Name)) + ")", $"{GameServer.CharacterLogName(c.Character)} used item {Name} (contained items: {string.Join(", ", ContainedItems.Select(i => i.Name))})",
ServerLog.MessageType.ItemInteraction); ServerLog.MessageType.ItemInteraction);
} }
@@ -11,10 +11,10 @@ namespace Barotrauma.Networking
{ {
private static UInt32 LastIdentifier = 0; private static UInt32 LastIdentifier = 0;
public bool Expired => ExpirationTime is { } expirationTime && DateTime.Now > expirationTime; public bool Expired => ExpirationTime.TryUnwrap(out var expirationTime) && SerializableDateTime.LocalNow > expirationTime;
public BannedPlayer( public BannedPlayer(
string name, Either<Address, AccountId> addressOrAccountId, string reason, DateTime? expirationTime) string name, Either<Address, AccountId> addressOrAccountId, string reason, Option<SerializableDateTime> expirationTime)
{ {
this.Name = name; this.Name = name;
this.AddressOrAccountId = addressOrAccountId; this.AddressOrAccountId = addressOrAccountId;
@@ -39,6 +39,7 @@ namespace Barotrauma.Networking
{ {
LoadBanList(); LoadBanList();
} }
RemoveExpired();
} }
private void LoadLegacyBanList() private void LoadLegacyBanList()
@@ -69,7 +70,7 @@ namespace Barotrauma.Networking
{ {
if (DateTime.TryParse(separatedLine[2], out DateTime parsedTime)) if (DateTime.TryParse(separatedLine[2], out DateTime parsedTime))
{ {
expirationTime = parsedTime; expirationTime = DateTime.SpecifyKind(parsedTime, DateTimeKind.Local);
} }
else else
{ {
@@ -80,15 +81,18 @@ namespace Barotrauma.Networking
} }
string reason = separatedLine.Length > 3 ? string.Join(",", separatedLine.Skip(3)) : ""; string reason = separatedLine.Length > 3 ? string.Join(",", separatedLine.Skip(3)) : "";
if (expirationTime.HasValue && DateTime.Now > expirationTime.Value) { continue; } var serializableExpirationTime
= expirationTime.HasValue
? Option<SerializableDateTime>.Some(new SerializableDateTime(expirationTime.Value))
: Option<SerializableDateTime>.None();
if (AccountId.Parse(endpointStr).TryUnwrap(out var accountId)) if (AccountId.Parse(endpointStr).TryUnwrap(out var accountId))
{ {
bannedPlayers.Add(new BannedPlayer(name, accountId, reason, expirationTime)); bannedPlayers.Add(new BannedPlayer(name, accountId, reason, serializableExpirationTime));
} }
else if (Address.Parse(endpointStr).TryUnwrap(out var address)) else if (Address.Parse(endpointStr).TryUnwrap(out var address))
{ {
bannedPlayers.Add(new BannedPlayer(name, address, reason, expirationTime)); bannedPlayers.Add(new BannedPlayer(name, address, reason, serializableExpirationTime));
} }
} }
@@ -109,10 +113,22 @@ namespace Barotrauma.Networking
var name = element.GetAttributeString("name", "")!; var name = element.GetAttributeString("name", "")!;
var reason = element.GetAttributeString("reason", "")!; var reason = element.GetAttributeString("reason", "")!;
DateTime? expirationTime = DateTime.FromBinary(unchecked((long)element.GetAttributeUInt64("expirationtime", 0))); var expirationTime = Option<SerializableDateTime>.None();
var expirationTimeStr = element.GetAttributeString("expirationtime", "")!;
if (expirationTime < DateTime.Now) { expirationTime = null; }
if (UInt64.TryParse(expirationTimeStr, out var binaryDateTime) && binaryDateTime > 0)
{
// Backwards compatibility: if expirationtime is stored as an int,
// convert to SerializableDateTime with local timezone because
// banlists used to assume local time
expirationTime = Option<SerializableDateTime>.Some(
new SerializableDateTime(
DateTime.FromBinary((long)binaryDateTime),
SerializableTimeZone.LocalTimeZone));
}
expirationTime = expirationTime.Fallback(SerializableDateTime.Parse(expirationTimeStr));
if (accountId.IsNone() && address.IsNone()) { return Option<BannedPlayer>.None(); } if (accountId.IsNone() && address.IsNone()) { return Option<BannedPlayer>.None(); }
Either<Address, AccountId> addressOrAccountId = accountId.TryUnwrap(out var accId) Either<Address, AccountId> addressOrAccountId = accountId.TryUnwrap(out var accId)
@@ -171,14 +187,14 @@ namespace Barotrauma.Networking
string logMsg = "Banned " + name; string logMsg = "Banned " + name;
if (!string.IsNullOrEmpty(reason)) { logMsg += ", reason: " + reason; } if (!string.IsNullOrEmpty(reason)) { logMsg += ", reason: " + reason; }
if (duration.HasValue) { logMsg += ", duration: " + duration.Value.ToString(); } if (duration.HasValue) { logMsg += ", duration: " + duration.Value; }
DebugConsole.Log(logMsg); DebugConsole.Log(logMsg);
DateTime? expirationTime = null; Option<SerializableDateTime> expirationTime = Option<SerializableDateTime>.None();
if (duration.HasValue) if (duration.HasValue)
{ {
expirationTime = DateTime.Now + duration.Value; expirationTime = Option<SerializableDateTime>.Some(new SerializableDateTime(DateTime.Now + duration.Value));
} }
bannedPlayers.Add(new BannedPlayer(name, addressOrAccountId, reason, expirationTime)); bannedPlayers.Add(new BannedPlayer(name, addressOrAccountId, reason, expirationTime));
@@ -232,9 +248,10 @@ namespace Barotrauma.Networking
{ {
retVal.SetAttributeValue("address", address.StringRepresentation); retVal.SetAttributeValue("address", address.StringRepresentation);
} }
if (bannedPlayer.ExpirationTime is { } expirationTime) if (bannedPlayer.ExpirationTime.TryUnwrap(out var expirationTime))
{ {
retVal.SetAttributeValue("expirationtime", unchecked((ulong)expirationTime.ToBinary())); #warning TODO: stop writing binary DateTime representation after this gets on main
retVal.SetAttributeValue("expirationtime", expirationTime.ToLocalValue().ToBinary());
} }
return retVal; return retVal;
@@ -269,11 +286,11 @@ namespace Barotrauma.Networking
outMsg.WriteString(bannedPlayer.Name); outMsg.WriteString(bannedPlayer.Name);
outMsg.WriteUInt32(bannedPlayer.UniqueIdentifier); outMsg.WriteUInt32(bannedPlayer.UniqueIdentifier);
outMsg.WriteBoolean(bannedPlayer.ExpirationTime != null); outMsg.WriteBoolean(bannedPlayer.ExpirationTime.IsSome());
outMsg.WritePadBits(); outMsg.WritePadBits();
if (bannedPlayer.ExpirationTime != null) if (bannedPlayer.ExpirationTime.TryUnwrap(out var expirationTime))
{ {
double hoursFromNow = (bannedPlayer.ExpirationTime.Value - DateTime.Now).TotalHours; double hoursFromNow = (expirationTime.ToUtcValue() - DateTime.UtcNow).TotalHours;
outMsg.WriteDouble(hoursFromNow); outMsg.WriteDouble(hoursFromNow);
} }
@@ -140,6 +140,7 @@ namespace Barotrauma.Networking
ServerSettings = new ServerSettings(this, name, port, queryPort, maxPlayers, isPublic, attemptUPnP); ServerSettings = new ServerSettings(this, name, port, queryPort, maxPlayers, isPublic, attemptUPnP);
KarmaManager.SelectPreset(ServerSettings.KarmaPreset); KarmaManager.SelectPreset(ServerSettings.KarmaPreset);
ServerSettings.SetPassword(password); ServerSettings.SetPassword(password);
ServerSettings.SaveSettings();
Voting = new Voting(); Voting = new Voting();
@@ -3227,16 +3228,16 @@ namespace Barotrauma.Networking
} }
//too far to hear the msg -> don't send //too far to hear the msg -> don't send
if (string.IsNullOrWhiteSpace(modifiedMessage)) continue; if (string.IsNullOrWhiteSpace(modifiedMessage)) { continue; }
} }
break; break;
case ChatMessageType.Dead: case ChatMessageType.Dead:
//character still alive -> don't send //character still alive -> don't send
if (client != senderClient && client.Character != null && !client.Character.IsDead) continue; if (client != senderClient && client.Character != null && !client.Character.IsDead) { continue; }
break; break;
case ChatMessageType.Private: case ChatMessageType.Private:
//private msg sent to someone else than this client -> don't send //private msg sent to someone else than this client -> don't send
if (client != targetClient && client != senderClient) continue; if (client != targetClient && client != senderClient) { continue; }
break; break;
} }
@@ -3272,11 +3273,17 @@ namespace Barotrauma.Networking
//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.Sender)) { continue; }
} }
SendDirectChatMessage(new OrderChatMessage(message.Order, 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.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))
{
//send to chat-linked wifi components
Signal s = new Signal(message.Text, sender: message.Sender, source: senderRadio.Item);
senderRadio.TransmitSignal(s, sentFromChat: true);
}
} }
} }
@@ -10,6 +10,7 @@ namespace Barotrauma.Networking
segmentTable.StartNewSegment(ServerNetSegment.ChatMessage); segmentTable.StartNewSegment(ServerNetSegment.ChatMessage);
msg.WriteUInt16(NetStateID); msg.WriteUInt16(NetStateID);
msg.WriteRangedInteger((int)ChatMessageType.Order, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); msg.WriteRangedInteger((int)ChatMessageType.Order, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1);
msg.WriteString(Text);
msg.WriteString(SenderName); msg.WriteString(SenderName);
msg.WriteBoolean(SenderClient != null); msg.WriteBoolean(SenderClient != null);
if (SenderClient != null) if (SenderClient != null)
@@ -246,7 +246,7 @@ namespace Barotrauma.Networking
{ {
case ConnectionInitialization.ContentPackageOrder: case ConnectionInitialization.ContentPackageOrder:
DateTime timeNow = DateTime.UtcNow; SerializableDateTime timeNow = SerializableDateTime.UtcNow;
structToSend = new ServerPeerContentPackageOrderPacket structToSend = new ServerPeerContentPackageOrderPacket
{ {
ServerName = GameMain.Server.ServerName, ServerName = GameMain.Server.ServerName,
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>0.21.1.0</Version> <Version>0.21.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
@@ -351,21 +351,6 @@ namespace Barotrauma
} }
} }
public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam)
{
if (myTeam == otherTeam) { return true; }
return myTeam switch
{
// NPCs are friendly to the same team and the friendly NPCs
CharacterTeamType.None or CharacterTeamType.Team1 or CharacterTeamType.Team2 => otherTeam == CharacterTeamType.FriendlyNPC,
// Friendly NPCs are friendly to both player teams
CharacterTeamType.FriendlyNPC => otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2,
_ => true
};
}
public static bool IsOnFriendlyTeam(Character me, Character other) => IsOnFriendlyTeam(me.TeamID, other.TeamID);
public void ReequipUnequipped() public void ReequipUnequipped()
{ {
foreach (var item in unequippedItems) foreach (var item in unequippedItems)
@@ -206,13 +206,19 @@ namespace Barotrauma
private set; private set;
} = new HashSet<Submarine>(); } = new HashSet<Submarine>();
public bool IsTargetingPlayerTeam => IsTargetInPlayerTeam(SelectedAiTarget);
public static bool IsTargetBeingChasedBy(Character target, Character character) public static bool IsTargetBeingChasedBy(Character target, Character character)
=> character?.AIController is EnemyAIController enemyAI && enemyAI.SelectedAiTarget?.Entity == target && (enemyAI.State == AIState.Attack || enemyAI.State == AIState.Aggressive); => character?.AIController is EnemyAIController enemyAI && enemyAI.SelectedAiTarget?.Entity == target && (enemyAI.State == AIState.Attack || enemyAI.State == AIState.Aggressive);
public bool IsBeingChasedBy(Character c) => IsTargetBeingChasedBy(Character, c); public bool IsBeingChasedBy(Character c) => IsTargetBeingChasedBy(Character, c);
private bool IsBeingChased => IsBeingChasedBy(SelectedAiTarget?.Entity as Character); private bool IsBeingChased => IsBeingChasedBy(SelectedAiTarget?.Entity as Character);
private bool IsTargetInPlayerTeam(AITarget target) => target?.Entity?.Submarine != null && target.Entity.Submarine.Info.IsPlayer || target?.Entity is Character targetCharacter && targetCharacter.IsOnPlayerTeam; private static bool IsTargetInPlayerTeam(AITarget target) => target?.Entity?.Submarine != null && target.Entity.Submarine.Info.IsPlayer || target?.Entity is Character targetCharacter && targetCharacter.IsOnPlayerTeam;
private bool IsAttackingOwner(Character other) =>
PetBehavior != null && PetBehavior.Owner != null &&
!other.IsUnconscious && !other.IsArrested &&
other.AIController is HumanAIController humanAI &&
humanAI.ObjectiveManager.CurrentObjective is AIObjectiveCombat combat &&
combat.Enemy != null && combat.Enemy == PetBehavior.Owner;
private bool reverse; private bool reverse;
public bool Reverse public bool Reverse
@@ -355,7 +361,7 @@ namespace Barotrauma
{ {
targetingTag = "owner"; targetingTag = "owner";
} }
else if (targetCharacter.AIController is HumanAIController && !IsOnFriendlyTeam(Character, targetCharacter)) else if (PetBehavior != null && (!Character.IsOnFriendlyTeam(targetCharacter) || IsAttackingOwner(targetCharacter)))
{ {
targetingTag = "hostile"; targetingTag = "hostile";
} }
@@ -681,19 +687,22 @@ namespace Barotrauma
{ {
if (SelectedAiTarget.Entity is Character targetCharacter) if (SelectedAiTarget.Entity is Character targetCharacter)
{ {
bool IsValid(Character.Attacker a) bool ShouldRetaliate(Character.Attacker a)
{ {
Character c = a.Character; Character c = a.Character;
if (c.IsDead || c.Removed) { return false; } if (c == null || c.IsUnconscious || c.Removed) { return false; }
if (!Character.IsFriendly(c)) { return true; } // Can't target characters of same species/group because that would make us hostile to all friendly characters in the same species/group.
if (!c.IsPlayer) { return false; } if (Character.IsSameSpeciesOrGroup(c)) { return false; }
// Only apply the threshold to players if (targetCharacter.IsSameSpeciesOrGroup(c)) { return false; }
return a.Damage >= selectedTargetingParams.Threshold; if (c.IsPlayer || Character.IsOnFriendlyTeam(c))
{
return a.Damage >= selectedTargetingParams.Threshold;
}
return true;
} }
Character attacker = targetCharacter.LastAttackers.LastOrDefault(IsValid)?.Character; Character attacker = targetCharacter.LastAttackers.LastOrDefault(ShouldRetaliate)?.Character;
if (attacker?.AiTarget != null && !Character.IsSameSpeciesOrGroup(attacker) && !targetCharacter.IsSameSpeciesOrGroup(attacker)) if (attacker?.AiTarget != null)
{ {
// Can't retaliate on characters of same species or group because that would make us hostile to all friendly characters in the same group.
ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2); ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2);
SelectTarget(attacker.AiTarget); SelectTarget(attacker.AiTarget);
State = AIState.Attack; State = AIState.Attack;
@@ -1502,7 +1511,7 @@ namespace Barotrauma
{ {
hitTarget = limb.character; hitTarget = limb.character;
} }
if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget)) if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget) && !IsAttackingOwner(hitTarget))
{ {
return true; return true;
} }
@@ -2316,7 +2325,7 @@ namespace Barotrauma
{ {
t = limb.character; t = limb.character;
} }
if (t != null && (t == target || !Character.IsFriendly(t))) if (t != null && (t == target || (!Character.IsFriendly(t) || IsAttackingOwner(t))))
{ {
return true; return true;
} }
@@ -310,7 +310,7 @@ namespace Barotrauma
UseIndoorSteeringOutside = false; UseIndoorSteeringOutside = false;
} }
if (Character.Submarine == null || Character.IsOnPlayerTeam && !Character.IsEscorted && !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID)) if (Character.Submarine == null || Character.IsOnPlayerTeam && !Character.IsEscorted && !Character.IsOnFriendlyTeam(Character.Submarine.TeamID))
{ {
// Spot enemies while staying outside or inside an enemy ship. // Spot enemies while staying outside or inside an enemy ship.
// does not apply for escorted characters, such as prisoners or terrorists who have their own behavior // does not apply for escorted characters, such as prisoners or terrorists who have their own behavior
@@ -541,7 +541,7 @@ namespace Barotrauma
if (Character.LockHands) { return; } if (Character.LockHands) { return; }
if (ObjectiveManager.CurrentObjective == null) { return; } if (ObjectiveManager.CurrentObjective == null) { return; }
if (Character.CurrentHull == null) { return; } if (Character.CurrentHull == null) { return; }
bool oxygenLow = !Character.AnimController.HeadInWater && Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold && Character.NeedsOxygen; bool shouldActOnSuffocation = Character.IsLowInOxygen && !Character.AnimController.HeadInWater && HasDivingSuit(Character, requireOxygenTank: false) && !HasItem(Character, AIObjectiveFindDivingGear.OXYGEN_SOURCE, out _, conditionPercentage: 1);
bool isCarrying = ObjectiveManager.HasActiveObjective<AIObjectiveContainItem>() || ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>(); bool isCarrying = ObjectiveManager.HasActiveObjective<AIObjectiveContainItem>() || ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>();
bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective) bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective)
@@ -566,17 +566,17 @@ namespace Barotrauma
gotoObjective.Abandon = true; gotoObjective.Abandon = true;
} }
} }
if (!oxygenLow) if (!shouldActOnSuffocation)
{ {
return; return;
} }
} }
// Diving gear // Diving gear
if (oxygenLow || findItemState != FindItemState.OtherItem) if (shouldActOnSuffocation || findItemState != FindItemState.OtherItem)
{ {
bool needsGear = NeedsDivingGear(Character.CurrentHull, out _); bool needsGear = NeedsDivingGear(Character.CurrentHull, out _);
if (!needsGear || oxygenLow) if (!needsGear || shouldActOnSuffocation)
{ {
bool isCurrentObjectiveFindSafety = ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>(); bool isCurrentObjectiveFindSafety = ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>();
bool shouldKeepTheGearOn = bool shouldKeepTheGearOn =
@@ -591,14 +591,14 @@ namespace Barotrauma
Character.CurrentHull.IsWetRoom; Character.CurrentHull.IsWetRoom;
bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character; bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character;
bool removeDivingSuit = !shouldKeepTheGearOn && !IsOrderedToWait(); bool removeDivingSuit = !shouldKeepTheGearOn && !IsOrderedToWait();
if (oxygenLow && Character.CurrentHull.Oxygen > 0 && (!isCurrentObjectiveFindSafety || Character.OxygenAvailable < 1)) if (shouldActOnSuffocation && Character.CurrentHull.Oxygen > 0 && (!isCurrentObjectiveFindSafety || Character.OxygenAvailable < 1))
{ {
shouldKeepTheGearOn = false; shouldKeepTheGearOn = false;
// Remove the suit before we pass out // Remove the suit before we pass out
removeDivingSuit = true; removeDivingSuit = true;
} }
bool takeMaskOff = !shouldKeepTheGearOn; bool takeMaskOff = !shouldKeepTheGearOn;
if (!shouldKeepTheGearOn && !oxygenLow) if (!shouldKeepTheGearOn && !shouldActOnSuffocation)
{ {
if (ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>()) if (ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>())
{ {
@@ -647,7 +647,7 @@ namespace Barotrauma
var divingSuit = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR); var divingSuit = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR);
if (divingSuit != null && !divingSuit.HasTag(AIObjectiveFindDivingGear.DIVING_GEAR_WEARABLE_INDOORS)) if (divingSuit != null && !divingSuit.HasTag(AIObjectiveFindDivingGear.DIVING_GEAR_WEARABLE_INDOORS))
{ {
if (oxygenLow || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) if (shouldActOnSuffocation || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{ {
divingSuit.Drop(Character); divingSuit.Drop(Character);
HandleRelocation(divingSuit); HandleRelocation(divingSuit);
@@ -982,7 +982,7 @@ namespace Barotrauma
if (target.CurrentHull != hull) { continue; } if (target.CurrentHull != hull) { continue; }
if (AIObjectiveRescueAll.IsValidTarget(target, Character)) if (AIObjectiveRescueAll.IsValidTarget(target, Character))
{ {
if (AddTargets<AIObjectiveRescueAll, Character>(Character, target) && newOrder == null && !ObjectiveManager.HasActiveObjective<AIObjectiveRescue>()) if (AddTargets<AIObjectiveRescueAll, Character>(Character, target) && newOrder == null && (!Character.IsMedic || Character == target) && !ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
{ {
var orderPrefab = OrderPrefab.Prefabs["requestfirstaid"]; var orderPrefab = OrderPrefab.Prefabs["requestfirstaid"];
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
@@ -1161,7 +1161,7 @@ namespace Barotrauma
freezeAI = true; freezeAI = true;
} }
} }
if (attacker == null || attacker.IsDead || attacker.Removed) if (attacker == null || attacker.IsUnconscious || attacker.Removed)
{ {
// Don't react to the damage if there's no attacker. // Don't react to the damage if there's no attacker.
// We might consider launching the retreat combat objective in some cases, so that the bot does not just stand somewhere getting damaged and dying. // We might consider launching the retreat combat objective in some cases, so that the bot does not just stand somewhere getting damaged and dying.
@@ -1199,7 +1199,7 @@ namespace Barotrauma
return; return;
} }
float cumulativeDamage = realDamage + Character.GetDamageDoneByAttacker(attacker); float cumulativeDamage = realDamage + Character.GetDamageDoneByAttacker(attacker);
bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && Character.CombatAction == null; bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && attacker.CombatAction == null;
if (isAccidental) if (isAccidental)
{ {
if (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold) if (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold)
@@ -1279,7 +1279,7 @@ namespace Barotrauma
if (otherCharacter.Submarine != attacker.Submarine) { continue; } if (otherCharacter.Submarine != attacker.Submarine) { continue; }
if (otherCharacter.Info?.Job == null || otherCharacter.IsInstigator) { continue; } if (otherCharacter.Info?.Job == null || otherCharacter.IsInstigator) { continue; }
if (otherCharacter.IsPlayer) { continue; } if (otherCharacter.IsPlayer) { continue; }
if (!(otherCharacter.AIController is HumanAIController otherHumanAI)) { continue; } if (otherCharacter.AIController is not HumanAIController otherHumanAI) { continue; }
if (!otherHumanAI.IsFriendly(Character)) { continue; } if (!otherHumanAI.IsFriendly(Character)) { continue; }
bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull); bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull);
if (!isWitnessing) if (!isWitnessing)
@@ -1299,7 +1299,7 @@ namespace Barotrauma
AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage = 0, bool isWitnessing = false) AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage = 0, bool isWitnessing = false)
{ {
if (!(c.AIController is HumanAIController humanAI)) { return AIObjectiveCombat.CombatMode.None; } if (c.AIController is not HumanAIController humanAI) { return AIObjectiveCombat.CombatMode.None; }
if (!IsFriendly(attacker)) if (!IsFriendly(attacker))
{ {
if (c.Submarine == null) if (c.Submarine == null)
@@ -1327,7 +1327,7 @@ namespace Barotrauma
} }
if (attacker.IsPlayer && c.TeamID == attacker.TeamID) if (attacker.IsPlayer && c.TeamID == attacker.TeamID)
{ {
if (GameMain.IsSingleplayer || Character.TeamID != attacker.TeamID) if (GameMain.IsSingleplayer || c.TeamID != attacker.TeamID)
{ {
// Bots in the player team never act aggressively in single player when attacked by the player // Bots in the player team never act aggressively in single player when attacked by the player
// In multiplayer, they react only to players attacking them or other crew members // In multiplayer, they react only to players attacking them or other crew members
@@ -1345,11 +1345,11 @@ namespace Barotrauma
isAttackerFightingEnemy = true; isAttackerFightingEnemy = true;
return AIObjectiveCombat.CombatMode.None; return AIObjectiveCombat.CombatMode.None;
} }
if (isWitnessing && Character.CombatAction != null && !c.IsSecurity) if (isWitnessing && c.CombatAction != null && !c.IsSecurity)
{ {
return Character.CombatAction.WitnessReaction; return c.CombatAction.WitnessReaction;
} }
if (attacker.IsPlayer && FindInstigator() is Character instigator) if (!attacker.IsInstigator && c.IsOnFriendlyTeam(attacker) && FindInstigator() is Character instigator)
{ {
// The guards don't react to player's aggressions when there's an instigator around // The guards don't react to player's aggressions when there's an instigator around
isAttackerFightingEnemy = true; isAttackerFightingEnemy = true;
@@ -1359,11 +1359,11 @@ namespace Barotrauma
{ {
if (c.IsSecurity) if (c.IsSecurity)
{ {
return Character.CombatAction != null ? Character.CombatAction.GuardReaction : AIObjectiveCombat.CombatMode.None; return attacker.CombatAction != null ? attacker.CombatAction.GuardReaction : AIObjectiveCombat.CombatMode.Offensive;
} }
else else
{ {
return Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.None; return attacker.CombatAction != null ? attacker.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat;
} }
} }
else else
@@ -1567,20 +1567,20 @@ namespace Barotrauma
return false; return false;
} }
public static bool HasDivingGear(Character character, float conditionPercentage = 0) => HasDivingSuit(character, conditionPercentage) || HasDivingMask(character, conditionPercentage); public static bool HasDivingGear(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) => HasDivingSuit(character, conditionPercentage, requireOxygenTank) || HasDivingMask(character, conditionPercentage, requireOxygenTank);
/// <summary> /// <summary>
/// Check whether the character has a diving suit in usable condition plus some oxygen. /// Check whether the character has a diving suit in usable condition plus some oxygen.
/// </summary> /// </summary>
public static bool HasDivingSuit(Character character, float conditionPercentage = 0) public static bool HasDivingSuit(Character character, float conditionPercentage = 0, bool requireOxygenTank = true)
=> HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true, => HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, requireOxygenTank ? AIObjectiveFindDivingGear.OXYGEN_SOURCE : Identifier.Empty, conditionPercentage, requireEquipped: true,
predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes)); predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes));
/// <summary> /// <summary>
/// Check whether the character has a diving mask in usable condition plus some oxygen. /// Check whether the character has a diving mask in usable condition plus some oxygen.
/// </summary> /// </summary>
public static bool HasDivingMask(Character character, float conditionPercentage = 0) public static bool HasDivingMask(Character character, float conditionPercentage = 0, bool requireOxygenTank = true)
=> HasItem(character, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true); => HasItem(character, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out _, requireOxygenTank ? AIObjectiveFindDivingGear.OXYGEN_SOURCE : Identifier.Empty, conditionPercentage, requireEquipped: true);
private static List<Item> matchingItems = new List<Item>(); private static List<Item> matchingItems = new List<Item>();
@@ -2045,7 +2045,7 @@ namespace Barotrauma
public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false) public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false)
{ {
bool sameTeam = me.TeamID == other.TeamID; bool sameTeam = me.TeamID == other.TeamID;
bool teamGood = sameTeam || !onlySameTeam && IsOnFriendlyTeam(me, other); bool teamGood = sameTeam || !onlySameTeam && me.IsOnFriendlyTeam(other);
if (!teamGood) { return false; } if (!teamGood) { return false; }
if (!me.IsSameSpeciesOrGroup(other)) { return false; } if (!me.IsSameSpeciesOrGroup(other)) { return false; }
if (me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign) if (me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
@@ -352,7 +352,7 @@ namespace Barotrauma
Weapon = null; Weapon = null;
continue; continue;
} }
if (WeaponComponent.IsLoaded(character)) if (WeaponComponent.IsNotEmpty(character))
{ {
// All good, the weapon is loaded // All good, the weapon is loaded
break; break;
@@ -470,7 +470,7 @@ namespace Barotrauma
// Not in the inventory anymore or cannot find the weapon component // Not in the inventory anymore or cannot find the weapon component
return false; return false;
} }
if (!WeaponComponent.IsLoaded(character)) if (!WeaponComponent.IsNotEmpty(character))
{ {
// Try reloading (and seek ammo) // Try reloading (and seek ammo)
if (!Reload(seekAmmo)) if (!Reload(seekAmmo))
@@ -541,7 +541,7 @@ namespace Barotrauma
priority /= 2; priority /= 2;
} }
} }
if (!weapon.IsLoaded(character)) if (!weapon.IsNotEmpty(character))
{ {
if (weapon is RangedWeapon && !isAllowedToSeekWeapons) if (weapon is RangedWeapon && !isAllowedToSeekWeapons)
{ {
@@ -554,7 +554,15 @@ namespace Barotrauma
priority /= 2; priority /= 2;
} }
} }
if (Enemy.IsKnockedDown)
if (Enemy.Params.Health.StunImmunity)
{
if (weapon.Item.HasTag("stunner"))
{
priority /= 2;
}
}
else if (Enemy.IsKnockedDown)
{ {
// Enemy is stunned, reduce the priority of stunner weapons. // Enemy is stunned, reduce the priority of stunner weapons.
Attack attack = GetAttackDefinition(weapon); Attack attack = GetAttackDefinition(weapon);
@@ -244,7 +244,8 @@ namespace Barotrauma
public bool IsInTargetSlot(Item item) public bool IsInTargetSlot(Item item)
{ {
if (container?.Inventory is ItemInventory inventory && TargetSlot is not null) if (TargetSlot == null) { return true; }
if (container?.Inventory is ItemInventory inventory)
{ {
return inventory.IsInSlot(item, (int)TargetSlot); return inventory.IsInSlot(item, (int)TargetSlot);
} }
@@ -63,15 +63,16 @@ namespace Barotrauma
} }
else else
{ {
if (HumanAIController.NeedsDivingGear(character.CurrentHull, out bool needsSuit) && if ((character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false)) ||
(HumanAIController.NeedsDivingGear(character.CurrentHull, out bool needsSuit) &&
(needsSuit ? (needsSuit ?
!HumanAIController.HasDivingSuit(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)) : !HumanAIController.HasDivingSuit(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)) :
!HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)))) !HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)))))
{ {
Priority = 100; Priority = 100;
} }
else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) && else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) &&
character.Submarine != null && !AIController.IsOnFriendlyTeam(character.TeamID, character.Submarine.TeamID)) character.Submarine != null && !character.IsOnFriendlyTeam(character.Submarine.TeamID))
{ {
// Ordered to follow, hold position, or return back to main sub inside a hostile sub // Ordered to follow, hold position, or return back to main sub inside a hostile sub
// -> ignore find safety unless we need to find a diving gear // -> ignore find safety unless we need to find a diving gear
@@ -137,12 +138,14 @@ namespace Barotrauma
private float retryTimer; private float retryTimer;
protected override void Act(float deltaTime) protected override void Act(float deltaTime)
{ {
if (resetPriority) { return; }
var currentHull = character.CurrentHull; var currentHull = character.CurrentHull;
bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false);
bool dangerousPressure = currentHull == null || currentHull.LethalPressure > 0 && character.PressureProtection <= 0; bool dangerousPressure = currentHull == null || currentHull.LethalPressure > 0 && character.PressureProtection <= 0;
if (!character.LockHands && (!dangerousPressure || cannotFindSafeHull)) if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull))
{ {
bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit); bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit);
bool needsEquipment = false; bool needsEquipment = shouldActOnSuffocation;
if (needsDivingSuit) if (needsDivingSuit)
{ {
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.GetMinOxygen(character)); needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.GetMinOxygen(character));
@@ -178,7 +178,7 @@ namespace Barotrauma
requiredCondition = () => requiredCondition = () =>
Leak.Submarine == character.Submarine && Leak.Submarine == character.Submarine &&
Leak.linkedTo.Any(e => e is Hull h && (character.CurrentHull == h || h.linkedTo.Contains(character.CurrentHull))), Leak.linkedTo.Any(e => e is Hull h && (character.CurrentHull == h || h.linkedTo.Contains(character.CurrentHull))),
endNodeFilter = n => n.Waypoint.CurrentHull != null && Leak.linkedTo.Any(e => e is Hull h && h == n.Waypoint.CurrentHull), endNodeFilter = IsSuitableEndNode,
// The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue) // The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue)
SpeakCannotReachCondition = () => !CheckObjectiveSpecific() SpeakCannotReachCondition = () => !CheckObjectiveSpecific()
}, },
@@ -197,6 +197,14 @@ namespace Barotrauma
} }
}, },
onCompleted: () => RemoveSubObjective(ref gotoObjective)); onCompleted: () => RemoveSubObjective(ref gotoObjective));
bool IsSuitableEndNode(PathNode n)
{
if (n.Waypoint.CurrentHull is null) { return false; }
if (n.Waypoint.CurrentHull.ConnectedGaps.Contains(Leak)) { return true; }
// Accept also nodes located in the linked hulls (multi-hull rooms)
return Leak.linkedTo.Any(e => e is Hull h && h.linkedTo.Contains(n.Waypoint.CurrentHull));
}
} }
} }
@@ -54,7 +54,7 @@ namespace Barotrauma
public bool AllowVariants { get; set; } public bool AllowVariants { get; set; }
public bool Equip { get; set; } public bool Equip { get; set; }
public bool Wear { get; set; } public bool Wear { get; set; }
public bool RequireLoaded { get; set; } public bool RequireNonEmpty { get; set; }
public bool EvaluateCombatPriority { get; set; } public bool EvaluateCombatPriority { get; set; }
public bool CheckPathForEachItem { get; set; } public bool CheckPathForEachItem { get; set; }
public bool SpeakIfFails { get; set; } public bool SpeakIfFails { get; set; }
@@ -391,10 +391,10 @@ namespace Barotrauma
{ {
if (!itemInventory.Container.HasRequiredItems(character, addMessage: false)) { continue; } if (!itemInventory.Container.HasRequiredItems(character, addMessage: false)) { continue; }
} }
float itemPriority = 1; float itemPriority = item.Prefab.BotPriority;
if (GetItemPriority != null) if (GetItemPriority != null)
{ {
itemPriority = GetItemPriority(item); itemPriority *= GetItemPriority(item);
} }
Entity rootInventoryOwner = item.GetRootInventoryOwner(); Entity rootInventoryOwner = item.GetRootInventoryOwner();
if (rootInventoryOwner is Item ownerItem) if (rootInventoryOwner is Item ownerItem)
@@ -513,7 +513,7 @@ namespace Barotrauma
float lowestCost = float.MaxValue; float lowestCost = float.MaxValue;
foreach (MapEntityPrefab prefab in MapEntityPrefab.List) foreach (MapEntityPrefab prefab in MapEntityPrefab.List)
{ {
if (!(prefab is ItemPrefab itemPrefab)) { continue; } if (prefab is not ItemPrefab itemPrefab) { continue; }
if (IdentifiersOrTags.Any(id => id == prefab.Identifier || prefab.Tags.Contains(id))) if (IdentifiersOrTags.Any(id => id == prefab.Identifier || prefab.Tags.Contains(id)))
{ {
float cost = itemPrefab.DefaultPrice != null && itemPrefab.CanBeBought ? float cost = itemPrefab.DefaultPrice != null && itemPrefab.CanBeBought ?
@@ -561,7 +561,7 @@ namespace Barotrauma
if (ignoredIdentifiersOrTags != null && CheckItemIdentifiersOrTags(item, ignoredIdentifiersOrTags)) { return false; } if (ignoredIdentifiersOrTags != null && CheckItemIdentifiersOrTags(item, ignoredIdentifiersOrTags)) { return false; }
if (item.Condition < TargetCondition) { return false; } if (item.Condition < TargetCondition) { return false; }
if (ItemFilter != null && !ItemFilter(item)) { return false; } if (ItemFilter != null && !ItemFilter(item)) { return false; }
if (RequireLoaded && item.Components.Any(i => !i.IsLoaded(character))) { return false; } if (RequireNonEmpty && item.Components.Any(i => !i.IsNotEmpty(character))) { return false; }
return CheckItemIdentifiersOrTags(item, IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf)); return CheckItemIdentifiersOrTags(item, IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf));
} }
@@ -21,7 +21,7 @@ namespace Barotrauma
public bool CheckInventory { get; set; } public bool CheckInventory { get; set; }
public bool EvaluateCombatPriority { get; set; } public bool EvaluateCombatPriority { get; set; }
public bool CheckPathForEachItem { get; set; } public bool CheckPathForEachItem { get; set; }
public bool RequireLoaded { get; set; } public bool RequireNonEmpty { get; set; }
public bool RequireAllItems { get; set; } public bool RequireAllItems { get; set; }
private readonly ImmutableArray<Identifier> gearTags; private readonly ImmutableArray<Identifier> gearTags;
@@ -61,7 +61,7 @@ namespace Barotrauma
AllowStealing = AllowStealing, AllowStealing = AllowStealing,
ignoredIdentifiersOrTags = ignoredTags, ignoredIdentifiersOrTags = ignoredTags,
CheckPathForEachItem = CheckPathForEachItem, CheckPathForEachItem = CheckPathForEachItem,
RequireLoaded = RequireLoaded, RequireNonEmpty = RequireNonEmpty,
ItemCount = count, ItemCount = count,
SpeakIfFails = RequireAllItems SpeakIfFails = RequireAllItems
}, },
@@ -364,8 +364,7 @@ namespace Barotrauma
CurrentOrders.RemoveAt(i); CurrentOrders.RemoveAt(i);
continue; continue;
} }
var currentOrderInfo = character.GetCurrentOrder(currentOrder); if (character.GetCurrentOrder(currentOrder) is Order currentOrderInfo)
if (currentOrderInfo is Order)
{ {
int currentPriority = currentOrderInfo.ManualPriority; int currentPriority = currentOrderInfo.ManualPriority;
if (currentOrder.ManualPriority != currentPriority) if (currentOrder.ManualPriority != currentPriority)
@@ -539,7 +538,8 @@ namespace Barotrauma
KeepActiveWhenReady = true, KeepActiveWhenReady = true,
CheckInventory = true, CheckInventory = true,
Equip = false, Equip = false,
FindAllItems = true FindAllItems = true,
RequireNonEmpty = false
}; };
break; break;
case "findweapon": case "findweapon":
@@ -555,7 +555,8 @@ namespace Barotrauma
KeepActiveWhenReady = false, KeepActiveWhenReady = false,
CheckInventory = false, CheckInventory = false,
EvaluateCombatPriority = true, EvaluateCombatPriority = true,
FindAllItems = false FindAllItems = false,
RequireNonEmpty = true
}; };
} }
prepareObjective.KeepActiveWhenReady = false; prepareObjective.KeepActiveWhenReady = false;
@@ -600,9 +601,9 @@ namespace Barotrauma
Order dismissOrder = currentOrder.GetDismissal(); Order dismissOrder = currentOrder.GetDismissal();
#if CLIENT #if CLIENT
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer) if (GameMain.GameSession?.CrewManager is CrewManager cm && cm.IsSinglePlayer)
{ {
GameMain.GameSession.CrewManager.SetCharacterOrder(character, dismissOrder); character.SetOrder(dismissOrder, isNewOrder: true, speak: false);
} }
#else #else
GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(dismissOrder, character, character)); GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(dismissOrder, character, character));
@@ -27,6 +27,7 @@ namespace Barotrauma
public bool FindAllItems { get; set; } public bool FindAllItems { get; set; }
public bool Equip { get; set; } public bool Equip { get; set; }
public bool EvaluateCombatPriority { get; set; } public bool EvaluateCombatPriority { get; set; }
public bool RequireNonEmpty { get; set; }
private AIObjective GetSubObjective() private AIObjective GetSubObjective()
{ {
@@ -74,7 +75,7 @@ namespace Barotrauma
Abandon = true; Abandon = true;
} }
else if (items.Any(i => i.Components.Any(i => !i.IsLoaded(character)))) else if (items.Any(i => i.Components.Any(i => !i.IsNotEmpty(character))))
{ {
Reset(); Reset();
} }
@@ -106,7 +107,7 @@ namespace Barotrauma
CheckInventory = CheckInventory, CheckInventory = CheckInventory,
Equip = Equip, Equip = Equip,
EvaluateCombatPriority = EvaluateCombatPriority, EvaluateCombatPriority = EvaluateCombatPriority,
RequireLoaded = true, RequireNonEmpty = RequireNonEmpty,
RequireAllItems = requireAll RequireAllItems = requireAll
}, },
onCompleted: () => onCompleted: () =>
@@ -157,7 +158,7 @@ namespace Barotrauma
{ {
EvaluateCombatPriority = EvaluateCombatPriority, EvaluateCombatPriority = EvaluateCombatPriority,
SpeakIfFails = true, SpeakIfFails = true,
RequireLoaded = true RequireNonEmpty = RequireNonEmpty
}; };
} }
if (!TryAddSubObjective(ref getSingleItemObjective, getItemConstructor, if (!TryAddSubObjective(ref getSingleItemObjective, getItemConstructor,
@@ -320,10 +320,10 @@ namespace Barotrauma
foreach (KeyValuePair<Identifier, float> treatmentSuitability in currentTreatmentSuitabilities) foreach (KeyValuePair<Identifier, float> treatmentSuitability in currentTreatmentSuitabilities)
{ {
if (treatmentSuitability.Value <= cprSuitability) { continue; } if (treatmentSuitability.Value <= cprSuitability) { continue; }
if (MapEntityPrefab.Find(null, treatmentSuitability.Key, showErrorMessages: false) is ItemPrefab itemPrefab) if (ItemPrefab.Prefabs.TryGet(treatmentSuitability.Key, out ItemPrefab itemPrefab))
{ {
if (!Item.ItemList.Any(it => ((MapEntity)it).Prefab.Identifier == treatmentSuitability.Key)) { continue; } if (Item.ItemList.None(it => it.Prefab.Identifier == treatmentSuitability.Key)) { continue; }
suitableItemIdentifiers.Add(treatmentSuitability.Key); suitableItemIdentifiers.Add(itemPrefab.Identifier);
//only list the first 4 items //only list the first 4 items
if (itemNameList.Count < 4) if (itemNameList.Count < 4)
{ {
@@ -482,18 +482,6 @@ namespace Barotrauma
public static IEnumerable<Affliction> GetSortedAfflictions(Character character, bool excludeBuffs = true) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions(), excludeBuffs); public static IEnumerable<Affliction> GetSortedAfflictions(Character character, bool excludeBuffs = true) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions(), excludeBuffs);
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character)
{
var allAfflictions = character.CharacterHealth.GetAllAfflictions();
foreach (Affliction affliction in allAfflictions)
{
if (affliction.Prefab.IsBuff || affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; }
if (!affliction.Prefab.TreatmentSuitability.Any(kvp => kvp.Value > 0)) { continue; }
if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; }
yield return affliction;
}
}
public override void Reset() public override void Reset()
{ {
base.Reset(); base.Reset();
@@ -26,7 +26,7 @@ namespace Barotrauma
// When targeting player characters, always treat them when ordered, else use the threshold so that minor/non-severe damage is ignored. // When targeting player characters, always treat them when ordered, else use the threshold so that minor/non-severe damage is ignored.
// If we ignore any damage when the player orders a bot to do healings, it's observed to cause confusion among the players. // If we ignore any damage when the player orders a bot to do healings, it's observed to cause confusion among the players.
// On the other hand, if the bots too eagerly heal characters when it's not necessary, it's inefficient and can feel frustrating, because it can't be controlled. // On the other hand, if the bots too eagerly heal characters when it's not necessary, it's inefficient and can feel frustrating, because it can't be controlled.
return character == target || manager.HasOrder<AIObjectiveRescueAll>() ? (target.IsPlayer ? 100 : vitalityThresholdForOrders) : vitalityThreshold; return character == target || manager.HasOrder<AIObjectiveRescueAll>() ? (target.IsPlayer && target.HealthPercentage < 100 ? 100 : vitalityThresholdForOrders) : vitalityThreshold;
} }
} }
@@ -67,15 +67,34 @@ namespace Barotrauma
float vitality = 100; float vitality = 100;
vitality -= character.Bleeding * 2; vitality -= character.Bleeding * 2;
vitality += Math.Min(character.Oxygen, 0); vitality += Math.Min(character.Oxygen, 0);
vitality -= character.CharacterHealth.GetAfflictionStrength("paralysis"); foreach (Affliction affliction in GetTreatableAfflictions(character))
foreach (Affliction affliction in AIObjectiveRescue.GetTreatableAfflictions(character))
{ {
float strength = character.CharacterHealth.GetPredictedStrength(affliction, predictFutureDuration: 10.0f); float strength = character.CharacterHealth.GetPredictedStrength(affliction, predictFutureDuration: 10.0f);
vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100; vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100;
if (affliction.Prefab.AfflictionType == "paralysis")
{
vitality -= affliction.Strength;
}
else if (affliction.Prefab.AfflictionType == "poison")
{
vitality -= affliction.Strength;
}
} }
return Math.Clamp(vitality, 0, 100); return Math.Clamp(vitality, 0, 100);
} }
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character)
{
var allAfflictions = character.CharacterHealth.GetAllAfflictions();
foreach (Affliction affliction in allAfflictions)
{
if (affliction.Prefab.IsBuff || affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; }
if (affliction.Prefab.TreatmentSuitability.None(kvp => kvp.Value > 0)) { continue; }
if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; }
yield return affliction;
}
}
protected override AIObjective ObjectiveConstructor(Character target) protected override AIObjective ObjectiveConstructor(Character target)
=> new AIObjectiveRescue(character, target, objectiveManager, PriorityModifier); => new AIObjectiveRescue(character, target, objectiveManager, PriorityModifier);
@@ -454,7 +454,7 @@ namespace Barotrauma
aiming = false; aiming = false;
wasAimingMelee = aimingMelee; wasAimingMelee = aimingMelee;
aimingMelee = false; aimingMelee = false;
IsHanging = false; IsHanging = IsHanging && character.IsRagdolled;
} }
void UpdateStanding() void UpdateStanding()
@@ -489,7 +489,7 @@ namespace Barotrauma
LocalizedString displayName = Params.DisplayName; LocalizedString displayName = Params.DisplayName;
if (displayName.IsNullOrWhiteSpace()) if (displayName.IsNullOrWhiteSpace())
{ {
if (string.IsNullOrWhiteSpace(Params.SpeciesTranslationOverride)) if (Params.SpeciesTranslationOverride.IsEmpty)
{ {
displayName = TextManager.Get($"Character.{SpeciesName}"); displayName = TextManager.Get($"Character.{SpeciesName}");
} }
@@ -752,7 +752,7 @@ namespace Barotrauma
get get
{ {
if (IsUnconscious) { return true; } if (IsUnconscious) { return true; }
return CharacterHealth.GetAllAfflictions().Any(a => a.Prefab.AfflictionType == "paralysis" && a.Strength >= a.Prefab.MaxStrength); return CharacterHealth.GetAllAfflictions().Any(a => a.Prefab.Identifier == "paralysis" && a.Strength >= a.Prefab.MaxStrength);
} }
} }
@@ -822,6 +822,7 @@ namespace Barotrauma
public AIState AIState => AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle; public AIState AIState => AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle;
public bool IsLatched => AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached; public bool IsLatched => AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached;
public float EmpVulnerability => Params.Health.EmpVulnerability; public float EmpVulnerability => Params.Health.EmpVulnerability;
public float PoisonVulnerability => Params.Health.PoisonVulnerability;
public float Bloodloss public float Bloodloss
{ {
@@ -1040,6 +1041,8 @@ namespace Barotrauma
public bool InWater => AnimController is AnimController { InWater: true }; public bool InWater => AnimController is AnimController { InWater: true };
public bool IsLowInOxygen => NeedsOxygen && OxygenAvailable < CharacterHealth.LowOxygenThreshold;
public bool GodMode = false; public bool GodMode = false;
public CampaignMode.InteractionType CampaignInteractionType; public CampaignMode.InteractionType CampaignInteractionType;
@@ -2871,6 +2874,23 @@ namespace Barotrauma
} }
else else
{ {
#if CLIENT
if (Controlled == this)
{
HealingCooldown.PutOnCooldown();
}
#elif SERVER
if (GameMain.Server?.ConnectedClients is { } clients)
{
foreach (Client c in clients)
{
if (c.Character != this) { continue; }
HealingCooldown.SetCooldown(c);
break;
}
}
#endif
SelectCharacter(FocusedCharacter); SelectCharacter(FocusedCharacter);
#if CLIENT #if CLIENT
if (Controlled == this) if (Controlled == this)
@@ -3791,9 +3811,10 @@ namespace Barotrauma
message.SendDelay -= deltaTime; message.SendDelay -= deltaTime;
if (message.SendDelay > 0.0f) { continue; } if (message.SendDelay > 0.0f) { continue; }
bool canUseRadio = ChatMessage.CanUseRadio(this, out WifiComponent radio);
if (message.MessageType == null) if (message.MessageType == null)
{ {
message.MessageType = ChatMessage.CanUseRadio(this) ? ChatMessageType.Radio : ChatMessageType.Default; message.MessageType = canUseRadio ? ChatMessageType.Radio : ChatMessageType.Default;
} }
#if CLIENT #if CLIENT
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer) if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer)
@@ -3803,6 +3824,11 @@ namespace Barotrauma
{ {
GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(Name, modifiedMessage, message.MessageType.Value, this); GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(Name, modifiedMessage, message.MessageType.Value, this);
} }
if (canUseRadio)
{
Signal s = new Signal(modifiedMessage, sender: this, source: radio.Item);
radio.TransmitSignal(s, sentFromChat: true);
}
} }
#endif #endif
#if SERVER #if SERVER
@@ -4122,13 +4148,13 @@ namespace Barotrauma
OnAttackedProjSpecific(attacker, attackResult, stun); OnAttackedProjSpecific(attacker, attackResult, stun);
if (!wasDead) if (!wasDead)
{ {
TryAdjustAttackerSkill(attacker, CharacterHealth.Vitality - prevVitality); TryAdjustAttackerSkill(attacker, attackResult);
} }
}; }
if (attackResult.Damage > 0) if (attackResult.Damage > 0)
{ {
LastDamage = attackResult; LastDamage = attackResult;
if (attacker != null) if (attacker != null && attacker != this && !attacker.Removed)
{ {
AddAttacker(attacker, attackResult.Damage); AddAttacker(attacker, attackResult.Damage);
AddEncounter(attacker); AddEncounter(attacker);
@@ -4143,26 +4169,84 @@ namespace Barotrauma
partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult, float stun); partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult, float stun);
public void TryAdjustAttackerSkill(Character attacker, float healthChange) public void TryAdjustAttackerSkill(Character attacker, AttackResult attackResult)
{ {
if (attacker == null) { return; } if (attacker == null) { return; }
if (!attacker.IsOnPlayerTeam) { return; }
bool isEnemy = AIController is EnemyAIController || TeamID != attacker.TeamID; bool isEnemy = AIController is EnemyAIController || TeamID != attacker.TeamID;
if (isEnemy) if (!isEnemy) { return; }
float weaponDamage = 0;
float medicalDamage = 0;
foreach (var affliction in attackResult.Afflictions)
{ {
if (healthChange < 0.0f) if (affliction.Prefab.IsBuff) { continue; }
if (Params.IsMachine && !affliction.Prefab.AffectMachines) { continue; }
if (affliction.Prefab.AfflictionType == "poison" || affliction.Prefab.AfflictionType == "paralysis")
{ {
float attackerSkillLevel = attacker.GetSkillLevel("weapons"); if (!Params.Health.PoisonImmunity)
attacker.Info?.IncreaseSkillLevel("weapons".ToIdentifier(), {
-healthChange * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, 1.0f)); float relativeVitality = MaxVitality / 100f;
// Undo the applied modifiers to get the base value. Poison damage is multiplied by max vitality when it's applied.
float dmg = affliction.Strength;
if (relativeVitality > 0)
{
dmg /= relativeVitality;
}
if (PoisonVulnerability > 0)
{
dmg /= PoisonVulnerability;
}
float strength = MaxVitality;
if (Params.AI != null)
{
strength = Params.AI.CombatStrength;
}
// Adjust the skill gain by the strength of the target. Combat strength >= 1000 gives 2x bonus, combat strength < 333 less than 1x.
float vitalityFactor = MathHelper.Lerp(0.5f, 2f, MathUtils.InverseLerp(0, 1000, strength));
dmg *= vitalityFactor;
medicalDamage += dmg * affliction.Prefab.MedicalSkillGain;
}
} }
else
{
medicalDamage += affliction.GetVitalityDecrease(null) * affliction.Prefab.MedicalSkillGain;
}
weaponDamage += affliction.GetVitalityDecrease(null) * affliction.Prefab.WeaponsSkillGain;
} }
else if (healthChange > 0.0f) if (medicalDamage > 0)
{ {
float attackerSkillLevel = attacker.GetSkillLevel("medical"); IncreaseSkillLevel("medical".ToIdentifier(), medicalDamage);
attacker.Info?.IncreaseSkillLevel("medical".ToIdentifier(),
healthChange * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, 1.0f));
} }
if (weaponDamage > 0)
{
IncreaseSkillLevel("weapons".ToIdentifier(), weaponDamage);
}
void IncreaseSkillLevel(Identifier skill, float damage)
{
float attackerSkillLevel = attacker.GetSkillLevel(skill);
// The formula is too generous on low skill levels, hence the minimum divider.
float minSkillDivider = 15f;
attacker.Info?.IncreaseSkillLevel(skill, damage * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, minSkillDivider));
}
}
public void TryAdjustHealerSkill(Character healer, float healthChange = 0, Affliction affliction = null)
{
if (healer == null) { return; }
bool isEnemy = AIController is EnemyAIController || TeamID != healer.TeamID;
if (isEnemy) { return; }
float medicalGain = healthChange;
if (affliction?.Prefab is { IsBuff: true } && (!Params.IsMachine || affliction.Prefab.AffectMachines))
{
medicalGain += affliction.Strength * affliction.Prefab.MedicalSkillGain;
}
if (medicalGain <= 0) { return; }
Identifier skill = new Identifier("medical");
float attackerSkillLevel = healer.GetSkillLevel(skill);
// The formula is too generous on low skill levels, hence the minimum divider.
float minSkillDivider = 15f;
healer.Info?.IncreaseSkillLevel(skill, medicalGain * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, minSkillDivider));
} }
/// <summary> /// <summary>
@@ -5240,7 +5324,24 @@ namespace Barotrauma
public bool IsFriendly(Character other) => IsFriendly(this, other); public bool IsFriendly(Character other) => IsFriendly(this, other);
public static bool IsFriendly(Character me, Character other) => AIController.IsOnFriendlyTeam(me, other) && IsSameSpeciesOrGroup(me, other); public static bool IsFriendly(Character me, Character other) => IsOnFriendlyTeam(me, other) && IsSameSpeciesOrGroup(me, other);
public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam)
{
if (myTeam == otherTeam) { return true; }
return myTeam switch
{
// NPCs are friendly to the same team and the friendly NPCs
CharacterTeamType.None or CharacterTeamType.Team1 or CharacterTeamType.Team2 => otherTeam == CharacterTeamType.FriendlyNPC,
// Friendly NPCs are friendly to both player teams
CharacterTeamType.FriendlyNPC => otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2,
_ => true
};
}
public static bool IsOnFriendlyTeam(Character me, Character other) => IsOnFriendlyTeam(me.TeamID, other.TeamID);
public bool IsOnFriendlyTeam(Character other) => IsOnFriendlyTeam(TeamID, other.TeamID);
public bool IsOnFriendlyTeam(CharacterTeamType otherTeam) => IsOnFriendlyTeam(TeamID, otherTeam);
public bool IsSameSpeciesOrGroup(Character other) => IsSameSpeciesOrGroup(this, other); public bool IsSameSpeciesOrGroup(Character other) => IsSameSpeciesOrGroup(this, other);
@@ -321,6 +321,7 @@ namespace Barotrauma
{ {
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>(); public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
public readonly float MinInterval, MaxInterval; public readonly float MinInterval, MaxInterval;
public readonly float MinStrength, MaxStrength;
public PeriodicEffect(ContentXElement element, string parentDebugName) public PeriodicEffect(ContentXElement element, string parentDebugName)
{ {
@@ -335,8 +336,10 @@ namespace Barotrauma
} }
else else
{ {
MinInterval = Math.Max(element.GetAttributeFloat("mininterval", 1.0f), 1.0f); MinInterval = Math.Max(element.GetAttributeFloat(nameof(MinInterval), 1.0f), 1.0f);
MaxInterval = Math.Max(element.GetAttributeFloat("maxinterval", 1.0f), MinInterval); MaxInterval = Math.Max(element.GetAttributeFloat(nameof(MaxInterval), 1.0f), MinInterval);
MinStrength = Math.Max(element.GetAttributeFloat(nameof(MinStrength), 0f), 0f);
MaxStrength = Math.Max(element.GetAttributeFloat(nameof(MaxStrength), MinStrength), MinStrength);
} }
} }
} }
@@ -415,8 +418,8 @@ namespace Barotrauma
//how much karma changes when a player applies this affliction to someone (per strength of the affliction) //how much karma changes when a player applies this affliction to someone (per strength of the affliction)
public float KarmaChangeOnApplied; public float KarmaChangeOnApplied;
public float BurnOverlayAlpha; public readonly float BurnOverlayAlpha;
public float DamageOverlayAlpha; public readonly float DamageOverlayAlpha;
//steam achievement given when the affliction is removed from the controlled character //steam achievement given when the affliction is removed from the controlled character
public readonly Identifier AchievementOnRemoved; public readonly Identifier AchievementOnRemoved;
@@ -427,6 +430,20 @@ namespace Barotrauma
public readonly Sprite AfflictionOverlay; public readonly Sprite AfflictionOverlay;
public readonly bool AfflictionOverlayAlphaIsLinear; public readonly bool AfflictionOverlayAlphaIsLinear;
public readonly bool DamageParticles;
/// <summary>
/// An arbitrary modifier that affects how much medical skill is increased when you apply the affliction on a target.
/// If the affliction causes damage or is of type poison or paralysis, the skill is increased only when the target is hostile.
/// If the affliction is of type buff, the skill is increased only when the target is friendly.
/// </summary>
public readonly float MedicalSkillGain;
/// <summary>
/// An arbitrary modifier that affects how much weapons skill is increased when you apply the affliction on a target.
/// The skill is increased only when the target is hostile.
/// </summary>
public readonly float WeaponsSkillGain;
private readonly List<Effect> effects = new List<Effect>(); private readonly List<Effect> effects = new List<Effect>();
private readonly List<PeriodicEffect> periodicEffects = new List<PeriodicEffect>(); private readonly List<PeriodicEffect> periodicEffects = new List<PeriodicEffect>();
@@ -528,6 +545,10 @@ namespace Barotrauma
ResetBetweenRounds = element.GetAttributeBool("resetbetweenrounds", false); ResetBetweenRounds = element.GetAttributeBool("resetbetweenrounds", false);
DamageParticles = element.GetAttributeBool(nameof(DamageParticles), true);
WeaponsSkillGain = element.GetAttributeFloat(nameof(WeaponsSkillGain), 0.0f);
MedicalSkillGain = element.GetAttributeFloat(nameof(MedicalSkillGain), 0.0f);
List<Description> descriptions = new List<Description>(); List<Description> descriptions = new List<Description>();
foreach (var subElement in element.Elements()) foreach (var subElement in element.Elements())
{ {
@@ -708,7 +708,7 @@ namespace Barotrauma
return; return;
} }
} }
if (Character.Params.Health.PoisonImmunity && newAffliction.Prefab.AfflictionType == "poison") { return; } if (Character.Params.Health.PoisonImmunity && (newAffliction.Prefab.AfflictionType == "poison" || newAffliction.Prefab.AfflictionType == "paralysis")) { return; }
if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == "emp") { return; } if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == "emp") { return; }
if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab) if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab)
{ {
@@ -490,13 +490,9 @@ namespace Barotrauma
public int RefJointIndex => Params.RefJoint; public int RefJointIndex => Params.RefJoint;
private List<WearableSprite> wearingItems; public readonly List<WearableSprite> WearingItems = new List<WearableSprite>();
public List<WearableSprite> WearingItems
{
get { return wearingItems; }
}
public List<WearableSprite> OtherWearables { get; private set; } = new List<WearableSprite>(); public readonly List<WearableSprite> OtherWearables = new List<WearableSprite>();
public bool PullJointEnabled public bool PullJointEnabled
{ {
@@ -640,7 +636,6 @@ namespace Barotrauma
this.ragdoll = ragdoll; this.ragdoll = ragdoll;
this.character = character; this.character = character;
this.Params = limbParams; this.Params = limbParams;
wearingItems = new List<WearableSprite>();
dir = Direction.Right; dir = Direction.Right;
body = new PhysicsBody(limbParams); body = new PhysicsBody(limbParams);
type = limbParams.Type; type = limbParams.Type;
@@ -772,7 +767,7 @@ namespace Barotrauma
tempModifiers.Add(damageModifier); tempModifiers.Add(damageModifier);
} }
} }
foreach (WearableSprite wearable in wearingItems) foreach (WearableSprite wearable in WearingItems)
{ {
foreach (DamageModifier damageModifier in wearable.WearableComponent.DamageModifiers) foreach (DamageModifier damageModifier in wearable.WearableComponent.DamageModifiers)
{ {
@@ -791,10 +786,14 @@ namespace Barotrauma
} }
if (!foundMatchingModifier && random > affliction.Probability) { continue; } if (!foundMatchingModifier && random > affliction.Probability) { continue; }
float finalDamageModifier = damageMultiplier; float finalDamageModifier = damageMultiplier;
if (affliction.Prefab.AfflictionType == "emp" && character.EmpVulnerability > 0) if (character.EmpVulnerability > 0 && affliction.Prefab.AfflictionType == "emp")
{ {
finalDamageModifier *= character.EmpVulnerability; finalDamageModifier *= character.EmpVulnerability;
} }
if (!character.Params.Health.PoisonImmunity && (affliction.Prefab.AfflictionType == "poison" || affliction.Prefab.AfflictionType == "paralysis"))
{
finalDamageModifier *= character.PoisonVulnerability;
}
foreach (DamageModifier damageModifier in tempModifiers) foreach (DamageModifier damageModifier in tempModifiers)
{ {
float damageModifierValue = damageModifier.DamageMultiplier; float damageModifierValue = damageModifier.DamageMultiplier;
@@ -21,7 +21,7 @@ namespace Barotrauma
public Identifier SpeciesName { get; private set; } public Identifier SpeciesName { get; private set; }
[Serialize("", IsPropertySaveable.Yes, description: "If the creature is a variant that needs to use a pre-existing translation."), Editable] [Serialize("", IsPropertySaveable.Yes, description: "If the creature is a variant that needs to use a pre-existing translation."), Editable]
public string SpeciesTranslationOverride { get; private set; } public Identifier SpeciesTranslationOverride { get; private set; }
[Serialize("", IsPropertySaveable.Yes, description: "If the display name is not defined, the game first tries to find the translated name. If that is not found, the species name will be used."), Editable] [Serialize("", IsPropertySaveable.Yes, description: "If the display name is not defined, the game first tries to find the translated name. If that is not found, the species name will be used."), Editable]
public string DisplayName { get; private set; } public string DisplayName { get; private set; }
@@ -501,6 +501,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes), Editable] [Serialize(false, IsPropertySaveable.Yes), Editable]
public bool PoisonImmunity { get; set; } public bool PoisonImmunity { get; set; }
[Serialize(1f, IsPropertySaveable.Yes, description: "1 = default, 0 = immune."), Editable(MinValueFloat = 0f, MaxValueFloat = 1000, DecimalCount = 1)]
public float PoisonVulnerability { get; set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable] [Serialize(0f, IsPropertySaveable.Yes), Editable]
public float EmpVulnerability { get; set; } public float EmpVulnerability { get; set; }
@@ -42,7 +42,7 @@ namespace Barotrauma
public readonly Version GameVersion; public readonly Version GameVersion;
public readonly string ModVersion; public readonly string ModVersion;
public Md5Hash Hash { get; private set; } public Md5Hash Hash { get; private set; }
public readonly Option<DateTime> InstallTime; public readonly Option<SerializableDateTime> InstallTime;
public ImmutableArray<ContentFile> Files { get; private set; } public ImmutableArray<ContentFile> Files { get; private set; }
@@ -73,7 +73,7 @@ namespace Barotrauma
Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(steamWorkshopId.Value); Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(steamWorkshopId.Value);
if (item is null) { return true; } if (item is null) { return true; }
return item.Value.LatestUpdateTime <= installTime; return item.Value.LatestUpdateTime <= installTime.ToUtcValue();
} }
public int Index => ContentPackageManager.EnabledPackages.IndexOf(this); public int Index => ContentPackageManager.EnabledPackages.IndexOf(this);
@@ -106,10 +106,7 @@ namespace Barotrauma
GameVersion = rootElement.GetAttributeVersion("gameversion", GameMain.Version); GameVersion = rootElement.GetAttributeVersion("gameversion", GameMain.Version);
ModVersion = rootElement.GetAttributeString("modversion", DefaultModVersion); ModVersion = rootElement.GetAttributeString("modversion", DefaultModVersion);
UInt64 installTimeUnix = rootElement.GetAttributeUInt64("installtime", 0); InstallTime = rootElement.GetAttributeDateTime("installtime");
InstallTime = installTimeUnix != 0
? Option<DateTime>.Some(ToolBox.Epoch.ToDateTime(installTimeUnix))
: Option<DateTime>.None();
var fileResults = rootElement.Elements() var fileResults = rootElement.Elements()
.Select(e => ContentFile.CreateFromXElement(this, e)) .Select(e => ContentFile.CreateFromXElement(this, e))
@@ -1868,6 +1868,7 @@ namespace Barotrauma
commands.Add(new Command("followsub", "Toggle whether the camera should follow the nearest submarine (client-only).", null)); commands.Add(new Command("followsub", "Toggle whether the camera should follow the nearest submarine (client-only).", null));
commands.Add(new Command("toggleaitargets|aitargets", "Toggle the visibility of AI targets (= targets that enemies can detect and attack/escape from) (client-only).", null, isCheat: true)); commands.Add(new Command("toggleaitargets|aitargets", "Toggle the visibility of AI targets (= targets that enemies can detect and attack/escape from) (client-only).", null, isCheat: true));
commands.Add(new Command("debugai", "Toggle the ai debug mode on/off (works properly only in single player).", null, isCheat: true)); commands.Add(new Command("debugai", "Toggle the ai debug mode on/off (works properly only in single player).", null, isCheat: true));
commands.Add(new Command("devmode", "Toggle the dev mode on/off (client-only).", null, isCheat: true));
InitProjectSpecific(); InitProjectSpecific();
@@ -19,7 +19,9 @@ partial class UIHighlightAction : EventAction
TurbineOutputSlider, TurbineOutputSlider,
DeconstructButton, DeconstructButton,
RechargeSpeedSlider, RechargeSpeedSlider,
CPRButton CPRButton,
CloseButton,
MessageBoxCloseButton
} }
[Serialize(ElementId.None, IsPropertySaveable.Yes)] [Serialize(ElementId.None, IsPropertySaveable.Yes)]
@@ -21,7 +21,7 @@ namespace Barotrauma
for (int i = 0; i < Submarine.MainSubs.Length; i++) for (int i = 0; i < Submarine.MainSubs.Length; i++)
{ {
var sub = Submarine.MainSubs[i]; var sub = Submarine.MainSubs[i];
if (sub == null || sub.Info.InitialSuppliesSpawned || !sub.Info.IsPlayer) { continue; } if (sub == null || sub.Info.InitialSuppliesSpawned || sub.Info.IsManuallyOutfitted || !sub.Info.IsPlayer) { continue; }
//1st pass: items defined in the start item set, only spawned in the main sub (not drones/shuttles or other linked subs) //1st pass: items defined in the start item set, only spawned in the main sub (not drones/shuttles or other linked subs)
SpawnStartItems(sub, startItemSet); SpawnStartItems(sub, startItemSet);
//2nd pass: items defined using preferred containers, spawned in the main sub and all the linked subs (drones, shuttles etc) //2nd pass: items defined using preferred containers, spawned in the main sub and all the linked subs (drones, shuttles etc)
@@ -5,6 +5,7 @@ using FarseerPhysics;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Xml.Linq; using System.Xml.Linq;
@@ -13,13 +14,11 @@ namespace Barotrauma
abstract partial class CampaignMode : GameMode abstract partial class CampaignMode : GameMode
{ {
[NetworkSerialize] [NetworkSerialize]
public struct SaveInfo : INetSerializableStruct public readonly record struct SaveInfo(
{ string FilePath,
public string FilePath; Option<SerializableDateTime> SaveTime,
public int SaveTime; string SubmarineName,
public string SubmarineName; ImmutableArray<string> EnabledContentPackageNames) : INetSerializableStruct;
public string[] EnabledContentPackageNames;
}
public const int MaxMoney = int.MaxValue / 2; //about 1 billion public const int MaxMoney = int.MaxValue / 2; //about 1 billion
public const int InitialMoney = 8500; public const int InitialMoney = 8500;
@@ -1114,7 +1113,6 @@ namespace Barotrauma
if (item.Components.None(c => c is Pickable)) { continue; } if (item.Components.None(c => c is Pickable)) { continue; }
if (item.Components.Any(c => c is Pickable p && p.IsAttached)) { continue; } if (item.Components.Any(c => c is Pickable p && p.IsAttached)) { continue; }
if (item.Components.Any(c => c is Wire w && w.Connections.Any(c => c != null))) { continue; } if (item.Components.Any(c => c is Wire w && w.Connections.Any(c => c != null))) { continue; }
if (item.Container?.GetComponent<ItemContainer>() is { DrawInventory: false }) { continue; }
itemsToTransfer.Add((item, item.Container)); itemsToTransfer.Add((item, item.Container));
item.Submarine = null; item.Submarine = null;
} }
@@ -546,9 +546,7 @@ namespace Barotrauma
StatusEffect.StopAll(); StatusEffect.StopAll();
#if CLIENT #if CLIENT
#if !DEBUG GameMain.LightManager.LosEnabled = (GameMain.Client == null || GameMain.Client.CharacterInfo != null) && !GameMain.DevMode;
GameMain.LightManager.LosEnabled = GameMain.Client == null || GameMain.Client.CharacterInfo != null;
#endif
if (GameMain.LightManager.LosEnabled) { GameMain.LightManager.LosAlpha = 1f; } if (GameMain.LightManager.LosEnabled) { GameMain.LightManager.LosAlpha = 1f; }
if (GameMain.Client == null) { GameMain.LightManager.LosMode = GameSettings.CurrentConfig.Graphics.LosMode; } if (GameMain.Client == null) { GameMain.LightManager.LosMode = GameSettings.CurrentConfig.Graphics.LosMode; }
#endif #endif
@@ -1074,7 +1072,10 @@ namespace Barotrauma
XDocument doc = new XDocument(new XElement("Gamesession")); XDocument doc = new XDocument(new XElement("Gamesession"));
XElement rootElement = doc.Root ?? throw new NullReferenceException("Game session XML element is invalid: document is null."); XElement rootElement = doc.Root ?? throw new NullReferenceException("Game session XML element is invalid: document is null.");
rootElement.Add(new XAttribute("savetime", ToolBox.Epoch.NowLocal)); rootElement.Add(new XAttribute("savetime", SerializableDateTime.UtcNow.ToUnixTime()));
#warning TODO: after this gets on main, replace savetime with the commented line
//rootElement.Add(new XAttribute("savetime", SerializableDateTime.LocalNow));
rootElement.Add(new XAttribute("version", GameMain.Version)); rootElement.Add(new XAttribute("version", GameMain.Version));
if (Submarine?.Info != null && !Submarine.Removed && Campaign != null) if (Submarine?.Info != null && !Submarine.Removed && Campaign != null)
{ {

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