Unstable 0.17.4.0

This commit is contained in:
Markus Isberg
2022-03-30 00:08:09 +09:00
parent 2968e23ae8
commit c1b8e5a341
177 changed files with 3388 additions and 1977 deletions
@@ -1,185 +0,0 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
namespace Barotrauma
{
class CameraTransition
{
public bool Running
{
get;
private set;
}
public Camera AssignedCamera;
private readonly Alignment? cameraStartPos;
private readonly Alignment? cameraEndPos;
private readonly float? startZoom;
private readonly float? endZoom;
public readonly float WaitDuration;
public readonly float PanDuration;
public readonly bool FadeOut;
public readonly bool LosFadeIn;
private readonly CoroutineHandle updateCoroutine;
private Character prevControlled;
public bool AllowInterrupt = false;
public bool RemoveControlFromCharacter = true;
public CameraTransition(ISpatialEntity targetEntity, Camera cam, Alignment? cameraStartPos, Alignment? cameraEndPos, bool fadeOut = true, bool losFadeIn = false, float waitDuration = 0f, float panDuration = 10.0f, float? startZoom = null, float? endZoom = null)
{
WaitDuration = waitDuration;
PanDuration = panDuration;
FadeOut = fadeOut;
LosFadeIn = losFadeIn;
this.cameraStartPos = cameraStartPos;
this.cameraEndPos = cameraEndPos;
this.startZoom = startZoom;
this.endZoom = endZoom;
AssignedCamera = cam;
if (targetEntity == null) { return; }
Running = true;
CoroutineManager.StopCoroutines("CameraTransition");
updateCoroutine = CoroutineManager.StartCoroutine(Update(targetEntity, cam), "CameraTransition");
}
public void Stop()
{
CoroutineManager.StopCoroutines(updateCoroutine);
Running = false;
#if CLIENT
if (FadeOut) { GUI.ScreenOverlayColor = Color.TransparentBlack; }
if (prevControlled != null && !prevControlled.Removed)
{
Character.Controlled = prevControlled;
}
#endif
}
private IEnumerable<CoroutineStatus> Update(ISpatialEntity targetEntity, Camera cam)
{
if (targetEntity == null || (targetEntity is Entity e && e.Removed)) { yield return CoroutineStatus.Success; }
prevControlled = Character.Controlled;
if (RemoveControlFromCharacter)
{
#if CLIENT
GameMain.LightManager.LosEnabled = false;
#endif
Character.Controlled = null;
}
cam.TargetPos = Vector2.Zero;
float startZoom = this.startZoom ?? cam.Zoom;
float endZoom = this.endZoom ?? 0.5f;
Vector2 initialCameraPos = cam.Position;
Vector2? initialTargetPos = targetEntity?.WorldPosition;
float timer = -WaitDuration;
while (timer < PanDuration)
{
float clampedTimer = Math.Max(timer, 0f);
if (Screen.Selected != GameMain.GameScreen)
{
yield return new WaitForSeconds(0.1f);
#if CLIENT
if (FadeOut) { GUI.ScreenOverlayColor = Color.TransparentBlack; }
#endif
Running = false;
yield return CoroutineStatus.Success;
}
//switched control to some other character during the transition -> remove control again
if (Character.Controlled != null)
{
prevControlled = Character.Controlled;
if (RemoveControlFromCharacter)
{
#if CLIENT
GameMain.LightManager.LosEnabled = false;
#endif
Character.Controlled = null;
}
}
if (prevControlled != null && prevControlled.Removed)
{
prevControlled = null;
}
#if CLIENT
if (AllowInterrupt && PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape))
{
break;
}
#endif
Vector2 minPos = targetEntity.WorldPosition;
Vector2 maxPos = targetEntity.WorldPosition;
if (targetEntity is Submarine sub)
{
minPos = new Vector2(sub.WorldPosition.X - sub.Borders.Width / 2, sub.WorldPosition.Y - sub.Borders.Height / 2);
maxPos = new Vector2(sub.WorldPosition.X + sub.Borders.Width / 2, sub.WorldPosition.Y + sub.Borders.Height / 2);
}
Vector2 startPos = cameraStartPos.HasValue ?
new Vector2(
MathHelper.Lerp(minPos.X, maxPos.X, (cameraStartPos.Value.ToVector2().X + 1.0f) / 2.0f),
MathHelper.Lerp(maxPos.Y, minPos.Y, (cameraStartPos.Value.ToVector2().Y + 1.0f) / 2.0f)) :
initialCameraPos;
if (!cameraStartPos.HasValue && initialTargetPos.HasValue)
{
startPos += targetEntity.WorldPosition - initialTargetPos.Value;
}
Vector2 endPos = cameraEndPos.HasValue ?
new Vector2(
MathHelper.Lerp(minPos.X, maxPos.X, (cameraEndPos.Value.ToVector2().X + 1.0f) / 2.0f),
MathHelper.Lerp(maxPos.Y, minPos.Y, (cameraEndPos.Value.ToVector2().Y + 1.0f) / 2.0f)) :
prevControlled?.WorldPosition ?? targetEntity.WorldPosition;
Vector2 cameraPos = Vector2.SmoothStep(startPos, endPos, clampedTimer / PanDuration);
cam.Translate(cameraPos - cam.Position);
#if CLIENT
cam.Zoom = MathHelper.SmoothStep(startZoom, endZoom, clampedTimer / PanDuration);
if (clampedTimer / PanDuration > 0.9f)
{
if (FadeOut) { GUI.ScreenOverlayColor = Color.Lerp(Color.TransparentBlack, Color.Black, ((clampedTimer / PanDuration) - 0.9f) * 10.0f); }
}
if (LosFadeIn && clampedTimer / PanDuration > 0.8f)
{
GameMain.LightManager.LosAlpha = ((clampedTimer / PanDuration) - 0.8f) * 5.0f;
Lights.LightManager.ViewTarget = prevControlled ?? (targetEntity as Entity);
GameMain.LightManager.LosEnabled = true;
}
#endif
timer += CoroutineManager.UnscaledDeltaTime;
yield return CoroutineStatus.Running;
}
Running = false;
yield return new WaitForSeconds(0.1f);
#if CLIENT
GUI.ScreenOverlayColor = Color.TransparentBlack;
GameMain.LightManager.LosEnabled = true;
GameMain.LightManager.LosAlpha = 1f;
#endif
if (prevControlled != null && !prevControlled.Removed)
{
Character.Controlled = prevControlled;
}
yield return CoroutineStatus.Success;
}
}
}
@@ -56,7 +56,7 @@ namespace Barotrauma
private readonly float updateTargetsInterval = 1;
private readonly float updateMemoriesInverval = 1;
private readonly float attackLimbResetInterval = 2;
private readonly float attackLimbSelectionInterval = 3;
// Min priority for the memorized targets. The actual value fades gradually, unless kept fresh by selecting the target.
private const float minPriority = 10;
@@ -65,10 +65,10 @@ namespace Barotrauma
private float updateTargetsTimer;
private float updateMemoriesTimer;
private float attackLimbResetTimer;
private float attackLimbSelectionTimer;
private bool IsAttackRunning => AttackingLimb != null && AttackingLimb.attack.IsRunning;
private bool IsCoolDownRunning => AttackingLimb != null && AttackingLimb.attack.CoolDownTimer > 0 || _previousAttackingLimb != null && _previousAttackingLimb.attack.CoolDownTimer > 0;
private bool IsAttackRunning => AttackLimb != null && AttackLimb.attack.IsRunning;
private bool IsCoolDownRunning => AttackLimb != null && AttackLimb.attack.CoolDownTimer > 0 || _previousAttackLimb != null && _previousAttackLimb.attack.CoolDownTimer > 0;
public float CombatStrength => AIParams.CombatStrength;
private float Sight => AIParams.Sight;
private float Hearing => AIParams.Hearing;
@@ -77,25 +77,25 @@ namespace Barotrauma
private FishAnimController FishAnimController => Character.AnimController as FishAnimController;
private Limb _attackingLimb;
private Limb _previousAttackingLimb;
public Limb AttackingLimb
private Limb _attackLimb;
private Limb _previousAttackLimb;
public Limb AttackLimb
{
get { return _attackingLimb; }
get { return _attackLimb; }
private set
{
attackLimbResetTimer = 0;
if (_attackingLimb != value)
if (_attackLimb != value)
{
_previousAttackingLimb = _attackingLimb;
_previousAttackLimb = _attackLimb;
_previousAttackLimb?.AttachedRope?.Snap();
}
if (_attackingLimb != null && value != _attackingLimb && _attackingLimb.attack.CoolDownTimer > 0)
else if (_attackLimb != null && _attackLimb.attack.CoolDownTimer <= 0)
{
SetAimTimer();
_attackLimb.AttachedRope?.Snap();
}
_attackingLimb = value;
_attackLimb = value;
attackVector = null;
Reverse = _attackingLimb != null && _attackingLimb.attack.Reverse;
Reverse = _attackLimb != null && _attackLimb.attack.Reverse;
}
}
@@ -425,7 +425,8 @@ namespace Barotrauma
private void ReleaseDragTargets()
{
if (Character.Inventory != null)
AttackLimb?.AttachedRope?.Snap();
if (Character.Params.CanInteract && Character.Inventory != null)
{
Character.HeldItems.ForEach(i => i.GetComponent<Holdable>()?.GetRope()?.Snap());
}
@@ -600,7 +601,7 @@ namespace Barotrauma
UpdatePatrol(deltaTime);
break;
case AIState.Attack:
run = !IsCoolDownRunning || AttackingLimb != null && AttackingLimb.attack.FullSpeedAfterAttack;
run = !IsCoolDownRunning || AttackLimb != null && AttackLimb.attack.FullSpeedAfterAttack;
UpdateAttack(deltaTime);
break;
case AIState.Eat:
@@ -620,7 +621,7 @@ namespace Barotrauma
return;
}
float squaredDistance = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition);
var attackLimb = AttackingLimb ?? GetAttackLimb(SelectedAiTarget.WorldPosition);
var attackLimb = AttackLimb ?? GetAttackLimb(SelectedAiTarget.WorldPosition);
if (attackLimb != null && squaredDistance <= Math.Pow(attackLimb.attack.Range, 2))
{
run = true;
@@ -875,7 +876,10 @@ namespace Barotrauma
if (followLastTarget)
{
var target = SelectedAiTarget ?? _lastAiTarget;
if (target?.Entity != null && !target.Entity.Removed && PreviousState == AIState.Attack && Character.CurrentHull == null)
if (target?.Entity != null && !target.Entity.Removed &&
PreviousState == AIState.Attack && Character.CurrentHull == null &&
(_previousAttackLimb?.attack == null ||
_previousAttackLimb?.attack is Attack previousAttack && (previousAttack.AfterAttack != AIBehaviorAfterAttack.FallBack || previousAttack.CoolDownTimer <= 0)))
{
// Keep heading to the last known position of the target
var memory = GetTargetMemory(target, false);
@@ -1126,31 +1130,42 @@ namespace Barotrauma
return;
}
}
attackLimbSelectionTimer -= deltaTime;
if (AttackLimb == null || attackLimbSelectionTimer <= 0)
{
attackLimbSelectionTimer = attackLimbSelectionInterval * Rand.Range(0.9f, 1.1f);
if (!IsAttackRunning && !IsCoolDownRunning)
{
AttackLimb = GetAttackLimb(attackWorldPos);
}
}
bool canAttack = true;
bool pursue = false;
if (IsCoolDownRunning)
if (IsCoolDownRunning && (_previousAttackLimb == null || AttackLimb == null || AttackLimb.attack.CoolDownTimer > 0))
{
var currentAttackLimb = AttackingLimb ?? _previousAttackingLimb;
var currentAttackLimb = AttackLimb ?? _previousAttackLimb;
if (currentAttackLimb.attack.CoolDownTimer >= currentAttackLimb.attack.CoolDown + currentAttackLimb.attack.CurrentRandomCoolDown - currentAttackLimb.attack.AfterAttackDelay)
{
return;
}
switch (currentAttackLimb.attack.AfterAttack)
AIBehaviorAfterAttack activeBehavior = currentAttackLimb.attack.AfterAttack;
switch (activeBehavior)
{
case AIBehaviorAfterAttack.Pursue:
case AIBehaviorAfterAttack.PursueIfCanAttack:
if (currentAttackLimb.attack.SecondaryCoolDown <= 0)
{
// No (valid) secondary cooldown defined.
if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue)
if (activeBehavior == AIBehaviorAfterAttack.Pursue)
{
canAttack = false;
pursue = true;
}
else
{
UpdateFallBack(attackWorldPos, deltaTime, true);
UpdateFallBack(attackWorldPos, deltaTime, followThrough: true);
return;
}
}
@@ -1162,13 +1177,13 @@ namespace Barotrauma
if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget)
{
canAttack = false;
if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.PursueIfCanAttack)
if (activeBehavior == AIBehaviorAfterAttack.PursueIfCanAttack)
{
// Fall back if cannot attack.
UpdateFallBack(attackWorldPos, deltaTime, true);
UpdateFallBack(attackWorldPos, deltaTime, followThrough: true);
return;
}
AttackingLimb = null;
AttackLimb = null;
}
else
{
@@ -1177,19 +1192,19 @@ namespace Barotrauma
if (newLimb != null)
{
// Attack with the new limb
AttackingLimb = newLimb;
AttackLimb = newLimb;
}
else
{
// No new limb was found.
if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue)
if (activeBehavior == AIBehaviorAfterAttack.Pursue)
{
canAttack = false;
pursue = true;
}
else
{
UpdateFallBack(attackWorldPos, deltaTime, true);
UpdateFallBack(attackWorldPos, deltaTime, followThrough: true);
return;
}
}
@@ -1204,10 +1219,15 @@ namespace Barotrauma
break;
case AIBehaviorAfterAttack.FallBackUntilCanAttack:
case AIBehaviorAfterAttack.FollowThroughUntilCanAttack:
case AIBehaviorAfterAttack.ReverseUntilCanAttack:
if (activeBehavior == AIBehaviorAfterAttack.ReverseUntilCanAttack)
{
Reverse = true;
}
if (currentAttackLimb.attack.SecondaryCoolDown <= 0)
{
// No (valid) secondary cooldown defined.
UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
return;
}
else
@@ -1217,7 +1237,7 @@ namespace Barotrauma
// Don't allow attacking when the attack target has just changed.
if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget)
{
UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
return;
}
else
@@ -1227,12 +1247,12 @@ namespace Barotrauma
if (newLimb != null)
{
// Attack with the new limb
AttackingLimb = newLimb;
AttackLimb = newLimb;
}
else
{
// No new limb was found.
UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
return;
}
}
@@ -1240,7 +1260,7 @@ namespace Barotrauma
else
{
// Cooldown not yet expired -> steer away from the target
UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
return;
}
}
@@ -1269,7 +1289,7 @@ namespace Barotrauma
if (newLimb != null)
{
// Attack with the new limb
AttackingLimb = newLimb;
AttackLimb = newLimb;
}
else
{
@@ -1291,7 +1311,12 @@ namespace Barotrauma
UpdateFallBack(attackWorldPos, deltaTime, followThrough: true);
return;
case AIBehaviorAfterAttack.FallBack:
case AIBehaviorAfterAttack.Reverse:
default:
if (activeBehavior == AIBehaviorAfterAttack.Reverse)
{
Reverse = true;
}
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
return;
}
@@ -1303,12 +1328,13 @@ namespace Barotrauma
if (canAttack)
{
if (AttackingLimb == null || !IsValidAttack(AttackingLimb, Character.GetAttackContexts(), SelectedAiTarget?.Entity as IDamageable))
if (AttackLimb == null || !IsValidAttack(AttackLimb, Character.GetAttackContexts(), SelectedAiTarget?.Entity))
{
AttackingLimb = GetAttackLimb(attackWorldPos);
AttackLimb = GetAttackLimb(attackWorldPos);
}
canAttack = AttackingLimb != null && AttackingLimb.attack.CoolDownTimer <= 0;
canAttack = AttackLimb != null && AttackLimb.attack.CoolDownTimer <= 0;
}
if (!AIParams.CanOpenDoors)
{
if (!Character.AnimController.SimplePhysicsEnabled && SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null && (!canAttackDoors || !canAttackWalls || !AIParams.TargetOuterWalls))
@@ -1347,8 +1373,8 @@ namespace Barotrauma
// Target a specific limb instead of the target center position
if (wallTarget == null && targetCharacter != null)
{
var targetLimbType = AttackingLimb.Params.Attack.Attack.TargetLimbType;
attackTargetLimb = GetTargetLimb(AttackingLimb, targetCharacter, targetLimbType);
var targetLimbType = AttackLimb.Params.Attack.Attack.TargetLimbType;
attackTargetLimb = GetTargetLimb(AttackLimb, targetCharacter, targetLimbType);
if (attackTargetLimb == null)
{
State = AIState.Idle;
@@ -1361,7 +1387,7 @@ namespace Barotrauma
}
}
Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackingLimb.WorldPosition;
Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackLimb.WorldPosition;
Vector2 toTarget = attackWorldPos - attackLimbPos;
// Add a margin when the target is moving away, because otherwise it might be difficult to reach it if the attack takes some time to execute
if (wallTarget != null && Character.Submarine == null)
@@ -1389,23 +1415,23 @@ namespace Barotrauma
Vector2 CalculateMargin(Vector2 targetVelocity)
{
if (targetVelocity == Vector2.Zero) { return Vector2.Zero; }
float diff = AttackingLimb.attack.Range - AttackingLimb.attack.DamageRange;
if (diff <= 0 || toTarget.LengthSquared() <= MathUtils.Pow2(AttackingLimb.attack.DamageRange)) { return Vector2.Zero; }
float diff = AttackLimb.attack.Range - AttackLimb.attack.DamageRange;
if (diff <= 0 || toTarget.LengthSquared() <= MathUtils.Pow2(AttackLimb.attack.DamageRange)) { return Vector2.Zero; }
float dot = Vector2.Dot(Vector2.Normalize(targetVelocity), Vector2.Normalize(Character.AnimController.Collider.LinearVelocity));
if (dot <= 0 || !MathUtils.IsValid(dot)) { return Vector2.Zero; }
float distanceOffset = diff * AttackingLimb.attack.Duration;
float distanceOffset = diff * AttackLimb.attack.Duration;
// Intentionally omit the unit conversion because we use distanceOffset as a multiplier.
return targetVelocity * distanceOffset * dot;
}
// Check that we can reach the target
distance = toTarget.Length();
canAttack = distance < AttackingLimb.attack.Range;
canAttack = distance < AttackLimb.attack.Range;
// Crouch if the target is down (only humanoids), so that we can reach it.
if (Character.AnimController is HumanoidAnimController humanoidAnimController && distance < AttackingLimb.attack.Range * 2)
if (Character.AnimController is HumanoidAnimController humanoidAnimController && distance < AttackLimb.attack.Range * 2)
{
if (Math.Abs(toTarget.Y) > AttackingLimb.attack.Range / 2 && Math.Abs(toTarget.X) <= AttackingLimb.attack.Range)
if (Math.Abs(toTarget.Y) > AttackLimb.attack.Range / 2 && Math.Abs(toTarget.X) <= AttackLimb.attack.Range)
{
humanoidAnimController.Crouching = true;
}
@@ -1413,14 +1439,14 @@ namespace Barotrauma
if (canAttack)
{
if (AttackingLimb.attack.Ranged)
if (AttackLimb.attack.Ranged)
{
// Check that is facing the target
float offset = AttackingLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
Vector2 forward = VectorExtensions.Forward(AttackingLimb.body.TransformedRotation - offset * Character.AnimController.Dir);
float offset = AttackLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
Vector2 forward = VectorExtensions.Forward(AttackLimb.body.TransformedRotation - offset * Character.AnimController.Dir);
float angle = VectorExtensions.Angle(forward, toTarget);
canAttack = angle < MathHelper.ToRadians(AttackingLimb.attack.RequiredAngle);
if (canAttack && AttackingLimb.attack.AvoidFriendlyFire)
canAttack = angle < MathHelper.ToRadians(AttackLimb.attack.RequiredAngle);
if (canAttack && AttackLimb.attack.AvoidFriendlyFire)
{
float minDistance = MathUtils.Pow(ConvertUnits.ToDisplayUnits(Character.AnimController.Collider.GetMaxExtent() * 3), 2);
bool IsFarEnough(Character other) => Vector2.DistanceSquared(Character.WorldPosition, other.WorldPosition) > minDistance;
@@ -1434,11 +1460,11 @@ namespace Barotrauma
}
if (canAttack)
{
canAttack = !IsBlocked(attackSimPos) && !IsBlocked(AttackingLimb.SimPosition + forward * ConvertUnits.ToSimUnits(AttackingLimb.attack.Range));
canAttack = !IsBlocked(attackSimPos) && !IsBlocked(AttackLimb.SimPosition + forward * ConvertUnits.ToSimUnits(AttackLimb.attack.Range));
bool IsBlocked(Vector2 targetPosition)
{
foreach (var body in Submarine.PickBodies(AttackingLimb.SimPosition, targetPosition, myBodies, Physics.CollisionCharacter))
foreach (var body in Submarine.PickBodies(AttackLimb.SimPosition, targetPosition, myBodies, Physics.CollisionCharacter))
{
Character hitTarget = null;
if (body.UserData is Character c)
@@ -1460,22 +1486,8 @@ namespace Barotrauma
}
}
}
else if (!IsAttackRunning && !IsCoolDownRunning)
{
// If not, reset the attacking limb, if the cooldown is not running
// Don't use the property, because we don't want cancel reversing, if we are reversing.
if (attackLimbResetTimer > attackLimbResetInterval)
{
_attackingLimb = null;
attackLimbResetTimer = 0;
}
else
{
attackLimbResetTimer += deltaTime;
}
}
}
Limb steeringLimb = canAttack && !AttackingLimb.attack.Ranged ? AttackingLimb : null;
Limb steeringLimb = canAttack && !AttackLimb.attack.Ranged ? AttackLimb : null;
if (steeringLimb == null)
{
// If the attacking limb is a hand or claw, for example, using it as the steering limb can end in the result where the character circles around the target.
@@ -1490,9 +1502,9 @@ namespace Barotrauma
var pathSteering = SteeringManager as IndoorsSteeringManager;
if (AttackingLimb != null && AttackingLimb.attack.Retreat)
if (AttackLimb != null && AttackLimb.attack.Retreat)
{
UpdateFallBack(attackWorldPos, deltaTime, false);
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
}
else
{
@@ -1527,7 +1539,7 @@ namespace Barotrauma
}
// When pursuing, we don't want to pursue too close
float max = 300;
float margin = AttackingLimb != null ? Math.Min(AttackingLimb.attack.Range * 0.9f, max) : max;
float margin = AttackLimb != null ? Math.Min(AttackLimb.attack.Range * 0.9f, max) : max;
if (!canAttack || distance > margin)
{
// Steer towards the target if in the same room and swimming
@@ -1558,10 +1570,10 @@ namespace Barotrauma
}
else
{
if (AttackingLimb.attack.Ranged)
if (AttackLimb.attack.Ranged)
{
float dir = Character.AnimController.Dir;
if (dir > 0 && attackWorldPos.X > AttackingLimb.WorldPosition.X + margin || dir < 0 && attackWorldPos.X < AttackingLimb.WorldPosition.X - margin)
if (dir > 0 && attackWorldPos.X > AttackLimb.WorldPosition.X + margin || dir < 0 && attackWorldPos.X < AttackLimb.WorldPosition.X - margin)
{
SteeringManager.Reset();
}
@@ -1658,9 +1670,9 @@ namespace Barotrauma
}
break;
case CirclePhase.CloseIn:
if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * GetStrikeDistanceMultiplier(targetSub.Velocity))
if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * GetStrikeDistanceMultiplier(targetSub.Velocity))
{
strikeTimer = AttackingLimb.attack.CoolDown;
strikeTimer = AttackLimb.attack.CoolDown;
CirclePhase = CirclePhase.Strike;
}
else if (!breakCircling && sqrDistToSub <= MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance / 2) && targetSub.Velocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed()))
@@ -1703,10 +1715,10 @@ namespace Barotrauma
// When the offset position is outside of the sub it happens that the creature sometimes reaches the target point,
// which makes it continue circling around the point (as supposed)
// But when there is some offset and the offset is too near, this is not what we want.
if (AttackingLimb != null && sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance))
if (AttackLimb != null && sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance))
{
CirclePhase = CirclePhase.Strike;
strikeTimer = AttackingLimb.attack.CoolDown;
strikeTimer = AttackLimb.attack.CoolDown;
}
else
{
@@ -1741,9 +1753,9 @@ namespace Barotrauma
}
}
}
if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity)))
if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity)))
{
strikeTimer = AttackingLimb.attack.CoolDown;
strikeTimer = AttackLimb.attack.CoolDown;
CirclePhase = CirclePhase.Strike;
}
canAttack = false;
@@ -1800,7 +1812,7 @@ namespace Barotrauma
}
}
if (!canAttack || distance > Math.Min(AttackingLimb.attack.Range * 0.9f, 100))
if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100))
{
if (pathSteering != null)
{
@@ -1811,7 +1823,7 @@ namespace Barotrauma
SteeringManager.SteeringSeek(steerPos, 10);
}
}
else if (AttackingLimb.attack.Ranged)
else if (AttackLimb.attack.Ranged)
{
// Too close
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
@@ -1824,18 +1836,18 @@ namespace Barotrauma
}
if (canAttack)
{
if (!UpdateLimbAttack(deltaTime, AttackingLimb, attackSimPos, distance, attackTargetLimb))
if (!UpdateLimbAttack(deltaTime, AttackLimb, attackSimPos, distance, attackTargetLimb))
{
IgnoreTarget(SelectedAiTarget);
}
}
else if (IsAttackRunning)
{
AttackingLimb.attack.ResetAttackTimer();
AttackLimb.attack.ResetAttackTimer();
}
}
private bool IsValidAttack(Limb attackingLimb, IEnumerable<AttackContext> currentContexts, IDamageable target)
private bool IsValidAttack(Limb attackingLimb, IEnumerable<AttackContext> currentContexts, Entity target)
{
if (attackingLimb == null) { return false; }
if (target == null) { return false; }
@@ -1854,10 +1866,11 @@ namespace Barotrauma
// Check that is approximately facing the target
Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : attackingLimb.WorldPosition;
Vector2 toTarget = attackWorldPos - attackLimbPos;
if (attack.MinRange > 0 && toTarget.LengthSquared() < MathUtils.Pow2(attack.MinRange)) { return false; }
float offset = attackingLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
Vector2 forward = VectorExtensions.Forward(attackingLimb.body.TransformedRotation - offset * Character.AnimController.Dir);
float angle = VectorExtensions.Angle(forward, toTarget);
if (angle > MathHelper.ToRadians(attack.RequiredAngle)) { return false; }
float angle = MathHelper.ToDegrees(VectorExtensions.Angle(forward, toTarget));
if (angle > attack.RequiredAngle) { return false; }
}
return true;
}
@@ -1867,7 +1880,7 @@ namespace Barotrauma
private Limb GetAttackLimb(Vector2 attackWorldPos, Limb ignoredLimb = null)
{
var currentContexts = Character.GetAttackContexts();
IDamageable target = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity as IDamageable;
Entity target = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity;
if (target == null) { return null; }
Limb selectedLimb = null;
float currentPriority = -1;
@@ -1901,12 +1914,13 @@ namespace Barotrauma
float CalculatePriority(Limb limb, Vector2 attackPos)
{
if (Character.AnimController.SimplePhysicsEnabled) { return 1 + limb.attack.Priority; }
float prio = 1 + limb.attack.Priority;
if (Character.AnimController.SimplePhysicsEnabled) { return prio; }
float dist = Vector2.Distance(limb.WorldPosition, attackPos);
// The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it.
// We also need a max value that is more than the actual range.
float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, limb.attack.Range * 3, dist));
return (1 + limb.attack.Priority) * distanceFactor;
return prio * distanceFactor;
}
}
@@ -1919,7 +1933,7 @@ namespace Barotrauma
Character.AnimController.ReleaseStuckLimbs();
LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 1);
if (attacker == null || attacker.AiTarget == null || attacker.Removed || attacker.IsDead) { return; }
if (Character.Params.CanInteract && attackResult.Damage > 10)
if (attackResult.Damage >= AIParams.DamageThreshold)
{
ReleaseDragTargets();
}
@@ -2007,9 +2021,11 @@ namespace Barotrauma
if (State == AIState.Attack && (IsAttackRunning || IsCoolDownRunning))
{
// Don't retaliate or escape while performing an attack/under cooldown
retaliate = false;
avoidGunFire = false;
if (IsAttackRunning)
{
avoidGunFire = false;
}
}
if (retaliate)
{
@@ -2022,7 +2038,7 @@ namespace Barotrauma
}
}
}
else if (avoidGunFire)
else if (avoidGunFire && attackResult.Damage >= AIParams.DamageThreshold)
{
State = AIState.Escape;
avoidTimer = AIParams.AvoidTime * Rand.Range(0.75f, 1.25f);
@@ -2114,7 +2130,7 @@ namespace Barotrauma
{
if (attackingLimb.attack.CoolDownTimer > 0)
{
SetAimTimer();
SetAimTimer(Math.Min(attackingLimb.attack.CoolDown, 1.5f));
// Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon
float greed = AIParams.AggressionGreed;
if (!(damageTarget is Character))
@@ -2245,19 +2261,19 @@ namespace Barotrauma
// TODO: test adding some random variance here?
attackVector = attackWorldPos - WorldPosition;
}
Vector2 attackDir = Vector2.Normalize(followThrough ? attackVector.Value : -attackVector.Value);
if (!MathUtils.IsValid(attackDir))
Vector2 dir = Vector2.Normalize(followThrough ? attackVector.Value : -attackVector.Value);
if (!MathUtils.IsValid(dir))
{
attackDir = Vector2.UnitY;
dir = Vector2.UnitY;
}
steeringManager.SteeringManual(deltaTime, attackDir);
if (Character.AnimController.InWater)
steeringManager.SteeringManual(deltaTime, dir);
if (Character.AnimController.InWater && !Reverse)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
}
if (checkBlocking)
{
return !IsBlocked(deltaTime, SimPosition + attackDir * (avoidLookAheadDistance / 2));
return !IsBlocked(deltaTime, SimPosition + dir * (avoidLookAheadDistance / 2));
}
return true;
}
@@ -2810,7 +2826,7 @@ namespace Barotrauma
if (Character.Submarine == null && aiTarget.Entity?.Submarine != null && targetCharacter == null)
{
if (targetParams.AttackPattern == AttackPattern.Circle || targetParams.AttackPattern == AttackPattern.Sweep)
if (targetParams.PrioritizeSubCenter || targetParams.AttackPattern == AttackPattern.Circle || targetParams.AttackPattern == AttackPattern.Sweep)
{
if (!isAnyTargetClose)
{
@@ -2990,7 +3006,7 @@ namespace Barotrauma
if (HasValidPath(requireNonDirty: true)) { return; }
wallHits.Clear();
Structure wall = null;
Vector2 rayStart = AttackingLimb != null ? AttackingLimb.SimPosition : SimPosition;
Vector2 rayStart = AttackLimb != null ? AttackLimb.SimPosition : SimPosition;
if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Target))
{
Vector2 rayEnd = SelectedAiTarget.SimPosition;
@@ -3303,6 +3319,7 @@ namespace Barotrauma
foreach (var triggerObject in activeTriggers)
{
AITrigger trigger = triggerObject.Key;
if (trigger.IsPermanent) { continue; }
trigger.UpdateTimer(deltaTime);
if (!trigger.IsActive)
{
@@ -3471,7 +3488,7 @@ namespace Barotrauma
disableTailCoroutine = null;
}
Character.AnimController.ReleaseStuckLimbs();
AttackingLimb = null;
AttackLimb = null;
movementMargin = 0;
ResetEscape();
if (isStateChanged && to == AIState.Idle && from != to)
@@ -148,14 +148,14 @@ namespace Barotrauma
}
if (TargetCharacter != null)
{
if (enemyAI.AttackingLimb?.attack == null)
if (enemyAI.AttackLimb?.attack == null)
{
DeattachFromBody(reset: true, cooldown: 1);
}
else
{
float range = enemyAI.AttackingLimb.attack.DamageRange * 2f;
if (Vector2.DistanceSquared(TargetCharacter.WorldPosition, enemyAI.AttackingLimb.WorldPosition) > range * range)
float range = enemyAI.AttackLimb.attack.DamageRange * 2f;
if (Vector2.DistanceSquared(TargetCharacter.WorldPosition, enemyAI.AttackLimb.WorldPosition) > range * range)
{
DeattachFromBody(reset: true, cooldown: 1);
}
@@ -265,11 +265,11 @@ namespace Barotrauma
if (enemyAI.IsSteeringThroughGap) { break; }
if (_attachPos == Vector2.Zero) { break; }
if (!AttachToSub && !AttachToCharacters) { break; }
if (enemyAI.AttackingLimb == null) { break; }
if (enemyAI.AttackLimb == null) { break; }
if (targetBody == null) { break; }
if (IsAttached && AttachJoints[0].BodyB == targetBody) { break; }
Vector2 referencePos = TargetCharacter != null ? TargetCharacter.WorldPosition : ConvertUnits.ToDisplayUnits(transformedAttachPos);
if (Vector2.DistanceSquared(referencePos, enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange)
if (Vector2.DistanceSquared(referencePos, enemyAI.AttackLimb.WorldPosition) < enemyAI.AttackLimb.attack.DamageRange * enemyAI.AttackLimb.attack.DamageRange)
{
AttachToBody(transformedAttachPos);
}
@@ -112,6 +112,13 @@ namespace Barotrauma
}
foreach (FireSource fs in targetHull.FireSources)
{
if (fs == null) { continue; }
if (fs.Removed) { continue; }
if (character.CurrentHull == null)
{
Abandon = true;
break;
}
float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X) - fs.DamageRange;
float yDist = Math.Abs(character.WorldPosition.Y - fs.WorldPosition.Y);
bool inRange = xDist + yDist < extinguisher.Range;
@@ -153,7 +160,7 @@ namespace Barotrauma
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref gotoObjective)))
{
gotoObjective.requiredCondition = () => targetHull == null || character.CanSeeTarget(targetHull);
gotoObjective.requiredCondition = () => character.CanSeeTarget(targetHull);
}
}
else
@@ -164,7 +164,7 @@ namespace Barotrauma
TargetName = Leak.FlowTargetHull?.DisplayName,
requiredCondition = () =>
Leak.Submarine == character.Submarine &&
(Leak.FlowTargetHull != null && character.CurrentHull == Leak.FlowTargetHull || character.CanSeeTarget(Leak)),
Leak.linkedTo.Any(e => e is Hull h && character.CurrentHull == h),
// 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()
},
@@ -99,8 +99,7 @@ namespace Barotrauma
{
if (InWater || !CanWalk)
{
float avg = (SwimSlowParams.MovementSpeed + SwimFastParams.MovementSpeed) / 2.0f;
return TargetMovement.LengthSquared() > avg * avg;
return TargetMovement.LengthSquared() > SwimSlowParams.MovementSpeed;
}
else
{
@@ -448,21 +448,18 @@ namespace Barotrauma
movement = TargetMovement;
bool isMoving = movement.LengthSquared() > 0.00001f;
var mainLimb = MainLimb;
if (isMoving)
float t = 0.5f;
if (isMoving && !SimplePhysicsEnabled && CurrentSwimParams.RotateTowardsMovement)
{
float t = 0.5f;
if (!SimplePhysicsEnabled && CurrentSwimParams.RotateTowardsMovement)
Vector2 forward = VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2);
float dot = Vector2.Dot(forward, Vector2.Normalize(movement));
if (dot < 0)
{
Vector2 forward = VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2);
float dot = Vector2.Dot(forward, Vector2.Normalize(movement));
if (dot < 0)
{
// Reduce the linear movement speed when not facing the movement direction
t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f);
}
// Reduce the linear movement speed when not facing the movement direction
t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f);
}
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t);
}
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t);
//limbs are disabled when simple physics is enabled, no need to move them
if (SimplePhysicsEnabled) { return; }
mainLimb.PullJointEnabled = true;
@@ -443,8 +443,6 @@ namespace Barotrauma
if (CurrentGroundedParams == null) { return; }
Vector2 handPos;
//if you're allergic to magic numbers, stop reading now
Limb leftFoot = GetLimb(LimbType.LeftFoot);
Limb rightFoot = GetLimb(LimbType.RightFoot);
Limb head = GetLimb(LimbType.Head);
@@ -599,16 +597,20 @@ namespace Barotrauma
{
float torsoAngle = TorsoAngle.Value;
float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes");
if (Crouching && !movingHorizontally) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; }
if (Crouching && !movingHorizontally && !aiming) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; }
torsoAngle -= herpesStrength / 150.0f;
torso.body.SmoothRotate(torsoAngle * Dir, CurrentGroundedParams.TorsoTorque);
}
if (HeadAngle.HasValue)
if (!aiming && CurrentGroundedParams.FixedHeadAngle && HeadAngle.HasValue)
{
float headAngle = HeadAngle.Value;
if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; }
head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque);
}
else
{
RotateHead(head);
}
if (!onGround)
{
@@ -883,23 +885,17 @@ namespace Barotrauma
}
}
float targetSpeed = TargetMovement.Length();
if (targetSpeed > 0.1f)
if (aiming)
{
if (!aiming)
{
float newRotation = MathUtils.VectorToAngle(TargetMovement) - MathHelper.PiOver2;
Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
}
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
Vector2 diff = (mousePos - torso.SimPosition) * Dir;
float newRotation = MathUtils.VectorToAngle(diff);
Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
}
else
else if (targetSpeed > 0.1f)
{
if (aiming)
{
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
Vector2 diff = (mousePos - torso.SimPosition) * Dir;
float newRotation = MathUtils.VectorToAngle(diff);
Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
}
float newRotation = MathUtils.VectorToAngle(TargetMovement) - MathHelper.PiOver2;
Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
}
torso.body.MoveToPos(Collider.SimPosition + new Vector2((float)Math.Sin(-Collider.Rotation), (float)Math.Cos(-Collider.Rotation)) * 0.4f, 5.0f);
@@ -914,13 +910,14 @@ namespace Barotrauma
{
torso.body.SmoothRotate(Collider.Rotation, CurrentSwimParams.TorsoTorque);
}
if (HeadAngle.HasValue)
if (!aiming && CurrentSwimParams.FixedHeadAngle && HeadAngle.HasValue)
{
head.body.SmoothRotate(Collider.Rotation + HeadAngle.Value * Dir, CurrentSwimParams.HeadTorque);
}
else
{
head.body.SmoothRotate(Collider.Rotation, CurrentSwimParams.HeadTorque);
RotateHead(head);
}
//dont try to move upwards if head is already out of water
@@ -951,7 +948,18 @@ namespace Barotrauma
if (isNotRemote)
{
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, movementLerp);
float t = movementLerp;
if (targetSpeed > 0.00001f && !SimplePhysicsEnabled)
{
Vector2 forward = VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2);
float dot = Vector2.Dot(forward, Vector2.Normalize(movement));
if (dot < 0)
{
// Reduce the linear movement speed when not facing the movement direction
t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f);
}
}
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t);
}
WalkPos += movement.Length();
@@ -1227,7 +1235,11 @@ namespace Barotrauma
//apply forces to the collider to move the Character up/down
Collider.ApplyForce((climbForce * 20.0f + subSpeed * 50.0f) * Collider.Mass);
if (!aiming)
if (aiming)
{
RotateHead(head);
}
else
{
float movementMultiplier = targetMovement.Y < 0 ? 0 : 1;
head.body.SmoothRotate(MathHelper.PiOver4 * movementMultiplier * Dir, WalkParams.HeadTorque);
@@ -1710,6 +1722,24 @@ namespace Barotrauma
}
}
private void RotateHead(Limb head)
{
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
Vector2 dir = (mousePos - head.SimPosition) * Dir;
float rot = MathUtils.VectorToAngle(dir);
var neckJoint = GetJointBetweenLimbs(LimbType.Head, LimbType.Torso);
if (neckJoint != null)
{
float offset = MathUtils.WrapAnglePi(GetLimb(LimbType.Torso).body.Rotation);
float lowerLimit = neckJoint.LowerLimit + offset;
float upperLimit = neckJoint.UpperLimit + offset;
float min = Math.Min(lowerLimit, upperLimit);
float max = Math.Max(lowerLimit, upperLimit);
rot = Math.Clamp(rot, min, max);
}
head.body.SmoothRotate(rot, CurrentAnimationParams.HeadTorque);
}
private void FootIK(Limb foot, Vector2 pos, float legTorque, float footTorque, float footAngle)
{
if (!MathUtils.IsValid(pos))
@@ -805,7 +805,7 @@ namespace Barotrauma
SeverLimbJointProjSpecific(limbJoint, playSound: true);
if (GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(character, new Character.StatusEventData());
GameMain.NetworkMember.CreateEntityEvent(character, new Character.CharacterStatusEventData());
}
return true;
}
@@ -37,7 +37,9 @@ namespace Barotrauma
Pursue,
FollowThrough,
FollowThroughUntilCanAttack,
IdleUntilCanAttack
IdleUntilCanAttack,
Reverse,
ReverseUntilCanAttack
}
struct AttackResult
@@ -102,7 +104,7 @@ namespace Barotrauma
public bool Retreat { get; private set; }
private float _range;
[Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target before the AI tries to attack."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target before the AI tries to attack."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
public float Range
{
get => _range * RangeMultiplier;
@@ -110,13 +112,16 @@ namespace Barotrauma
}
private float _damageRange;
[Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target to do damage. In distance-based hit detection, the hit will be registered as soon as the target is within the damage range, unless the attack duration has expired."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target to do damage. In distance-based hit detection, the hit will be registered as soon as the target is within the damage range, unless the attack duration has expired."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
public float DamageRange
{
get => _damageRange * RangeMultiplier;
set => _damageRange = value;
}
[Serialize(0.0f, IsPropertySaveable.Yes, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
public float MinRange { get; private set; }
[Serialize(0.25f, IsPropertySaveable.Yes, description: "An approximation of the attack duration. Effectively defines the time window in which the hit can be registered. If set to too low value, it's possible that the attack won't hit the target in time."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2)]
public float Duration { get; private set; }
@@ -356,17 +361,17 @@ namespace Barotrauma
{
Deserialize(element);
if (element.Attribute("damage") != null ||
element.Attribute("bluntdamage") != null ||
element.Attribute("burndamage") != null ||
element.Attribute("bleedingdamage") != null)
if (element.GetAttribute("damage") != null ||
element.GetAttribute("bluntdamage") != null ||
element.GetAttribute("burndamage") != null ||
element.GetAttribute("bleedingdamage") != null)
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Define damage as afflictions instead of using the damage attribute (e.g. <Affliction identifier=\"internaldamage\" strength=\"10\" />).");
}
//if level wall damage is not defined, default to the structure damage
if (element.Attribute("LevelWallDamage") == null &&
element.Attribute("levelwalldamage") == null)
if (element.GetAttribute("LevelWallDamage") == null &&
element.GetAttribute("levelwalldamage") == null)
{
LevelWallDamage = StructureDamage;
}
@@ -382,7 +387,7 @@ namespace Barotrauma
break;
case "affliction":
AfflictionPrefab afflictionPrefab;
if (subElement.Attribute("name") != null)
if (subElement.GetAttribute("name") != null)
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - define afflictions using identifiers instead of names.");
string afflictionName = subElement.GetAttributeString("name", "").ToLowerInvariant();
@@ -686,7 +691,7 @@ namespace Barotrauma
public bool IsValidTarget(AttackTarget targetType) => TargetType == AttackTarget.Any || TargetType == targetType;
public bool IsValidTarget(IDamageable target)
public bool IsValidTarget(Entity target)
{
return TargetType switch
{
@@ -893,6 +893,7 @@ namespace Barotrauma
public bool GodMode = false;
public CampaignMode.InteractionType CampaignInteractionType;
public Identifier MerchantIdentifier;
private bool accessRemovedCharacterErrorShown;
public override Vector2 SimPosition
@@ -1885,7 +1886,7 @@ namespace Barotrauma
if (!attack.IsValidContext(currentContexts)) { return false; }
if (attackTarget != null)
{
if (!attack.IsValidTarget(attackTarget)) { return false; }
if (!attack.IsValidTarget(attackTarget as Entity)) { return false; }
if (attackTarget is ISerializableEntity se && attackTarget is Character)
{
if (attack.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) { return false; }
@@ -2011,6 +2012,8 @@ namespace Barotrauma
public bool CanSeeCharacter(Character target)
{
System.Diagnostics.Debug.Assert(target != null);
if (target == null) { return false; }
if (target.Removed) { return false; }
Limb seeingLimb = GetSeeingLimb();
if (CanSeeTarget(target, seeingLimb)) { return true; }
@@ -2045,7 +2048,6 @@ namespace Barotrauma
if (leftExtremity != null && CanSeeTarget(leftExtremity, seeingLimb)) { return true; }
if (rightExtremity != null && CanSeeTarget(rightExtremity, seeingLimb)) { return true; }
}
return false;
}
@@ -2056,6 +2058,8 @@ namespace Barotrauma
public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null)
{
System.Diagnostics.Debug.Assert(target != null);
if (target == null) { return false; }
seeingEntity ??= AnimController.SimplePhysicsEnabled ? this : GetSeeingLimb() as ISpatialEntity;
if (seeingEntity == null) { return false; }
ISpatialEntity sourceEntity = seeingEntity ;
@@ -3148,7 +3152,8 @@ namespace Barotrauma
var itemContainer = item?.GetComponent<ItemContainer>();
if (itemContainer == null) { return; }
foreach (Item inventoryItem in Inventory.AllItemsMod)
List<Item> inventoryItems = new List<Item>(Inventory.AllItemsMod);
foreach (Item inventoryItem in inventoryItems)
{
if (!itemContainer.Inventory.TryPutItem(inventoryItem, user: null, createNetworkEvent: createNetworkEvents))
{
@@ -3156,17 +3161,25 @@ namespace Barotrauma
inventoryItem.Drop(dropper: this, createNetworkEvent: createNetworkEvents);
}
}
//this needs to happen after the items have been dropped (we can no longer sync dropping the items if the character has been removed)
Spawner.AddEntityToRemoveQueue(this);
}
}
Spawner.AddEntityToRemoveQueue(this);
else
{
Spawner.AddEntityToRemoveQueue(this);
}
}
public void DespawnNow(bool createNetworkEvents = true)
{
despawnTimer = GameSettings.CurrentConfig.CorpseDespawnDelay;
UpdateDespawn(1.0f, ignoreThresholds: true, createNetworkEvents: createNetworkEvents);
Spawner.Update(createNetworkEvents);
//update twice: first to spawn the duffel bag and move the items into it, then to remove the character
for (int i = 0; i < 2; i++)
{
Spawner.Update(createNetworkEvents);
}
}
public static void RemoveByPrefab(CharacterPrefab prefab)
@@ -4012,7 +4025,7 @@ namespace Barotrauma
if (GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(this, new StatusEventData());
GameMain.NetworkMember.CreateEntityEvent(this, new CharacterStatusEventData());
}
isDead = true;
@@ -4168,6 +4181,11 @@ namespace Barotrauma
}
DebugConsole.Log("Removing character " + Name + " (ID: " + ID + ")");
#if CLIENT
//ensure we apply any pending inventory updates to drop any items that need to be dropped when the character despawns
Inventory?.ApplyReceivedState();
#endif
base.Remove();
foreach (Item heldItem in HeldItems.ToList())
@@ -4179,12 +4197,12 @@ namespace Barotrauma
#if CLIENT
GameMain.GameSession?.CrewManager?.KillCharacter(this, resetCrewListIndex: false);
if (Controlled == this) { Controlled = null; }
#endif
CharacterList.Remove(this);
if (Controlled == this) { Controlled = null; }
if (Inventory != null)
{
foreach (Item item in Inventory.AllItems)
@@ -4266,7 +4284,7 @@ namespace Barotrauma
if (!MathUtils.NearlyEqual(newItem.Condition, newItem.MaxCondition) &&
GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(newItem, new StatusEventData());
newItem.CreateStatusEvent();
}
#if SERVER
newItem.GetComponent<Terminal>()?.SyncHistory();
@@ -51,7 +51,7 @@ namespace Barotrauma
}
}
public struct StatusEventData : IEventData
public struct CharacterStatusEventData : IEventData
{
public EventType EventType => EventType.Status;
}
@@ -852,6 +852,19 @@ namespace Barotrauma
#if CLIENT
public void RecreateHead(MultiplayerPreferences characterSettings)
{
if (characterSettings.HairIndex == -1 &&
characterSettings.BeardIndex == -1 &&
characterSettings.MoustacheIndex == -1 &&
characterSettings.FaceAttachmentIndex == -1)
{
//randomize if nothing is set
SetAttachments(Rand.RandSync.Unsynced);
characterSettings.HairIndex = Head.HairIndex;
characterSettings.BeardIndex = Head.BeardIndex;
characterSettings.MoustacheIndex = Head.MoustacheIndex;
characterSettings.FaceAttachmentIndex = Head.FaceAttachmentIndex;
}
RecreateHead(
characterSettings.TagSet.ToImmutableHashSet(),
characterSettings.HairIndex,
@@ -859,9 +872,14 @@ namespace Barotrauma
characterSettings.MoustacheIndex,
characterSettings.FaceAttachmentIndex);
Head.SkinColor = characterSettings.SkinColor;
Head.HairColor = characterSettings.HairColor;
Head.FacialHairColor = characterSettings.FacialHairColor;
Head.SkinColor = ChooseColor(SkinColors, characterSettings.SkinColor);
Head.HairColor = ChooseColor(HairColors, characterSettings.HairColor);
Head.FacialHairColor = ChooseColor(FacialHairColors, characterSettings.FacialHairColor);
Color ChooseColor(in ImmutableArray<(Color Color, float Commonness)> availableColors, Color chosenColor)
{
return availableColors.Any(c => c.Color == chosenColor) ? chosenColor : SelectRandomColor(availableColors, Rand.RandSync.Unsynced);
}
}
#endif
@@ -287,7 +287,7 @@ namespace Barotrauma
StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName));
}
if (element.Attribute("interval") != null)
if (element.GetAttribute("interval") != null)
{
MinInterval = MaxInterval = Math.Max(element.GetAttributeFloat("interval", 1.0f), 1.0f);
}
@@ -409,7 +409,7 @@ namespace Barotrauma
HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier).ToLowerInvariant(), 1f);
BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost).ToLowerInvariant(), 0);
if (element.Attribute("nameidentifier") != null)
if (element.GetAttribute("nameidentifier") != null)
{
Name = TextManager.Get(element.GetAttributeString("nameidentifier", string.Empty)).Fallback(Name);
}
@@ -47,22 +47,24 @@ namespace Barotrauma
HighlightSprite = new Sprite(subElement);
break;
case "vitalitymultiplier":
if (subElement.Attribute("name") != null)
if (subElement.GetAttribute("name") != null)
{
DebugConsole.ThrowError("Error in character health config (" + characterHealth.Character.Name + ") - define vitality multipliers using affliction identifiers or types instead of names.");
continue;
}
Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", "");
Identifier afflictionType = subElement.GetAttributeIdentifier("type", "");
float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f);
if (!afflictionIdentifier.IsEmpty)
var vitalityMultipliers = subElement.GetAttributeIdentifierArray("identifier", null) ?? subElement.GetAttributeIdentifierArray("identifiers", null);
if (vitalityMultipliers == null)
{
VitalityMultipliers.Add(afflictionIdentifier, multiplier);
vitalityMultipliers = subElement.GetAttributeIdentifierArray("type", null) ?? subElement.GetAttributeIdentifierArray("types", null);
}
if (vitalityMultipliers != null)
{
float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f);
vitalityMultipliers.ForEach(i => VitalityMultipliers.Add(i, multiplier));
}
else
{
VitalityTypeMultipliers.Add(afflictionType, multiplier);
DebugConsole.ThrowError($"Error in character health config {characterHealth.Character.Name}: affliction identifier(s) or type(s) not defined in the \"VitalityMultiplier\" elements!");
}
break;
}
@@ -540,6 +540,8 @@ namespace Barotrauma
private set;
}
public Items.Components.Rope AttachedRope { get; set; }
public string Name => Params.Name;
// These properties are exposed for status effects
@@ -103,6 +103,9 @@ namespace Barotrauma
[Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float HandMoveStrength { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "Is the head angle fixed or does the angle follow the mouse position?"), Editable]
public bool FixedHeadAngle { get; set; }
}
abstract class HumanGroundedParams : GroundedMovementParams, IHumanAnimation
@@ -131,7 +134,7 @@ namespace Barotrauma
get => MathHelper.ToDegrees(FootAngleInRadians);
set
{
FootAngleInRadians = MathHelper.ToRadians(value);
FootAngleInRadians = MathHelper.ToRadians(value);
}
}
public float FootAngleInRadians { get; private set; }
@@ -156,6 +159,9 @@ namespace Barotrauma
[Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float HandMoveStrength { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "Is the head angle fixed or does the angle follow the mouse position?"), Editable]
public bool FixedHeadAngle { get; set; }
}
public interface IHumanAnimation
@@ -166,5 +172,7 @@ namespace Barotrauma
float ArmMoveStrength { get; set; }
float HandMoveStrength { get; set; }
bool FixedHeadAngle { get; set; }
}
}
@@ -104,6 +104,9 @@ namespace Barotrauma
[Serialize(10f, IsPropertySaveable.Yes, "How frequent the recurring idle and attack sounds are?"), Editable(MinValueFloat = 1f, MaxValueFloat = 100f)]
public float SoundInterval { get; set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool DrawLast { get; set; }
public readonly CharacterFile File;
public XDocument VariantFile { get; private set; }
@@ -127,31 +130,36 @@ namespace Barotrauma
public override ContentXElement MainElement => base.MainElement.IsOverride() ? base.MainElement.FirstElement() : base.MainElement;
public static XElement CreateVariantXml(XElement selfXml, XElement parentXml)
public static XElement CreateVariantXml(XElement variantXML, XElement baseXML)
{
XElement newXml = selfXml.CreateVariantXML(parentXml);
XElement selfAi = selfXml.GetChildElement("ai");
XElement parentAi = parentXml.GetChildElement("ai");
if (parentAi is null || parentAi.Elements().None()
|| selfAi is null || selfAi.Elements().None())
XElement newXml = variantXML.CreateVariantXML(baseXML);
XElement variantAi = variantXML.GetChildElement("ai");
XElement baseAi = baseXML.GetChildElement("ai");
if (baseAi is null || baseAi.Elements().None()
|| variantAi is null || variantAi.Elements().None())
{
return newXml;
}
//discard the inherited targets, just keep the new ones
// CreateVariantXML seems to merge the ai targets so that in the new xml we have both the old and the new target definitions.
var finalAiElement = newXml.GetChildElement("ai");
foreach (var finalTarget in finalAiElement!.Elements().ToArray())
var processedTags = new HashSet<string>();
foreach (var aiTarget in finalAiElement.Elements().ToArray())
{
finalTarget.Remove();
string tag = aiTarget.GetAttributeString("tag", null);
if (tag == null) { continue; }
if (processedTags.Contains(tag))
{
aiTarget.Remove();
continue;
}
processedTags.Add(tag);
var matchInSelf = variantAi.Elements().FirstOrDefault(e => e.GetAttributeString("tag", null) == tag);
var matchInParent = baseAi.Elements().FirstOrDefault(e => e.GetAttributeString("tag", null) == tag);
if (matchInSelf != null && matchInParent != null)
{
aiTarget.ReplaceWith(new XElement(matchInSelf));
}
}
foreach (var inheritorTarget in selfAi.Elements())
{
finalAiElement.Add(new XElement(inheritorTarget));
}
return newXml;
}
@@ -574,6 +582,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes, description: "The character will flee for a brief moment when being shot at if not performing an attack."), Editable]
public bool AvoidGunfire { get; private set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "How much damage is required for single attack to trigger avoiding/releasing targets."), Editable(minValue: 0f, maxValue: 1000f)]
public float DamageThreshold { get; private set; }
[Serialize(3f, IsPropertySaveable.Yes, description: "How long the creature avoids gunfire. Also used when the creature is unlatched."), Editable(minValue: 0f, maxValue: 100f)]
public float AvoidTime { get; private set; }
@@ -785,6 +796,9 @@ namespace Barotrauma
[Serialize(AttackPattern.Straight, IsPropertySaveable.Yes), Editable]
public AttackPattern AttackPattern { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the AI will give more priority to targets close to the horizontal middle of the sub. Only applies to walls, hulls, and items like sonar. Circle and Sweep always does this regardless of this property."), Editable]
public bool PrioritizeSubCenter { get; set; }
#region Sweep
[Serialize(0f, IsPropertySaveable.Yes, description: "Use to define a distance at which the creature starts the sweeping movement."), Editable(MinValueFloat = 0, MaxValueFloat = 10000, ValueStep = 1, DecimalCount = 0)]
public float SweepDistance { get; private set; }
@@ -8,7 +8,7 @@ namespace Barotrauma.Abilities
public AbilityConditionItemInSubmarine(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
if (conditionElement.Attribute("submarinetype") != null)
if (conditionElement.GetAttribute("submarinetype") != null)
{
submarineType = conditionElement.GetAttributeEnum<SubmarineType>("submarinetype", SubmarineType.Player);
}
@@ -11,7 +11,7 @@ namespace Barotrauma.Abilities
public AbilityConditionLocation(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
if (conditionElement.Attribute("hasoutpost") != null)
if (conditionElement.GetAttribute("hasoutpost") != null)
{
hasOutpost = conditionElement.GetAttributeBool("hasoutpost", false);
}
@@ -1,7 +1,4 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class AbilityConditionInHull : AbilityConditionDataless
{
@@ -1,7 +1,4 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class AbilityConditionInWater : AbilityConditionDataless
{
@@ -45,7 +45,7 @@ namespace Barotrauma
}
}
if (element.Attribute("description") != null)
if (element.GetAttribute("description") != null)
{
string description = element.GetAttributeString("description", string.Empty);
Description = Description.Fallback(TextManager.Get(description)).Fallback(description);
@@ -30,6 +30,7 @@ namespace Barotrauma
public readonly bool NotSyncedInMultiplayer;
public readonly ImmutableHashSet<Type>? AlternativeTypes;
public readonly ImmutableHashSet<Identifier> Names;
private readonly MethodInfo? contentPathMutator;
public TypeInfo(Type type)
{
@@ -40,9 +41,11 @@ namespace Barotrauma
var notSyncedInMultiplayerAttribute = type.GetCustomAttribute<NotSyncedInMultiplayer>();
NotSyncedInMultiplayer = notSyncedInMultiplayerAttribute != null;
AlternativeTypes = reqByCoreAttribute?.AlternativeTypes;
contentPathMutator
= Type.GetMethod(nameof(MutateContentPath), BindingFlags.Static | BindingFlags.Public);
HashSet<Identifier> names = new HashSet<Identifier> { type.Name.RemoveFromEnd("File").ToIdentifier() };
if (type.GetCustomAttribute<AlternativeContentTypeNames>()?.Names is { } altNames)
if (type.GetCustomAttribute<AlternativeContentTypeNames>(inherit: false)?.Names is { } altNames)
{
names.UnionWith(altNames);
}
@@ -50,6 +53,10 @@ namespace Barotrauma
Names = names.ToImmutableHashSet();
}
public ContentPath MutateContentPath(ContentPath path)
=> (ContentPath?)contentPathMutator?.Invoke(null, new object[] { path })
?? path;
public ContentFile? CreateInstance(ContentPackage contentPackage, ContentPath path) =>
(ContentFile?)Activator.CreateInstance(Type, contentPackage, path);
}
@@ -80,6 +87,7 @@ namespace Barotrauma
}
try
{
filePath = type.MutateContentPath(filePath);
if (!File.Exists(filePath.FullPath))
{
return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": file not found.");
@@ -0,0 +1,28 @@
using System;
using Barotrauma.IO;
namespace Barotrauma
{
sealed class ServerExecutableFile : OtherFile
{
//This content type doesn't do very much on its own, it's handled manually by the Host Server menu
public ServerExecutableFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
public static ContentPath MutateContentPath(ContentPath path)
{
if (File.Exists(path.FullPath)) { return path; }
string rawValueWithoutExtension()
=> Barotrauma.IO.Path.Combine(
Barotrauma.IO.Path.GetDirectoryName(path.RawValue ?? ""),
Barotrauma.IO.Path.GetFileNameWithoutExtension(path.RawValue ?? "")).CleanUpPath();
path = ContentPath.FromRaw(path.ContentPackage, rawValueWithoutExtension());
if (File.Exists(path.FullPath)) { return path; }
path = ContentPath.FromRaw(path.ContentPackage,
rawValueWithoutExtension() + ".exe");
return path;
}
}
}
@@ -110,7 +110,7 @@ namespace Barotrauma
!expectedHash.IsNullOrWhiteSpace() &&
!expectedHash.Equals(Hash.StringRepresentation, StringComparison.OrdinalIgnoreCase);
public IEnumerable<T> GetFiles<T>() where T : ContentFile => Files.Where(f => f is T).Cast<T>();
public IEnumerable<T> GetFiles<T>() where T : ContentFile => Files.OfType<T>();
public IEnumerable<ContentFile> GetFiles(Type type)
=> !type.IsSubclassOf(typeof(ContentFile))
@@ -8,7 +8,7 @@ using System.Xml.Linq;
namespace Barotrauma
{
[AttributeUsage(AttributeTargets.Class)]
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class RequiredByCorePackage : Attribute
{
public readonly ImmutableHashSet<Type> AlternativeTypes;
@@ -18,7 +18,7 @@ namespace Barotrauma
}
}
[AttributeUsage(AttributeTargets.Class)]
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class AlternativeContentTypeNames : Attribute
{
public readonly ImmutableHashSet<Identifier> Names;
@@ -51,7 +51,7 @@ namespace Barotrauma
Core?.UnloadPackage();
Core = newCore;
foreach (var p in newCore.LoadPackageEnumerable()) { yield return p; }
ThrowIfDuplicates(All);
SortContent();
yield return new LoadProgress(1.0f);
}
@@ -60,7 +60,7 @@ namespace Barotrauma
if (Core == null) { return; }
Core.UnloadPackage();
Core.LoadPackage();
ThrowIfDuplicates(All);
SortContent();
}
public static void EnableRegular(RegularPackage p)
@@ -133,7 +133,7 @@ namespace Barotrauma
}
}
public static void SortContent()
private static void SortContent()
{
ThrowIfDuplicates(All);
All
@@ -165,6 +165,20 @@ namespace Barotrauma
SetRegular(Regular.Where(p => ContentPackageManager.RegularPackages.Contains(p)).ToArray());
}
public static void RefreshUpdatedMods()
{
if (Core != null && !ContentPackageManager.CorePackages.Contains(Core))
{
SetCore(ContentPackageManager.WorkshopPackages.Core.FirstOrDefault(p => p.SteamWorkshopId == Core.SteamWorkshopId) ??
ContentPackageManager.CorePackages.First());
}
SetRegular(Regular
.Select(p => ContentPackageManager.RegularPackages.Contains(p)
? p
: ContentPackageManager.WorkshopPackages.Regular.FirstOrDefault(p2 => p2.SteamWorkshopId == p.SteamWorkshopId))
.ToArray());
}
public static void BackUp()
{
if (BackupPackages.Core != null || BackupPackages.Regular != null)
@@ -327,7 +341,8 @@ namespace Barotrauma
=> LocalPackages.Regular.CollectionConcat(WorkshopPackages.Regular);
public static IEnumerable<ContentPackage> AllPackages
=> LocalPackages.CollectionConcat(WorkshopPackages);
=> VanillaCorePackage.ToEnumerable().CollectionConcat(LocalPackages).CollectionConcat(WorkshopPackages)
.OfType<ContentPackage>();
public static void UpdateContentPackageList()
{
@@ -447,13 +462,13 @@ namespace Barotrauma
int pkgCount = 1 + enabledRegularPackages.Count; //core + regular
loadingRange = new Range<float>(0.01f, 1.0f / pkgCount);
loadingRange = new Range<float>(0.01f, 0.01f + (0.99f / pkgCount));
foreach (var p in EnabledPackages.SetCoreEnumerable(enabledCorePackage))
{
yield return p.Transform(loadingRange);
}
loadingRange = new Range<float>(1.0f / pkgCount, 1.0f);
loadingRange = new Range<float>(0.01f + (0.99f / pkgCount), 1.0f);
foreach (var p in EnabledPackages.SetRegularEnumerable(enabledRegularPackages))
{
yield return p.Transform(loadingRange);
@@ -49,7 +49,7 @@ namespace Barotrauma
.Replace(string.Format(OtherModDirFmt, ContentPackage.SteamWorkshopId.ToString(CultureInfo.InvariantCulture)), modPath, StringComparison.OrdinalIgnoreCase);
}
}
var allPackages = ContentPackageManager.AllPackages;
var allPackages = ContentPackageManager.EnabledPackages.All;
foreach (Identifier otherModName in otherMods)
{
if (!UInt64.TryParse(otherModName.Value, out UInt64 workshopId)) { workshopId = 0; }
@@ -53,8 +53,6 @@ namespace Barotrauma
public IEnumerable<ContentXElement> GetChildElements(string name)
=> Elements().Where(e => string.Equals(name, e.Name.LocalName, StringComparison.CurrentCultureIgnoreCase));
public XAttribute? Attribute(string name) => Element.Attribute(name);
public XAttribute? GetAttribute(string name) => Element.GetAttribute(name);
public IEnumerable<XAttribute> Attributes() => Element.Attributes();
@@ -11,7 +11,7 @@ namespace Barotrauma
{
Message = $"\"{whoAsked?.Name ?? "[NULL]"}\" depends on a package " +
$"with name or ID \"{missingPackage ?? "[NULL]"}\" " +
$"that is not currently installed.";
$"that is not currently enabled.";
}
}
}
@@ -168,25 +168,34 @@ namespace Barotrauma
};
}));
void printMapEntityPrefabs<T>(IEnumerable<T> prefabs) where T : MapEntityPrefab
{
NewMessage("***************", Color.Cyan);
foreach (T prefab in prefabs)
{
if (prefab.Name.IsNullOrEmpty()) { continue; }
string text = $"- {prefab.Name}";
if (prefab.Tags.Any())
{
text += $" ({string.Join(", ", prefab.Tags)})";
}
if (prefab.AllowedLinks?.Any() ?? false)
{
text += $", Links: {string.Join(", ", prefab.AllowedLinks)}";
}
NewMessage(text, prefab.ContentPackage == ContentPackageManager.VanillaCorePackage ? Color.Cyan : Color.Purple);
}
NewMessage("***************", Color.Cyan);
}
commands.Add(new Command("items|itemlist", "itemlist: List all the item prefabs available for spawning.", (string[] args) =>
{
NewMessage("***************", Color.Cyan);
foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs)
{
if (itemPrefab.Name.IsNullOrEmpty()) { continue; }
string text = $"- {itemPrefab.Name}";
if (itemPrefab.Tags.Any())
{
text += $" ({string.Join(", ", itemPrefab.Tags)})";
}
if (itemPrefab.AllowedLinks.Any())
{
text += $", Links: {string.Join(", ", itemPrefab.AllowedLinks)}";
}
NewMessage(text, Color.Cyan);
}
NewMessage("***************", Color.Cyan);
printMapEntityPrefabs(ItemPrefab.Prefabs);
}));
commands.Add(new Command("itemassemblies", "itemassemblies: List all the item assemblies available for spawning.", (string[] args) =>
{
printMapEntityPrefabs(ItemAssemblyPrefab.Prefabs);
}));
@@ -202,6 +211,7 @@ namespace Barotrauma
string[] creatureAndJobNames =
CharacterPrefab.Prefabs.Select(p => p.Identifier.Value)
.Concat(JobPrefab.Prefabs.Select(p => p.Identifier.Value))
.OrderBy(s => s)
.ToArray();
return new string[][]
@@ -732,9 +742,16 @@ namespace Barotrauma
{
#if CLIENT
if (Screen.Selected == GameMain.SubEditorScreen) { return; }
Character.Controlled = null;
GameMain.GameScreen.Cam.TargetPos = Vector2.Zero;
GameMain.Client?.SendConsoleCommand("freecam");
if (GameMain.Client == null)
{
Character.Controlled = null;
GameMain.GameScreen.Cam.TargetPos = Vector2.Zero;
}
else
{
GameMain.Client?.SendConsoleCommand("freecam");
}
#endif
}, isCheat: true));
@@ -1786,15 +1803,26 @@ namespace Barotrauma
{
if (GameMain.GameSession?.Map?.CurrentLocation is Location location)
{
var msg = "--- Location: " + location.Name + " ---";
msg += "\nBalance: " + location.StoreCurrentBalance;
msg += "\nPrice modifier: " + location.StorePriceModifier + "%";
msg += "\nDaily specials:";
location.DailySpecials.ForEach(i => msg += "\n - " + i.Name.Value);
msg += "\nRequested goods:";
location.RequestedGoods.ForEach(i => msg += "\n - " + i.Name.Value);
NewMessage(msg);
if (location.Stores != null)
{
var msg = "--- Location: " + location.Name + " ---";
foreach (var store in location.Stores)
{
msg += $"\nStore identifier: {store.Value.Identifier}";
msg += $"\nBalance: {store.Value.Balance}";
msg += $"\nPrice modifier: {store.Value.PriceModifier}%";
msg += "\nDaily specials:";
store.Value.DailySpecials.ForEach(i => msg += $"\n - {i.Name}");
msg += "\nRequested goods:";
store.Value.RequestedGoods.ForEach(i => msg += $"\n - {i.Name}");
}
NewMessage(msg);
}
else
{
NewMessage($"No stores at {location}, can't show store info.");
}
}
else
{
@@ -2382,7 +2410,7 @@ namespace Barotrauma
public static void ThrowError(LocalizedString error, Exception e = null, bool createMessageBox = false, bool appendStackTrace = false)
{
ThrowError(error.Value);
ThrowError(error.Value, e, createMessageBox, appendStackTrace);
}
public static void ThrowError(string error, Exception e = null, bool createMessageBox = false, bool appendStackTrace = false)
@@ -32,7 +32,7 @@ namespace Barotrauma
public ArtifactEvent(EventPrefab prefab)
: base(prefab)
{
if (prefab.ConfigElement.Attribute("itemname") != null)
if (prefab.ConfigElement.GetAttribute("itemname") != null)
{
DebugConsole.ThrowError("Error in ArtifactEvent - use item identifier instead of the name of the item.");
string itemName = prefab.ConfigElement.GetAttributeString("itemname", "");
@@ -30,7 +30,7 @@ namespace Barotrauma
public SubactionGroup(ScriptedEvent scriptedEvent, ContentXElement elem)
{
Text = elem.Attribute("text")?.Value ?? "";
Text = elem.GetAttribute("text")?.Value ?? "";
Actions = new List<EventAction>();
EndConversation = elem.GetAttributeBool("endconversation", false);
foreach (var e in elem.Elements())
@@ -153,6 +153,13 @@ namespace Barotrauma
swarmSpawned = true;
}
#if DEBUG || UNSTABLE
if (State == 1 && !level.CheckBeaconActive())
{
DebugConsole.ThrowError("Beacon became inactive!");
State = 2;
}
#endif
}
public override void End()
@@ -248,7 +248,7 @@ namespace Barotrauma
{
if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit)
{
int deliveredItemCount = items.Count(i => i.CurrentHull != null && !i.Removed && i.Condition > 0.0f);
int deliveredItemCount = items.Count(it => IsItemDelivered(it));
if (deliveredItemCount / (float)items.Count >= requiredDeliveryAmount)
{
GiveReward();
@@ -267,5 +267,12 @@ namespace Barotrauma
items.Clear();
failed = !completed;
}
private bool IsItemDelivered(Item item)
{
if (item.Removed || item.Condition <= 0.0f || Submarine.MainSub == null) { return false; }
var submarine = item.Submarine ?? item.GetRootContainer()?.Submarine;
return submarine == Submarine.MainSub || Submarine.MainSub.GetConnectedSubs().Contains(submarine);
}
}
}
@@ -419,17 +419,17 @@ namespace Barotrauma
public static int DistributeRewardsToCrew(IEnumerable<Character> crew, int totalReward)
{
int remainingRewards = totalReward;
HashSet<Character> nonBotCrew = crew.Where(c => !c.IsBot).ToHashSet();
float sum = nonBotCrew.Sum(c => c.Wallet.RewardDistribution);
if (sum == 0) { return remainingRewards; }
foreach (Character character in nonBotCrew)
float sum = GetRewardDistibutionSum(crew);
if (MathUtils.NearlyEqual(sum, 0)) { return remainingRewards; }
foreach (Character character in crew)
{
float rewardWeight = character.Wallet.RewardDistribution / sum;
int reward = (int)Math.Floor(totalReward * rewardWeight);
reward = Math.Max(remainingRewards, reward);
int rewardDistribution = character.Wallet.RewardDistribution;
float rewardWeight = sum > 100 ? rewardDistribution / sum : rewardDistribution / 100f;
int reward = (int)(totalReward * rewardWeight);
reward = Math.Min(remainingRewards, reward);
character.Wallet.Give(reward);
remainingRewards -= reward;
if (0 >= remainingRewards) { break; }
if (remainingRewards <= 0) { break; }
}
return remainingRewards;
@@ -442,26 +442,27 @@ namespace Barotrauma
IEnumerable<Character> characters = crewManager.GetCharacters();
#if SERVER
return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(IsAlive).Concat(characters);
return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null && !c.IsDead).Concat(characters);
#elif CLIENT
return characters;
#endif
static bool IsAlive(Character c) { return c?.Info != null && !c.IsDead; }
}
public static int GetRewardDistibutionSum(IEnumerable<Character> crew, int rewardDistribution = 0) => crew.Sum(c => c.Wallet.RewardDistribution) + rewardDistribution;
public static (int Amount, int Percentage) GetRewardShare(int rewardDistribution, IEnumerable<Character> crew, Option<int> reward)
public static (int Amount, int Percentage, float Sum) GetRewardShare(int rewardDistribution, IEnumerable<Character> crew, Option<int> reward)
{
float sum = crew.Sum(c => c.Wallet.RewardDistribution) + rewardDistribution;
if (sum == 0) { return (0, 0); }
float sum = GetRewardDistibutionSum(crew, rewardDistribution);
if (MathUtils.NearlyEqual(sum, 0)) { return (0, 0, sum); }
float rewardWeight = rewardDistribution / sum;
float rewardWeight = sum > 100 ? rewardDistribution / sum : rewardDistribution / 100f;
int rewardPercentage = (int)(rewardWeight * 100);
return reward switch
{
Some<int> { Value: var amount } => ((int)(amount * rewardWeight), rewardPercentage),
None<int> _ => (0, rewardPercentage),
Some<int> { Value: var amount } => ((int)(amount * rewardWeight), rewardPercentage, sum),
None<int> _ => (0, rewardPercentage, sum),
_ => throw new ArgumentOutOfRangeException()
};
}
@@ -209,7 +209,7 @@ namespace Barotrauma
break;
case "locationtype":
case "connectiontype":
if (subElement.Attribute("identifier") != null)
if (subElement.GetAttribute("identifier") != null)
{
AllowedLocationTypes.Add(subElement.GetAttributeIdentifier("identifier", ""));
}
@@ -228,7 +228,7 @@ namespace Barotrauma
}
GiveReward();
completed = true;
if (level?.LevelData != null && Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase) || t.Equals("huntinggroundsnoreward", StringComparison.OrdinalIgnoreCase)))
if (level?.LevelData != null && Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase)))
{
level.LevelData.HasHuntingGrounds = false;
}
@@ -48,7 +48,7 @@ namespace Barotrauma
{
containerTag = prefab.ConfigElement.GetAttributeString("containertag", "");
if (prefab.ConfigElement.Attribute("itemname") != null)
if (prefab.ConfigElement.GetAttribute("itemname") != null)
{
DebugConsole.ThrowError("Error in SalvageMission - use item identifier instead of the name of the item.");
string itemName = prefab.ConfigElement.GetAttributeString("itemname", "");
@@ -30,23 +30,21 @@ namespace Barotrauma
}
#if CLIENT
public PurchasedItem(ItemPrefab itemPrefab, int quantity, Client buyer = null)
public PurchasedItem(ItemPrefab itemPrefab, int quantity)
: this(itemPrefab, quantity, buyer: null) { }
#endif
public PurchasedItem(ItemPrefab itemPrefab, int quantity, Client buyer)
{
ItemPrefab = itemPrefab;
Quantity = quantity;
IsStoreComponentEnabled = null;
BuyerCharacterInfoId = buyer?.Character?.Info?.ID ?? Character.Controlled?.Info?.ID ?? 0;
}
#elif SERVER
public PurchasedItem(ItemPrefab itemPrefab, int quantity, Client buyer)
{
ItemPrefab = itemPrefab;
Quantity = quantity;
IsStoreComponentEnabled = null;
BuyerCharacterInfoId = buyer?.Character?.Info?.ID ?? 0;
}
#endif
public override string ToString()
{
return $"{ItemPrefab.Name} ({Quantity})";
}
}
class SoldItem
@@ -133,11 +131,11 @@ namespace Barotrauma
public const int MaxQuantity = 100;
public List<PurchasedItem> ItemsInBuyCrate { get; } = new List<PurchasedItem>();
public List<PurchasedItem> ItemsInSellCrate { get; } = new List<PurchasedItem>();
public List<PurchasedItem> ItemsInSellFromSubCrate { get; } = new List<PurchasedItem>();
public List<PurchasedItem> PurchasedItems { get; } = new List<PurchasedItem>();
public List<SoldItem> SoldItems { get; } = new List<SoldItem>();
public Dictionary<Identifier, List<PurchasedItem>> ItemsInBuyCrate { get; } = new Dictionary<Identifier, List<PurchasedItem>>();
public Dictionary<Identifier, List<PurchasedItem>> ItemsInSellCrate { get; } = new Dictionary<Identifier, List<PurchasedItem>>();
public Dictionary<Identifier, List<PurchasedItem>> ItemsInSellFromSubCrate { get; } = new Dictionary<Identifier, List<PurchasedItem>>();
public Dictionary<Identifier, List<PurchasedItem>> PurchasedItems { get; } = new Dictionary<Identifier, List<PurchasedItem>>();
public Dictionary<Identifier, List<SoldItem>> SoldItems { get; } = new Dictionary<Identifier, List<SoldItem>>();
private readonly CampaignMode campaign;
@@ -154,6 +152,60 @@ namespace Barotrauma
this.campaign = campaign;
}
private List<T> GetItems<T>(Identifier identifier, Dictionary<Identifier, List<T>> items, bool create = false)
{
if (items.TryGetValue(identifier, out var storeSpecificItems) && storeSpecificItems != null)
{
return storeSpecificItems;
}
else if (create)
{
storeSpecificItems = new List<T>();
items.Add(identifier, storeSpecificItems);
return storeSpecificItems;
}
else
{
return new List<T>();
}
}
public List<PurchasedItem> GetBuyCrateItems(Identifier identifier, bool create = false) => GetItems(identifier, ItemsInBuyCrate, create);
public List<PurchasedItem> GetBuyCrateItems(Location.StoreInfo store, bool create = false) => GetBuyCrateItems(store?.Identifier ?? Identifier.Empty, create);
public PurchasedItem GetBuyCrateItem(Identifier identifier, ItemPrefab prefab) => GetBuyCrateItems(identifier)?.FirstOrDefault(i => i.ItemPrefab == prefab);
public PurchasedItem GetBuyCrateItem(Location.StoreInfo store, ItemPrefab prefab) => GetBuyCrateItem(store?.Identifier ?? Identifier.Empty, prefab);
public List<PurchasedItem> GetSellCrateItems(Identifier identifier, bool create = false) => GetItems(identifier, ItemsInSellCrate, create);
public List<PurchasedItem> GetSellCrateItems(Location.StoreInfo store, bool create = false) => GetSellCrateItems(store?.Identifier ?? Identifier.Empty, create);
public PurchasedItem GetSellCrateItem(Identifier identifier, ItemPrefab prefab) => GetSellCrateItems(identifier)?.FirstOrDefault(i => i.ItemPrefab == prefab);
public PurchasedItem GetSellCrateItem(Location.StoreInfo store, ItemPrefab prefab) => GetSellCrateItem(store?.Identifier ?? Identifier.Empty, prefab);
public List<PurchasedItem> GetSubCrateItems(Identifier identifier, bool create = false) => GetItems(identifier, ItemsInSellFromSubCrate, create);
public List<PurchasedItem> GetSubCrateItems(Location.StoreInfo store, bool create = false) => GetSubCrateItems(store?.Identifier ?? Identifier.Empty, create);
public PurchasedItem GetSubCrateItem(Identifier identifier, ItemPrefab prefab) => GetSubCrateItems(identifier)?.FirstOrDefault(i => i.ItemPrefab == prefab);
public PurchasedItem GetSubCrateItem(Location.StoreInfo store, ItemPrefab prefab) => GetSubCrateItem(store?.Identifier ?? Identifier.Empty, prefab);
public List<PurchasedItem> GetPurchasedItems(Identifier identifier, bool create = false) => GetItems(identifier, PurchasedItems, create);
public List<PurchasedItem> GetPurchasedItems(Location.StoreInfo store, bool create = false) => GetPurchasedItems(store?.Identifier ?? Identifier.Empty, create);
public PurchasedItem GetPurchasedItem(Identifier identifier, ItemPrefab prefab) => GetPurchasedItems(identifier)?.FirstOrDefault(i => i.ItemPrefab == prefab);
public PurchasedItem GetPurchasedItem(Location.StoreInfo store, ItemPrefab prefab) => GetPurchasedItem(store?.Identifier ?? Identifier.Empty, prefab);
public List<SoldItem> GetSoldItems(Identifier identifier, bool create = false) => GetItems(identifier, SoldItems, create);
public List<SoldItem> GetSoldItems(Location.StoreInfo store, bool create = false) => GetSoldItems(store?.Identifier ?? Identifier.Empty, create);
public void ClearItemsInBuyCrate()
{
ItemsInBuyCrate.Clear();
@@ -172,61 +224,62 @@ namespace Barotrauma
OnItemsInSellFromSubCrateChanged?.Invoke();
}
public void SetPurchasedItems(List<PurchasedItem> items)
public void SetPurchasedItems(Dictionary<Identifier, List<PurchasedItem>> purchasedItems)
{
PurchasedItems.Clear();
PurchasedItems.AddRange(items);
foreach (var entry in purchasedItems)
{
PurchasedItems.Add(entry.Key, entry.Value);
}
OnPurchasedItemsChanged?.Invoke();
}
public void ModifyItemQuantityInBuyCrate(ItemPrefab itemPrefab, int changeInQuantity, Client client = null)
public void ModifyItemQuantityInBuyCrate(Identifier storeIdentifier, ItemPrefab itemPrefab, int changeInQuantity, Client client = null)
{
var itemInCrate = ItemsInBuyCrate.Find(i => i.ItemPrefab == itemPrefab);
if (itemInCrate != null)
if (GetBuyCrateItem(storeIdentifier, itemPrefab) is { } item)
{
itemInCrate.Quantity += changeInQuantity;
if (itemInCrate.Quantity < 1)
item.Quantity += changeInQuantity;
if (item.Quantity < 1)
{
ItemsInBuyCrate.Remove(itemInCrate);
GetBuyCrateItems(storeIdentifier, create: true).Remove(item);
}
}
else if (changeInQuantity > 0)
{
itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity, client);
ItemsInBuyCrate.Add(itemInCrate);
GetBuyCrateItems(storeIdentifier, create: true).Add(new PurchasedItem(itemPrefab, changeInQuantity, client));
}
OnItemsInBuyCrateChanged?.Invoke();
}
public void ModifyItemQuantityInSubSellCrate(ItemPrefab itemPrefab, int changeInQuantity, Client client = null)
public void ModifyItemQuantityInSubSellCrate(Identifier storeIdentifier, ItemPrefab itemPrefab, int changeInQuantity, Client client = null)
{
var itemInCrate = ItemsInSellFromSubCrate.Find(i => i.ItemPrefab == itemPrefab);
if (itemInCrate != null)
if (GetSubCrateItem(storeIdentifier, itemPrefab) is { } item)
{
itemInCrate.Quantity += changeInQuantity;
if (itemInCrate.Quantity < 1)
item.Quantity += changeInQuantity;
if (item.Quantity < 1)
{
ItemsInSellFromSubCrate.Remove(itemInCrate);
GetSubCrateItems(storeIdentifier)?.Remove(item);
}
}
else if (changeInQuantity > 0)
{
itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity, client);
ItemsInSellFromSubCrate.Add(itemInCrate);
GetSubCrateItems(storeIdentifier, create: true).Add(new PurchasedItem(itemPrefab, changeInQuantity, client));
}
OnItemsInSellFromSubCrateChanged?.Invoke();
}
public void PurchaseItems(List<PurchasedItem> itemsToPurchase, bool removeFromCrate, Client client = null)
public void PurchaseItems(Identifier storeIdentifier, List<PurchasedItem> itemsToPurchase, bool removeFromCrate, Client client = null)
{
// Check all the prices before starting the transaction
// to make sure the modifiers stay the same for the whole transaction
Dictionary<ItemPrefab, int> buyValues = GetBuyValuesAtCurrentLocation(itemsToPurchase.Select(i => i.ItemPrefab));
var store = Location.GetStore(storeIdentifier);
if (store == null) { return; }
var itemsPurchasedFromStore = GetPurchasedItems(storeIdentifier, create: true);
// Check all the prices before starting the transaction to make sure the modifiers stay the same for the whole transaction
var buyValues = GetBuyValuesAtCurrentLocation(storeIdentifier, itemsToPurchase.Select(i => i.ItemPrefab));
var itemsInStoreCrate = GetBuyCrateItems(storeIdentifier, create: true);
foreach (PurchasedItem item in itemsToPurchase)
{
// Add to the purchased items
var purchasedItem = PurchasedItems.Find(pi => pi.ItemPrefab == item.ItemPrefab);
var purchasedItem = itemsPurchasedFromStore.Find(pi => pi.ItemPrefab == item.ItemPrefab);
if (purchasedItem != null)
{
purchasedItem.Quantity += item.Quantity;
@@ -234,53 +287,54 @@ namespace Barotrauma
else
{
purchasedItem = new PurchasedItem(item.ItemPrefab, item.Quantity, client);
PurchasedItems.Add(purchasedItem);
itemsPurchasedFromStore.Add(purchasedItem);
}
// Exchange money
var itemValue = item.Quantity * buyValues[item.ItemPrefab];
int itemValue = item.Quantity * buyValues[item.ItemPrefab];
campaign.GetWallet(client).TryDeduct(itemValue);
GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value);
Location.StoreCurrentBalance += itemValue;
store.Balance += itemValue;
if (removeFromCrate)
{
// Remove from the shopping crate
var crateItem = ItemsInBuyCrate.Find(pi => pi.ItemPrefab == item.ItemPrefab);
if (crateItem != null)
if (itemsInStoreCrate.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } crateItem)
{
crateItem.Quantity -= item.Quantity;
if (crateItem.Quantity < 1) { ItemsInBuyCrate.Remove(crateItem); }
if (crateItem.Quantity < 1) { itemsInStoreCrate.Remove(crateItem); }
}
}
}
OnPurchasedItemsChanged?.Invoke();
}
public Dictionary<ItemPrefab, int> GetBuyValuesAtCurrentLocation(IEnumerable<ItemPrefab> items)
public Dictionary<ItemPrefab, int> GetBuyValuesAtCurrentLocation(Identifier storeIdentifier, IEnumerable<ItemPrefab> items)
{
var buyValues = new Dictionary<ItemPrefab, int>();
var store = Location?.GetStore(storeIdentifier);
if (store == null) { return buyValues; }
foreach (var item in items)
{
if (item == null) { continue; }
if (!buyValues.ContainsKey(item))
{
var buyValue = Location?.GetAdjustedItemBuyPrice(item) ?? 0;
int buyValue = store?.GetAdjustedItemBuyPrice(item) ?? 0;
buyValues.Add(item, buyValue);
}
}
return buyValues;
}
public Dictionary<ItemPrefab, int> GetSellValuesAtCurrentLocation(IEnumerable<ItemPrefab> items)
public Dictionary<ItemPrefab, int> GetSellValuesAtCurrentLocation(Identifier storeIdentifier, IEnumerable<ItemPrefab> items)
{
var sellValues = new Dictionary<ItemPrefab, int>();
var store = Location?.GetStore(storeIdentifier);
if (store == null) { return sellValues; }
foreach (var item in items)
{
if (item == null) { continue; }
if (!sellValues.ContainsKey(item))
{
var sellValue = Location?.GetAdjustedItemSellPrice(item) ?? 0;
int sellValue = store?.GetAdjustedItemSellPrice(item) ?? 0;
sellValues.Add(item, sellValue);
}
}
@@ -289,7 +343,13 @@ namespace Barotrauma
public void CreatePurchasedItems()
{
CreateItems(PurchasedItems, Submarine.MainSub);
var items = new List<PurchasedItem>();
foreach (var storeSpecificItems in PurchasedItems)
{
items.AddRange(storeSpecificItems.Value);
}
CreateItems(items, Submarine.MainSub);
PurchasedItems.Clear();
OnPurchasedItemsChanged?.Invoke();
}
@@ -509,33 +569,41 @@ namespace Barotrauma
public void SavePurchasedItems(XElement parentElement)
{
var itemsElement = new XElement("cargo");
foreach (PurchasedItem item in PurchasedItems)
foreach (var storeSpecificItems in PurchasedItems)
{
if (item?.ItemPrefab == null) { continue; }
itemsElement.Add(new XElement("item",
new XAttribute("id", item.ItemPrefab.Identifier),
new XAttribute("qty", item.Quantity),
new XAttribute("buyer", item.BuyerCharacterInfoId)));
foreach (var item in storeSpecificItems.Value)
{
if (item?.ItemPrefab == null) { continue; }
itemsElement.Add(new XElement("item",
new XAttribute("id", item.ItemPrefab.Identifier),
new XAttribute("qty", item.Quantity),
new XAttribute("storeid", storeSpecificItems.Key),
new XAttribute("buyer", item.BuyerCharacterInfoId)));
}
}
parentElement.Add(itemsElement);
}
public void LoadPurchasedItems(XElement element)
{
var purchasedItems = new List<PurchasedItem>();
var purchasedItems = new Dictionary<Identifier, List<PurchasedItem>>();
if (element != null)
{
foreach (XElement itemElement in element.GetChildElements("item"))
{
string id = itemElement.GetAttributeString("id", null);
if (string.IsNullOrWhiteSpace(id)) { continue; }
var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == id);
string prefabId = itemElement.GetAttributeString("id", null);
if (string.IsNullOrWhiteSpace(prefabId)) { continue; }
var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == prefabId);
if (prefab == null) { continue; }
int qty = itemElement.GetAttributeInt("qty", 0);
Identifier storeId = itemElement.GetAttributeIdentifier("storeid", "merchant");
int buyerId = itemElement.GetAttributeInt("buyer", 0);
purchasedItems.Add(new PurchasedItem(prefab, qty, buyerId));
if (!purchasedItems.TryGetValue(storeId, out var storeItems))
{
storeItems = new List<PurchasedItem>();
purchasedItems.Add(storeId, storeItems);
}
storeItems.Add(new PurchasedItem(prefab, qty, buyerId));
}
}
SetPurchasedItems(purchasedItems);
@@ -25,12 +25,18 @@ namespace Barotrauma
public int Balance;
}
/// <summary>
/// Network message for the server to update wallet values to clients
/// </summary>
internal struct NetWalletUpdate : INetSerializableStruct
{
[NetworkSerialize(ArrayMaxSize = NetConfig.MaxPlayers + 1)]
public NetWalletTransaction[] Transactions;
}
/// <summary>
/// Network message for the client to transfer money between wallets
/// </summary>
[NetworkSerialize]
internal struct NetWalletTransfer : INetSerializableStruct
{
@@ -39,7 +45,10 @@ namespace Barotrauma
public int Amount;
}
internal struct NetWalletSalaryUpdate : INetSerializableStruct
/// <summary>
/// Network message for the client to set the salary of someone
/// </summary>
internal struct NetWalletSetSalaryUpdate : INetSerializableStruct
{
[NetworkSerialize]
public ushort Target;
@@ -48,6 +57,10 @@ namespace Barotrauma
public int NewRewardDistribution;
}
/// <summary>
/// Represents the difference in balance and salary when a wallet gets updated
/// Not really used right now but could be used for notifications when receiving funds similar to how talents do it
/// </summary>
[NetworkSerialize]
internal struct WalletChangedData : INetSerializableStruct
{
@@ -82,6 +95,9 @@ namespace Barotrauma
}
}
/// <summary>
/// Represents an update that changed the amount of money or salary of the wallet
/// </summary>
[NetworkSerialize]
internal struct NetWalletTransaction : INetSerializableStruct
{
@@ -321,15 +321,32 @@ namespace Barotrauma
}
if (levelData.HasHuntingGrounds)
{
var huntingGroundsMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t.Equals("huntinggroundsnoreward", StringComparison.OrdinalIgnoreCase)));
var huntingGroundsMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase)));
if (!huntingGroundsMissionPrefabs.Any())
{
DebugConsole.AddWarning("Could not find a hunting grounds mission for the level. No mission with the tag \"huntinggroundsnoreward\" found.");
DebugConsole.AddWarning("Could not find a hunting grounds mission for the level. No mission with the tag \"huntinggrounds\" found.");
}
else
{
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(huntingGroundsMissionPrefabs, p => (float)Math.Max(p.Commonness, 0.1f), rand);
// Adjust the prefab commonness based on the difficulty tag
var prefabs = huntingGroundsMissionPrefabs.ToList();
var weights = prefabs.Select(p => (float)Math.Max(p.Commonness, 1)).ToList();
for (int i = 0; i < prefabs.Count; i++)
{
var prefab = prefabs[i];
var weight = weights[i];
if (prefab.Tags.Contains("easy"))
{
weight *= MathHelper.Lerp(0.2f, 2f, MathUtils.InverseLerp(80, LevelData.HuntingGroundsDifficultyThreshold, levelData.Difficulty));
}
else if (prefab.Tags.Contains("hard"))
{
weight *= MathHelper.Lerp(0.5f, 1.5f, MathUtils.InverseLerp(LevelData.HuntingGroundsDifficultyThreshold + 10, 80, levelData.Difficulty));
}
weights[i] = weight;
}
var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(prefabs, weights, rand);
if (!Missions.Any(m => m.Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase))))
{
extraMissions.Add(huntingGroundsMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub));
@@ -596,22 +613,22 @@ namespace Barotrauma
if (map != null && CargoManager != null)
{
map.CurrentLocation.RegisterTakenItems(takenItems);
map.CurrentLocation.AddToStock(CargoManager.SoldItems);
map.CurrentLocation.AddStock(CargoManager.SoldItems);
CargoManager.ClearSoldItemsProjSpecific();
map.CurrentLocation.RemoveFromStock(CargoManager.PurchasedItems);
map.CurrentLocation.RemoveStock(CargoManager.PurchasedItems);
}
if (GameMain.NetworkMember == null)
{
CargoManager.ClearItemsInBuyCrate();
CargoManager.ClearItemsInSellCrate();
CargoManager.ClearItemsInSellFromSubCrate();
CargoManager?.ClearItemsInBuyCrate();
CargoManager?.ClearItemsInSellCrate();
CargoManager?.ClearItemsInSellFromSubCrate();
}
else
{
if (GameMain.NetworkMember.IsServer)
{
CargoManager?.ClearItemsInBuyCrate();
// TODO: CargoManager?.ClearItemsInSellFromSubCrate();
CargoManager?.ClearItemsInSellFromSubCrate();
}
else if (GameMain.NetworkMember.IsClient)
{
@@ -772,6 +789,11 @@ namespace Barotrauma
public void AssignNPCMenuInteraction(Character character, InteractionType interactionType)
{
character.CampaignInteractionType = interactionType;
if (character.CampaignInteractionType == InteractionType.Store &&
character.HumanPrefab is { Identifier: var merchantId })
{
character.MerchantIdentifier = merchantId;
}
character.DisableHealthWindow =
interactionType != InteractionType.None &&
interactionType != InteractionType.Examine &&
@@ -1,4 +1,5 @@
using Barotrauma.IO;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -211,7 +212,6 @@ namespace Barotrauma
#endif
}
public static List<SubmarineInfo> GetCampaignSubs()
{
bool isSubmarineVisible(SubmarineInfo s)
@@ -242,5 +242,78 @@ namespace Barotrauma
return availableSubs;
}
private static void WriteItems(IWriteMessage msg, Dictionary<Identifier, List<PurchasedItem>> purchasedItems)
{
msg.Write((byte)purchasedItems.Count);
foreach (var storeItems in purchasedItems)
{
msg.Write(storeItems.Key);
msg.Write((UInt16)storeItems.Value.Count);
foreach (var item in storeItems.Value)
{
msg.Write(item.ItemPrefab.Identifier);
msg.WriteRangedInteger(item.Quantity, 0, CargoManager.MaxQuantity);
}
}
}
private static Dictionary<Identifier, List<PurchasedItem>> ReadPurchasedItems(IReadMessage msg, Client sender)
{
var items = new Dictionary<Identifier, List<PurchasedItem>>();
byte storeCount = msg.ReadByte();
for (int i = 0; i < storeCount; i++)
{
Identifier storeId = msg.ReadIdentifier();
items.Add(storeId, new List<PurchasedItem>());
UInt16 itemCount = msg.ReadUInt16();
for (int j = 0; j < itemCount; j++)
{
Identifier itemId = msg.ReadIdentifier();
int quantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity);
items[storeId].Add(new PurchasedItem(ItemPrefab.Prefabs[itemId], quantity, sender));
}
}
return items;
}
private static void WriteItems(IWriteMessage msg, Dictionary<Identifier, List<SoldItem>> soldItems)
{
msg.Write((byte)soldItems.Count);
foreach (var storeItems in soldItems)
{
msg.Write(storeItems.Key);
msg.Write((UInt16)storeItems.Value.Count);
foreach (var item in storeItems.Value)
{
msg.Write(item.ItemPrefab.Identifier);
msg.Write((UInt16)item.ID);
msg.Write(item.Removed);
msg.Write(item.SellerID);
msg.Write((byte)item.Origin);
}
}
}
private static Dictionary<Identifier, List<SoldItem>> ReadSoldItems(IReadMessage msg)
{
var soldItems = new Dictionary<Identifier, List<SoldItem>>();
byte storeCount = msg.ReadByte();
for (int i = 0; i < storeCount; i++)
{
Identifier storeId = msg.ReadIdentifier();
soldItems.Add(storeId, new List<SoldItem>());
UInt16 itemCount = msg.ReadUInt16();
for (int j = 0; j < storeCount; j++)
{
Identifier prefabId = msg.ReadIdentifier();
UInt16 itemId = msg.ReadUInt16();
bool removed = msg.ReadBoolean();
byte sellerId = msg.ReadByte();
byte origin = msg.ReadByte();
soldItems[storeId].Add(new SoldItem(ItemPrefab.Prefabs[prefabId], itemId, removed, sellerId, (SoldItem.SellOrigin)origin));
}
}
return soldItems;
}
}
}
@@ -754,9 +754,9 @@ namespace Barotrauma
try
{
IEnumerable<Character> crewCharacters = GetSessionCrewCharacters();
ImmutableArray<Character> crewCharacters = GetSessionCrewCharacters().ToImmutableArray();
int prevMoney = (GameMode as CampaignMode)?.Bank.Balance ?? 0; // FIXME personal wallets - reward distribution
int prevMoney = GetAmountOfMoney(crewCharacters);
foreach (Mission mission in missions)
{
@@ -828,7 +828,7 @@ namespace Barotrauma
LogEndRoundStats(eventId);
if (GameMode is CampaignMode campaignMode)
{
GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", campaignMode.Bank.Balance - prevMoney); // FIXME personal wallets - reward distrubiton
GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", GetAmountOfMoney(crewCharacters) - prevMoney);
campaignMode.TotalPlayTime += roundDuration;
}
#if CLIENT
@@ -840,6 +840,17 @@ namespace Barotrauma
{
RoundEnding = false;
}
int GetAmountOfMoney(IEnumerable<Character> crew)
{
if (!(GameMode is CampaignMode campaign)) { return 0; }
return GameMain.NetworkMember switch
{
null => campaign.Bank.Balance,
_ => crew.Sum(c => c.Wallet.Balance) + campaign.Bank.Balance
};
}
}
public void LogEndRoundStats(string eventId)
@@ -216,7 +216,7 @@ namespace Barotrauma
price = 0;
}
if (Campaign.GetWallet(client).TryDeduct(price)) // FIXME personal wallets
if (Campaign.GetWallet(client).TryDeduct(price))
{
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
@@ -74,7 +74,7 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, IsPropertySaveable.No, description: "Should the OnUse StatusEffects trigger when docking (on vanilla docking ports these effects emit particles and play a sound).)")]
[Editable, Serialize(true, IsPropertySaveable.No, description: "Should the OnUse StatusEffects trigger when docking (on vanilla docking ports these effects emit particles and play a sound).)")]
public bool ApplyEffectsOnDocking
{
get;
@@ -200,7 +200,7 @@ namespace Barotrauma.Items.Components
{
int index = i - 1;
string attributeName = "handle" + i;
var attribute = element.Attribute(attributeName);
var attribute = element.GetAttribute(attributeName);
// If no value is defind for handle2, use the value of handle1.
var value = attribute != null ? ConvertUnits.ToSimUnits(XMLExtensions.ParseVector2(attribute.Value)) : previousValue;
handlePos[index] = value;
@@ -129,7 +129,7 @@ namespace Barotrauma.Items.Components
{
this.item = item;
if (element.Attribute("limbfixamount") != null)
if (element.GetAttribute("limbfixamount") != null)
{
DebugConsole.ThrowError("Error in item \"" + item.Name + "\" - RepairTool damage should be configured using a StatusEffect with Afflictions, not the limbfixamount attribute.");
}
@@ -140,10 +140,10 @@ namespace Barotrauma.Items.Components
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "fixable":
if (subElement.Attribute("name") != null)
if (subElement.GetAttribute("name") != null)
{
DebugConsole.ThrowError("Error in RepairTool " + item.Name + " - use identifiers instead of names to configure fixable entities.");
fixableEntities.Add(subElement.Attribute("name").Value.ToIdentifier());
fixableEntities.Add(subElement.GetAttribute("name").Value.ToIdentifier());
}
else
{
@@ -344,7 +344,7 @@ namespace Barotrauma.Items.Components
break;
case "requiredskill":
case "requiredskills":
if (subElement.Attribute("name") != null)
if (subElement.GetAttribute("name") != null)
{
DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - skill requirement in component " + GetType().ToString() + " should use a skill identifier instead of the name of the skill.");
continue;
@@ -424,6 +424,15 @@ namespace Barotrauma.Items.Components
}
}
public override void UpdateBroken(float deltaTime, Camera cam)
{
//update when the item is broken too to get OnContaining effects to execute and contained item positions to update
if (IsActive)
{
Update(deltaTime, cam);
}
}
public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null)
{
return AllowAccess && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg);
@@ -127,7 +127,7 @@ namespace Barotrauma.Items.Components
{
if (subElement.Name != "limbposition") { continue; }
string limbStr = subElement.GetAttributeString("limb", "");
if (!Enum.TryParse(subElement.Attribute("limb").Value, out LimbType limbType))
if (!Enum.TryParse(subElement.GetAttribute("limb").Value, out LimbType limbType))
{
DebugConsole.ThrowError($"Error in item \"{item.Name}\" - {limbStr} is not a valid limb type.");
}
@@ -186,8 +186,19 @@ namespace Barotrauma.Items.Components
if (!isClient)
{
MoveIngredientsToInputContainer(selectedItem);
if (selectedItem.RequiredMoney > 0)
{
if (GameMain.GameSession?.GameMode is MultiPlayerCampaign)
{
user.Wallet.Deduct(selectedItem.RequiredMoney);
}
else if (GameMain.GameSession?.GameMode is CampaignMode campaign)
{
campaign.Bank.Deduct(selectedItem.RequiredMoney);
}
}
}
requiredTime = GetRequiredTime(fabricatedItem, user);
timeUntilReady = requiredTime;
@@ -508,6 +519,22 @@ namespace Barotrauma.Items.Components
if (fabricableItem == null) { return false; }
if (fabricableItem.RequiresRecipe && (character == null || !character.HasRecipeForItem(fabricableItem.TargetItem.Identifier))) { return false; }
if (fabricableItem.RequiredMoney > 0)
{
if (GameMain.GameSession?.GameMode is MultiPlayerCampaign)
{
if (character?.Wallet == null || character.Wallet.Balance < fabricableItem.RequiredMoney) { return false; }
}
else if (GameMain.GameSession?.GameMode is CampaignMode campaign)
{
if (campaign.Bank.Balance < fabricableItem.RequiredMoney) { return false; }
}
else
{
return false;
}
}
return fabricableItem.RequiredItems.All(requiredItem =>
{
int availablePrefabsAmount = 0;
@@ -169,7 +169,7 @@ namespace Barotrauma.Items.Components
hull.BallastFlora = new BallastFloraBehavior(hull, ballastFloraPrefab, offset, firstGrowth: true);
#if SERVER
hull.BallastFlora.SendNetworkMessage(new BallastFloraBehavior.SpawnEventData());
hull.BallastFlora.CreateNetworkMessage(new BallastFloraBehavior.SpawnEventData());
#endif
}
@@ -1,5 +1,4 @@
using System;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
#if CLIENT
@@ -151,20 +150,6 @@ namespace Barotrauma.Items.Components
}
set
{
if (powerIn != null)
{
if (powerIn.Grid != null)
{
powerIn.Grid.Voltage = Math.Max(0.0f, value);
}
}
else if (powerOut != null)
{
if (powerOut.Grid != null)
{
powerOut.Grid.Voltage = Math.Max(0.0f, value);
}
}
voltage = Math.Max(0.0f, value);
}
}
@@ -213,11 +198,6 @@ namespace Barotrauma.Items.Components
powerOnSoundPlayed = false;
}
#endif
if (powerIn == null)
{
//power down the device here if it has no power connection (= receives power from contained battery cells instead of the "normal" power logic)
Voltage -= deltaTime;
}
}
public override void Update(float deltaTime, Camera cam)
@@ -470,6 +450,11 @@ namespace Barotrauma.Items.Components
//Determine if devices are adding a load or providing power, also resolve solo nodes
foreach (Powered powered in poweredList)
{
//Make voltage decay to ensure the device powers down.
//This only effects devices with no power input (whose voltage is set by other means, e.g. status effects from a contained battery)
//or devices that have been disconnected from the power grid - other devices use the voltage of the grid instead.
powered.Voltage -= deltaTime;
//Handle the device if it's got a power connection
if (powered.powerIn != null && powered.powerOut != powered.powerIn)
{
@@ -500,7 +485,7 @@ namespace Barotrauma.Items.Components
}
else
{
powered.CurrPowerConsumption = powered.GetConnectionPowerOut(powered.powerIn, 0, powered.MinMaxPowerOut(powered.powerIn, 0), 0);
powered.CurrPowerConsumption = -powered.GetConnectionPowerOut(powered.powerIn, 0, powered.MinMaxPowerOut(powered.powerIn, 0), 0);
powered.GridResolved(powered.powerIn);
}
}
@@ -541,7 +526,7 @@ namespace Barotrauma.Items.Components
else
{
//Perform power calculations for the singular connection
float loadOut = powered.GetConnectionPowerOut(powered.powerOut, 0, powered.MinMaxPowerOut(powered.powerOut, 0), 0);
float loadOut = -powered.GetConnectionPowerOut(powered.powerOut, 0, powered.MinMaxPowerOut(powered.powerOut, 0), 0);
if (powered is PowerTransfer pt2)
{
pt2.PowerLoad = loadOut;
@@ -667,7 +652,7 @@ namespace Barotrauma.Items.Components
public static bool ValidPowerConnection(Connection conn1, Connection conn2)
{
return conn1.IsPower && conn2.IsPower && (conn1.Item.HasTag("junctionbox") || conn2.Item.HasTag("junctionbox") || conn1.IsOutput != conn2.IsOutput || (conn1.Item.HasTag("dock") && conn2.Item.HasTag("dock")));
return conn1.IsPower && conn2.IsPower && (conn1.Item.HasTag("junctionbox") || conn2.Item.HasTag("junctionbox") || conn1.Item.HasTag("dock") || conn2.Item.HasTag("dock") || conn1.IsOutput != conn2.IsOutput);
}
/// <summary>
@@ -7,7 +7,6 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Voronoi2;
namespace Barotrauma.Items.Components
@@ -49,9 +48,8 @@ namespace Barotrauma.Items.Components
//continuous collision detection is used while the projectile is moving faster than this
const float ContinuousCollisionThreshold = 5.0f;
//a duration during which the projectile won't drop from the body it's stuck to
private const float PersistentStickJointDuration = 1.0f;
private PrismaticJoint stickJoint;
private Joint stickJoint;
private Vector2 jointAxis;
public Attack Attack { get; private set; }
@@ -86,8 +84,6 @@ namespace Barotrauma.Items.Components
get { return hits; }
}
private float persistentStickJointTimer;
[Serialize(10.0f, IsPropertySaveable.No, description: "The impulse applied to the physics body of the item when it's launched. Higher values make the projectile faster.")]
public float LaunchImpulse { get; set; }
@@ -116,13 +112,6 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, IsPropertySaveable.No, description: "When set to true, the item won't fall of a target it's stuck to unless removed.")]
public bool StickPermanently
{
get;
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the character it hits.")]
public bool StickToCharacters
{
@@ -151,6 +140,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, IsPropertySaveable.No, description: "")]
public bool StickToLightTargets
{
get;
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Hitscan projectiles cast a ray forwards and immediately hit whatever the ray hits. "+
"It is recommended to use hitscans for very fast-moving projectiles such as bullets, because using extremely fast launch velocities may cause physics glitches.")]
public bool Hitscan
@@ -212,16 +208,36 @@ namespace Barotrauma.Items.Components
set;
}
private float stickTimer;
[Serialize(0f, IsPropertySaveable.No)]
public float StickDuration
{
get;
set;
}
[Serialize(-1f, IsPropertySaveable.No)]
public float MaxJointTranslation
{
get;
set;
}
private float maxJointTranslationInSimUnits = -1;
[Serialize(true, IsPropertySaveable.No)]
public bool Prismatic
{
get;
set;
}
public Body StickTarget
{
get;
private set;
}
public bool IsStuckToTarget
{
get { return StickTarget != null; }
}
public bool IsStuckToTarget => StickTarget != null;
private Category originalCollisionCategories;
private Category originalCollisionTargets;
@@ -660,23 +676,22 @@ namespace Barotrauma.Items.Components
if (stickJoint == null) { return; }
if (persistentStickJointTimer > 0.0f && !StickPermanently)
if (StickDuration > 0 && stickTimer > 0)
{
persistentStickJointTimer -= deltaTime;
stickTimer -= deltaTime;
return;
}
float absoluteMaxTranslation = 100;
// Update the item's transform to make sure it's inside the same sub as the target (or outside)
if (StickTarget?.UserData is Limb target && target.Submarine != item.Submarine || Math.Abs(stickJoint.JointTranslation) > 100.0f)
if (StickTarget?.UserData is Limb target && target.Submarine != item.Submarine || stickJoint is PrismaticJoint prismaticJoint && Math.Abs(prismaticJoint.JointTranslation) > absoluteMaxTranslation)
{
item.UpdateTransform();
}
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
if (StickTargetRemoved() ||
(!StickPermanently && (stickJoint.JointTranslation < stickJoint.LowerLimit * 0.9f || stickJoint.JointTranslation > stickJoint.UpperLimit * 0.9f)) ||
Math.Abs(stickJoint.JointTranslation) > 100.0f) //failsafe unstick if the target is still extremely far
if (StickTargetRemoved() || stickJoint is PrismaticJoint pJoint && Math.Abs(pJoint.JointTranslation) > maxJointTranslationInSimUnits)
{
Unstick();
#if SERVER
@@ -706,7 +721,7 @@ namespace Barotrauma.Items.Components
if (hits.Contains(target.Body)) { return false; }
if (target.Body.UserData is Submarine)
{
if (ShouldIgnoreSubmarineCollision(ref target, contact)) { return false; }
if (ShouldIgnoreSubmarineCollision(ref target, contact)) { return false; }
}
else if (target.Body.UserData is Limb limb)
{
@@ -778,7 +793,10 @@ namespace Barotrauma.Items.Components
Vector2.Dot(item.body.SimPosition - launchPos, dir) > 0)
{
target = wallBody.FixtureList.First();
if (hits.Contains(target.Body)) { return true; }
if (hits.Contains(target.Body))
{
return true;
}
}
else
{
@@ -936,14 +954,12 @@ namespace Barotrauma.Items.Components
{
item.body.LinearVelocity *= deflectedSpeedMultiplier;
}
else if ( // When hitting characters the collision normal seems to sometimes point into wrong direction, resulting in a failed attempt to stick
//Vector2.Dot(Vector2.Normalize(velocity), collisionNormal) < 0.0f &&
hits.Count() >= MaxTargetsToHit &&
target.Body.Mass > item.body.Mass * 0.5f &&
else if ( stickJoint == null && StickTarget == null &&
StickToStructures && target.Body.UserData is Structure ||
((StickToLightTargets || target.Body.Mass > item.body.Mass * 0.5f) &&
(DoesStick ||
(StickToCharacters && (target.Body.UserData is Limb || target.Body.UserData is Character)) ||
(StickToStructures && target.Body.UserData is Structure) ||
(StickToItems && target.Body.UserData is Item)))
(StickToItems && target.Body.UserData is Item))))
{
Vector2 dir = new Vector2(
(float)Math.Cos(item.body.Rotation),
@@ -1025,30 +1041,39 @@ namespace Barotrauma.Items.Components
private void StickToTarget(Body targetBody, Vector2 axis)
{
if (stickJoint != null) { return; }
stickJoint = new PrismaticJoint(targetBody, item.body.FarseerBody, item.body.SimPosition, axis, true)
jointAxis = axis;
item.body.ResetDynamics();
if (Prismatic)
{
MotorEnabled = true,
MaxMotorForce = 30.0f,
LimitEnabled = true,
Breakpoint = 1000.0f
};
stickJoint = new PrismaticJoint(targetBody, item.body.FarseerBody, item.body.SimPosition, axis, useWorldCoordinates: true)
{
MotorEnabled = true,
MaxMotorForce = 30.0f,
LimitEnabled = true,
Breakpoint = 1000.0f
};
if (StickPermanently)
{
stickJoint.LowerLimit = stickJoint.UpperLimit = 0.0f;
item.body.ResetDynamics();
if (maxJointTranslationInSimUnits == -1)
{
if (item.Sprite != null && MaxJointTranslation < 0)
{
MaxJointTranslation = item.Sprite.size.X / 2 * item.Scale;
}
MaxJointTranslation = Math.Min(MaxJointTranslation, 1000);
maxJointTranslationInSimUnits = ConvertUnits.ToSimUnits(MaxJointTranslation);
}
}
else if (item.Sprite != null)
else
{
stickJoint.LowerLimit = ConvertUnits.ToSimUnits(item.Sprite.size.X * -0.3f * item.Scale);
stickJoint.UpperLimit = ConvertUnits.ToSimUnits(item.Sprite.size.X * 0.3f * item.Scale);
stickJoint = new WeldJoint(targetBody, item.body.FarseerBody, item.body.SimPosition, item.body.SimPosition, useWorldCoordinates: true)
{
FrequencyHz = 10.0f,
DampingRatio = 0.5f
};
}
persistentStickJointTimer = PersistentStickJointDuration;
stickTimer = StickDuration;
StickTarget = targetBody;
GameMain.World.Add(stickJoint);
IsActive = true;
if (targetBody.UserData is Limb limb)
{
@@ -1,9 +1,9 @@
using Barotrauma.Networking;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
@@ -12,8 +12,36 @@ namespace Barotrauma.Items.Components
private ISpatialEntity source;
private Item target;
private Vector2? launchDir;
private void SetSource(ISpatialEntity source)
{
this.source = source;
if (source is Limb sourceLimb)
{
sourceLimb.AttachedRope = this;
float offset = sourceLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
launchDir = VectorExtensions.Forward(sourceLimb.body.TransformedRotation - offset * sourceLimb.character.AnimController.Dir);
}
}
private void ResetSource()
{
if (source is Limb sourceLimb && sourceLimb.AttachedRope == this)
{
sourceLimb.AttachedRope = null;
}
source = null;
}
private float snapTimer;
private const float SnapAnimDuration = 1.0f;
[Serialize(1.0f, IsPropertySaveable.No, description: "")]
public float SnapAnimDuration
{
get;
set;
}
private float raycastTimer;
private const float RayCastInterval = 0.2f;
@@ -46,6 +74,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(360.0f, IsPropertySaveable.No, description: "The maximum angle from the source to the target until the rope breaks.")]
public float MaxAngle
{
get;
set;
}
[Serialize(true, IsPropertySaveable.No, description: "Should the rope snap when it collides with a structure/submarine (if not, it will just go through it).")]
public bool SnapOnCollision
{
@@ -115,8 +150,8 @@ namespace Barotrauma.Items.Components
{
System.Diagnostics.Debug.Assert(source != null);
System.Diagnostics.Debug.Assert(target != null);
this.source = source;
this.target = target;
SetSource(source);
Snapped = false;
ApplyStatusEffects(ActionType.OnUse, 1.0f, worldPosition: item.WorldPosition);
IsActive = true;
@@ -127,7 +162,7 @@ namespace Barotrauma.Items.Components
if (source == null || target == null || target.Removed ||
(source is Entity sourceEntity && sourceEntity.Removed))
{
source = null;
ResetSource();
target = null;
IsActive = false;
return;
@@ -144,12 +179,27 @@ namespace Barotrauma.Items.Components
}
Vector2 diff = target.WorldPosition - source.WorldPosition;
if (diff.LengthSquared() > MaxLength * MaxLength)
float lengthSqr = diff.LengthSquared();
if (lengthSqr > MaxLength * MaxLength)
{
Snap();
return;
}
if (MaxAngle < 180 && lengthSqr > 2500)
{
if (launchDir == null)
{
launchDir = diff;
}
float angle = MathHelper.ToDegrees(VectorExtensions.Angle(launchDir.Value, diff));
if (angle > MaxAngle)
{
Snap();
return;
}
}
#if CLIENT
item.ResetCachedVisibleSize();
#endif
@@ -99,7 +99,7 @@ namespace Barotrauma.Items.Components
string displayNameTag = "", fallbackTag = "";
//if displayname is not present, attempt to find it from the prefab
if (element.Attribute("displayname") == null)
if (element.GetAttribute("displayname") == null)
{
foreach (var subElement in item.Prefab.ConfigElement.Elements())
{
@@ -66,7 +66,7 @@ namespace Barotrauma.Items.Components
HasPropertyName = !PropertyName.IsEmpty;
IsIntegerInput = HasPropertyName && element.Name.ToString().ToLowerInvariant() == "integerinput";
if (element.Attribute("signal") is XAttribute attribute)
if (element.GetAttribute("signal") is XAttribute attribute)
{
Signal = attribute.Value;
ShouldSetProperty = HasPropertyName;
@@ -154,7 +154,7 @@ namespace Barotrauma.Items.Components
IsActive = true;
//backwards compatibility
if (element.Attribute("range") != null)
if (element.GetAttribute("range") != null)
{
rangeX = rangeY = element.GetAttributeFloat("range", 0.0f);
}
@@ -323,7 +323,7 @@ namespace Barotrauma.Items.Components
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "sprite":
if (subElement.Attribute("texture") == null)
if (subElement.GetAttribute("texture") == null)
{
DebugConsole.ThrowError("Item \"" + item.Name + "\" doesn't have a texture specified!");
return;
@@ -1389,7 +1389,10 @@ namespace Barotrauma
/// </summary>
public static void UpdateHulls()
{
foreach (Item item in ItemList) item.FindHull();
foreach (Item item in ItemList)
{
item.FindHull();
}
}
public Hull FindHull()
@@ -1677,12 +1680,17 @@ namespace Barotrauma
if (!(GameMain.NetworkMember is { IsServer: true })) { return; }
if (!conditionUpdatePending) { return; }
GameMain.NetworkMember.CreateEntityEvent(this, new StatusEventData());
CreateStatusEvent();
lastSentCondition = condition;
sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval;
conditionUpdatePending = false;
}
public void CreateStatusEvent()
{
GameMain.NetworkMember.CreateEntityEvent(this, new ItemStatusEventData());
}
private bool isActive = true;
public override void Update(float deltaTime, Camera cam)
@@ -2955,7 +2963,7 @@ namespace Barotrauma
/// <returns></returns>
public static Item Load(ContentXElement element, Submarine submarine, bool createNetworkEvent, IdRemap idRemap)
{
string name = element.Attribute("name").Value;
string name = element.GetAttribute("name").Value;
Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty);
if (string.IsNullOrWhiteSpace(name) && identifier.IsEmpty)
@@ -63,7 +63,7 @@ namespace Barotrauma
}
}
private readonly struct StatusEventData : IEventData
private readonly struct ItemStatusEventData : IEventData
{
public EventType EventType => EventType.Status;
}
@@ -106,12 +106,13 @@ namespace Barotrauma
public readonly Identifier TargetItemPrefabIdentifier;
public ItemPrefab TargetItem => ItemPrefab.Prefabs[TargetItemPrefabIdentifier];
private Lazy<LocalizedString> displayName;
private readonly Lazy<LocalizedString> displayName;
public LocalizedString DisplayName
=> ItemPrefab.Prefabs.ContainsKey(TargetItemPrefabIdentifier) ? displayName.Value : "";
public readonly ImmutableArray<RequiredItem> RequiredItems;
public readonly ImmutableArray<Identifier> SuitableFabricatorIdentifiers;
public readonly float RequiredTime;
public readonly int RequiredMoney;
public readonly bool RequiresRecipe;
public readonly float OutCondition; //Percentage-based from 0 to 1
public readonly ImmutableArray<Skill> RequiredSkills;
@@ -130,6 +131,7 @@ namespace Barotrauma
var requiredSkills = new List<Skill>();
RequiredTime = element.GetAttributeFloat("requiredtime", 1.0f);
RequiredMoney = element.GetAttributeInt("requiredmoney", 0);
OutCondition = element.GetAttributeFloat("outcondition", 1.0f);
if (OutCondition > 1.0f)
{
@@ -322,8 +324,13 @@ namespace Barotrauma
private PriceInfo defaultPrice;
public PriceInfo DefaultPrice => defaultPrice;
private ImmutableDictionary<Identifier, PriceInfo> locationPrices;
private ImmutableDictionary<Identifier, PriceInfo> StorePrices { get; set; }
public bool CanBeBought => (DefaultPrice != null && DefaultPrice.CanBeBought) ||
(StorePrices != null && StorePrices.Any(p => p.Value.CanBeBought));
/// <summary>
/// Any item with a Price element in the definition can be sold everywhere.
/// </summary>
public bool CanBeSold => DefaultPrice != null;
/// <summary>
/// Defines areas where the item can be interacted with. If RequireBodyInsideTrigger is set to true, the character
@@ -393,13 +400,6 @@ namespace Barotrauma
/// </summary>
public bool? AllowAsExtraCargo { get; private set; }
public bool CanBeBought => (DefaultPrice != null && DefaultPrice.CanBeBought) || (locationPrices != null && locationPrices.Any(p => p.Value.CanBeBought));
/// <summary>
/// Any item with a Price element in the definition can be sold everywhere.
/// </summary>
public bool CanBeSold => DefaultPrice != null;
public bool RandomDeconstructionOutput { get; private set; }
public int RandomDeconstructionOutputAmount { get; private set; }
@@ -675,11 +675,11 @@ namespace Barotrauma
var deconstructItems = new List<DeconstructItem>();
var fabricationRecipes = new Dictionary<uint, FabricationRecipe>();
var treatmentSuitability = new Dictionary<Identifier, float>();
var locationPrices = new Dictionary<Identifier, PriceInfo>();
var storePrices = new Dictionary<Identifier, PriceInfo>();
var preferredContainers = new List<PreferredContainer>();
DeconstructTime = 1.0f;
if (ConfigElement.Attribute("allowasextracargo") != null)
if (ConfigElement.GetAttribute("allowasextracargo") != null)
{
AllowAsExtraCargo = ConfigElement.GetAttributeBool("allowasextracargo", false);
}
@@ -690,7 +690,7 @@ namespace Barotrauma
this.tags = ConfigElement.GetAttributeIdentifierArray("Tags", Array.Empty<Identifier>()).ToImmutableHashSet();
}
if (ConfigElement.Attribute("cargocontainername") != null)
if (ConfigElement.GetAttribute("cargocontainername") != null)
{
DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - cargo container should be configured using the item's identifier, not the name.");
}
@@ -731,39 +731,46 @@ namespace Barotrauma
CanSpriteFlipY = subElement.GetAttributeBool("canflipy", true);
sprite = new Sprite(subElement, spriteFolder, lazyLoad: true);
if (subElement.Attribute("sourcerect") == null &&
subElement.Attribute("sheetindex") == null)
if (subElement.GetAttribute("sourcerect") == null &&
subElement.GetAttribute("sheetindex") == null)
{
DebugConsole.ThrowError($"Warning - sprite sourcerect not configured for item \"{ToString()}\"!");
}
Size = Sprite.size;
if (subElement.Attribute("name") == null && !Name.IsNullOrWhiteSpace())
if (subElement.GetAttribute("name") == null && !Name.IsNullOrWhiteSpace())
{
Sprite.Name = Name.Value;
}
Sprite.EntityIdentifier = Identifier;
break;
case "price":
if (subElement.Attribute("baseprice") != null)
if (subElement.GetAttribute("baseprice") != null)
{
foreach (Tuple<Identifier, PriceInfo> priceInfo in PriceInfo.CreatePriceInfos(subElement, out this.defaultPrice))
foreach (var priceInfo in PriceInfo.CreatePriceInfos(subElement, out defaultPrice))
{
if (priceInfo == null) { continue; }
locationPrices.Add(priceInfo.Item1, priceInfo.Item2);
if (priceInfo.StoreIdentifier.IsEmpty) { continue; }
if (storePrices.ContainsKey(priceInfo.StoreIdentifier))
{
DebugConsole.AddWarning($"Error in item prefab \"{this}\": price for the store \"{priceInfo.StoreIdentifier}\" defined more than once.");
storePrices[priceInfo.StoreIdentifier] = priceInfo;
}
else
{
storePrices.Add(priceInfo.StoreIdentifier, priceInfo);
}
}
}
else if (subElement.Attribute("buyprice") != null)
else if (subElement.GetAttribute("buyprice") != null && subElement.GetAttributeIdentifier("locationtype", "") is { IsEmpty: false } locationType) // Backwards compatibility
{
Identifier locationType = subElement.GetAttributeIdentifier("locationtype", "");
if (locationPrices.ContainsKey(locationType))
if (storePrices.ContainsKey(locationType))
{
DebugConsole.AddWarning($"Error in item prefab \"{ToString()}\": price for the location type \"{locationType}\" defined more than once.");
locationPrices[locationType] = new PriceInfo(subElement);
DebugConsole.AddWarning($"Error in item prefab \"{this}\": price for the location type \"{locationType}\" defined more than once.");
storePrices[locationType] = new PriceInfo(subElement);
}
else
{
locationPrices.Add(locationType, new PriceInfo(subElement));
storePrices.Add(locationType, new PriceInfo(subElement));
}
}
break;
@@ -851,7 +858,7 @@ namespace Barotrauma
}
break;
case "suitabletreatment":
if (subElement.Attribute("name") != null)
if (subElement.GetAttribute("name") != null)
{
DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - suitable treatments should be defined using item identifiers, not item names.");
}
@@ -870,15 +877,15 @@ namespace Barotrauma
this.DeconstructItems = deconstructItems.ToImmutableArray();
this.FabricationRecipes = fabricationRecipes.ToImmutableDictionary();
this.treatmentSuitability = treatmentSuitability.ToImmutableDictionary();
this.locationPrices = locationPrices.ToImmutableDictionary();
StorePrices = storePrices.ToImmutableDictionary();
this.PreferredContainers = preferredContainers.ToImmutableArray();
this.LevelCommonness = levelCommonness.ToImmutableDictionary();
this.LevelQuantity = levelQuantity.ToImmutableDictionary();
// Backwards compatibility
if (locationPrices != null && locationPrices.Any())
if (storePrices.Any())
{
this.defaultPrice ??= new PriceInfo(GetMinPrice() ?? 0, false);
defaultPrice ??= new PriceInfo(GetMinPrice() ?? 0, false);
}
HideConditionInTooltip = ConfigElement.GetAttributeBool("hideconditionintooltip", HideConditionBar);
@@ -930,31 +937,109 @@ namespace Barotrauma
return treatmentSuitability.TryGetValue(treatmentIdentifier, out float suitability) ? suitability : 0.0f;
}
public PriceInfo GetPriceInfo(Location location)
#region Pricing
public PriceInfo GetPriceInfo(Location.StoreInfo store)
{
if (location?.Type == null) { return null; }
var locationTypeId = location.Type.Identifier;
if (locationPrices != null && locationPrices.ContainsKey(locationTypeId))
if (!store.Identifier.IsEmpty && StorePrices != null && StorePrices.TryGetValue(store.Identifier, out var storePriceInfo))
{
return locationPrices[locationTypeId];
return storePriceInfo;
}
else
{
return DefaultPrice;
}
}
public bool CanBeBoughtAtLocation(Location location, out PriceInfo priceInfo)
public bool CanBeBoughtFrom(Location.StoreInfo store, out PriceInfo priceInfo)
{
priceInfo = null;
if (location?.Type == null) { return false; }
priceInfo = GetPriceInfo(location);
return
priceInfo != null &&
priceInfo.CanBeBought &&
(location.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty;
priceInfo = GetPriceInfo(store);
return priceInfo != null && priceInfo.CanBeBought && (store.Location?.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty;
}
public bool CanBeBoughtFrom(Location location)
{
if (location?.Stores == null) { return false; }
foreach (var store in location.Stores)
{
var priceInfo = GetPriceInfo(store.Value);
if (priceInfo == null) { continue; }
if (!priceInfo.CanBeBought) { continue; }
if ((location.LevelData?.Difficulty ?? 0) < priceInfo.MinLevelDifficulty) { continue; }
return true;
}
return false;
}
public int? GetMinPrice()
{
int? minPrice = StorePrices.Values.Min(p => p.Price);
if (minPrice.HasValue)
{
if (DefaultPrice != null)
{
return minPrice < DefaultPrice.Price ? minPrice : DefaultPrice.Price;
}
else
{
return minPrice.Value;
}
}
else
{
return DefaultPrice?.Price;
}
}
public ImmutableDictionary<Identifier, PriceInfo> GetBuyPricesUnder(int maxCost = 0)
{
var prices = new Dictionary<Identifier, PriceInfo>();
if (StorePrices != null)
{
foreach (var storePrice in StorePrices)
{
var priceInfo = storePrice.Value;
if (priceInfo == null)
{
continue;
}
if (!priceInfo.CanBeBought)
{
continue;
}
if (priceInfo.Price < maxCost || maxCost == 0)
{
prices.Add(storePrice.Key, priceInfo);
}
}
}
return prices.ToImmutableDictionary();
}
public ImmutableDictionary<Identifier, PriceInfo> GetSellPricesOver(int minCost = 0, bool sellingImportant = true)
{
var prices = new Dictionary<Identifier, PriceInfo>();
if (!CanBeSold && sellingImportant)
{
return prices.ToImmutableDictionary();
}
foreach (var storePrice in StorePrices)
{
var priceInfo = storePrice.Value;
if (priceInfo == null)
{
continue;
}
if (priceInfo.Price > minCost)
{
prices.Add(storePrice.Key, priceInfo);
}
}
return prices.ToImmutableDictionary();
}
#endregion
public static ItemPrefab Find(string name, Identifier identifier)
{
if (string.IsNullOrEmpty(name) && identifier.IsEmpty)
@@ -988,77 +1073,6 @@ namespace Barotrauma
return prefab;
}
public int? GetMinPrice()
{
int? minPrice = locationPrices != null && locationPrices.Values.Any() ? locationPrices?.Values.Min(p => p.Price) : null;
if (minPrice.HasValue)
{
if (DefaultPrice != null)
{
return minPrice < DefaultPrice.Price ? minPrice : DefaultPrice.Price;
}
else
{
return minPrice.Value;
}
}
else
{
return DefaultPrice?.Price;
}
}
public ImmutableDictionary<Identifier, PriceInfo> GetBuyPricesUnder(int maxCost = 0)
{
Dictionary<Identifier, PriceInfo> priceLocations = new Dictionary<Identifier, PriceInfo>();
if (locationPrices != null)
{
foreach (KeyValuePair<Identifier, PriceInfo> locationPrice in locationPrices)
{
PriceInfo priceInfo = locationPrice.Value;
if (priceInfo == null)
{
continue;
}
if (!priceInfo.CanBeBought)
{
continue;
}
if (priceInfo.Price < maxCost || maxCost == 0)
{
priceLocations.Add(locationPrice.Key, priceInfo);
}
}
}
return priceLocations.ToImmutableDictionary();
}
public ImmutableDictionary<Identifier, PriceInfo> GetSellPricesOver(int minCost = 0, bool sellingImportant = true)
{
Dictionary<Identifier, PriceInfo> priceLocations = new Dictionary<Identifier, PriceInfo>();
if (!CanBeSold && sellingImportant)
{
return priceLocations.ToImmutableDictionary();
}
foreach (KeyValuePair<Identifier, PriceInfo> locationPrice in locationPrices)
{
PriceInfo priceInfo = locationPrice.Value;
if (priceInfo == null)
{
continue;
}
if (priceInfo.Price > minCost)
{
priceLocations.Add(locationPrice.Key, priceInfo);
}
}
return priceLocations.ToImmutableDictionary();
}
public bool IsContainerPreferred(Item item, ItemContainer targetContainer, out bool isPreferencesDefined, out bool isSecondary, bool requireConditionRequirement = false)
{
isPreferencesDefined = PreferredContainers.Any();
@@ -167,7 +167,7 @@ namespace Barotrauma
public static RelatedItem Load(ContentXElement element, bool returnEmpty, string parentDebugName)
{
Identifier[] identifiers;
if (element.Attribute("name") != null)
if (element.GetAttribute("name") != null)
{
//backwards compatibility + a console warning
DebugConsole.ThrowError("Error in RelatedItem config (" + (string.IsNullOrEmpty(parentDebugName) ? element.ToString() : parentDebugName) + ") - use item tags or identifiers instead of names.");
@@ -520,8 +520,9 @@ namespace Barotrauma.MapCreatures.Behavior
if (branch.Health > branch.MaxHealth * 0.9f || branch.DisconnectedFromRoot) { continue; }
float branchHealAmount = (float)(MaxBranchHealthRegenDistance - branch.BranchDepth) / MaxBranchHealthRegenDistance * healAmount;
if (branchHealAmount <= 0.0f) { continue; }
float prevHealth = branch.Health;
branch.Health += branchHealAmount;
branch.AccumulatedDamage -= branchHealAmount;
branch.AccumulatedDamage += (prevHealth - branch.Health);
}
}
StateMachine.Update(deltaTime);
@@ -633,7 +634,8 @@ namespace Barotrauma.MapCreatures.Behavior
{
if (branch.ParentBranch != null && (branch.ParentBranch.DisconnectedFromRoot || branch.ParentBranch.Health <= 0.0f))
{
DamageBranch(branch, deltaTime * MathHelper.Lerp(10.0f, 0.01f, branch.ParentBranch.Health / branch.ParentBranch.MaxHealth), AttackType.CutFromRoot);
float speed = MathHelper.Lerp(5.0f, 0.1f, branch.ParentBranch.Health / branch.ParentBranch.MaxHealth);
DamageBranch(branch, speed * speed * deltaTime, AttackType.CutFromRoot);
}
if (branch.Health <= 0.0f)
{
@@ -836,7 +838,7 @@ namespace Barotrauma.MapCreatures.Behavior
}
#if SERVER
SendNetworkMessage(new BranchCreateEventData(newBranch, parent));
CreateNetworkMessage(new BranchCreateEventData(newBranch, parent));
#endif
return true;
}
@@ -878,7 +880,7 @@ namespace Barotrauma.MapCreatures.Behavior
#if SERVER
if (!load)
{
SendNetworkMessage(new InfectEventData(target, InfectEventData.InfectState.Yes, branch));
CreateNetworkMessage(new InfectEventData(target, InfectEventData.InfectState.Yes, branch));
}
#endif
}
@@ -955,8 +957,6 @@ namespace Barotrauma.MapCreatures.Behavior
/// <param name="branch"></param>
private void CreateBody(BallastFloraBranch branch)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
Rectangle rect = branch.Rect;
Vector2 pos = Parent.Position + Offset + branch.Position;
@@ -975,6 +975,14 @@ namespace Barotrauma.MapCreatures.Behavior
public void DamageBranch(BallastFloraBranch branch, float amount, AttackType type, Character? attacker = null)
{
float damage = amount;
if (damage > 0)
{
damage = Math.Min(damage, branch.Health);
}
else
{
damage = Math.Max(damage, branch.Health - branch.MaxHealth);
}
if (type != AttackType.Other && type != AttackType.CutFromRoot)
{
@@ -983,8 +991,28 @@ namespace Barotrauma.MapCreatures.Behavior
if (branch.IsRootGrowth && root != null && root.Health > 0.0f) { return; }
// damage is handled server side currently
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (type != AttackType.Other && type != AttackType.CutFromRoot)
{
branch.AccumulatedDamage += damage;
Anger += damage * 0.001f;
}
if (GameMain.NetworkMember != null)
{
// damage is handled server side
if (GameMain.NetworkMember.IsClient)
{
return;
}
else
{
//accumulate damage on the server's side to ensure clients get notified
if (type == AttackType.Other || type == AttackType.CutFromRoot)
{
branch.AccumulatedDamage += damage;
}
}
}
if (attacker != null && toxinsCooldown <= 0)
{
@@ -1014,11 +1042,6 @@ namespace Barotrauma.MapCreatures.Behavior
}
branch.Health -= damage;
if (type != AttackType.Other && type != AttackType.CutFromRoot)
{
branch.AccumulatedDamage += damage;
Anger += damage * 0.001f;
}
#if SERVER
GameMain.Server?.KarmaManager?.OnBallastFloraDamaged(attacker, damage);
@@ -1110,7 +1133,7 @@ namespace Barotrauma.MapCreatures.Behavior
#if SERVER
if (!wasRemoved)
{
SendNetworkMessage(new BranchRemoveEventData(branch));
CreateNetworkMessage(new BranchRemoveEventData(branch));
}
#endif
}
@@ -1141,7 +1164,7 @@ namespace Barotrauma.MapCreatures.Behavior
}
});
#if SERVER
SendNetworkMessage(new InfectEventData(item, InfectEventData.InfectState.No, null));
CreateNetworkMessage(new InfectEventData(item, InfectEventData.InfectState.No, null));
#endif
}
@@ -1159,7 +1182,7 @@ namespace Barotrauma.MapCreatures.Behavior
StateMachine?.State?.Exit();
#if SERVER
SendNetworkMessage(new KillEventData());
CreateNetworkMessage(new KillEventData());
#endif
}
@@ -1181,7 +1204,7 @@ namespace Barotrauma.MapCreatures.Behavior
_entityList.Remove(this);
#if SERVER
SendNetworkMessage(new KillEventData());
CreateNetworkMessage(new KillEventData());
#endif
}
@@ -88,7 +88,7 @@ namespace Barotrauma
flash = element.GetAttributeBool("flash", showEffects);
flashDuration = element.GetAttributeFloat("flashduration", 0.05f);
if (element.Attribute("flashrange") != null) { flashRange = element.GetAttributeFloat("flashrange", 100.0f); }
if (element.GetAttribute("flashrange") != null) { flashRange = element.GetAttributeFloat("flashrange", 100.0f); }
flashColor = element.GetAttributeColor("flashcolor", Color.LightYellow);
EmpStrength = element.GetAttributeFloat("empstrength", 0.0f);
@@ -739,7 +739,7 @@ namespace Barotrauma
{
Rectangle rect = Rectangle.Empty;
if (element.Attribute("rect") != null)
if (element.GetAttribute("rect") != null)
{
rect = element.GetAttributeRect("rect", Rectangle.Empty);
}
@@ -747,15 +747,15 @@ namespace Barotrauma
{
//backwards compatibility
rect = new Rectangle(
int.Parse(element.Attribute("x").Value),
int.Parse(element.Attribute("y").Value),
int.Parse(element.Attribute("width").Value),
int.Parse(element.Attribute("height").Value));
int.Parse(element.GetAttribute("x").Value),
int.Parse(element.GetAttribute("y").Value),
int.Parse(element.GetAttribute("width").Value),
int.Parse(element.GetAttribute("height").Value));
}
bool isHorizontal = rect.Height > rect.Width;
var horizontalAttribute = element.Attribute("horizontal");
var horizontalAttribute = element.GetAttribute("horizontal");
if (horizontalAttribute != null)
{
isHorizontal = horizontalAttribute.Value.ToString() == "true";
@@ -1564,7 +1564,7 @@ namespace Barotrauma
public static Hull Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
{
Rectangle rect;
if (element.Attribute("rect") != null)
if (element.GetAttribute("rect") != null)
{
rect = element.GetAttributeRect("rect", Rectangle.Empty);
}
@@ -1572,10 +1572,10 @@ namespace Barotrauma
{
//backwards compatibility
rect = new Rectangle(
int.Parse(element.Attribute("x").Value),
int.Parse(element.Attribute("y").Value),
int.Parse(element.Attribute("width").Value),
int.Parse(element.Attribute("height").Value));
int.Parse(element.GetAttribute("x").Value),
int.Parse(element.GetAttribute("y").Value),
int.Parse(element.GetAttribute("width").Value),
int.Parse(element.GetAttribute("height").Value));
}
var hull = new Hull(MapEntityPrefab.Find(null, "hull"), rect, submarine, idRemap.GetOffsetId(element))
@@ -1639,7 +1639,7 @@ namespace Barotrauma
}
SerializableProperty.DeserializeProperties(hull, element);
if (element.Attribute("oxygen") == null) { hull.Oxygen = hull.Volume; }
if (element.GetAttribute("oxygen") == null) { hull.Oxygen = hull.Volume; }
return hull;
}
@@ -160,17 +160,20 @@ namespace Barotrauma
public void Delete()
{
Dispose();
if (File.Exists(ContentFile.Path))
try
{
try
if (ContentPackage is { Files: { Length: 1 } }
&& ContentPackageManager.LocalPackages.Contains(ContentPackage))
{
File.Delete(ContentFile.Path);
}
catch (Exception e)
{
DebugConsole.ThrowError("Deleting item assembly \"" + Name + "\" failed.", e);
Directory.Delete(ContentPackage.Dir, recursive: true);
ContentPackageManager.LocalPackages.Refresh();
ContentPackageManager.EnabledPackages.DisableRemovedMods();
}
}
catch (Exception e)
{
DebugConsole.ThrowError("Deleting item assembly \"" + Name + "\" failed.", e);
}
}
public override void Dispose() { }
@@ -3826,12 +3826,12 @@ namespace Barotrauma
if (location != null)
{
DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location: {location.Name}, level type: {LevelData.Type})");
outpost = OutpostGenerator.Generate(outpostGenerationParams, location, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost);
outpost = OutpostGenerator.Generate(outpostGenerationParams, location, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost, LevelData.AllowInvalidOutpost);
}
else
{
DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location type: {locationType}, level type: {LevelData.Type})");
outpost = OutpostGenerator.Generate(outpostGenerationParams, locationType, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost);
outpost = OutpostGenerator.Generate(outpostGenerationParams, locationType, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost, LevelData.AllowInvalidOutpost);
}
foreach (string categoryToHide in locationType.HideEntitySubcategories)
@@ -31,8 +31,20 @@ namespace Barotrauma
public bool HasHuntingGrounds, OriginallyHadHuntingGrounds;
/// <summary>
/// Minimum difficulty of the level before hunting grounds can appear.
/// </summary>
public const float HuntingGroundsDifficultyThreshold = 25;
/// <summary>
/// Probability of hunting grounds appearing in 100% difficulty levels.
/// </summary>
public const float MaxHuntingGroundsProbability = 0.3f;
public OutpostGenerationParams ForceOutpostGenerationParams;
public bool AllowInvalidOutpost;
public readonly Point Size;
public readonly int InitialDepth;
@@ -150,11 +162,7 @@ namespace Barotrauma
}
else
{
//minimum difficulty of the level before hunting grounds can appear
float huntingGroundsDifficultyThreshold = 25;
//probability of hunting grounds appearing in 100% difficulty levels
float maxHuntingGroundsProbability = 0.3f;
HasHuntingGrounds = OriginallyHadHuntingGrounds = rand.NextDouble() < MathUtils.InverseLerp(huntingGroundsDifficultyThreshold, 100.0f, Difficulty) * maxHuntingGroundsProbability;
HasHuntingGrounds = OriginallyHadHuntingGrounds = rand.NextDouble() < MathUtils.InverseLerp(HuntingGroundsDifficultyThreshold, 100.0f, Difficulty) * MaxHuntingGroundsProbability;
HasBeaconStation = !HasHuntingGrounds && rand.NextDouble() < locationConnection.Locations.Select(l => l.Type.BeaconStationChance).Max();
}
IsBeaconActive = false;
@@ -235,7 +235,7 @@ namespace Barotrauma
UseNetworkSyncing = element.GetAttributeBool("networksyncing", false);
unrotatedForce =
element.Attribute("force") != null && element.Attribute("force").Value.Contains(',') ?
element.GetAttribute("force") != null && element.GetAttribute("force").Value.Contains(',') ?
element.GetAttributeVector2("force", Vector2.Zero) :
new Vector2(element.GetAttributeFloat("force", 0.0f), 0.0f);
File diff suppressed because it is too large Load Diff
@@ -182,8 +182,7 @@ namespace Barotrauma
{
for (int i = 0; i < Connections.Count; i++)
{
float maxHuntingGroundsProbability = 0.3f;
Connections[i].LevelData.HasHuntingGrounds = Rand.Range(0.0f, 1.0f) < Connections[i].Difficulty / 100.0f * maxHuntingGroundsProbability;
Connections[i].LevelData.HasHuntingGrounds = Rand.Range(0.0f, 1.0f) < Connections[i].Difficulty / 100.0f * LevelData.MaxHuntingGroundsProbability;
connectionElements[i].SetAttributeValue("hashuntinggrounds", true);
}
}
@@ -232,7 +231,7 @@ namespace Barotrauma
System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation.");
CurrentLocation.Discover(true);
CurrentLocation.CreateStore();
CurrentLocation.CreateStores();
InitProjectSpecific();
}
@@ -680,7 +679,7 @@ namespace Barotrauma
CurrentLocation.Discover();
SelectedLocation = null;
CurrentLocation.CreateStore();
CurrentLocation.CreateStores();
OnLocationChanged?.Invoke(prevLocation, CurrentLocation);
if (GameMain.GameSession is { Campaign: { CampaignMetadata: { } metadata } })
@@ -719,7 +718,7 @@ namespace Barotrauma
}
}
CurrentLocation.CreateStore();
CurrentLocation.CreateStores();
OnLocationChanged?.Invoke(prevLocation, CurrentLocation);
}
@@ -857,16 +856,14 @@ namespace Barotrauma
if (location == CurrentLocation || location == SelectedLocation || location.IsGateBetweenBiomes) { continue; }
ProgressLocationTypeChanges(location);
if (location.Discovered)
if (!ProgressLocationTypeChanges(location) && location.Discovered)
{
location.UpdateStore();
location.UpdateStores();
}
}
}
private void ProgressLocationTypeChanges(Location location)
private bool ProgressLocationTypeChanges(Location location)
{
location.TimeSinceLastTypeChange++;
location.LocationTypeChangeCooldown--;
@@ -886,9 +883,8 @@ namespace Barotrauma
location.PendingLocationTypeChange.Value.parentMission);
if (location.PendingLocationTypeChange.Value.delay <= 0)
{
ChangeLocationType(location, location.PendingLocationTypeChange.Value.typeChange);
return ChangeLocationType(location, location.PendingLocationTypeChange.Value.typeChange);
}
return;
}
}
@@ -920,9 +916,9 @@ namespace Barotrauma
}
else
{
ChangeLocationType(location, selectedTypeChange);
return ChangeLocationType(location, selectedTypeChange);
}
return;
return false;
}
}
@@ -941,6 +937,8 @@ namespace Barotrauma
}
}
}
return false;
}
public int DistanceToClosestLocationWithOutpost(Location startingLocation, out Location endingLocation)
@@ -988,7 +986,7 @@ namespace Barotrauma
return distance;
}
private void ChangeLocationType(Location location, LocationTypeChange change)
private bool ChangeLocationType(Location location, LocationTypeChange change)
{
string prevName = location.Name;
@@ -996,7 +994,7 @@ namespace Barotrauma
if (newType == null)
{
DebugConsole.ThrowError($"Failed to change the type of the location \"{location.Name}\". Location type \"{change.ChangeToType}\" not found.");
return;
return false;
}
if (newType.OutpostTeam != location.Type.OutpostTeam ||
@@ -1013,6 +1011,7 @@ namespace Barotrauma
location.TimeSinceLastTypeChange = 0;
location.LocationTypeChangeCooldown = change.CooldownAfterChange;
location.PendingLocationTypeChange = null;
return true;
}
partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change);
@@ -1091,7 +1090,7 @@ namespace Barotrauma
}
}
location.LoadStore(subElement);
location.LoadStores(subElement);
location.LoadMissions(subElement);
break;
@@ -59,8 +59,19 @@ namespace Barotrauma
}
public static readonly Md5Hash Blank = new Md5Hash(new string('0', 32));
private static readonly Regex removeWhitespaceRegex = new Regex(@"\s+", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
private static string RemoveWhitespace(string s)
{
StringBuilder sb = new StringBuilder(s.Length / 2); // Reserve half the size of the original string because
// that's probably close enough to the size of the result
for (int i = 0; i < s.Length; i++)
{
if (char.IsWhiteSpace(s[i])) { continue; }
sb.Append(s[i]);
}
return sb.ToString();
}
//thanks to Jlobblet for this regex
private static readonly Regex stringHashRegex = new Regex(@"^[0-9a-fA-F]{7,32}$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
@@ -175,7 +186,7 @@ namespace Barotrauma
}
if (options.HasFlag(StringHashOptions.IgnoreWhitespace))
{
str = removeWhitespaceRegex.Replace(str, "");
str = RemoveWhitespace(str);
}
byte[] bytes = Encoding.UTF8.GetBytes(str);
return CalculateForBytes(bytes);
@@ -156,6 +156,8 @@ namespace Barotrauma
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
private ImmutableHashSet<Identifier> StoreIdentifiers { get; set; }
#warning TODO: this shouldn't really accept any ContentFile, issue is that RuinConfigFile and OutpostConfigFile are separate derived classes
public OutpostGenerationParams(ContentXElement element, ContentFile file) : base(file, element.GetAttributeIdentifier("identifier", ""))
{
@@ -230,6 +232,26 @@ namespace Barotrauma
return humanPrefabCollections.GetRandom(randSync);
}
public ImmutableHashSet<Identifier> GetStoreIdentifiers()
{
if (StoreIdentifiers == null)
{
var storeIdentifiers = new HashSet<Identifier>();
foreach (var collection in humanPrefabCollections)
{
foreach (var prefab in collection)
{
if (prefab?.CampaignInteractionType == CampaignMode.InteractionType.Store)
{
storeIdentifiers.Add(prefab.Identifier);
}
}
}
StoreIdentifiers = storeIdentifiers.ToImmutableHashSet();
}
return StoreIdentifiers;
}
public override void Dispose() { }
}
}
@@ -57,17 +57,17 @@ namespace Barotrauma
}
}
public static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, bool onlyEntrance = false)
public static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, bool onlyEntrance = false, bool allowInvalidOutpost = false)
{
return Generate(generationParams, locationType, location: null, onlyEntrance);
return Generate(generationParams, locationType, location: null, onlyEntrance, allowInvalidOutpost);
}
public static Submarine Generate(OutpostGenerationParams generationParams, Location location, bool onlyEntrance = false)
public static Submarine Generate(OutpostGenerationParams generationParams, Location location, bool onlyEntrance = false, bool allowInvalidOutpost = false)
{
return Generate(generationParams, location.Type, location, onlyEntrance);
return Generate(generationParams, location.Type, location, onlyEntrance, allowInvalidOutpost);
}
private static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, Location location, bool onlyEntrance = false)
private static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, Location location, bool onlyEntrance = false, bool allowInvalidOutpost = false)
{
var outpostModuleFiles = ContentPackageManager.EnabledPackages.All
.SelectMany(p => p.GetFiles<OutpostModuleFile>())
@@ -197,12 +197,19 @@ namespace Barotrauma
AppendToModule(selectedModules.Last(), outpostModules.ToList(), pendingModuleFlags, selectedModules, locationType, allowExtendBelowInitialModule: generationParams is RuinGeneration.RuinGenerationParams);
if (pendingModuleFlags.Any(flag => flag != "none"))
{
remainingTries--;
if (remainingTries <= 0)
if (!allowInvalidOutpost)
{
DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough doors at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags));
remainingTries--;
if (remainingTries <= 0)
{
DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags));
}
continue;
}
else
{
DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags) + ". Won't retry because invalid outposts are allowed.");
}
continue;
}
var outpostInfo = new SubmarineInfo()
@@ -328,7 +335,10 @@ namespace Barotrauma
selectedModule.Offset =
(selectedModule.PreviousGap.WorldPosition + selectedModule.PreviousModule.Offset) -
selectedModule.ThisGap.WorldPosition;
selectedModule.Offset += moveDir * generationParams.MinHallwayLength;
if (selectedModule.PreviousGap.ConnectedDoor != null || selectedModule.ThisGap.ConnectedDoor != null)
{
selectedModule.Offset += moveDir * generationParams.MinHallwayLength;
}
}
entities[selectedModule] = moduleEntities;
}
@@ -464,6 +474,16 @@ namespace Barotrauma
pendingModuleFlags.Remove(initialModuleFlag);
pendingModuleFlags.Insert(0, initialModuleFlag);
if (pendingModuleFlags.Count > totalModuleCount)
{
DebugConsole.ThrowError($"Error during outpost generation. {pendingModuleFlags.Count} modules set to be used the outpost, but total module count is only {totalModuleCount}. Leaving out some of the modules...");
int removeCount = pendingModuleFlags.Count - totalModuleCount;
for (int i = 0; i < removeCount; i++)
{
pendingModuleFlags.Remove(pendingModuleFlags.Last());
}
}
return pendingModuleFlags;
}
@@ -486,46 +506,71 @@ namespace Barotrauma
if (pendingModuleFlags.Count == 0) { return true; }
List<PlacedModule> placedModules = new List<PlacedModule>();
foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient))
for (int i = 0; i < 2; i++)
{
if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; }
if (!allowExtendBelowInitialModule)
//try placing a module meant for this location type first, and if that fails, try choosing whatever fits
bool allowDifferentLocationType = i > 0;
foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient))
{
//don't continue downwards if it'd extend below the airlock
if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; }
}
if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition))
{
var newModule = AppendModule(currentModule, GetOpposingGapPosition(gapPosition), availableModules, pendingModuleFlags, selectedModules, locationType);
if (newModule != null) { placedModules.Add(newModule); }
if (pendingModuleFlags.Count == 0) { return true; }
if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; }
if (!allowExtendBelowInitialModule)
{
//don't continue downwards if it'd extend below the airlock
if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; }
}
PlacedModule newModule = null;
//try appending to the current module if possible
if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition))
{
newModule = AppendModule(currentModule, GetOpposingGapPosition(gapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType);
}
if (newModule != null)
{
placedModules.Add(newModule);
}
else
{
//couldn't append to current module, try one of the other placed modules
foreach (PlacedModule otherModule in selectedModules)
{
if (otherModule == currentModule) { continue; }
foreach (OutpostModuleInfo.GapPosition otherGapPosition in
GapPositions.Where(g => !otherModule.UsedGapPositions.HasFlag(g) && otherModule.Info.OutpostModuleInfo.GapPositions.HasFlag(g)))
{
newModule = AppendModule(otherModule, GetOpposingGapPosition(otherGapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType);
if (newModule != null)
{
placedModules.Add(newModule);
break;
}
}
if (newModule != null) { break; }
}
}
if (pendingModuleFlags.Count == 0) { return true; }
}
}
//couldn't place anything, retry
if (placedModules.Count == 0 && retry && !selectedModules.Any(m => m != currentModule && m.PreviousModule == currentModule.PreviousModule))
//couldn't place a module anywhere, we're probably fucked!
if (placedModules.Count == 0 && retry && currentModule.PreviousModule != null && !selectedModules.Any(m => m != currentModule && m.PreviousModule == currentModule))
{
//try to append to some other module first
foreach (PlacedModule otherModule in selectedModules)
{
if (AppendToModule(otherModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false, allowExtendBelowInitialModule: allowExtendBelowInitialModule))
{
return true;
}
}
//try to replace the previously placed module with something else that we can append to
var failedModule = currentModule;
for (int i = 0; i < 10; i++)
{
selectedModules.Remove(currentModule);
assertAllPreviousModulesPresent();
//readd the module types that the previous module was supposed to fulfill to the pending module types
pendingModuleFlags.AddRange(currentModule.FulfilledModuleTypes);
if (!availableModules.Contains(currentModule.Info)) { availableModules.Add(currentModule.Info); }
//retry
currentModule = AppendModule(currentModule.PreviousModule, currentModule.ThisGapPosition, availableModules, pendingModuleFlags, selectedModules, locationType);
currentModule = AppendModule(currentModule.PreviousModule, currentModule.ThisGapPosition, availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType: true);
assertAllPreviousModulesPresent();
if (currentModule == null) { break; }
if (AppendToModule(currentModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false, allowExtendBelowInitialModule: allowExtendBelowInitialModule))
{
assertAllPreviousModulesPresent();
return true;
}
}
@@ -534,9 +579,14 @@ namespace Barotrauma
foreach (PlacedModule placedModule in placedModules)
{
AppendToModule(placedModule, availableModules, pendingModuleFlags, selectedModules, locationType);
AppendToModule(placedModule, availableModules, pendingModuleFlags, selectedModules, locationType, allowExtendBelowInitialModule: allowExtendBelowInitialModule);
}
return placedModules.Count > 0;
void assertAllPreviousModulesPresent()
{
System.Diagnostics.Debug.Assert(selectedModules.All(m => m.PreviousModule == null || selectedModules.Contains(m.PreviousModule)));
}
}
/// <summary>
@@ -553,7 +603,8 @@ namespace Barotrauma
List<SubmarineInfo> availableModules,
List<Identifier> pendingModuleFlags,
List<PlacedModule> selectedModules,
LocationType locationType)
LocationType locationType,
bool allowDifferentLocationType)
{
if (pendingModuleFlags.Count == 0) { return null; }
@@ -562,7 +613,7 @@ namespace Barotrauma
foreach (Identifier moduleFlag in pendingModuleFlags)
{
flagToPlace = moduleFlag;
nextModule = GetRandomModule(currentModule?.Info?.OutpostModuleInfo, availableModules, flagToPlace, gapPosition, locationType);
nextModule = GetRandomModule(currentModule?.Info?.OutpostModuleInfo, availableModules, flagToPlace, gapPosition, locationType, allowDifferentLocationType);
if (nextModule != null) { break; }
}
@@ -603,6 +654,7 @@ namespace Barotrauma
foreach (PlacedModule otherModule in modules2)
{
if (module == otherModule) { continue; }
if (module.PreviousModule == otherModule && module.PreviousGap.ConnectedDoor == null && module.ThisGap.ConnectedDoor == null) { continue; }
if (ModulesOverlap(module, otherModule))
{
module1 = module;
@@ -775,22 +827,25 @@ namespace Barotrauma
}
}
private static SubmarineInfo GetRandomModule(OutpostModuleInfo prevModule, IEnumerable<SubmarineInfo> modules, Identifier moduleFlag, OutpostModuleInfo.GapPosition gapPosition, LocationType locationType)
private static SubmarineInfo GetRandomModule(OutpostModuleInfo prevModule, IEnumerable<SubmarineInfo> modules, Identifier moduleFlag, OutpostModuleInfo.GapPosition gapPosition, LocationType locationType, bool allowDifferentLocationType)
{
IEnumerable<SubmarineInfo> availableModules = null;
if (moduleFlag.IsEmpty || moduleFlag.Equals("none"))
{
availableModules = modules
.Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || (m.OutpostModuleInfo.ModuleFlags.Count() == 1 && m.OutpostModuleInfo.ModuleFlags.Contains("none".ToIdentifier())) && m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition));
.Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || (m.OutpostModuleInfo.ModuleFlags.Count() == 1 && m.OutpostModuleInfo.ModuleFlags.Contains("none".ToIdentifier())));
}
else
{
availableModules = modules
.Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag) && m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition));
.Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag));
}
availableModules = availableModules.Where(m => m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition) && m.OutpostModuleInfo.CanAttachToPrevious.HasFlag(gapPosition));
if (prevModule != null)
{
availableModules = availableModules.Where(m => CanAttachTo(m.OutpostModuleInfo, prevModule) && CanAttachTo(prevModule, m.OutpostModuleInfo));
availableModules = availableModules.Where(m => CanAttachTo(m.OutpostModuleInfo, prevModule));// && CanAttachTo(prevModule, m.OutpostModuleInfo));
}
if (availableModules.Count() == 0) { return null; }
@@ -800,15 +855,22 @@ namespace Barotrauma
availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier));
//if not found, search for modules suitable for any location type
if (!modulesSuitableForLocationType.Any())
if (allowDifferentLocationType && !modulesSuitableForLocationType.Any())
{
modulesSuitableForLocationType = availableModules.Where(m => !m.OutpostModuleInfo.AllowedLocationTypes.Any());
}
if (!modulesSuitableForLocationType.Any())
{
DebugConsole.NewMessage($"Could not find a suitable module for the location type {locationType}. Module flag: {moduleFlag}.", Color.Orange);
return ToolBox.SelectWeightedRandom(availableModules.ToList(), availableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient);
if (allowDifferentLocationType)
{
DebugConsole.NewMessage($"Could not find a suitable module for the location type {locationType}. Module flag: {moduleFlag}.", Color.Orange);
return ToolBox.SelectWeightedRandom(availableModules.ToList(), availableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient);
}
else
{
return null;
}
}
else
{
@@ -1039,16 +1101,20 @@ namespace Barotrauma
if (hallwayLength <= 1.0f) { continue; }
var suitableModules = availableModules.Where(m =>
m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.Info.OutpostModuleInfo.ModuleFlags.Contains(s)) &&
m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.PreviousModule.Info.OutpostModuleInfo.ModuleFlags.Contains(s)));
if (suitableModules.Count() == 0)
Identifier moduleFlag = (isHorizontal ? "hallwayhorizontal" : "hallwayvertical").ToIdentifier();
var hallwayModules = availableModules.Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag));
var suitableHallwayModules = hallwayModules.Where(m =>
m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.Info.OutpostModuleInfo.ModuleFlags.Contains(s)) &&
m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.PreviousModule.Info.OutpostModuleInfo.ModuleFlags.Contains(s)));
if (suitableHallwayModules.Count() == 0)
{
suitableModules = availableModules.Where(m =>
suitableHallwayModules = hallwayModules.Where(m =>
!m.OutpostModuleInfo.AllowAttachToModules.Any() ||
m.OutpostModuleInfo.AllowAttachToModules.All(s => s == "any"));
}
var hallwayInfo = GetRandomModule(suitableModules, (isHorizontal ? "hallwayhorizontal" : "hallwayvertical").ToIdentifier(), locationType);
var hallwayInfo = GetRandomModule(suitableHallwayModules, moduleFlag, locationType);
if (hallwayInfo == null)
{
DebugConsole.ThrowError($"Generating hallways between outpost modules failed. No {(isHorizontal ? "horizontal" : "vertical")} hallway modules suitable for use between the modules \"{module.Info.DisplayName}\" and \"{module.PreviousModule.Info.DisplayName}\".");
@@ -1170,7 +1236,7 @@ namespace Barotrauma
var startWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == module.ThisGap);
if (startWaypoint == null)
{
DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {GetOpposingGapPosition(module.ThisGapPosition).ToString().ToLower()} gap of the module \"{module.Info.Name}\".");
DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {module.ThisGapPosition.ToString().ToLower()} gap of the module \"{module.Info.Name}\".");
continue;
}
var endWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == module.PreviousGap);
@@ -45,6 +45,9 @@ namespace Barotrauma
[Serialize(GapPosition.None, IsPropertySaveable.Yes, description: "Which sides of the module have gaps on them (i.e. from which sides the module can be attached to other modules). Center = no gaps available.")]
public GapPosition GapPositions { get; set; }
[Serialize(GapPosition.Right | GapPosition.Left | GapPosition.Bottom | GapPosition.Top, IsPropertySaveable.Yes, description: "Which sides of this module are allowed to attach to the previously placed module. E.g. if you want a module to always attach to the left side of the docking module, you could set this to Right.")]
public GapPosition CanAttachToPrevious { get; set; }
public string Name { get; private set; }
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
@@ -6,30 +6,31 @@ namespace Barotrauma
{
class PriceInfo
{
public readonly int Price;
public readonly bool CanBeBought;
public int Price { get; }
public bool CanBeBought { get; }
//minimum number of items available at a given store
public readonly int MinAvailableAmount;
public int MinAvailableAmount { get; }
//maximum number of items available at a given store
public readonly int MaxAvailableAmount;
public int MaxAvailableAmount { get; }
/// <summary>
/// Can the item be a Daily Special or a Requested Good
/// </summary>
public bool CanBeSpecial { get; }
/// <summary>
/// The item isn't available in stores unless the level's difficulty is above this value
/// </summary>
public int MinLevelDifficulty { get; }
/// <summary>
/// The cost of item when sold by the store. Higher modifier means the item costs more to buy from the store.
/// </summary>
public float BuyingPriceMultiplier { get; } = 1f;
public bool DisplayNonEmpty { get; } = false;
public Identifier StoreIdentifier { get; }
/// <summary>
/// Used when both <see cref="MinAvailableAmount"/> and <see cref="MaxAvailableAmount"/> are set to 0.
/// </summary>
public const int DefaultAmount = 5;
/// <summary>
/// Can the item be a Daily Special or a Requested Good
/// </summary>
public readonly bool CanBeSpecial;
/// <summary>
/// The item isn't available in stores unless the level's difficulty is above this value
/// </summary>
public readonly int MinLevelDifficulty;
/// <summary>
/// The cost of item when sold by the store. Higher modifier means the item costs more to buy from the store.
/// </summary>
public readonly float BuyingPriceMultiplier = 1f;
public bool DisplayNonEmpty { get; } = false;
/// <summary>
/// Support for the old style of determining item prices
@@ -42,14 +43,16 @@ namespace Barotrauma
MinLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0);
BuyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f);
CanBeBought = true;
var minAmount = GetMinAmount(element);
int minAmount = GetMinAmount(element);
MinAvailableAmount = Math.Min(minAmount, CargoManager.MaxQuantity);
var maxAmount = GetMaxAmount(element);
int maxAmount = GetMaxAmount(element);
maxAmount = Math.Min(maxAmount, CargoManager.MaxQuantity);
MaxAvailableAmount = Math.Max(maxAmount, MinAvailableAmount);
}
public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true, int minLevelDifficulty = 0, float buyingPriceMultiplier = 1f, bool displayNonEmpty = false)
public PriceInfo(int price, bool canBeBought,
int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true, int minLevelDifficulty = 0, float buyingPriceMultiplier = 1f,
bool displayNonEmpty = false, string storeIdentifier = null)
{
Price = price;
CanBeBought = canBeBought;
@@ -60,48 +63,67 @@ namespace Barotrauma
MinLevelDifficulty = minLevelDifficulty;
CanBeSpecial = canBeSpecial;
DisplayNonEmpty = displayNonEmpty;
StoreIdentifier = new Identifier(storeIdentifier);
}
public static List<Tuple<Identifier, PriceInfo>> CreatePriceInfos(XElement element, out PriceInfo defaultPrice)
public static List<PriceInfo> CreatePriceInfos(XElement element, out PriceInfo defaultPrice)
{
var priceInfos = new List<PriceInfo>();
defaultPrice = null;
int basePrice = element.GetAttributeInt("baseprice", 0);
bool soldByDefault = element.GetAttributeBool("soldbydefault", true);
int minAmount = GetMinAmount(element);
int maxAmount = GetMaxAmount(element);
int minLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0);
bool canBeSpecial = element.GetAttributeBool("canbespecial", true);
float buyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f);
bool displayNonEmpty = element.GetAttributeBool("displaynonempty", false);
var priceInfos = new List<Tuple<Identifier, PriceInfo>>();
bool soldByDefault = element.GetAttributeBool("sold", element.GetAttributeBool("soldbydefault", true));
foreach (XElement childElement in element.GetChildElements("price"))
{
float priceMultiplier = childElement.GetAttributeFloat("multiplier", 1.0f);
bool sold = childElement.GetAttributeBool("sold", soldByDefault);
priceInfos.Add(new Tuple<Identifier, PriceInfo>(childElement.GetAttributeIdentifier("locationtype", ""),
new PriceInfo((int)(priceMultiplier * basePrice), sold,
bool sold = childElement.GetAttributeBool("sold", soldByDefault);
int storeMinLevelDifficulty = childElement.GetAttributeInt("minleveldifficulty", minLevelDifficulty);
float storeBuyingMultiplier = childElement.GetAttributeFloat("buyingpricemultiplier", buyingPriceMultiplier);
string backwardsCompatibleIdentifier = childElement.GetAttributeString("locationtype", "");
if (!string.IsNullOrEmpty(backwardsCompatibleIdentifier))
{
backwardsCompatibleIdentifier = $"merchant{backwardsCompatibleIdentifier}";
}
string[] storeIdentifiers = childElement.GetAttributeStringArray("storeidentifiers", new string[1] { backwardsCompatibleIdentifier });
foreach (string id in storeIdentifiers)
{
if (string.IsNullOrEmpty(id)) { continue; }
// TODO: Add some error messages if we have defined the min or max amount while the item is not sold
var priceInfo = new PriceInfo((int)(priceMultiplier * basePrice),
sold,
sold ? GetMinAmount(childElement, minAmount) : 0,
sold ? GetMaxAmount(childElement, maxAmount) : 0,
canBeSpecial,
childElement.GetAttributeInt("minleveldifficulty", minLevelDifficulty),
childElement.GetAttributeFloat("buyingpricemultiplier", buyingPriceMultiplier),
displayNonEmpty)));
storeMinLevelDifficulty,
storeBuyingMultiplier,
displayNonEmpty,
id);
priceInfos.Add(priceInfo);
}
}
bool canBeBoughtAtOtherLocations = soldByDefault && element.GetAttributeBool("soldeverywhere", true);
defaultPrice = new PriceInfo(basePrice, canBeBoughtAtOtherLocations,
canBeBoughtAtOtherLocations ? minAmount : 0,
canBeBoughtAtOtherLocations ? maxAmount : 0,
canBeSpecial, minLevelDifficulty, buyingPriceMultiplier, displayNonEmpty);
bool soldElsewhere = soldByDefault && element.GetAttributeBool("soldelsewhere", element.GetAttributeBool("soldeverywhere", false));
defaultPrice = new PriceInfo(basePrice,
soldElsewhere,
soldElsewhere ? minAmount : 0,
soldElsewhere ? maxAmount : 0,
canBeSpecial,
minLevelDifficulty,
buyingPriceMultiplier,
displayNonEmpty);
return priceInfos;
}
private static int GetMinAmount(XElement element, int defaultValue = 0) => element != null ?
element.GetAttributeInt("minamount", element.GetAttributeInt("minavailable", defaultValue)) : defaultValue;
element.GetAttributeInt("minamount", element.GetAttributeInt("minavailable", defaultValue)) :
defaultValue;
private static int GetMaxAmount(XElement element, int defaultValue = 0) => element != null ?
element.GetAttributeInt("maxamount", element.GetAttributeInt("maxavailable", defaultValue)) : defaultValue;
element.GetAttributeInt("maxamount", element.GetAttributeInt("maxavailable", defaultValue)) :
defaultValue;
}
}
@@ -48,11 +48,11 @@ namespace Barotrauma
{
if (!subs.Any()) yield return CoroutineStatus.Success;
Character.Controlled = null;
cam.TargetPos = Vector2.Zero;
#if CLIENT
Character.Controlled = null;
GameMain.LightManager.LosEnabled = false;
#endif
cam.TargetPos = Vector2.Zero;
Level.Loaded.TopBarrier.Enabled = false;
@@ -1374,7 +1374,7 @@ namespace Barotrauma
public static Structure Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
{
string name = element.Attribute("name").Value;
string name = element.GetAttribute("name").Value;
Identifier identifier = element.GetAttributeIdentifier("identifier", "");
StructurePrefab prefab = FindPrefab(name, identifier);
@@ -1440,12 +1440,12 @@ namespace Barotrauma
if (element.GetAttributeBool("flippedy", false)) { s.FlipY(false); }
//structures with a body drop a shadow by default
if (element.Attribute("usedropshadow") == null)
if (element.GetAttribute("usedropshadow") == null)
{
s.UseDropShadow = prefab.Body;
}
if (element.Attribute("noaitarget") == null)
if (element.GetAttribute("noaitarget") == null)
{
s.NoAITarget = prefab.NoAITarget;
}
@@ -162,7 +162,7 @@ namespace Barotrauma
tags.Add(tag.Trim().ToIdentifier());
}
if (element.Attribute("ishorizontal") != null)
if (element.GetAttribute("ishorizontal") != null)
{
IsHorizontal = element.GetAttributeBool("ishorizontal", false);
}
@@ -177,8 +177,8 @@ namespace Barotrauma
{
case "sprite":
Sprite = new Sprite(subElement, lazyLoad: true);
if (subElement.Attribute("sourcerect") == null &&
subElement.Attribute("sheetindex") == null)
if (subElement.GetAttribute("sourcerect") == null &&
subElement.GetAttribute("sheetindex") == null)
{
DebugConsole.ThrowError("Warning - sprite sourcerect not configured for structure \"" + Name + "\"!");
}
@@ -191,7 +191,7 @@ namespace Barotrauma
CanSpriteFlipX = subElement.GetAttributeBool("canflipx", true);
CanSpriteFlipY = subElement.GetAttributeBool("canflipy", true);
if (subElement.Attribute("name") == null && !Name.IsNullOrWhiteSpace())
if (subElement.GetAttribute("name") == null && !Name.IsNullOrWhiteSpace())
{
Sprite.Name = Name.Value;
}
@@ -199,7 +199,7 @@ namespace Barotrauma
break;
case "backgroundsprite":
BackgroundSprite = new Sprite(subElement, lazyLoad: true);
if (subElement.Attribute("sourcerect") == null && Sprite != null)
if (subElement.GetAttribute("sourcerect") == null && Sprite != null)
{
BackgroundSprite.SourceRect = Sprite.SourceRect;
BackgroundSprite.size = Sprite.size;
@@ -223,7 +223,7 @@ namespace Barotrauma
int groupID = 0;
DecorativeSprite decorativeSprite = null;
if (subElement.Attribute("texture") == null)
if (subElement.GetAttribute("texture") == null)
{
groupID = subElement.GetAttributeInt("randomgroupid", 0);
}
@@ -284,10 +284,10 @@ namespace Barotrauma
}
//backwards compatibility
if (element.Attribute("size") == null)
if (element.GetAttribute("size") == null)
{
Size = Vector2.Zero;
if (element.Attribute("width") == null && element.Attribute("height") == null)
if (element.GetAttribute("width") == null && element.GetAttribute("height") == null)
{
Size = Sprite.SourceRect.Size.ToVector2();
}
@@ -322,7 +322,7 @@ namespace Barotrauma
#endif
Tags = tags.ToImmutableHashSet();
AllowedLinks = Enumerable.Empty<Identifier>().ToImmutableHashSet();
AllowedLinks = ImmutableHashSet<Identifier>.Empty;
}
protected override void CreateInstance(Rectangle rect)
@@ -253,7 +253,7 @@ namespace Barotrauma
get { return subBody == null ? Vector2.Zero : subBody.Velocity; }
set
{
if (subBody == null) return;
if (subBody == null) { return; }
subBody.Velocity = value;
}
}
@@ -969,8 +969,6 @@ namespace Barotrauma
public void Update(float deltaTime)
{
//if (PlayerInput.KeyHit(InputType.Crouch) && (this == MainSub)) FlipX();
if (Info.IsWreck)
{
WreckAI?.Update(deltaTime);
@@ -266,7 +266,7 @@ namespace Barotrauma
GameVersion = original.GameVersion;
Type = original.Type;
SubmarineClass = original.SubmarineClass;
hash = !string.IsNullOrEmpty(original.FilePath) ? original.MD5Hash : null;
hash = !string.IsNullOrEmpty(original.FilePath) && File.Exists(original.FilePath) ? original.MD5Hash : null;
Dimensions = original.Dimensions;
CargoCapacity = original.CargoCapacity;
FilePath = original.FilePath;
@@ -299,7 +299,7 @@ namespace Barotrauma
DebugConsole.NewMessage("Opening submarine file \"" + FilePath + "\" failed, retrying in 250 ms...");
Thread.Sleep(250);
}
if (doc == null || doc.Root == null)
if (doc?.Root == null)
{
IsFileCorrupted = true;
return;
@@ -987,8 +987,8 @@ namespace Barotrauma
public static WayPoint Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
{
Rectangle rect = new Rectangle(
int.Parse(element.Attribute("x").Value),
int.Parse(element.Attribute("y").Value),
int.Parse(element.GetAttribute("x").Value),
int.Parse(element.GetAttribute("y").Value),
(int)Submarine.GridSize.X, (int)Submarine.GridSize.Y);
@@ -1024,9 +1024,9 @@ namespace Barotrauma
w.gapId = idRemap.GetOffsetId(element.GetAttributeInt("gap", 0));
int i = 0;
while (element.Attribute("linkedto" + i) != null)
while (element.GetAttribute("linkedto" + i) != null)
{
int srcId = int.Parse(element.Attribute("linkedto" + i).Value);
int srcId = int.Parse(element.GetAttribute("linkedto" + i).Value);
int destId = idRemap.GetOffsetId(srcId);
if (destId > 0)
{
@@ -31,7 +31,7 @@ namespace Barotrauma.Networking
ERROR, //tell the server that an error occurred
CREW, //hiring UI
MEDICAL, //medical clinic
MONEY, //wallet updates
TRANSFER_MONEY, // wallet transfers
REWARD_DISTRIBUTION, // wallet reward distribution
READY_CHECK,
READY_TO_SPAWN
@@ -1,6 +1,7 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Globalization;
using Barotrauma.IO;
@@ -975,18 +976,33 @@ namespace Barotrauma.Networking
private void InitMonstersEnabled()
{
//monster spawn settings
MonsterEnabled ??= CharacterPrefab.Prefabs.Select(p => (p.Identifier, true)).ToDictionary();
if (MonsterEnabled is null || MonsterEnabled.Count != CharacterPrefab.Prefabs.Count())
{
MonsterEnabled = CharacterPrefab.Prefabs.Select(p => (p.Identifier, true)).ToDictionary();
}
}
private static IReadOnlyList<Identifier> ExtractAndSortKeys(IReadOnlyDictionary<Identifier, bool> monsterEnabled)
=> monsterEnabled.Keys
.OrderBy(k => CharacterPrefab.Prefabs[k].UintIdentifier)
.ToImmutableArray();
public void ReadMonsterEnabled(IReadMessage inc)
{
InitMonstersEnabled();
List<Identifier> monsterNames = MonsterEnabled.Keys
.OrderBy(k => CharacterPrefab.Prefabs[k].UintIdentifier)
.ToList();
foreach (Identifier s in monsterNames)
var monsterNames = ExtractAndSortKeys(MonsterEnabled);
uint receivedMonsterCount = inc.ReadVariableUInt32();
if (monsterNames.Count != receivedMonsterCount)
{
MonsterEnabled[s] = inc.ReadBoolean();
inc.BitPosition += (int)receivedMonsterCount;
DebugConsole.AddWarning($"Expected monster count {monsterNames.Count}, got {receivedMonsterCount}");
}
else
{
foreach (Identifier s in monsterNames)
{
MonsterEnabled[s] = inc.ReadBoolean();
}
}
inc.ReadPadBits();
}
@@ -994,11 +1010,10 @@ namespace Barotrauma.Networking
public void WriteMonsterEnabled(IWriteMessage msg, Dictionary<Identifier, bool> monsterEnabled = null)
{
//monster spawn settings
if (monsterEnabled == null) { monsterEnabled = MonsterEnabled; }
List<Identifier> monsterNames = monsterEnabled.Keys
.OrderBy(k => CharacterPrefab.Prefabs[k].UintIdentifier)
.ToList();
InitMonstersEnabled();
monsterEnabled ??= MonsterEnabled;
var monsterNames = ExtractAndSortKeys(monsterEnabled);
msg.WriteVariableUInt32((uint)monsterNames.Count);
foreach (Identifier s in monsterNames)
{
msg.Write(monsterEnabled[s]);
@@ -172,11 +172,11 @@ namespace Barotrauma
return !texName.IsNullOrEmpty() & !texName.Contains("/") && !texName.Contains("%ModDir", StringComparison.OrdinalIgnoreCase);
}
public static ContentPath GetAttributeContentPath(this XElement element, string name,
ContentPackage contentPackage)
public static ContentPath GetAttributeContentPath(this XElement element, string name, ContentPackage contentPackage)
{
if (element?.GetAttribute(name) == null) { return null; }
return ContentPath.FromRaw(contentPackage, GetAttributeString(element.GetAttribute(name), null));
var attribute = element?.GetAttribute(name);
if (attribute == null) { return null; }
return ContentPath.FromRaw(contentPackage, GetAttributeString(attribute, null));
}
public static Identifier GetAttributeIdentifier(this XElement element, string name, string defaultValue)
@@ -10,6 +10,7 @@ using System.Xml.Linq;
using Barotrauma.IO;
#if CLIENT
using Barotrauma.ClientSource.Settings;
using Barotrauma.Networking;
using Microsoft.Xna.Framework.Input;
#endif
@@ -453,8 +454,12 @@ namespace Barotrauma
bool languageChanged = currentConfig.Language != newConfig.Language;
bool audioOutputChanged = currentConfig.Audio.AudioOutputDevice != newConfig.Audio.AudioOutputDevice;
bool voiceCaptureChanged = currentConfig.Audio.VoiceCaptureDevice != newConfig.Audio.VoiceCaptureDevice;
bool textScaleChanged = Math.Abs(currentConfig.Graphics.TextScale - newConfig.Graphics.TextScale) > MathF.Pow(2.0f, -7);
currentConfig = newConfig;
#warning TODO: Implement program state updates;
#if CLIENT
if (setGraphicsMode)
@@ -462,6 +467,24 @@ namespace Barotrauma
GameMain.Instance.ApplyGraphicsSettings();
}
if (audioOutputChanged)
{
GameMain.SoundManager?.InitializeAlcDevice(currentConfig.Audio.AudioOutputDevice);
}
if (voiceCaptureChanged)
{
VoipCapture.ChangeCaptureDevice(currentConfig.Audio.VoiceCaptureDevice);
}
if (textScaleChanged)
{
foreach (var font in GUIStyle.Fonts.Values)
{
font.Prefabs.ForEach(p => p.LoadFont());
}
}
GameMain.SoundManager?.ApplySettings();
#endif
if (languageChanged) { TextManager.ClearCache(); }
@@ -72,6 +72,8 @@ namespace Barotrauma
case "targetitemcomponent":
case "targetself":
case "targetcontainer":
case "targetgrandparent":
case "targetcontaineditem":
return false;
default:
return true;
@@ -68,15 +68,21 @@ namespace Barotrauma
public bool IsTriggered { get; private set; }
public float Timer { get; private set; } = -1;
public float Timer { get; private set; }
public bool IsActive { get; private set; }
public bool IsPermanent { get; private set; }
public void Launch()
{
IsTriggered = true;
IsActive = true;
Timer = Duration;
IsPermanent = Duration <= 0;
if (!IsPermanent)
{
Timer = Duration;
}
}
public void Reset()
@@ -88,6 +94,7 @@ namespace Barotrauma
public void UpdateTimer(float deltaTime)
{
if (IsPermanent) { return; }
Timer -= deltaTime;
if (Timer < 0)
{
@@ -410,7 +417,7 @@ namespace Barotrauma
public static StatusEffect Load(ContentXElement element, string parentDebugName)
{
if (element.Attribute("delay") != null || element.Attribute("delaytype") != null)
if (element.GetAttribute("delay") != null || element.GetAttribute("delaytype") != null)
{
return new DelayedEffect(element, parentDebugName);
}
@@ -642,7 +649,7 @@ namespace Barotrauma
break;
case "affliction":
AfflictionPrefab afflictionPrefab;
if (subElement.Attribute("name") != null)
if (subElement.GetAttribute("name") != null)
{
DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - define afflictions using identifiers instead of names.");
string afflictionName = subElement.GetAttributeString("name", "");
@@ -670,7 +677,7 @@ namespace Barotrauma
break;
case "reduceaffliction":
if (subElement.Attribute("name") != null)
if (subElement.GetAttribute("name") != null)
{
DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - define afflictions using identifiers or types instead of names.");
ReduceAffliction.Add((
@@ -1243,24 +1250,27 @@ namespace Barotrauma
{
for (int i = 0; i < targets.Count; i++)
{
if (targets[i] is Character character)
var target = targets[i];
Limb targetLimb = target as Limb;
if (targetLimb == null && target is Character character)
{
foreach (Limb limb in character.AnimController.Limbs)
{
if (limb.body == sourceBody)
{
targetLimb = limb;
if (breakLimb)
{
character.TrySeverLimbJoints(limb, severLimbsProbability: 100, damage: 100, allowBeheading: true, attacker: user);
}
else
{
limb.HideAndDisable(hideLimbTimer);
}
break;
}
}
}
if (hideLimb)
{
targetLimb?.HideAndDisable(hideLimbTimer);
}
}
}
@@ -173,10 +173,13 @@ namespace Barotrauma.Steam
private InstallTaskCounter(UInt64 id) { itemId = id; }
public static bool IsInstalling(Steamworks.Ugc.Item item)
=> IsInstalling(item.Id);
public static bool IsInstalling(ulong itemId)
{
lock (mutex)
{
return installers.Any(i => i.itemId == item.Id);
return installers.Any(i => i.itemId == itemId);
}
}
@@ -193,9 +196,9 @@ namespace Barotrauma.Steam
}
}
public static async Task<InstallTaskCounter> Create(Steamworks.Ugc.Item item)
public static async Task<InstallTaskCounter> Create(ulong itemId)
{
var retVal = new InstallTaskCounter(item.Id);
var retVal = new InstallTaskCounter(itemId);
await retVal.Init();
return retVal;
}
@@ -260,19 +263,15 @@ namespace Barotrauma.Steam
public static bool IsInstalling(Steamworks.Ugc.Item item)
=> InstallTaskCounter.IsInstalling(item);
private static async Task InstallMod(ulong id)
{
var item = await GetItem(id);
if (item is null) { return; }
await InstallMod(item.Value);
}
private static async Task InstallMod(Steamworks.Ugc.Item item)
{
await Task.Yield();
using var installCounter = await InstallTaskCounter.Create(item);
using var installCounter = await InstallTaskCounter.Create(id);
var itemNullable = await GetItem(id);
if (!(itemNullable is { } item)) { return; }
await Task.Yield();
string itemTitle = item.Title.Trim();
UInt64 itemId = item.Id;
string itemDirectory = item.Directory;
@@ -8,6 +8,7 @@ using System.Collections.Immutable;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using System.Globalization;
namespace Barotrauma
{
@@ -349,6 +350,11 @@ namespace Barotrauma
description += extraDescriptionLine;
}
public static LocalizedString FormatCurrency(int amount)
{
return GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", amount));
}
public static LocalizedString GetServerMessage(string serverMessage)
{
return new ServerMsgLString(serverMessage);
@@ -2,19 +2,19 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma.IO
{
static class Validation
{
private static readonly string[] unwritableDirs = new string[] { "Content" };
private static readonly string[] unwritableExtensions = new string[]
private static readonly ImmutableArray<Identifier> unwritableDirs = new[] { "Content".ToIdentifier() }.ToImmutableArray();
private static readonly ImmutableArray<Identifier> unwritableExtensions = new[]
{
".pdb", ".com", ".scr", ".dylib", ".so", ".a", ".app", //executables and libraries (.exe and .dll handled separately in CanWrite)
".pdb", ".com", ".scr", ".dylib", ".so", ".a", ".app", //executables and libraries (.exe, .dll and .json handled separately in CanWrite)
".bat", ".sh", //shell scripts
".json" //deps.json
};
}.ToIdentifiers().ToImmutableArray();
/// <summary>
/// When set to true, the game is allowed to modify the vanilla content in debug builds. Has no effect in non-debug builds.
@@ -24,25 +24,27 @@ namespace Barotrauma.IO
public static bool CanWrite(string path, bool isDirectory)
{
path = System.IO.Path.GetFullPath(path).CleanUpPath();
string localModsDir = System.IO.Path.GetFullPath(ContentPackage.LocalModsDir).CleanUpPath();
string workshopModsDir = System.IO.Path.GetFullPath(ContentPackage.WorkshopModsDir).CleanUpPath();
if (!isDirectory)
{
string extension = System.IO.Path.GetExtension(path).Replace(" ", "");
if (unwritableExtensions.Any(e => e.Equals(extension, StringComparison.OrdinalIgnoreCase)))
Identifier extension = System.IO.Path.GetExtension(path).Replace(" ", "").ToIdentifier();
if (unwritableExtensions.Any(e => e == extension))
{
return false;
}
if (!path.StartsWith(System.IO.Path.GetFullPath("Mods/").CleanUpPath(), StringComparison.OrdinalIgnoreCase)
&& (extension.Equals(".dll", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".exe", StringComparison.OrdinalIgnoreCase)))
if (!path.StartsWith(workshopModsDir, StringComparison.OrdinalIgnoreCase)
&& !path.StartsWith(localModsDir, StringComparison.OrdinalIgnoreCase)
&& (extension == ".dll" || extension == ".exe" || extension == ".json"))
{
return false;
}
}
foreach (string unwritableDir in unwritableDirs)
foreach (var unwritableDir in unwritableDirs)
{
string dir = System.IO.Path.GetFullPath(unwritableDir).CleanUpPath();
string dir = System.IO.Path.GetFullPath(unwritableDir.Value).CleanUpPath();
if (path.StartsWith(dir, StringComparison.InvariantCultureIgnoreCase))
{