Unstable v0.19.1.0

This commit is contained in:
Juan Pablo Arce
2022-08-19 13:59:08 -03:00
parent 6b55adcdd9
commit 1219615d64
192 changed files with 3875 additions and 2648 deletions
@@ -87,11 +87,11 @@ namespace Barotrauma
if (_attackLimb != value)
{
_previousAttackLimb = _attackLimb;
_previousAttackLimb?.AttachedRope?.Snap();
if (_previousAttackLimb != null && _previousAttackLimb.attack.SnapRopeOnNewAttack) { _previousAttackLimb.AttachedRope?.Snap(); }
}
else if (_attackLimb != null && _attackLimb.attack.CoolDownTimer <= 0)
{
_attackLimb.AttachedRope?.Snap();
if (_attackLimb != null && _attackLimb.attack.SnapRopeOnNewAttack) { _attackLimb.AttachedRope?.Snap(); }
}
_attackLimb = value;
attackVector = null;
@@ -3660,7 +3660,7 @@ namespace Barotrauma
targetDir = Vector2.UnitY;
}
}
float margin = 30000;
float margin = Level.OutsideBoundsCurrentMargin;
if (pos.X < -margin)
{
// Too far left
@@ -197,6 +197,10 @@ namespace Barotrauma
return;
}
character.SelectedItem = null;
if (character.SelectedSecondaryItem != null && !character.SelectedSecondaryItem.IsLadder)
{
character.SelectedSecondaryItem = null;
}
if (Target is Entity e)
{
if (e.Removed)
@@ -1234,7 +1234,7 @@ namespace Barotrauma
{
//find the room which the limb is in
//the room where the ragdoll is in is used as the "guess", meaning that it's checked first
Hull limbHull = currentHull == null ? null : Hull.FindHull(limb.WorldPosition, currentHull);
Hull newHull = currentHull == null ? null : Hull.FindHull(limb.WorldPosition, currentHull);
bool prevInWater = limb.InWater;
limb.InWater = false;
@@ -1243,38 +1243,37 @@ namespace Barotrauma
{
limb.InWater = false;
}
else if (limbHull == null)
else if (newHull == null)
{
//limb isn't in any room -> it's in the water
limb.InWater = true;
if (limb.type == LimbType.Head) headInWater = true;
if (limb.type == LimbType.Head) { headInWater = true; }
}
else if (limbHull.WaterVolume > 0.0f && Submarine.RectContains(limbHull.Rect, limb.Position))
else if (newHull.WaterVolume > 0.0f && Submarine.RectContains(newHull.Rect, limb.Position))
{
if (limb.Position.Y < limbHull.Surface)
if (limb.Position.Y < newHull.Surface)
{
limb.InWater = true;
surfaceY = limbHull.Surface;
surfaceY = newHull.Surface;
if (limb.type == LimbType.Head)
{
headInWater = true;
}
}
//the limb has gone through the surface of the water
if (Math.Abs(limb.LinearVelocity.Y) > 5.0f && limb.InWater != prevInWater)
if (Math.Abs(limb.LinearVelocity.Y) > 5.0f && limb.InWater != prevInWater && newHull == limb.Hull)
{
Splash(limb, limbHull);
Splash(limb, newHull);
//if the Character dropped into water, create a wave
if (limb.LinearVelocity.Y < 0.0f)
{
Vector2 impulse = limb.LinearVelocity * limb.Mass;
int n = (int)((limb.Position.X - limbHull.Rect.X) / Hull.WaveWidth);
limbHull.WaveVel[n] += MathHelper.Clamp(impulse.Y, -5.0f, 5.0f);
int n = (int)((limb.Position.X - newHull.Rect.X) / Hull.WaveWidth);
newHull.WaveVel[n] += MathHelper.Clamp(impulse.Y, -5.0f, 5.0f);
}
}
}
limb.Hull = newHull;
limb.Update(deltaTime);
}
@@ -1492,12 +1491,12 @@ namespace Barotrauma
}
if (flowForce.LengthSquared() > 0.001f)
{
Collider.ApplyForce(flowForce);
{
Collider.ApplyForce(flowForce * (Collider.Mass / Mass));
foreach (Limb limb in limbs)
{
if (!limb.InWater) { continue; }
limb.body.ApplyForce(flowForce);
limb.body.ApplyForce(flowForce * (limb.Mass / Mass * limbs.Length));
}
}
}
@@ -100,6 +100,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes, description: "Should the AI try to turn around when aiming with this attack?"), Editable]
public bool Reverse { get; private set; }
[Serialize(true, IsPropertySaveable.Yes, description: "Should the rope attached to this limb snap upon choosing a new attack?"), Editable]
public bool SnapRopeOnNewAttack { get; private set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Should the AI try to steer away from the target when aiming with this attack? Best combined with PassiveAggressive behavior."), Editable]
public bool Retreat { get; private set; }
@@ -309,7 +312,7 @@ namespace Barotrauma
List<Affliction> multipliedAfflictions = new List<Affliction>();
foreach (Affliction affliction in Afflictions.Keys)
{
multipliedAfflictions.Add(affliction.CreateMultiplied(multiplier));
multipliedAfflictions.Add(affliction.CreateMultiplied(multiplier, affliction.Probability));
}
return multipliedAfflictions;
}
@@ -40,7 +40,7 @@ namespace Barotrauma
}
set
{
if (value == enabled) return;
if (value == enabled) { return; }
if (Removed)
{
@@ -63,6 +63,32 @@ namespace Barotrauma
}
}
private bool disabledByEvent;
/// <summary>
/// MonsterEvents disable monsters (which includes removing them from the character list, so they essentially "don't exist") until they're ready to spawn
/// </summary>
public bool DisabledByEvent
{
get { return disabledByEvent; }
set
{
if (value == disabledByEvent) { return; }
disabledByEvent = value;
if (disabledByEvent)
{
Enabled = false;
CharacterList.Remove(this);
if (AiTarget != null) { AITarget.List.Remove(AiTarget); }
}
else
{
if (!CharacterList.Contains(this)) { CharacterList.Add(this); }
if (AiTarget != null && !AITarget.List.Contains(AiTarget)) { AITarget.List.Add(AiTarget); }
}
}
}
public Hull PreviousHull = null;
public Hull CurrentHull = null;
@@ -1247,23 +1273,30 @@ namespace Barotrauma
if (Params.Husk && speciesName != "husk" && Prefab.VariantOf != "husk")
{
// Get the non husked name and find the ragdoll with it
var matchingAffliction = AfflictionPrefab.List
.Where(p => p is AfflictionPrefabHusk)
.Select(p => p as AfflictionPrefabHusk)
.FirstOrDefault(p => p.TargetSpecies.Any(t => t == AfflictionHusk.GetNonHuskedSpeciesName(speciesName, p)));
Identifier nonHuskedSpeciesName = Identifier.Empty;
if (matchingAffliction == null)
AfflictionPrefabHusk matchingAffliction = null;
foreach (var huskPrefab in AfflictionPrefab.Prefabs.OfType<AfflictionPrefabHusk>())
{
DebugConsole.ThrowError("Cannot find a husk infection that matches this species! Please add the speciesnames as 'targets' in the husk affliction prefab definition!");
var nonHuskedName = AfflictionHusk.GetNonHuskedSpeciesName(speciesName, huskPrefab);
if (huskPrefab.TargetSpecies.Contains(nonHuskedName))
{
var huskedSpeciesName = AfflictionHusk.GetHuskedSpeciesName(nonHuskedName, huskPrefab);
if (huskedSpeciesName.Equals(speciesName))
{
nonHuskedSpeciesName = nonHuskedName;
matchingAffliction = huskPrefab;
break;
}
}
}
if (matchingAffliction == null || nonHuskedSpeciesName.IsEmpty)
{
DebugConsole.ThrowError($"Cannot find a husk infection that matches {speciesName}! Please make sure that the speciesname is added as 'targets' in the husk affliction prefab definition!\n"
+ "Note that all the infected speciesnames and files must stick the following pattern: [nonhuskedspeciesname][huskedspeciesname]. E.g. Humanhusk, Crawlerhusk, or Humancustomhusk, or Crawlerzombie. Not \"Customhumanhusk!\" or \"Zombiecrawler\"");
// Crashes if we fail to create a ragdoll -> Let's just use some ragdoll so that the user sees the error msg.
nonHuskedSpeciesName = IsHumanoid ? CharacterPrefab.HumanSpeciesName : "crawler".ToIdentifier();
speciesName = nonHuskedSpeciesName;
}
else
{
nonHuskedSpeciesName = AfflictionHusk.GetNonHuskedSpeciesName(speciesName, matchingAffliction);
}
if (ragdollParams == null && prefab.VariantOf == null)
{
Identifier name = Params.UseHuskAppendage ? nonHuskedSpeciesName : speciesName;
@@ -1287,6 +1287,11 @@ namespace Barotrauma
if (splitTag[0] != "name") { continue; }
if (splitTag[1] != Name) { continue; }
item.ReplaceTag(tag, $"name:{newName}");
var idCard = item.GetComponent<IdCard>();
if (idCard != null)
{
idCard.OwnerName = newName;
}
break;
}
}
@@ -50,6 +50,9 @@ namespace Barotrauma
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The probability for the affliction to be applied."), Editable(minValue: 0f, maxValue: 1f)]
public float Probability { get; set; } = 1.0f;
[Serialize(true, IsPropertySaveable.Yes, description: "Explosion damage is applied per each affected limb. Should this affliction damage be divided by the count of affected limbs (1-15) or applied in full? Default: true. Only affects explosions."), Editable]
public bool DivideByLimbCount { get; set; }
public float DamagePerSecond;
public float DamagePerSecondTimer;
public float PreviousVitalityDecrease;
@@ -96,9 +99,11 @@ namespace Barotrauma
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
public Affliction CreateMultiplied(float multiplier)
public Affliction CreateMultiplied(float multiplier, float probability)
{
return Prefab.Instantiate(NonClampedStrength * multiplier, Source);
var instance = Prefab.Instantiate(NonClampedStrength * multiplier, Source);
instance.Probability = probability;
return instance;
}
public override string ToString() => Prefab == null ? "Affliction (Invalid)" : $"Affliction ({Prefab.Name})";
@@ -450,13 +450,12 @@ namespace Barotrauma
public static Identifier GetHuskedSpeciesName(Identifier speciesName, AfflictionPrefabHusk prefab)
{
return prefab.HuskedSpeciesName.Replace(AfflictionPrefabHusk.Tag, speciesName);
return new Identifier(speciesName.Value + prefab.HuskedSpeciesName.Value);
}
public static Identifier GetNonHuskedSpeciesName(Identifier huskedSpeciesName, AfflictionPrefabHusk prefab)
{
Identifier nonTag = prefab.HuskedSpeciesName.Remove(AfflictionPrefabHusk.Tag);
return huskedSpeciesName.Remove(nonTag);
return huskedSpeciesName.Remove(prefab.HuskedSpeciesName);
}
}
}
@@ -63,8 +63,10 @@ namespace Barotrauma
if (HuskedSpeciesName.IsEmpty)
{
DebugConsole.NewMessage($"No 'huskedspeciesname' defined for the husk affliction ({Identifier}) in {element}", Color.Orange);
HuskedSpeciesName = "[speciesname]husk".ToIdentifier();
HuskedSpeciesName = "husk".ToIdentifier();
}
// Remove "[speciesname]" for backward support (we don't use it anymore)
HuskedSpeciesName = HuskedSpeciesName.Remove("[speciesname]").ToIdentifier();
TargetSpecies = element.GetAttributeIdentifierArray("targets", Array.Empty<Identifier>(), trim: true);
if (TargetSpecies.Length == 0)
{
@@ -108,7 +110,6 @@ namespace Barotrauma
public readonly Identifier HuskedSpeciesName;
public readonly Identifier[] TargetSpecies;
public static readonly Identifier Tag = "[speciesname]".ToIdentifier();
public readonly bool TransferBuffs;
public readonly bool SendMessages;
@@ -404,8 +405,18 @@ namespace Barotrauma
AfflictionType = element.GetAttributeIdentifier("type", "");
TranslationIdentifier = element.GetAttributeIdentifier("translationoverride", Identifier);
Name = TextManager.Get($"AfflictionName.{TranslationIdentifier}").Fallback(element.GetAttributeString("name", ""));
Description = TextManager.Get($"AfflictionDescription.{TranslationIdentifier}").Fallback(element.GetAttributeString("description", ""));
Name = TextManager.Get($"AfflictionName.{TranslationIdentifier}");
string fallbackName = element.GetAttributeString("name", "");
if (!string.IsNullOrEmpty(fallbackName))
{
Name = Name.Fallback(fallbackName);
}
Description = TextManager.Get($"AfflictionDescription.{TranslationIdentifier}");
string fallbackDescription = element.GetAttributeString("description", "");
if (!string.IsNullOrEmpty(fallbackDescription))
{
Description = Description.Fallback(fallbackDescription);
}
IsBuff = element.GetAttributeBool("isbuff", false);
HealableInMedicalClinic = element.GetAttributeBool("healableinmedicalclinic",
@@ -909,19 +909,11 @@ namespace Barotrauma
float vitalityDecrease = affliction.GetVitalityDecrease(this);
if (limbHealth != null)
{
if (limbHealth.VitalityMultipliers.ContainsKey(affliction.Prefab.Identifier))
{
vitalityDecrease *= limbHealth.VitalityMultipliers[affliction.Prefab.Identifier];
}
if (limbHealth.VitalityTypeMultipliers.ContainsKey(affliction.Prefab.AfflictionType))
{
vitalityDecrease *= limbHealth.VitalityTypeMultipliers[affliction.Prefab.AfflictionType];
}
vitalityDecrease *= GetVitalityMultiplier(affliction, limbHealth);
}
Vitality -= vitalityDecrease;
affliction.CalculateDamagePerSecond(vitalityDecrease);
}
#if CLIENT
if (IsUnconscious)
{
@@ -930,6 +922,33 @@ namespace Barotrauma
#endif
}
private float GetVitalityMultiplier(Affliction affliction, LimbHealth limbHealth)
{
float multiplier = 1.0f;
if (limbHealth.VitalityMultipliers.TryGetValue(affliction.Prefab.Identifier, out float vitalityMultiplier))
{
multiplier *= vitalityMultiplier;
}
if (limbHealth.VitalityTypeMultipliers.TryGetValue(affliction.Prefab.AfflictionType, out float vitalityTypeMultiplier))
{
multiplier *= vitalityTypeMultiplier;
}
return multiplier;
}
/// <summary>
/// How much vitality the affliction reduces, taking into account the effects of vitality modifiers on the limb the affliction is on (if limb-based)
/// </summary>
private float GetVitalityDecreaseWithVitalityMultipliers(Affliction affliction)
{
float vitalityDecrease = affliction.GetVitalityDecrease(this);
if (afflictions.TryGetValue(affliction, out LimbHealth limbHealth) && limbHealth != null)
{
vitalityDecrease *= GetVitalityMultiplier(affliction, limbHealth);
}
return vitalityDecrease;
}
private void Kill()
{
if (Unkillable || Character.GodMode) { return; }
@@ -218,6 +218,8 @@ namespace Barotrauma
public Vector2 StepOffset => ConvertUnits.ToSimUnits(Params.StepOffset) * ragdoll.RagdollParams.JointScale;
public Hull Hull;
public bool InWater { get; set; }
private FixedMouseJoint pullJoint;
@@ -720,11 +722,12 @@ namespace Barotrauma
tempModifiers.Clear();
var newAffliction = affliction;
float random = Rand.Value(Rand.RandSync.Unsynced);
if (random > affliction.Probability) { continue; }
bool foundMatchingModifier = false;
bool applyAffliction = true;
foreach (DamageModifier damageModifier in DamageModifiers)
{
if (!damageModifier.MatchesAffliction(affliction)) { continue; }
foundMatchingModifier = true;
if (random > affliction.Probability * damageModifier.ProbabilityMultiplier)
{
applyAffliction = false;
@@ -740,6 +743,7 @@ namespace Barotrauma
foreach (DamageModifier damageModifier in wearable.WearableComponent.DamageModifiers)
{
if (!damageModifier.MatchesAffliction(affliction)) { continue; }
foundMatchingModifier = true;
if (random > affliction.Probability * damageModifier.ProbabilityMultiplier)
{
applyAffliction = false;
@@ -751,6 +755,7 @@ namespace Barotrauma
}
}
}
if (!foundMatchingModifier && random > affliction.Probability) { continue; }
float finalDamageModifier = damageMultiplier;
foreach (DamageModifier damageModifier in tempModifiers)
{
@@ -763,7 +768,7 @@ namespace Barotrauma
}
if (!MathUtils.NearlyEqual(finalDamageModifier, 1.0f))
{
newAffliction = affliction.CreateMultiplied(finalDamageModifier);
newAffliction = affliction.CreateMultiplied(finalDamageModifier, affliction.Probability);
}
else
{
@@ -1,5 +1,3 @@
using Barotrauma;
namespace Barotrauma
{
@@ -7,7 +7,6 @@ using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Linq;
using Barotrauma.IO;
using Barotrauma.Steam;
@@ -430,7 +430,7 @@ namespace Barotrauma
if (GameMain.NetworkMember == null || args.Length == 0) return;
int.TryParse(args[0], out int id);
var client = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == id);
var client = GameMain.NetworkMember.ConnectedClients.Find(c => c.SessionId == id);
if (client == null)
{
ThrowError("Client id \"" + id + "\" not found.");
@@ -466,7 +466,7 @@ namespace Barotrauma
banDuration = parsedBanDuration;
}
GameMain.NetworkMember.BanPlayer(clientName, reason, false, banDuration);
GameMain.NetworkMember.BanPlayer(clientName, reason, banDuration);
});
});
},
@@ -485,7 +485,7 @@ namespace Barotrauma
if (GameMain.NetworkMember == null || args.Length == 0) return;
int.TryParse(args[0], out int id);
var client = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == id);
var client = GameMain.NetworkMember.ConnectedClients.Find(c => c.SessionId == id);
if (client == null)
{
ThrowError("Client id \"" + id + "\" not found.");
@@ -509,12 +509,12 @@ namespace Barotrauma
banDuration = parsedBanDuration;
}
GameMain.NetworkMember.BanPlayer(client.Name, reason, false, banDuration);
GameMain.NetworkMember.BanPlayer(client.Name, reason, banDuration);
});
});
}));
commands.Add(new Command("banendpoint|banip", "banendpoint [endpoint]: Ban the IP address/SteamID from the server.", null));
commands.Add(new Command("banaddress|banip", "banaddress [endpoint]: Ban the IP address/SteamID from the server.", null));
commands.Add(new Command("teleportcharacter|teleport", "teleport [character name]: Teleport the specified character to the position of the cursor. If the name parameter is omitted, the controlled character will be teleported.", null,
() =>
@@ -191,7 +191,6 @@ namespace Barotrauma
if (unlockPathEventPrefab != null)
{
var newEvent = unlockPathEventPrefab.CreateInstance();
newEvent.Init();
ActiveEvents.Add(newEvent);
}
else
@@ -223,6 +222,7 @@ namespace Barotrauma
PreloadContent(GetFilesToPreload());
roundDuration = 0.0f;
eventsInitialized = false;
isCrewAway = false;
crewAwayDuration = 0.0f;
crewAwayResetTimer = 0.0f;
@@ -460,7 +460,6 @@ namespace Barotrauma
var newEvent = eventPrefab.CreateInstance();
if (newEvent == null) { continue; }
newEvent.Init(eventSet);
if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; }
DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true);
if (!selectedEvents.ContainsKey(eventSet))
@@ -490,8 +489,6 @@ namespace Barotrauma
var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, rand);
var newEvent = eventPrefab.CreateInstance();
if (newEvent == null) { continue; }
newEvent.Init(eventSet);
DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true);
if (!selectedEvents.ContainsKey(eventSet))
{
selectedEvents.Add(eventSet, new List<Event>());
@@ -614,12 +611,25 @@ namespace Barotrauma
return true;
}
private bool eventsInitialized;
public void Update(float deltaTime)
{
if (!Enabled || level == null) { return; }
if (GameMain.GameSession.Campaign?.DisableEvents ?? false) { return; }
if (!eventsInitialized)
{
foreach (var eventSet in selectedEvents.Keys)
{
foreach (var ev in selectedEvents[eventSet])
{
ev.Init(eventSet);
}
}
eventsInitialized = true;
}
//clients only calculate the intensity but don't create any events
//(the intensity is used for controlling the background music)
CalculateCurrentIntensity(deltaTime);
@@ -100,7 +100,8 @@ namespace Barotrauma
if (!connectedSubs.Contains(item.Submarine)) { continue; }
if (item.GetComponent<PowerTransfer>() != null ||
item.GetComponent<PowerContainer>() != null ||
item.GetComponent<Reactor>() != null)
item.GetComponent<Reactor>() != null ||
item.GetComponent<Sonar>() != null)
{
item.InvulnerableToDamage = true;
}
@@ -125,8 +125,7 @@ namespace Barotrauma
var file = CharacterPrefab.FindBySpeciesName(SpeciesName)?.ContentFile;
if (file == null)
{
DebugConsole.ThrowError($"Failed to find config file for species \"{SpeciesName}\". Content package: \"{prefab.ConfigElement?.ContentPackage?.Name ?? "unknown"}\".");
disallowed = true;
DebugConsole.ThrowError($"Failed to find config file for species \"{SpeciesName}\"");
yield break;
}
else
@@ -147,6 +146,32 @@ namespace Barotrauma
{
DebugConsole.NewMessage("Initialized MonsterEvent (" + SpeciesName + ")", Color.White);
}
monsters = new List<Character>();
//+1 because Range returns an integer less than the max value
int amount = Rand.Range(MinAmount, MaxAmount + 1);
for (int i = 0; i < amount; i++)
{
string seed = Level.Loaded.Seed + i.ToString();
Character createdCharacter = Character.Create(SpeciesName, Vector2.Zero, seed, characterInfo: null, isRemotePlayer: false, hasAi: true, createNetworkEvent: true, throwErrorIfNotFound: false);
if (createdCharacter == null)
{
DebugConsole.AddWarning($"Error in MonsterEvent: failed to spawn the character \"{SpeciesName}\". Content package: \"{prefab.ConfigElement?.ContentPackage?.Name ?? "unknown"}\".");
disallowed = true;
continue;
}
if (GameMain.GameSession.IsCurrentLocationRadiated())
{
AfflictionPrefab radiationPrefab = AfflictionPrefab.RadiationSickness;
Affliction affliction = new Affliction(radiationPrefab, radiationPrefab.MaxStrength);
createdCharacter?.CharacterHealth.ApplyAffliction(null, affliction);
// TODO test multiplayer
createdCharacter?.Kill(CauseOfDeathType.Affliction, affliction, log: false);
}
createdCharacter.DisabledByEvent = true;
monsters.Add(createdCharacter);
}
}
private List<Level.InterestingPosition> GetAvailableSpawnPositions()
@@ -487,9 +512,6 @@ namespace Barotrauma
spawnPending = false;
//+1 because Range returns an integer less than the max value
int amount = Rand.Range(MinAmount, MaxAmount + 1);
monsters = new List<Character>();
float scatterAmount = scatter;
if (SpawnPosType.HasFlag(Level.PositionType.SidePath))
{
@@ -508,9 +530,9 @@ namespace Barotrauma
scatterAmount = 0;
}
for (int i = 0; i < amount; i++)
int i = 0;
foreach (Character monster in monsters)
{
string seed = Level.Loaded.Seed + i.ToString();
CoroutineManager.Invoke(() =>
{
//round ended before the coroutine finished
@@ -533,45 +555,33 @@ namespace Barotrauma
}
}
Character createdCharacter = Character.Create(SpeciesName, pos, seed, characterInfo: null, isRemotePlayer: false, hasAi: true, createNetworkEvent: true, throwErrorIfNotFound: false);
if (createdCharacter == null)
{
DebugConsole.AddWarning($"Error in MonsterEvent: failed to spawn the character \"{SpeciesName}\". Content package: \"{prefab.ConfigElement?.ContentPackage?.Name ?? "unknown"}\".");
disallowed = true;
return;
}
monster.Enabled = true;
monster.DisabledByEvent = false;
monster.AnimController.SetPosition(FarseerPhysics.ConvertUnits.ToSimUnits(pos));
var eventManager = GameMain.GameSession.EventManager;
if (eventManager != null)
{
if (SpawnPosType.HasFlag(Level.PositionType.MainPath) || SpawnPosType.HasFlag(Level.PositionType.SidePath))
{
eventManager.CumulativeMonsterStrengthMain += createdCharacter.Params.AI.CombatStrength;
eventManager.CumulativeMonsterStrengthMain += monster.Params.AI.CombatStrength;
eventManager.AddTimeStamp(this);
}
else if (SpawnPosType.HasFlag(Level.PositionType.Ruin))
{
eventManager.CumulativeMonsterStrengthRuins += createdCharacter.Params.AI.CombatStrength;
eventManager.CumulativeMonsterStrengthRuins += monster.Params.AI.CombatStrength;
}
else if (SpawnPosType.HasFlag(Level.PositionType.Wreck))
{
eventManager.CumulativeMonsterStrengthWrecks += createdCharacter.Params.AI.CombatStrength;
eventManager.CumulativeMonsterStrengthWrecks += monster.Params.AI.CombatStrength;
}
else if (SpawnPosType.HasFlag(Level.PositionType.Cave))
{
eventManager.CumulativeMonsterStrengthCaves += createdCharacter.Params.AI.CombatStrength;
eventManager.CumulativeMonsterStrengthCaves += monster.Params.AI.CombatStrength;
}
}
if (GameMain.GameSession.IsCurrentLocationRadiated())
{
AfflictionPrefab radiationPrefab = AfflictionPrefab.RadiationSickness;
Affliction affliction = new Affliction(radiationPrefab, radiationPrefab.MaxStrength);
createdCharacter?.CharacterHealth.ApplyAffliction(null, affliction);
// TODO test multiplayer
createdCharacter?.Kill(CauseOfDeathType.Affliction, affliction, log: false);
}
monsters.Add(createdCharacter);
if (monsters.Count == amount)
if (monster == monsters.Last())
{
spawnReady = true;
//this will do nothing if the monsters have no swarm behavior defined,
@@ -587,6 +597,7 @@ namespace Barotrauma
value: Timing.TotalTime - GameMain.GameSession.RoundStartTime);
}
}, delayBetweenSpawns * i);
i++;
}
}
@@ -3,7 +3,7 @@ using System;
namespace Barotrauma
{
public static class StringExtensions
static class StringExtensions
{
public static string FallbackNullOrEmpty(this string s, string fallback) => string.IsNullOrEmpty(s) ? fallback : s;
@@ -467,11 +467,11 @@ namespace Barotrauma
SetConsent(Consent.Error);
return;
}
loadedImplementation?.SetEnabledInfoLog(true);
loadedImplementation?.SetEnabledVerboseLog(true);
#if DEBUG
try
{
loadedImplementation?.SetEnabledInfoLog(true);
loadedImplementation?.SetEnabledVerboseLog(true);
}
catch (Exception e)
{
@@ -133,6 +133,8 @@ namespace Barotrauma
protected set;
}
public bool PurchasedLostShuttlesInLatestSave, PurchasedHullRepairsInLatestSave, PurchasedItemRepairsInLatestSave;
public virtual bool PurchasedHullRepairs { get; set; }
public virtual bool PurchasedLostShuttles { get; set; }
public virtual bool PurchasedItemRepairs { get; set; }
@@ -228,7 +230,8 @@ namespace Barotrauma
#if CLIENT
prevCampaignUIAutoOpenType = TransitionType.None;
#endif
if (PurchasedHullRepairs)
if (PurchasedHullRepairsInLatestSave)
{
foreach (Structure wall in Structure.WallList)
{
@@ -241,9 +244,9 @@ namespace Barotrauma
}
}
}
PurchasedHullRepairs = false;
PurchasedHullRepairsInLatestSave = PurchasedHullRepairs = false;
}
if (PurchasedItemRepairs)
if (PurchasedItemRepairsInLatestSave)
{
foreach (Item item in Item.ItemList)
{
@@ -256,9 +259,9 @@ namespace Barotrauma
}
}
}
PurchasedItemRepairs = false;
PurchasedItemRepairsInLatestSave = PurchasedItemRepairs = false;
}
PurchasedLostShuttles = false;
PurchasedLostShuttlesInLatestSave = PurchasedLostShuttles = false;
var connectedSubs = Submarine.MainSub.GetConnectedSubs();
wasDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost);
}
@@ -725,7 +728,7 @@ namespace Barotrauma
}
foreach (LocationConnection connection in Map.Connections)
{
connection.Difficulty = connection.Biome.MaxDifficulty;
connection.Difficulty = connection.Biome.AdjustedMaxDifficulty;
connection.LevelData = new LevelData(connection)
{
IsBeaconActive = false
@@ -734,7 +737,7 @@ namespace Barotrauma
}
foreach (Location location in Map.Locations)
{
location.LevelData = new LevelData(location, location.Biome.MaxDifficulty);
location.LevelData = new LevelData(location, location.Biome.AdjustedMaxDifficulty);
location.Reset();
}
Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation));
@@ -1,27 +1,16 @@
using System.Xml.Linq;
using Barotrauma.Networking;
namespace Barotrauma
{
partial class CharacterCampaignData
{
public CharacterInfo CharacterInfo
{
get;
private set;
}
public readonly CharacterInfo CharacterInfo;
public readonly string Name;
public string ClientEndPoint
{
get;
private set;
}
public ulong SteamID
{
get;
private set;
}
public readonly Address ClientAddress;
public readonly Option<AccountId> AccountId;
private XElement itemData;
private XElement healthData;
@@ -151,9 +151,9 @@ namespace Barotrauma
/// </summary>
private void Load(XElement element)
{
PurchasedLostShuttles = element.GetAttributeBool("purchasedlostshuttles", false);
PurchasedHullRepairs = element.GetAttributeBool("purchasedhullrepairs", false);
PurchasedItemRepairs = element.GetAttributeBool("purchaseditemrepairs", false);
PurchasedLostShuttlesInLatestSave = element.GetAttributeBool("purchasedlostshuttles", false);
PurchasedHullRepairsInLatestSave = element.GetAttributeBool("purchasedhullrepairs", false);
PurchasedItemRepairsInLatestSave = element.GetAttributeBool("purchaseditemrepairs", false);
CheatsEnabled = element.GetAttributeBool("cheatsenabled", false);
if (CheatsEnabled)
{
@@ -203,7 +203,7 @@ namespace Barotrauma.Items.Components
foreach ((Character character, Node node) in charactersInRange)
{
if (character == null || character.Removed) { continue; }
character.ApplyAttack(null, node.WorldPosition, attack, 1.0f);
character.ApplyAttack(null, node.WorldPosition, attack, MathHelper.Clamp(Voltage, 1.0f, MaxOverVoltageFactor));
}
}
DischargeProjSpecific();
@@ -295,12 +295,11 @@ namespace Barotrauma.Items.Components
if (attachable)
{
DeattachFromWall();
if (body != null)
{
item.body = body;
}
DeattachFromWall();
}
if (Pusher != null) { Pusher.Enabled = false; }
@@ -619,6 +618,10 @@ namespace Barotrauma.Items.Components
#if CLIENT
item.DrawDepthOffset = SpriteDepthWhenDropped - item.SpriteDepth;
#endif
foreach (LightComponent light in item.GetComponents<LightComponent>())
{
light.CheckIfNeedsUpdate();
}
}
public override void ParseMsg()
@@ -165,31 +165,32 @@ namespace Barotrauma.Items.Components
pickTimer = 0.0f;
while (pickTimer < requiredTime && Screen.Selected != GameMain.SubEditorScreen)
{
//cancel if the item is currently selected
//attempting to pick does not select the item, so if it is selected at this point, another ItemComponent
//must have been selected and we should not keep deattaching (happens when for example interacting with
//an electrical component while holding both a screwdriver and a wrench).
if (picker.IsAnySelectedItem(item)||
picker.IsKeyDown(InputType.Aim) ||
!picker.CanInteractWith(item) ||
item.Removed || item.ParentInventory != null)
if (!CoroutineManager.Paused)
{
StopPicking(picker);
yield return CoroutineStatus.Success;
}
//cancel if the item is currently selected
//attempting to pick does not select the item, so if it is selected at this point, another ItemComponent
//must have been selected and we should not keep deattaching (happens when for example interacting with
//an electrical component while holding both a screwdriver and a wrench).
if (picker.IsAnySelectedItem(item) ||
picker.IsKeyDown(InputType.Aim) ||
!picker.CanInteractWith(item) ||
item.Removed || item.ParentInventory != null)
{
StopPicking(picker);
yield return CoroutineStatus.Success;
}
#if CLIENT
Character.Controlled?.UpdateHUDProgressBar(
this,
item.WorldPosition,
pickTimer / requiredTime,
GUIStyle.Red, GUIStyle.Green,
!string.IsNullOrWhiteSpace(PickingMsg) ? PickingMsg : this is Door ? "progressbar.opening" : "progressbar.deattaching");
Character.Controlled?.UpdateHUDProgressBar(
this,
item.WorldPosition,
pickTimer / requiredTime,
GUIStyle.Red, GUIStyle.Green,
!string.IsNullOrWhiteSpace(PickingMsg) ? PickingMsg : this is Door ? "progressbar.opening" : "progressbar.deattaching");
#endif
picker.AnimController.UpdateUseItem(!picker.IsClimbing, item.WorldPosition + new Vector2(0.0f, 100.0f) * ((pickTimer / 10.0f) % 0.1f));
pickTimer += CoroutineManager.DeltaTime;
picker.AnimController.UpdateUseItem(!picker.IsClimbing, item.WorldPosition + new Vector2(0.0f, 100.0f) * ((pickTimer / 10.0f) % 0.1f));
pickTimer += CoroutineManager.DeltaTime;
}
yield return CoroutineStatus.Running;
}
@@ -96,7 +96,7 @@ namespace Barotrauma.Items.Components
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
progressTimer += deltaTime * Math.Min(powerConsumption <= 0.0f ? 1 : Voltage, 1.0f);
progressTimer += deltaTime * Math.Min(powerConsumption <= 0.0f ? 1 : Voltage, MaxOverVoltageFactor);
float tinkeringStrength = 0f;
if (repairable.IsTinkering)
@@ -205,18 +205,18 @@ namespace Barotrauma.Items.Components
foreach (DeconstructItem deconstructProduct in products)
{
CreateDeconstructProduct(deconstructProduct, inputItems, amountMultiplier);
CreateDeconstructProduct(deconstructProduct, inputItems, (int)(amountMultiplier * deconstructProduct.Amount));
}
}
else
{
foreach (DeconstructItem deconstructProduct in validDeconstructItems)
{
CreateDeconstructProduct(deconstructProduct, inputItems, amountMultiplier);
CreateDeconstructProduct(deconstructProduct, inputItems, (int)(amountMultiplier * deconstructProduct.Amount));
}
}
void CreateDeconstructProduct(DeconstructItem deconstructProduct, IEnumerable<Item> inputItems, float amountMultiplier)
void CreateDeconstructProduct(DeconstructItem deconstructProduct, IEnumerable<Item> inputItems, int amount)
{
float percentageHealth = targetItem.Condition / targetItem.MaxCondition;
@@ -284,7 +284,6 @@ namespace Barotrauma.Items.Components
user.CheckTalents(AbilityEffectType.OnItemDeconstructedInventory, itemDeconstructedInventory);
}
int amount = (int)amountMultiplier;
for (int i = 0; i < amount; i++)
{
Entity.Spawner.AddItemToSpawnQueue(itemPrefab, outputContainer.Inventory, condition, onSpawned: (Item spawnedItem) =>
@@ -117,7 +117,7 @@ namespace Barotrauma.Items.Components
Force = MathHelper.Lerp(force, (Voltage < MinVoltage) ? 0.0f : targetForce, deltaTime * 10.0f);
if (Math.Abs(Force) > 1.0f)
{
float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage, 1.0f);
float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage, MaxOverVoltageFactor);
float currForce = force * voltageFactor;
float condition = item.Condition / item.MaxCondition;
// Broken engine makes more noise.
@@ -305,7 +305,7 @@ namespace Barotrauma.Items.Components
float fabricationSpeedIncrease = 1f + tinkeringStrength * TinkeringSpeedIncrease;
timeUntilReady -= deltaTime * fabricationSpeedIncrease * Math.Min(powerConsumption <= 0 ? 1 : Voltage, 1.0f);
timeUntilReady -= deltaTime * fabricationSpeedIncrease * Math.Min(powerConsumption <= 0 ? 1 : Voltage, MaxOverVoltageFactor);
UpdateRequiredTimeProjSpecific();
@@ -371,8 +371,7 @@ namespace Barotrauma.Items.Components
var availableItems = availableIngredients[requiredPrefab.Identifier];
var availableItem = availableItems.FirstOrDefault(potentialPrefab =>
{
return potentialPrefab.ConditionPercentage >= requiredItem.MinCondition * 100.0f &&
potentialPrefab.ConditionPercentage <= requiredItem.MaxCondition * 100.0f;
return requiredItem.IsConditionSuitable(potentialPrefab.ConditionPercentage);
});
if (availableItem == null) { continue; }
@@ -616,8 +615,7 @@ namespace Barotrauma.Items.Components
var availablePrefabs = availableIngredients[requiredPrefab.Identifier];
foreach (Item availablePrefab in availablePrefabs)
{
if (availablePrefab.ConditionPercentage / 100.0f >= requiredItem.MinCondition &&
availablePrefab.ConditionPercentage / 100.0f <= requiredItem.MaxCondition)
if (requiredItem.IsConditionSuitable(availablePrefab.ConditionPercentage))
{
availablePrefabsAmount++;
}
@@ -732,9 +730,7 @@ namespace Barotrauma.Items.Components
var availablePrefabs = availableIngredients[requiredPrefab.Identifier];
var availablePrefab = availablePrefabs.FirstOrDefault(potentialPrefab =>
{
return !usedItems.Contains(potentialPrefab) &&
potentialPrefab.ConditionPercentage >= requiredItem.MinCondition * 100.0f &&
potentialPrefab.ConditionPercentage <= requiredItem.MaxCondition * 100.0f;
return !usedItems.Contains(potentialPrefab) && requiredItem.IsConditionSuitable(potentialPrefab.ConditionPercentage);
});
if (availablePrefab == null) { continue; }
@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
@@ -52,7 +51,7 @@ namespace Barotrauma.Items.Components
return;
}
CurrFlow = Math.Min(PowerConsumption > 0 ? Voltage : 1.0f, 1.0f) * generatedAmount * 100.0f;
CurrFlow = Math.Min(PowerConsumption > 0 ? Voltage : 1.0f, MaxOverVoltageFactor) * generatedAmount * 100.0f;
float conditionMult = item.Condition / item.MaxCondition;
//100% condition = 100% oxygen
//50% condition = 25% oxygen
@@ -130,7 +130,7 @@ namespace Barotrauma.Items.Components
if (item.CurrentHull == null) { return; }
float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, 1.0f);
float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, MaxOverVoltageFactor);
currFlow = flowPercentage / 100.0f * maxFlow * powerFactor;
@@ -11,6 +11,8 @@ namespace Barotrauma.Items.Components
{
const float NetworkUpdateIntervalHigh = 0.5f;
const float TemperatureBoostAmount = 20;
//the rate at which the reactor is being run on (higher rate -> higher temperature)
private float fissionRate;
@@ -46,6 +48,8 @@ namespace Barotrauma.Items.Components
private Vector2 optimalFissionRate, allowedFissionRate;
private Vector2 optimalTurbineOutput, allowedTurbineOutput;
private float temperatureBoost;
private bool _powerOn;
[Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes)]
@@ -241,6 +245,34 @@ namespace Barotrauma.Items.Components
}
#endif
if (signalControlledTargetFissionRate.HasValue && Math.Abs(signalControlledTargetFissionRate.Value - TargetFissionRate) > 1.0f)
{
TargetFissionRate = adjustValueWithoutOverShooting(TargetFissionRate, signalControlledTargetFissionRate.Value, deltaTime * 5.0f);
#if CLIENT
FissionRateScrollBar.BarScroll = TargetFissionRate / 100.0f;
#endif
}
else
{
signalControlledTargetFissionRate = null;
}
if (signalControlledTargetTurbineOutput.HasValue && Math.Abs(signalControlledTargetTurbineOutput.Value - TargetTurbineOutput) > 1.0f)
{
TargetTurbineOutput = adjustValueWithoutOverShooting(TargetTurbineOutput, signalControlledTargetTurbineOutput.Value, deltaTime * 5.0f);
#if CLIENT
TurbineOutputScrollBar.BarScroll = TargetTurbineOutput / 100.0f;
#endif
}
else
{
signalControlledTargetTurbineOutput = null;
}
static float adjustValueWithoutOverShooting(float current, float target, float speed)
{
return target < current ? Math.Max(target, current - speed) : Math.Min(target, current + speed);
}
prevAvailableFuel = AvailableFuel;
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
@@ -270,7 +302,10 @@ namespace Barotrauma.Items.Components
float temperatureDiff = (heatAmount - turbineOutput) - Temperature;
Temperature += MathHelper.Clamp(Math.Sign(temperatureDiff) * 10.0f * deltaTime, -Math.Abs(temperatureDiff), Math.Abs(temperatureDiff));
//if (item.InWater && AvailableFuel < 100.0f) Temperature -= 12.0f * deltaTime;
temperatureBoost = adjustValueWithoutOverShooting(temperatureBoost, 0.0f, deltaTime);
#if CLIENT
temperatureBoostUpButton.Enabled = temperatureBoostDownButton.Enabled = Math.Abs(temperatureBoost) < TemperatureBoostAmount * 0.9f;
#endif
FissionRate = MathHelper.Lerp(fissionRate, Math.Min(TargetFissionRate, AvailableFuel), deltaTime);
@@ -438,7 +473,7 @@ namespace Barotrauma.Items.Components
private float GetGeneratedHeat(float fissionRate)
{
return fissionRate * (prevAvailableFuel / 100.0f) * 2.0f;
return fissionRate * (prevAvailableFuel / 100.0f) * 2.0f + temperatureBoost;
}
/// <summary>
@@ -486,13 +521,15 @@ namespace Barotrauma.Items.Components
if (temperature > allowedTemperature.Y)
{
item.SendSignal("1", "meltdown_warning");
//faster meltdown if the item is in a bad condition
meltDownTimer += MathHelper.Lerp(deltaTime * 2.0f, deltaTime, item.Condition / item.MaxCondition);
if (meltDownTimer > MeltdownDelay)
if (!item.InvulnerableToDamage)
{
MeltDown();
return;
//faster meltdown if the item is in a bad condition
meltDownTimer += MathHelper.Lerp(deltaTime * 2.0f, deltaTime, item.Condition / item.MaxCondition);
if (meltDownTimer > MeltdownDelay)
{
MeltDown();
return;
}
}
}
else
@@ -797,30 +834,31 @@ namespace Barotrauma.Items.Components
AutoTemp = false;
TargetFissionRate = 0.0f;
TargetTurbineOutput = 0.0f;
if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; }
registerUnsentChanges();
}
break;
case "set_fissionrate":
if (PowerOn && float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float newFissionRate))
{
TargetFissionRate = MathHelper.Clamp(newFissionRate, 0.0f, 100.0f);
if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; }
#if CLIENT
FissionRateScrollBar.BarScroll = TargetFissionRate / 100.0f;
#endif
signalControlledTargetFissionRate = MathHelper.Clamp(newFissionRate, 0.0f, 100.0f);
registerUnsentChanges();
}
break;
case "set_turbineoutput":
if (PowerOn && float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float newTurbineOutput))
{
TargetTurbineOutput = MathHelper.Clamp(newTurbineOutput, 0.0f, 100.0f);
if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; }
#if CLIENT
TurbineOutputScrollBar.BarScroll = TargetTurbineOutput / 100.0f;
#endif
signalControlledTargetTurbineOutput = MathHelper.Clamp(newTurbineOutput, 0.0f, 100.0f);
registerUnsentChanges();
}
break;
}
void registerUnsentChanges()
{
if (GameMain.NetworkMember is { IsServer: true }) { unsentChanges = true; }
}
}
private float? signalControlledTargetFissionRate, signalControlledTargetTurbineOutput;
}
}
@@ -98,7 +98,7 @@ namespace Barotrauma.Items.Components
set { maxRechargeSpeed = Math.Max(value, 1.0f); }
}
[Editable, Serialize(10.0f, IsPropertySaveable.Yes, description: "The current recharge speed of the device.")]
[Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "The current recharge speed of the device.")]
public float RechargeSpeed
{
get { return rechargeSpeed; }
@@ -243,8 +243,13 @@ namespace Barotrauma.Items.Components
//damage the item if voltage is too high (except if running as a client)
float prevCondition = item.Condition;
item.Condition -= deltaTime * 10.0f;
//some randomness to prevent all junction boxes from breaking at the same time
if (Rand.Range(0.0f, 1.0f) < 0.01f)
{
//damaged boxes are more sensitive to overvoltage (also preventing all boxes from breaking at the same time)
float conditionFactor = MathHelper.Lerp(5.0f, 1.0f, item.Condition / item.MaxCondition);
item.Condition -= deltaTime * Rand.Range(10.0f, 500.0f) * conditionFactor;
}
if (item.Condition <= 0.0f && prevCondition > 0.0f)
{
overloadCooldownTimer = OverloadCooldown;
@@ -94,6 +94,11 @@ namespace Barotrauma.Items.Components
protected Connection powerIn, powerOut;
/// <summary>
/// Maximum voltage factor when the device is being overvolted. I.e. how many times more effectively the device can function when it's being overvolted
/// </summary>
protected const float MaxOverVoltageFactor = 2.0f;
protected virtual PowerPriority Priority { get { return PowerPriority.Default; } }
[Editable, Serialize(0.5f, IsPropertySaveable.Yes, description: "The minimum voltage required for the device to function. " +
@@ -244,6 +244,13 @@ namespace Barotrauma.Items.Components
private set;
}
[Serialize(false, IsPropertySaveable.No)]
public bool DamageDoors
{
get;
set;
}
public bool IsStuckToTarget => StickTarget != null;
private Category originalCollisionCategories;
@@ -288,7 +295,7 @@ namespace Barotrauma.Items.Components
}
}
private void Launch(Character user, Vector2 simPosition, float rotation, float damageMultiplier = 1f)
private void Launch(Character user, Vector2 simPosition, float rotation, float damageMultiplier = 1f, float launchImpulseModifier = 0f)
{
Item.body.ResetDynamics();
Item.SetTransform(simPosition, rotation);
@@ -299,7 +306,7 @@ namespace Barotrauma.Items.Components
// Set user for hitscan projectiles to work properly.
User = user;
// Need to set null for non-characterusable items.
Use(character: null);
Use(character: null, launchImpulseModifier);
// Set user for normal projectiles to work properly.
User = user;
if (Item.Removed) { return; }
@@ -312,7 +319,7 @@ namespace Barotrauma.Items.Components
}
}
public void Shoot(Character user, Vector2 weaponPos, Vector2 spawnPos, float rotation, List<Body> ignoredBodies, bool createNetworkEvent, float damageMultiplier = 1f)
public void Shoot(Character user, Vector2 weaponPos, Vector2 spawnPos, float rotation, List<Body> ignoredBodies, bool createNetworkEvent, float damageMultiplier = 1f, float launchImpulseModifier = 0f)
{
//add the limbs of the shooter to the list of bodies to be ignored
//so that the player can't shoot himself
@@ -320,7 +327,7 @@ namespace Barotrauma.Items.Components
Vector2 projectilePos = weaponPos;
//make sure there's no obstacles between the base of the weapon (or the shoulder of the character) and the end of the barrel
if (Submarine.PickBody(weaponPos, spawnPos, IgnoredBodies, Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking,
customPredicate: (Fixture f) => { return !IgnoredBodies.Contains(f.Body); }) == null)
customPredicate: (Fixture f) => { return IgnoredBodies == null || !IgnoredBodies.Contains(f.Body); }) == null)
{
//no obstacles -> we can spawn the projectile at the barrel
projectilePos = spawnPos;
@@ -334,7 +341,7 @@ namespace Barotrauma.Items.Components
projectilePos = newPos;
}
}
Launch(user, projectilePos, rotation, damageMultiplier);
Launch(user, projectilePos, rotation, damageMultiplier, launchImpulseModifier);
if (createNetworkEvent && !Item.Removed && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
#if SERVER
@@ -344,7 +351,7 @@ namespace Barotrauma.Items.Components
}
}
public bool Use(Character character = null)
public bool Use(Character character = null, float launchImpulseModifier = 0f)
{
if (character != null && !characterUsable) { return false; }
@@ -379,7 +386,7 @@ namespace Barotrauma.Items.Components
else
{
item.body.SetTransform(item.body.SimPosition, launchAngle);
float modifiedLaunchImpulse = LaunchImpulse * (1 + Rand.Range(-ImpulseSpread, ImpulseSpread));
float modifiedLaunchImpulse = (LaunchImpulse + launchImpulseModifier) * (1 + Rand.Range(-ImpulseSpread, ImpulseSpread));
DoLaunch(launchDir * modifiedLaunchImpulse * item.body.Mass);
System.Diagnostics.Debug.WriteLine("launch: " + modifiedLaunchImpulse + " - " + item.body.LinearVelocity);
}
@@ -723,7 +730,7 @@ namespace Barotrauma.Items.Components
private bool OnProjectileCollision(Fixture f1, Fixture target, Contact contact)
{
if (User != null && User.Removed) { User = null; return false; }
if (IgnoredBodies.Contains(target.Body)) { return false; }
if (IgnoredBodies != null && IgnoredBodies.Contains(target.Body)) { return false; }
//ignore character colliders (the projectile only hits limbs)
if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character)
{
@@ -828,7 +835,7 @@ namespace Barotrauma.Items.Components
private bool HandleProjectileCollision(Fixture target, Vector2 collisionNormal, Vector2 velocity)
{
if (User != null && User.Removed) { User = null; }
if (IgnoredBodies.Contains(target.Body)) { return false; }
if (IgnoredBodies != null && IgnoredBodies.Contains(target.Body)) { return false; }
//ignore character colliders (the projectile only hits limbs)
if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character)
{
@@ -870,7 +877,7 @@ namespace Barotrauma.Items.Components
else if ((target.Body.UserData as Item ?? (target.Body.UserData as ItemComponent)?.Item) is Item targetItem)
{
if (targetItem.Removed) { return false; }
if (Attack != null && targetItem.Prefab.DamagedByProjectiles && targetItem.Condition > 0)
if (Attack != null && (targetItem.Prefab.DamagedByProjectiles || DamageDoors && targetItem.GetComponent<Door>() != null) && targetItem.Condition > 0)
{
attackResult = Attack.DoDamage(User ?? Attacker, targetItem, item.WorldPosition, 1.0f);
#if CLIENT
@@ -1067,7 +1074,7 @@ namespace Barotrauma.Items.Components
item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel;
}
}
IgnoredBodies.Clear();
IgnoredBodies?.Clear();
}
private void StickToTarget(Body targetBody, Vector2 axis)
@@ -23,6 +23,9 @@ namespace Barotrauma.Items.Components
private readonly HashSet<Wire> wires;
public IReadOnlyCollection<Wire> Wires => wires;
private bool enumeratingWires;
private readonly HashSet<Wire> removedWires = new HashSet<Wire>();
private readonly Item item;
public readonly bool IsOutput;
@@ -239,7 +242,14 @@ namespace Barotrauma.Items.Components
}
prevOtherConnection.recipientsDirty = true;
}
wires.Remove(wire);
if (enumeratingWires)
{
removedWires.Add(wire);
}
else
{
wires.Remove(wire);
}
recipientsDirty = true;
}
@@ -278,6 +288,7 @@ namespace Barotrauma.Items.Components
public void SendSignal(Signal signal)
{
enumeratingWires = true;
foreach (var wire in wires)
{
Connection recipient = wire.OtherConnection(this);
@@ -301,6 +312,12 @@ namespace Barotrauma.Items.Components
}
}
}
enumeratingWires = false;
foreach (var removedWire in removedWires)
{
wires.Remove(removedWire);
}
removedWires.Clear();
}
public void ClearConnections()
@@ -313,13 +330,23 @@ namespace Barotrauma.Items.Components
Powered.ChangedConnections.Add(c);
}
}
foreach (var wire in wires)
{
wire.RemoveConnection(this);
recipientsDirty = true;
}
wires.Clear();
if (enumeratingWires)
{
foreach (var wire in wires)
{
removedWires.Add(wire);
}
}
else
{
wires.Clear();
}
}
public void InitializeFromLoaded()
@@ -247,9 +247,14 @@ namespace Barotrauma.Items.Components
}
public override void OnMapLoaded()
{
CheckIfNeedsUpdate();
}
public void CheckIfNeedsUpdate()
{
if (item.body == null && powerConsumption <= 0.0f && Parent == null && turret == null && IsOn &&
(statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) &&
(statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) &&
(IsActiveConditionals == null || IsActiveConditionals.Count == 0))
{
lightBrightness = 1.0f;
@@ -261,6 +266,10 @@ namespace Barotrauma.Items.Components
Light.ParentSub = item.Submarine;
#endif
}
else
{
IsActive = true;
}
}
public override void Update(float deltaTime, Camera cam)
@@ -21,12 +21,15 @@ namespace Barotrauma.Items.Components
private float rotation, targetRotation;
private float reload, reloadTime;
private float reload, reloadTime, delayBetweenBurst;
private int shotsPerBurst, shotCounter;
private float minRotation, maxRotation;
private float launchImpulse;
private float damageMultiplier;
private Camera cam;
private float angularVelocity;
@@ -94,6 +97,16 @@ namespace Barotrauma.Items.Components
get;
set;
}
public bool flipFiringOffset;
[Serialize(false, IsPropertySaveable.No, description: "If enabled, the firing offset will alternate from left to right (i.e. flipping the x-component of the offset each shot.)")]
public bool AlternatingFiringOffset
{
get;
set;
}
public Vector2 TransformedBarrelPos
{
get
@@ -116,6 +129,20 @@ namespace Barotrauma.Items.Components
set { reloadTime = value; }
}
[Editable(1, 100), Serialize(1, IsPropertySaveable.No, description: "How many projectiles needs to be shot before we add an extra break? Think of the double coilgun.")]
public int ShotsPerBurst
{
get { return shotsPerBurst; }
set { shotsPerBurst = value; }
}
[Editable(0.0f, 1000.0f, decimals: 3), Serialize(0.0f, IsPropertySaveable.No, description: "An extra delay between the bursts. Added to the reload.")]
public float DelayBetweenBursts
{
get { return delayBetweenBurst; }
set { delayBetweenBurst = value; }
}
[Editable(0.1f, 10f), Serialize(1.0f, IsPropertySaveable.No, description: "Modifies the duration of retraction of the barrell after recoil to get back to the original position after shooting. Reload time affects this too.")]
public float RetractionDurationMultiplier
{
@@ -137,6 +164,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(1.0f, IsPropertySaveable.No, description: "Multiplies the damage the turret deals by this amount.")]
public float DamageMultiplier
{
get { return damageMultiplier; }
set { damageMultiplier = value; }
}
[Serialize(1, IsPropertySaveable.No, description: "How many projectiles the weapon launches when fired once.")]
public int ProjectileCount
{
@@ -602,9 +636,9 @@ namespace Barotrauma.Items.Components
if (projectiles.Any())
{
ItemContainer projectileContainer = projectiles.First().Item.Container?.GetComponent<ItemContainer>();
if (projectileContainer != null && projectileContainer.Item != item)
{
projectileContainer?.Item.Use(deltaTime, null);
if (projectileContainer != null && projectileContainer.Item != item)
{
projectileContainer?.Item.Use(deltaTime, null);
}
}
else
@@ -764,6 +798,15 @@ namespace Barotrauma.Items.Components
private void Launch(Item projectile, Character user = null, float? launchRotation = null, float tinkeringStrength = 0f)
{
reload = reloadTime;
if (ShotsPerBurst > 1)
{
shotCounter++;
if (shotCounter >= ShotsPerBurst)
{
reload += DelayBetweenBursts;
shotCounter = 0;
}
}
reload /= 1f + (tinkeringStrength * TinkeringReloadDecrease);
if (user != null)
@@ -773,6 +816,10 @@ namespace Barotrauma.Items.Components
if (projectile != null)
{
if (AlternatingFiringOffset)
{
flipFiringOffset = !flipFiringOffset;
}
activeProjectiles.Add(projectile);
projectile.Drop(null, setTransform: false);
if (projectile.body != null)
@@ -796,9 +843,9 @@ namespace Barotrauma.Items.Components
projectileComponent.Attacker = projectileComponent.User = user;
if (projectileComponent.Attack != null)
{
projectileComponent.Attack.DamageMultiplier = 1f + (TinkeringDamageIncrease * tinkeringStrength);
projectileComponent.Attack.DamageMultiplier = (1f * DamageMultiplier) + (TinkeringDamageIncrease * tinkeringStrength);
}
projectileComponent.Use();
projectileComponent.Use(null, LaunchImpulse);
projectile.GetComponent<Rope>()?.Attach(item, projectile);
projectileComponent.User = user;
@@ -1452,7 +1499,9 @@ namespace Barotrauma.Items.Components
Vector2 transformedFiringOffset = Vector2.Zero;
if (useOffset)
{
transformedFiringOffset = MathUtils.RotatePoint(new Vector2(-FiringOffset.Y, -FiringOffset.X) * item.Scale, -rotation);
Vector2 currOffSet = FiringOffset;
if (flipFiringOffset) { currOffSet.X = -currOffSet.X; }
transformedFiringOffset = MathUtils.RotatePoint(new Vector2(-currOffSet.Y, -currOffSet.X) * item.Scale, -rotation);
}
return new Vector2(item.WorldRect.X + transformedBarrelPos.X + transformedFiringOffset.X, item.WorldRect.Y - transformedBarrelPos.Y + transformedFiringOffset.Y);
}
@@ -99,6 +99,7 @@ namespace Barotrauma
private bool hasComponentsToDraw;
public PhysicsBody body;
private float waterDragCoefficient;
public readonly XElement StaticBodyConfig;
@@ -860,12 +861,14 @@ namespace Barotrauma
SetActiveSprite();
ContentXElement bodyElement = null;
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "body":
float density = subElement.GetAttributeFloat("density", 10.0f);
bodyElement = subElement;
float density = subElement.GetAttributeFloat("density", Physics.NeutralDensity);
float minDensity = subElement.GetAttributeFloat("mindensity", density);
float maxDensity = subElement.GetAttributeFloat("maxdensity", density);
if (minDensity < maxDensity)
@@ -905,6 +908,7 @@ namespace Barotrauma
body = new PhysicsBody(subElement, ConvertUnits.ToSimUnits(Position), Scale, density, collisionCategory, collidesWith, findNewContacts: false);
body.FarseerBody.AngularDamping = subElement.GetAttributeFloat("angulardamping", 0.2f);
body.FarseerBody.LinearDamping = subElement.GetAttributeFloat("lineardamping", 0.1f);
body.FarseerBody.LinearDamping = subElement.GetAttributeFloat("lineardamping", 0.1f);
body.UserData = this;
break;
case "trigger":
@@ -994,6 +998,8 @@ namespace Barotrauma
if (body != null)
{
body.Submarine = submarine;
waterDragCoefficient = bodyElement.GetAttributeFloat("waterdragcoefficient",
GetComponent<Projectile>() != null || GetComponent<Throwable>() != null ? 0.1f : 1.0f);
}
//cache connections into a dictionary for faster lookups
@@ -1898,10 +1904,19 @@ namespace Barotrauma
if (needsWaterCheck)
{
bool wasInWater = inWater;
inWater = IsInWater();
bool waterProof = WaterProof;
if (inWater)
{
//the item has gone through the surface of the water
if (!wasInWater && CurrentHull != null && body != null && body.LinearVelocity.Y < -1.0f)
{
Splash();
//slow the item down (not physically accurate, but looks good enough)
body.LinearVelocity *= 0.2f;
}
Item container = this.Container;
while (!waterProof && container != null)
{
@@ -1929,7 +1944,8 @@ namespace Barotrauma
}
}
partial void Splash();
public void UpdateTransform()
{
if (body == null) { return; }
@@ -2017,23 +2033,47 @@ namespace Barotrauma
{
float floor = CurrentHull.Rect.Y - CurrentHull.Rect.Height;
float waterLevel = floor + CurrentHull.WaterVolume / CurrentHull.Rect.Width;
//forceFactor is 1.0f if the item is completely submerged,
//and goes to 0.0f as the item goes through the surface
forceFactor = Math.Min((waterLevel - Position.Y) / rect.Height, 1.0f);
if (forceFactor <= 0.0f) return;
if (forceFactor <= 0.0f) { return; }
}
bool moving = body.LinearVelocity.LengthSquared() > 0.001f;
float volume = body.Mass / body.Density;
if (moving)
{
//measure velocity from the velocity of the front of the item and apply the drag to the other end to get the drag to turn the item the "pointy end first"
var uplift = -GameMain.World.Gravity * forceFactor * volume;
//a more "proper" (but more expensive) way to do this would be to e.g. calculate the drag separately for each edge of the fixture
//but since we define the "front" as the "pointy end", we can cheat a bit by using that, and actually even make the drag appear more realistic in some cases
//(e.g. a bullet with a rectangular fixture would be just as "aerodynamic" travelling backwards, but with this method we get it to turn the correct way)
Vector2 localFront = body.GetLocalFront();
Vector2 frontVel = body.FarseerBody.GetLinearVelocityFromLocalPoint(localFront);
Vector2 drag = body.LinearVelocity * volume;
float speed = frontVel.Length();
float drag = speed * speed * waterDragCoefficient * volume * Physics.NeutralDensity;
//very small drag on active projectiles to prevent affecting their trajectories much
if (body.FarseerBody.IsBullet) { drag *= 0.1f; }
Vector2 dragVec = -frontVel / speed * drag;
body.ApplyForce((uplift - drag) * 10.0f);
//apply the force slightly towards the back of the item to make it turn the front first
Vector2 back = body.FarseerBody.GetWorldPoint(-localFront * 0.01f);
body.ApplyForce(dragVec, back);
}
//no need to apply buoyancy if the item is still and not light enough to float
if (moving || body.Density < 10.0f)
{
Vector2 buoyancy = -GameMain.World.Gravity * forceFactor * volume * Physics.NeutralDensity;
body.ApplyForce(buoyancy);
}
//apply simple angular drag
body.ApplyTorque(body.AngularVelocity * volume * -0.05f);
if (Math.Abs(body.AngularVelocity) > 0.0001f)
{
body.ApplyTorque(body.AngularVelocity * volume * -0.1f);
}
}
@@ -3270,7 +3310,7 @@ namespace Barotrauma
relativeOrigin = MathUtils.RotatePoint(relativeOrigin, -item.RotationRad);
Vector2 origin = new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2) + relativeOrigin;
item.rect.Location -= ((origin - oldOrigin) * scaleRelativeToPrefab).ToPoint();
item.rect.Location -= (origin - oldOrigin).ToPoint();
}
if (item.PurchasedNewSwap && !string.IsNullOrEmpty(appliedSwap.SwappableItem?.SpawnWithId))
@@ -14,6 +14,8 @@ namespace Barotrauma
readonly struct DeconstructItem
{
public readonly Identifier ItemIdentifier;
//number of items to output
public readonly int Amount;
//minCondition does <= check, meaning that below or equal to min condition will be skipped.
public readonly float MinCondition;
//maxCondition does > check, meaning that above this max the deconstruct item will be skipped.
@@ -37,6 +39,7 @@ namespace Barotrauma
public DeconstructItem(XElement element, Identifier parentDebugName)
{
ItemIdentifier = element.GetAttributeIdentifier("identifier", "");
Amount = element.GetAttributeInt("amount", 1);
MinCondition = element.GetAttributeFloat("mincondition", -0.1f);
MaxCondition = element.GetAttributeFloat("maxcondition", 1.0f);
OutConditionMin = element.GetAttributeFloat("outconditionmin", element.GetAttributeFloat("outcondition", 1.0f));
@@ -70,6 +73,20 @@ namespace Barotrauma
public readonly float MinCondition;
public readonly float MaxCondition;
public readonly bool UseCondition;
public bool IsConditionSuitable(float conditionPercentage)
{
float normalizedCondition = conditionPercentage / 100.0f;
if (MathUtils.NearlyEqual(normalizedCondition, MinCondition) || MathUtils.NearlyEqual(normalizedCondition, MaxCondition))
{
return true;
}
else if (normalizedCondition >= MinCondition && normalizedCondition <= MaxCondition)
{
return true;
}
return false;
}
}
public class RequiredItemByIdentifier : RequiredItem
@@ -393,12 +410,106 @@ namespace Barotrauma
private set;
}
public readonly struct CommonnessInfo
{
public float Commonness
{
get
{
return commonness;
}
}
public float AbyssCommonness
{
get
{
return abyssCommonness ?? 0.0f;
}
}
public float CaveCommonness
{
get
{
return caveCommonness ?? Commonness;
}
}
public bool CanAppear
{
get
{
if (Commonness > 0.0f) { return true; }
if (AbyssCommonness > 0.0f) { return true; }
if (CaveCommonness > 0.0f) { return true; }
return false;
}
}
public readonly float commonness;
public readonly float? abyssCommonness;
public readonly float? caveCommonness;
public CommonnessInfo(XElement element)
{
this.commonness = Math.Max(element?.GetAttributeFloat("commonness", 0.0f) ?? 0.0f, 0.0f);
float? abyssCommonness = null;
XAttribute abyssCommonnessAttribute = element?.GetAttribute("abysscommonness") ?? element?.GetAttribute("abyss");
if (abyssCommonnessAttribute != null)
{
abyssCommonness = Math.Max(abyssCommonnessAttribute.GetAttributeFloat(0.0f), 0.0f);
}
this.abyssCommonness = abyssCommonness;
float? caveCommonness = null;
XAttribute caveCommonnessAttribute = element?.GetAttribute("cavecommonness") ?? element?.GetAttribute("cave");
if (caveCommonnessAttribute != null)
{
caveCommonness = Math.Max(caveCommonnessAttribute.GetAttributeFloat(0.0f), 0.0f);
}
this.caveCommonness = caveCommonness;
}
public CommonnessInfo(float commonness, float? abyssCommonness, float? caveCommonness)
{
this.commonness = commonness;
this.abyssCommonness = abyssCommonness != null ? (float?)Math.Max(abyssCommonness.Value, 0.0f) : null;
this.caveCommonness = caveCommonness != null ? (float?)Math.Max(caveCommonness.Value, 0.0f) : null;
}
public CommonnessInfo WithInheritedCommonness(CommonnessInfo? parentInfo)
{
return new CommonnessInfo(commonness,
abyssCommonness ?? parentInfo?.abyssCommonness,
caveCommonness ?? parentInfo?.caveCommonness);
}
public CommonnessInfo WithInheritedCommonness(params CommonnessInfo?[] parentInfos)
{
CommonnessInfo info = this;
foreach (var parentInfo in parentInfos)
{
info = info.WithInheritedCommonness(parentInfo);
}
return info;
}
public float GetCommonness(Level.TunnelType tunnelType)
{
if (tunnelType == Level.TunnelType.Cave)
{
return CaveCommonness;
}
else
{
return Commonness;
}
}
}
/// <summary>
/// How likely it is for the item to spawn in a level of a given type.
/// Key = name of the LevelGenerationParameters (empty string = default value) /* TODO: empty string = default value???? */
/// Value = commonness
/// </summary>
public ImmutableDictionary<Identifier, float> LevelCommonness { get; private set; }
private ImmutableDictionary<Identifier, CommonnessInfo> LevelCommonness { get; set; }
public readonly struct FixedQuantityResourceInfo
{
@@ -747,7 +858,7 @@ namespace Barotrauma
AllowDroppingOnSwapWith = allowDroppingOnSwapWith.ToImmutableHashSet();
AllowDroppingOnSwap = allowDroppingOnSwapWith.Any();
var levelCommonness = new Dictionary<Identifier, float>();
var levelCommonness = new Dictionary<Identifier, CommonnessInfo>();
var levelQuantity = new Dictionary<Identifier, FixedQuantityResourceInfo>();
foreach (ContentXElement subElement in ConfigElement.Elements())
@@ -871,7 +982,7 @@ namespace Barotrauma
{
if (!levelCommonness.ContainsKey(levelName))
{
levelCommonness.Add(levelName, levelCommonnessElement.GetAttributeFloat("commonness", 0.0f));
levelCommonness.Add(levelName, new CommonnessInfo(levelCommonnessElement));
}
}
else
@@ -962,6 +1073,40 @@ namespace Barotrauma
this.allowedLinks = ConfigElement.GetAttributeIdentifierArray("allowedlinks", Array.Empty<Identifier>()).ToImmutableHashSet();
}
public CommonnessInfo? GetCommonnessInfo(Level level)
{
CommonnessInfo? levelCommonnessInfo = GetValueOrNull(level.GenerationParams.Identifier);
CommonnessInfo? biomeCommonnessInfo = GetValueOrNull(level.LevelData.Biome.Identifier);
CommonnessInfo? defaultCommonnessInfo = GetValueOrNull(Identifier.Empty);
if (levelCommonnessInfo.HasValue)
{
return levelCommonnessInfo?.WithInheritedCommonness(biomeCommonnessInfo, defaultCommonnessInfo);
}
else if (biomeCommonnessInfo.HasValue)
{
return biomeCommonnessInfo?.WithInheritedCommonness(defaultCommonnessInfo);
}
else if (defaultCommonnessInfo.HasValue)
{
return defaultCommonnessInfo;
}
return null;
CommonnessInfo? GetValueOrNull(Identifier identifier)
{
if (LevelCommonness.TryGetValue(identifier, out CommonnessInfo info))
{
return info;
}
else
{
return null;
}
}
}
public float GetTreatmentSuitability(Identifier treatmentIdentifier)
{
return treatmentSuitability.TryGetValue(treatmentIdentifier, out float suitability) ? suitability : 0.0f;
@@ -33,8 +33,6 @@ namespace Barotrauma
private readonly float? flashRange;
private readonly string decal;
private readonly float decalSize;
// used to apply friendly afflictions in an area without effects displaying
private readonly bool abilityExplosion;
private readonly bool applyToSelf;
private readonly float itemRepairStrength;
@@ -70,6 +68,7 @@ namespace Barotrauma
applyToSelf = element.GetAttributeBool("applytoself", true);
//the "abilityexplosion" field is kept for backwards compatibility (basically the opposite of "showeffects")
bool showEffects = !element.GetAttributeBool("abilityexplosion", false) && element.GetAttributeBool("showeffects", true);
sparks = element.GetAttributeBool("sparks", showEffects);
shockwave = element.GetAttributeBool("shockwave", showEffects);
@@ -131,8 +130,15 @@ namespace Barotrauma
float displayRange = Attack.Range;
if (damageSource is Item sourceItem)
{
displayRange *= 1.0f + sourceItem.GetQualityModifier(Quality.StatType.ExplosionRadius);
Attack.DamageMultiplier *= 1.0f + sourceItem.GetQualityModifier(Quality.StatType.ExplosionDamage);
var launcher = sourceItem.GetComponent<Projectile>()?.Launcher;
displayRange *=
1.0f
+ sourceItem.GetQualityModifier(Quality.StatType.ExplosionRadius)
+ (launcher?.GetQualityModifier(Quality.StatType.ExplosionRadius) ?? 0);
Attack.DamageMultiplier *=
1.0f
+ sourceItem.GetQualityModifier(Quality.StatType.ExplosionDamage)
+ (launcher?.GetQualityModifier(Quality.StatType.ExplosionDamage) ?? 0);
Attack.SourceItem ??= sourceItem;
}
@@ -203,7 +209,7 @@ namespace Barotrauma
}
}
if (MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(Attack.Stun, 0.0f) && MathUtils.NearlyEqual(Attack.GetTotalDamage(false), 0.0f) && !abilityExplosion)
if (MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(Attack.Stun, 0.0f) && Attack.Afflictions.None())
{
return;
}
@@ -293,12 +299,13 @@ namespace Barotrauma
Dictionary<Limb, float> distFactors = new Dictionary<Limb, float>();
Dictionary<Limb, float> damages = new Dictionary<Limb, float>();
List<Affliction> modifiedAfflictions = new List<Affliction>();
foreach (Limb limb in c.AnimController.Limbs)
{
if (limb.IsSevered || limb.IgnoreCollisions || !limb.body.Enabled) { continue; }
float dist = Vector2.Distance(limb.WorldPosition, worldPosition);
//calculate distance from the "outer surface" of the physics body
//doesn't take the rotation of the limb into account, but should be accurate enough for this purpose
float limbRadius = limb.body.GetMaxExtent();
@@ -313,17 +320,27 @@ namespace Barotrauma
{
distFactor *= GetObstacleDamageMultiplier(explosionPos, worldPosition, limb.SimPosition);
}
distFactors.Add(limb, distFactor);
if (distFactor > 0)
{
distFactors.Add(limb, distFactor);
}
}
foreach (Limb limb in distFactors.Keys)
{
if (!distFactors.TryGetValue(limb, out float distFactor)) { continue; }
modifiedAfflictions.Clear();
foreach (Affliction affliction in attack.Afflictions.Keys)
{
//previously the damage would be divided by the number of limbs (the intention was to prevent characters with more limbs taking more damage from explosions)
//that didn't work well on large characters like molochs and endworms: the explosions tend to only damage one or two of their limbs, and since the characters
//have lots of limbs, they tended to only take a fraction of the damage they should
//now we just divide by 10, which keeps the damage to normal-sized characters roughly the same as before and fixes the large characters
modifiedAfflictions.Add(affliction.CreateMultiplied(distFactor / 10));
// Shouldn't go above 15, or the damage can be unexpectedly low -> doesn't break armor
// Effectively this makes large explosions more effective against large creatures (because more limbs are affected), but I don't think that's necessarily a bad thing.
float limbCountFactor = Math.Min(distFactors.Count, 15);
float dmgMultiplier = distFactor;
if (affliction.DivideByLimbCount)
{
dmgMultiplier /= limbCountFactor;
}
modifiedAfflictions.Add(affliction.CreateMultiplied(dmgMultiplier, affliction.Probability));
}
c.LastDamageSource = damageSource;
if (attacker == null)
@@ -368,7 +385,7 @@ namespace Barotrauma
Vector2 limbDiff = Vector2.Normalize(limb.WorldPosition - worldPosition);
if (!MathUtils.IsValid(limbDiff)) { limbDiff = Rand.Vector(1.0f); }
Vector2 impulse = limbDiff * distFactor * force;
Vector2 impulsePoint = limb.SimPosition - limbDiff * limbRadius;
Vector2 impulsePoint = limb.SimPosition - limbDiff * limb.body.GetMaxExtent();
limb.body.ApplyLinearImpulse(impulse, impulsePoint, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.2f);
}
}
@@ -12,7 +12,9 @@ namespace Barotrauma
public readonly bool IsEndBiome;
public readonly float MinDifficulty;
public readonly float MaxDifficulty;
private readonly float maxDifficulty;
public float ActualMaxDifficulty => maxDifficulty;
public float AdjustedMaxDifficulty => maxDifficulty - 0.1f;
public readonly ImmutableHashSet<int> AllowedZones;
@@ -31,7 +33,7 @@ namespace Barotrauma
IsEndBiome = element.GetAttributeBool("endbiome", false);
AllowedZones = element.GetAttributeIntArray("AllowedZones", new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }).ToImmutableHashSet();
MinDifficulty = element.GetAttributeFloat("MinDifficulty", 0);
MaxDifficulty = element.GetAttributeFloat("MaxDifficulty", 100);
maxDifficulty = element.GetAttributeFloat("MaxDifficulty", 100);
}
public static Identifier ParseIdentifier(ContentXElement element)
@@ -79,7 +79,7 @@ namespace Barotrauma
set { maxBranchCount = Math.Max(value, minBranchCount); }
}
[Serialize(50, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 1000)]
[Serialize(50, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 10000)]
public int LevelObjectAmount
{
get;
@@ -8,6 +8,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using Voronoi2;
@@ -24,6 +25,22 @@ namespace Barotrauma
//all entities are disabled after they reach this depth
public const int MaxEntityDepth = -1000000;
public const float ShaftHeight = 1000.0f;
/// <summary>
/// How far outside the boundaries of the level the water current that pushes subs towards the level starts
/// </summary>
public const float OutsideBoundsCurrentMargin = 30000.0f;
/// <summary>
/// How far outside the boundaries of the level the strength of the current starts to increase exponentially
/// </summary>
public const float OutsideBoundsCurrentMarginExponential = 150000.0f;
/// <summary>
/// How far outside the boundaries of the level the current stops submarines entirely
/// </summary>
public const float OutsideBoundsCurrentHardLimit = 200000.0f;
/// <summary>
/// The level generator won't try to adjust the width of the main path above this limit.
/// </summary>
@@ -2437,16 +2454,27 @@ namespace Barotrauma
public List<ClusterLocation> ClusterLocations { get; }
public TunnelType TunnelType { get; }
public PathPoint(string id, Vector2 position, bool shouldContainResources, TunnelType tunnelType)
private PathPoint(string id, Vector2 position, bool shouldContainResources, TunnelType tunnelType, List<Identifier> resourceTags, List<Identifier> resourceIds, List<ClusterLocation> clusterLocations)
{
Id = id;
Id = id;
Position = position;
ShouldContainResources = shouldContainResources;
ResourceTags = new List<Identifier>();
ResourceIds = new List<Identifier>();
ClusterLocations = new List<ClusterLocation>();
ResourceTags = resourceTags;
ResourceIds = resourceIds;
ClusterLocations = clusterLocations;
TunnelType = tunnelType;
}
public PathPoint(string id, Vector2 position, bool shouldContainResources, TunnelType tunnelType)
: this(id, position, shouldContainResources, tunnelType, new List<Identifier>(), new List<Identifier>(), new List<ClusterLocation>())
{
}
public PathPoint WithResources(bool containsResources)
{
return new PathPoint(Id, Position, containsResources, TunnelType, ResourceTags, ResourceIds, ClusterLocations);
}
}
public List<ClusterLocation> AbyssResources { get; } = new List<ClusterLocation>();
@@ -2485,22 +2513,26 @@ namespace Barotrauma
// Such as the exploding crystals in The Great Sea
private void GenerateItems()
{
Identifier levelName = GenerationParams.Identifier;
float minCommonness = float.MaxValue, maxCommonness = float.MinValue;
List<(ItemPrefab itemPrefab, float commonness)> levelResources = new List<(ItemPrefab itemPrefab, float commonness)>();
var levelResources = new List<(ItemPrefab itemPrefab, ItemPrefab.CommonnessInfo commonnessInfo)>();
var fixedResources = new List<(ItemPrefab itemPrefab, ItemPrefab.FixedQuantityResourceInfo resourceInfo)>();
Vector2 commonnessRange = new Vector2(float.MaxValue, float.MinValue), caveCommonnessRange = new Vector2(float.MaxValue, float.MinValue);
foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs.OrderBy(p => p.UintIdentifier))
{
if (itemPrefab.LevelCommonness.TryGetValue(levelName, out float commonness) ||
itemPrefab.LevelCommonness.TryGetValue(LevelData.Biome.Identifier, out commonness) ||
itemPrefab.LevelCommonness.TryGetValue(Identifier.Empty, out commonness))
if (itemPrefab.GetCommonnessInfo(this) is { CanAppear: true } commonnessInfo)
{
if (commonness <= 0.0f) { continue; }
if (commonness < minCommonness) { minCommonness = commonness; }
if (commonness > maxCommonness) { maxCommonness = commonness; }
levelResources.Add((itemPrefab, commonness));
if (commonnessInfo.Commonness > 0.0)
{
if (commonnessInfo.Commonness < commonnessRange.X) { commonnessRange.X = commonnessInfo.Commonness; }
if (commonnessInfo.Commonness > commonnessRange.Y) { commonnessRange.Y = commonnessInfo.Commonness; }
}
if (commonnessInfo.CaveCommonness > 0.0)
{
if (commonnessInfo.CaveCommonness < caveCommonnessRange.X) { caveCommonnessRange.X = commonnessInfo.CaveCommonness; }
if (commonnessInfo.CaveCommonness > caveCommonnessRange.Y) { caveCommonnessRange.Y = commonnessInfo.CaveCommonness; }
}
levelResources.Add((itemPrefab, commonnessInfo));
}
else if (itemPrefab.LevelQuantity.TryGetValue(levelName, out var fixedQuantityResourceInfo) ||
else if (itemPrefab.LevelQuantity.TryGetValue(GenerationParams.Identifier, out var fixedQuantityResourceInfo) ||
itemPrefab.LevelQuantity.TryGetValue(Identifier.Empty, out fixedQuantityResourceInfo))
{
fixedResources.Add((itemPrefab, fixedQuantityResourceInfo));
@@ -2533,18 +2565,18 @@ namespace Barotrauma
}
}
//place some of the least common resources in the abyss
// Abyss Resources
AbyssResources.Clear();
int abyssClusterCount = (int)MathHelper.Lerp(GenerationParams.AbyssResourceClustersMin, GenerationParams.AbyssResourceClustersMax, Difficulty / 100.0f);
var abyssResourcePrefabs = levelResources.Where(r => r.commonnessInfo.AbyssCommonness > 0.0f);
int abyssClusterCount = (int)MathHelper.Lerp(GenerationParams.AbyssResourceClustersMin, GenerationParams.AbyssResourceClustersMax, MathUtils.InverseLerp(LevelData.Biome.MinDifficulty, LevelData.Biome.AdjustedMaxDifficulty, Difficulty));
for (int i = 0; i < abyssClusterCount; i++)
{
//use inverse commonness to select the abyss resources (the rarest ones are the most common in the abyss)
var selectedPrefab = ToolBox.SelectWeightedRandom(
levelResources.Select(it => it.itemPrefab).ToList(),
levelResources.Select(it => it.commonness <= 0.0f ? 0.0f : 1.0f / it.commonness).ToList(),
abyssResourcePrefabs.Select(r => r.itemPrefab).ToList(),
abyssResourcePrefabs.Select(r => r.commonnessInfo.AbyssCommonness).ToList(),
Rand.RandSync.ServerAndClient);
var location = allValidLocations.GetRandom(l =>
{
if (l.Cell == null || l.Edge == null) { return false; }
@@ -2554,14 +2586,16 @@ namespace Barotrauma
}, randSync: Rand.RandSync.ServerAndClient);
if (location.Cell == null || location.Edge == null) { break; }
int clusterSize = Rand.Range(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y + 1, Rand.RandSync.ServerAndClient);
PlaceResources(selectedPrefab, clusterSize, location, out var abyssResources);
PlaceResources(selectedPrefab, clusterSize, location, out var placedResources, maxResourceOverlap: 0);
var abyssClusterLocation = new ClusterLocation(location.Cell, location.Edge, initializeResourceList: true);
abyssClusterLocation.Resources.AddRange(abyssResources);
abyssClusterLocation.Resources.AddRange(placedResources);
AbyssResources.Add(abyssClusterLocation);
var locationIndex = allValidLocations.FindIndex(l => l.Equals(location));
allValidLocations.RemoveAt(locationIndex);
}
}
PathPoints.Clear();
nextPathPointId = 0;
@@ -2618,17 +2652,25 @@ namespace Barotrauma
int itemCount = 0;
Identifier[] exclusiveResourceTags = new Identifier[2] { "ore".ToIdentifier(), "plant".ToIdentifier() };
var disabledPathPoints = new List<string>();
// Create first cluster for each spawn point
foreach (var pathPoint in PathPoints.Where(p => p.ShouldContainResources))
foreach (var pathPoint in PathPoints)
{
if (itemCount >= GenerationParams.ItemCount) { break; }
if (!pathPoint.ShouldContainResources) { continue; }
GenerateFirstCluster(pathPoint);
if (pathPoint.ClusterLocations.Count > 0) { continue; }
disabledPathPoints.Add(pathPoint.Id);
}
// Don't try to spawn more resource clusters for points for which the initial cluster could not be spawned
foreach (string pathPointId in disabledPathPoints)
{
if (PathPoints.FirstOrNull(p => p.Id == pathPointId) is PathPoint pathPoint)
{
PathPoints.RemoveAll(p => p.Id == pathPointId);
PathPoints.Add(pathPoint.WithResources(false));
}
}
// Don't try to spawn more resource clusters for points
// for which the initial cluster could not be spawned
PathPoints.Where(p => p.ShouldContainResources && p.ClusterLocations.Count == 0)
.ForEach(p => p.ShouldContainResources = false);
var excludedPathPointIds = new List<string>();
while (itemCount < GenerationParams.ItemCount)
@@ -2647,35 +2689,16 @@ namespace Barotrauma
GenerateAdditionalCluster(pathPoint);
}
// If none of the point set to contain resources can take more resources,
// but we still haven't reached the item count set in the generation parameters...
while (itemCount < GenerationParams.ItemCount)
{
// We need to start filling some of the path points previously set to not contain resources
Func<PathPoint, bool> availablePathPoints = p => !excludedPathPointIds.Contains(p.Id) && p.ClusterLocations.None();
if (PathPoints.None(availablePathPoints)) { break; }
var pathPoint = PathPoints.GetRandom(availablePathPoints, randSync: Rand.RandSync.ServerAndClient);
if (!GenerateFirstCluster(pathPoint))
{
excludedPathPointIds.Add(pathPoint.Id);
continue;
}
while (pathPoint.NextClusterProbability > 0)
{
if (!GenerateAdditionalCluster(pathPoint)) { break; }
}
pathPoint.ShouldContainResources = pathPoint.ClusterLocations.Any();
}
#if DEBUG
DebugConsole.NewMessage("Level resources spawned: " + itemCount + "\n" +
" Spawn points containing resources: " + PathPoints.Where(p => p.ClusterLocations.Any()).Count() + "/" + PathPoints.Count + "\n" +
" Total value: " + PathPoints.Sum(p => p.ClusterLocations.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0))) + " mk");
int spawnPointsContainingResources = PathPoints.Where(p => p.ClusterLocations.Any()).Count();
string percentage = string.Format(CultureInfo.InvariantCulture, "{0:P2}", (float)spawnPointsContainingResources / PathPoints.Count);
DebugConsole.NewMessage($"Level resources spawned: {itemCount}\n" +
$" Spawn points containing resources: {spawnPointsContainingResources} ({percentage})\n" +
$" Total value: {PathPoints.Sum(p => p.ClusterLocations.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0)))} mk");
if (AbyssResources.Count > 0)
{
DebugConsole.NewMessage("Abyss resources spawned: " + AbyssResources.Sum(a => a.Resources.Count) + "\n" +
" Total value: " + AbyssResources.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0)) + " mk");
DebugConsole.NewMessage($"Abyss resources spawned: {AbyssResources.Sum(a => a.Resources.Count)}\n" +
$" Total value: {AbyssResources.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0))} mk");
}
#endif
@@ -2842,7 +2865,7 @@ namespace Barotrauma
{
selectedPrefab = ToolBox.SelectWeightedRandom(
levelResources.Select(it => it.itemPrefab).ToList(),
levelResources.Select(it => it.commonness).ToList(),
levelResources.Select(it => it.commonnessInfo.GetCommonness(pathPoint.TunnelType)).ToList(),
Rand.RandSync.ServerAndClient);
selectedPrefab.Tags.ForEach(t =>
{
@@ -2854,20 +2877,21 @@ namespace Barotrauma
}
else
{
var filteredResources = levelResources.Where(it =>
!pathPoint.ResourceIds.Contains(it.itemPrefab.Identifier) &&
pathPoint.ResourceTags.Any() && it.itemPrefab.Tags.Any(t => pathPoint.ResourceTags.Contains(t)));
selectedPrefab = ToolBox.SelectWeightedRandom(
var filteredResources = pathPoint.ResourceTags.None() ? levelResources :
levelResources.Where(it => it.itemPrefab.Tags.Any(t => pathPoint.ResourceTags.Contains(t)));
selectedPrefab = ToolBox.SelectWeightedRandom(
filteredResources.Select(it => it.itemPrefab).ToList(),
filteredResources.Select(it => it.commonness).ToList(),
filteredResources.Select(it => it.commonnessInfo.GetCommonness(pathPoint.TunnelType)).ToList(),
Rand.RandSync.ServerAndClient);
}
if (selectedPrefab == null) { return false; }
// Create resources for the cluster
var commonness = levelResources.First(r => r.itemPrefab == selectedPrefab).commonness;
var lerpAmount = MathUtils.InverseLerp(minCommonness, maxCommonness, commonness);
float commonness = levelResources.First(r => r.itemPrefab == selectedPrefab).commonnessInfo.GetCommonness(pathPoint.TunnelType);
float lerpAmount = pathPoint.TunnelType != TunnelType.Cave ?
MathUtils.InverseLerp(commonnessRange.X, commonnessRange.Y, commonness) :
MathUtils.InverseLerp(caveCommonnessRange.X, caveCommonnessRange.Y, commonness);
var maxClusterSize = (int)MathHelper.Lerp(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y, lerpAmount);
var maxFitOnEdge = GetMaxResourcesOnEdge(selectedPrefab, location, out var edgeLength);
maxClusterSize = Math.Min(maxClusterSize, maxFitOnEdge);
@@ -2898,6 +2922,7 @@ namespace Barotrauma
edgeLength = 0.0f;
if (location.Cell == null || location.Edge == null) { return 0; }
edgeLength = Vector2.Distance(location.Edge.Point1, location.Edge.Point2);
if (resourcePrefab == null) { return 0; }
return (int)Math.Floor(edgeLength / ((1.0f - maxResourceOverlap) * resourcePrefab.Size.X));
}
}
@@ -3041,15 +3066,19 @@ namespace Barotrauma
{
edgeLength ??= Vector2.Distance(location.Edge.Point1, location.Edge.Point2);
Vector2 edgeDir = (location.Edge.Point2 - location.Edge.Point1) / edgeLength.Value;
if (!MathUtils.IsValid(edgeDir))
{
edgeDir = Vector2.Zero;
}
var minResourceOverlap = -((edgeLength.Value - (resourceCount * resourcePrefab.Size.X)) / (resourceCount * resourcePrefab.Size.X));
minResourceOverlap = Math.Max(minResourceOverlap, 0.0f);
minResourceOverlap = Math.Clamp(minResourceOverlap, 0, maxResourceOverlap);
var lerpAmounts = new float[resourceCount];
lerpAmounts[0] = 0.0f;
var lerpAmount = 0.0f;
for (int i = 1; i < resourceCount; i++)
{
var overlap = Rand.Range(minResourceOverlap, maxResourceOverlap, sync: Rand.RandSync.ServerAndClient);
lerpAmount += ((1.0f - overlap) * resourcePrefab.Size.X) / edgeLength.Value;
lerpAmount += (1.0f - overlap) * resourcePrefab.Size.X / edgeLength.Value;
lerpAmounts[i] = Math.Clamp(lerpAmount, 0.0f, 1.0f);
}
var startOffset = Rand.Range(0.0f, 1.0f - lerpAmount, sync: Rand.RandSync.ServerAndClient);
@@ -323,7 +323,7 @@ namespace Barotrauma
set { caveCount = MathHelper.Clamp(value, 0, 100); }
}
[Serialize(100, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 10000)]
[Serialize(100, IsPropertySaveable.Yes, description: "The maximum number of level resources in the level."), Editable(MinValueInt = 0, MaxValueInt = 10000)]
public int ItemCount
{
get;
@@ -344,7 +344,7 @@ namespace Barotrauma
set;
}
[Serialize("2,8", IsPropertySaveable.Yes, description: "The minimum and maximum amount of resources in a single cluster. " +
[Serialize("3,6", IsPropertySaveable.Yes, description: "The minimum and maximum amount of resources in a single cluster. " +
"In addition to this, resource commonness affects the cluster size. Less common resources spawn in smaller clusters."), Editable(1, 20)]
public Point ResourceClusterSizeRange
{
@@ -149,7 +149,7 @@ namespace Barotrauma
Biome.Prefabs.FirstOrDefault(b => b.Identifier == biomeId) ??
Biome.Prefabs.FirstOrDefault(b => !b.OldIdentifier.IsEmpty && b.OldIdentifier == biomeId) ??
Biome.Prefabs.First();
connection.Difficulty = MathHelper.Clamp(connection.Difficulty, connection.Biome.MinDifficulty, connection.Biome.MaxDifficulty);
connection.Difficulty = MathHelper.Clamp(connection.Difficulty, connection.Biome.MinDifficulty, connection.Biome.AdjustedMaxDifficulty);
connection.LevelData = new LevelData(subElement.Element("Level"), connection.Difficulty);
Connections.Add(connection);
connectionElements.Add(subElement);
@@ -562,7 +562,7 @@ namespace Barotrauma
{
if (connection.Locations.Any(l => l.IsGateBetweenBiomes))
{
connection.Difficulty = connection.Locations.Min(l => l.Biome.MaxDifficulty);
connection.Difficulty = Math.Min(connection.Locations.Min(l => l.Biome.ActualMaxDifficulty), connection.Biome.AdjustedMaxDifficulty);
}
else
{
@@ -591,7 +591,7 @@ namespace Barotrauma
if (biome != null)
{
minDifficulty = biome.MinDifficulty;
maxDifficulty = biome.MaxDifficulty;
maxDifficulty = biome.AdjustedMaxDifficulty;
float diff = 1 - settingsFactor;
difficulty *= 1 - (1f / biome.AllowedZones.Max() * diff);
}
@@ -343,14 +343,12 @@ namespace Barotrauma
Math.Max(Body.LinearVelocity.Y, ConvertUnits.ToSimUnits(Level.Loaded.BottomPos - (worldBorders.Y - worldBorders.Height))));
}
//hard limit for how far outside the level the sub can go
float maxDist = 200000.0f;
//the force of the current starts to increase exponentially after this point
float exponentialForceIncreaseDist = 150000.0f;
float distance = Position.X < 0 ? Math.Abs(Position.X) : Position.X - Level.Loaded.Size.X;
float distance = Position.X < -Level.OutsideBoundsCurrentMargin ?
Math.Abs(Position.X + Level.OutsideBoundsCurrentMargin) :
Position.X - (Level.Loaded.Size.X + Level.OutsideBoundsCurrentMargin);
if (distance > 0)
{
if (distance > maxDist)
if (distance > Level.OutsideBoundsCurrentHardLimit)
{
if (Position.X < 0)
{
@@ -361,9 +359,9 @@ namespace Barotrauma
Body.LinearVelocity = new Vector2(Math.Min(0, Body.LinearVelocity.X), Body.LinearVelocity.Y);
}
}
if (distance > exponentialForceIncreaseDist)
if (distance > Level.OutsideBoundsCurrentMarginExponential)
{
distance += (float)Math.Pow((distance - exponentialForceIncreaseDist) * 0.01f, 2.0f);
distance += (float)Math.Pow((distance - Level.OutsideBoundsCurrentMarginExponential) * 0.01f, 2.0f);
}
float force = distance * 0.5f;
totalForce += (Position.X < 0 ? Vector2.UnitX : -Vector2.UnitX) * force;
@@ -0,0 +1,168 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using Barotrauma.Networking;
using Lidgren.Network;
namespace Barotrauma
{
interface IWritableBitField
{
public void WriteBoolean(bool b);
public void WriteInteger(int value, int min, int max);
public void WriteFloat(float value, float min, float max, int numberOfBits);
public void WriteToMessage(IWriteMessage msg);
}
interface IReadableBitField
{
public bool ReadBoolean();
public int ReadInteger(int min, int max);
public float ReadFloat(float min, float max, int numberOfBits);
}
sealed class WriteOnlyBitField : IWritableBitField, IDisposable
{
private const int AmountOfBoolsInByte = 7; // Reserve last bit for end marker
private readonly List<byte> Buffer = new List<byte>();
private int index;
private bool disposed;
public void WriteBoolean(bool b)
{
ThrowIfDisposed();
int arrayIndex = (int)Math.Floor(index / (float)AmountOfBoolsInByte);
if (arrayIndex >= Buffer.Count) { Buffer.Add(0); }
int bitIndex = index % AmountOfBoolsInByte;
Buffer[arrayIndex] |= (byte)(b ? 1u << bitIndex : 0);
index++;
}
public void WriteInteger(int value, int min, int max)
{
ThrowIfDisposed();
uint range = (uint)(max - min);
int numberOfBits = NetUtility.BitsToHoldUInt(range);
uint writeValue = (uint)(value - min);
for (int i = 0; i < numberOfBits; i++)
{
WriteBoolean((writeValue & (1u << i)) != 0);
}
}
public void WriteFloat(float value, float min, float max, int numberOfBits)
{
ThrowIfDisposed();
float range = max - min;
float unit = (value - min) / range;
uint maxVal = (1u << numberOfBits) - 1;
uint writeValue = (uint)(maxVal * unit);
for (int i = 0; i < numberOfBits; i++)
{
WriteBoolean((writeValue & (1u << i)) != 0);
}
}
public void WriteToMessage(IWriteMessage msg)
{
ThrowIfDisposed();
if (Buffer.Count == 0) { Buffer.Add(0); }
Buffer[^1] |= 1 << AmountOfBoolsInByte; // mark the last byte so we know when to stop reading
foreach (byte b in Buffer)
{
msg.Write(b);
}
Dispose();
}
public void Dispose()
{
disposed = true;
}
private void ThrowIfDisposed()
{
if (disposed) { throw new ObjectDisposedException(nameof(WriteOnlyBitField)); }
}
}
sealed class ReadOnlyBitField : IReadableBitField
{
private const int AmountOfBoolsInByte = 7; // Reserve last bit for end marker
private readonly ImmutableArray<byte> buffer;
private int index;
public ReadOnlyBitField(IReadMessage inc)
{
List<byte> bytes = new List<byte>();
byte currentByte;
int reads = 0;
do
{
currentByte = inc.ReadByte();
bytes.Add(currentByte);
reads++;
if (reads > 100)
{
throw new Exception($"Failed to find the end of the bit field after 100 reads. Terminating to prevent the game from freezing.");
}
}
while (!IsBitSet(currentByte, AmountOfBoolsInByte));
buffer = bytes.ToImmutableArray();
}
public bool ReadBoolean()
{
int arrayIndex = (int)MathF.Floor(index / (float)AmountOfBoolsInByte);
int bitIndex = index % AmountOfBoolsInByte;
index++;
return IsBitSet(buffer[arrayIndex], bitIndex);
}
public int ReadInteger(int min, int max)
{
uint range = (uint)(max - min);
int numberOfBits = NetUtility.BitsToHoldUInt(range);
uint value = 0;
for (int i = 0; i < numberOfBits; i++)
{
value |= ReadBoolean() ? 1u << i : 0u;
}
return (int)(min + value);
}
public float ReadFloat(float min, float max, int numberOfBits)
{
int maxInt = (1 << numberOfBits) - 1;
uint value = 0;
for (int i = 0; i < numberOfBits; i++)
{
value |= ReadBoolean() ? 1u << i : 0u;
}
float range = max - min;
return min + range * value / maxInt;
}
private static bool IsBitSet(byte b, int bitIndex) => (b & (1u << bitIndex)) != 0;
}
}
@@ -1,37 +1,34 @@
using Barotrauma.Steam;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma.Networking
{
#warning TODO: turn this into INetSerializableStruct
partial class BannedPlayer
{
public string Name;
public string EndPoint; public bool IsRangeBan;
public UInt64 SteamID;
public string Reason;
public DateTime? ExpirationTime;
public UInt16 UniqueIdentifier;
public readonly string Name;
public readonly Either<Address, AccountId> AddressOrAccountId;
private void ParseEndPointAsSteamId()
{
ulong endPointAsSteamId = SteamManager.SteamIDStringToUInt64(EndPoint);
if (endPointAsSteamId != 0 && SteamID == 0) { SteamID = endPointAsSteamId; }
}
public readonly string Reason;
public DateTime? ExpirationTime;
public readonly UInt32 UniqueIdentifier;
}
partial class BanList
{
private readonly List<BannedPlayer> bannedPlayers;
public IReadOnlyList<BannedPlayer> BannedPlayers => bannedPlayers;
public IEnumerable<string> BannedNames
{
get { return bannedPlayers.Select(bp => bp.Name); }
}
public IEnumerable<string> BannedEndPoints
public IEnumerable<Either<Address, AccountId>> BannedAddresses
{
get { return bannedPlayers.Select(bp => bp.EndPoint).Where(endPoint => !string.IsNullOrEmpty(endPoint)); }
get { return bannedPlayers.Select(bp => bp.AddressOrAccountId); }
}
partial void InitProjectSpecific();
@@ -42,19 +39,5 @@ namespace Barotrauma.Networking
bannedPlayers = new List<BannedPlayer>();
InitProjectSpecific();
}
public static string ToRange(string ip)
{
if (SteamManager.SteamIDStringToUInt64(ip) != 0) { return ip; }
for (int i = ip.Length - 1; i > 0; i--)
{
if (ip[i] == '.')
{
ip = ip.Substring(0, i) + ".x";
break;
}
}
return ip;
}
}
}
@@ -11,10 +11,10 @@ namespace Barotrauma.Networking
public string Name;
public Identifier PreferredJob;
public CharacterTeamType PreferredTeam;
public UInt16 NameID;
public UInt64 SteamID;
public byte ID;
public UInt16 CharacterID;
public UInt16 NameId;
public AccountInfo AccountInfo;
public byte SessionId;
public UInt16 CharacterId;
public float Karma;
public bool Muted;
public bool InGame;
@@ -28,10 +28,23 @@ namespace Barotrauma.Networking
{
public const int MaxNameLength = 32;
public string Name; public UInt16 NameID;
public byte ID;
public UInt64 SteamID;
public UInt64 OwnerSteamID;
public string Name; public UInt16 NameId;
/// <summary>
/// An ID for this client for the current session.
/// THIS IS NOT A PERSISTENT VALUE. DO NOT STORE THIS LONG-TERM.
/// IT CANNOT BE USED TO IDENTIFY PLAYERS ACROSS SESSIONS.
/// </summary>
public readonly byte SessionId;
public AccountInfo AccountInfo;
/// <summary>
/// The ID of the account used to authenticate this session.
/// This value can be used as a persistent value to identify
/// players in the banlist and campaign saves.
/// </summary>
public Option<AccountId> AccountId => AccountInfo.AccountId;
public LanguageIdentifier Language;
@@ -90,14 +103,14 @@ namespace Barotrauma.Networking
public UInt16 CharacterID;
private Vector2 spectate_position;
private Vector2 spectatePos;
public Vector2? SpectatePos
{
get
{
if (character == null || character.IsDead)
{
return spectate_position;
return spectatePos;
}
else
{
@@ -107,7 +120,7 @@ namespace Barotrauma.Networking
set
{
spectate_position = value.Value;
spectatePos = value.Value;
}
}
@@ -177,19 +190,13 @@ namespace Barotrauma.Networking
{
get { return kickVoters.Count; }
}
/*public Client(NetPeer server, string name, byte ID)
: this(name, ID)
{
}*/
partial void InitProjSpecific();
partial void DisposeProjSpecific();
public Client(string name, byte ID)
public Client(string name, byte sessionId)
{
this.Name = name;
this.ID = ID;
this.SessionId = sessionId;
kickVoters = new List<Client>();
@@ -234,13 +241,16 @@ namespace Barotrauma.Networking
return kickVoters.Contains(voter);
}
public bool HasKickVoteFromID(int id)
public bool HasKickVoteFromSessionId(int id)
{
return kickVoters.Any(k => k.ID == id);
return kickVoters.Any(k => k.SessionId == id);
}
public bool SessionOrAccountIdMatches(string userId)
=> (AccountId.IsSome() && Networking.AccountId.Parse(userId) == AccountId)
|| (byte.TryParse(userId, out byte sessionId) && SessionId == sessionId);
public static void UpdateKickVotes(List<Client> connectedClients)
public static void UpdateKickVotes(IReadOnlyList<Client> connectedClients)
{
foreach (Client client in connectedClients)
{
@@ -250,7 +260,7 @@ namespace Barotrauma.Networking
public void WritePermissions(IWriteMessage msg)
{
msg.Write(ID);
msg.Write(SessionId);
msg.WriteRangedInteger((int)Permissions, 0, (int)ClientPermissions.All);
if (HasPermission(ClientPermissions.ConsoleCommands))
{
@@ -32,8 +32,8 @@ namespace Barotrauma.Networking
class PermissionPreset
{
public static List<PermissionPreset> List = new List<PermissionPreset>();
public static readonly List<PermissionPreset> List = new List<PermissionPreset>();
public readonly LocalizedString Name;
public readonly LocalizedString Description;
public readonly ClientPermissions Permissions;
@@ -87,9 +87,11 @@ namespace Barotrauma.Networking
}
}
public bool MatchesPermissions(ClientPermissions permissions, HashSet<DebugConsole.Command> permittedConsoleCommands)
public bool MatchesPermissions(ClientPermissions permissions, ISet<DebugConsole.Command> permittedConsoleCommands)
{
return permissions == this.Permissions && PermittedCommands.SequenceEqual(permittedConsoleCommands);
return permissions == Permissions
&& PermittedCommands.All(permittedConsoleCommands.Contains)
&& permittedConsoleCommands.All(PermittedCommands.Contains);
}
}
}
@@ -3,10 +3,10 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
@@ -35,7 +35,7 @@ namespace Barotrauma
/// Using the attribute on the struct will make all fields and properties serialized
/// </remarks>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Struct | AttributeTargets.Property)]
public class NetworkSerialize : Attribute
public sealed class NetworkSerialize : Attribute
{
public int MaxValueInt = int.MaxValue;
public int MinValueInt = int.MinValue;
@@ -56,21 +56,37 @@ namespace Barotrauma
/// <summary>
/// Static class that contains serialize and deserialize functions for different types used in <see cref="INetSerializableStruct"/>
/// </summary>
public static class NetSerializableProperties
[SuppressMessage("ReSharper", "RedundantTypeArgumentsOfMethod")]
static class NetSerializableProperties
{
public readonly struct ReadWriteBehavior
public interface IReadWriteBehavior
{
public delegate dynamic? ReadDelegate(IReadMessage inc, Type type, NetworkSerialize attribute);
public delegate object? ReadDelegate(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField);
public delegate void WriteDelegate(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg);
public delegate void WriteDelegate(object? obj, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField);
public readonly ReadDelegate ReadAction;
public readonly WriteDelegate WriteAction;
public ReadDelegate ReadAction { get; }
public WriteDelegate WriteAction { get; }
}
public readonly struct ReadWriteBehavior<T> : IReadWriteBehavior
{
public delegate T ReadDelegate(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField);
public delegate void WriteDelegate(T obj, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField);
public IReadWriteBehavior.ReadDelegate ReadAction { get; }
public IReadWriteBehavior.WriteDelegate WriteAction { get; }
public ReadDelegate ReadActionDirect { get; }
public WriteDelegate WriteActionDirect { get; }
public ReadWriteBehavior(ReadDelegate readAction, WriteDelegate writeAction)
{
ReadAction = readAction;
WriteAction = writeAction;
ReadAction = (inc, attribute, bitField) => readAction(inc, attribute, bitField);
WriteAction = (o, attribute, msg, bitField) => writeAction((T)o!, attribute, msg, bitField);
ReadActionDirect = readAction;
WriteActionDirect = writeAction;
}
}
@@ -80,17 +96,18 @@ namespace Barotrauma
public delegate void SetValueDelegate(object? obj, object? value);
public readonly string Name;
public readonly Type Type;
public readonly ReadWriteBehavior Behavior;
public readonly IReadWriteBehavior Behavior;
public readonly NetworkSerialize Attribute;
public readonly SetValueDelegate SetValue;
public readonly GetValueDelegate GetValue;
public readonly bool HasOwnAttribute;
public CachedReflectedVariable(MemberInfo info, ReadWriteBehavior behavior, Type baseClassType)
public CachedReflectedVariable(MemberInfo info, IReadWriteBehavior behavior, Type baseClassType)
{
Behavior = behavior;
Name = info.Name;
switch (info)
{
case PropertyInfo pi:
@@ -126,343 +143,368 @@ namespace Barotrauma
private static readonly Dictionary<Type, ImmutableArray<CachedReflectedVariable>> CachedVariables = new Dictionary<Type, ImmutableArray<CachedReflectedVariable>>();
private static readonly ImmutableDictionary<Type, ReadWriteBehavior> TypeBehaviors = new Dictionary<Type, ReadWriteBehavior>
{
{ typeof(Boolean), new ReadWriteBehavior(ReadBoolean, WriteDynamic) },
{ typeof(Byte), new ReadWriteBehavior(ReadByte, WriteDynamic) },
{ typeof(UInt16), new ReadWriteBehavior(ReadUInt16, WriteDynamic) },
{ typeof(Int16), new ReadWriteBehavior(ReadInt16, WriteDynamic) },
{ typeof(UInt32), new ReadWriteBehavior(ReadUInt32, WriteDynamic) },
{ typeof(Int32), new ReadWriteBehavior(ReadInt32, WriteInt32) },
{ typeof(UInt64), new ReadWriteBehavior(ReadUInt64, WriteDynamic) },
{ typeof(Int64), new ReadWriteBehavior(ReadInt64, WriteDynamic) },
{ typeof(Single), new ReadWriteBehavior(ReadSingle, WriteSingle) },
{ typeof(Double), new ReadWriteBehavior(ReadDouble, WriteDynamic) },
{ typeof(String), new ReadWriteBehavior(ReadString, WriteDynamic) },
{ typeof(Identifier), new ReadWriteBehavior(ReadIdentifier, WriteDynamic) },
{ typeof(Color), new ReadWriteBehavior(ReadColor, WriteColor) },
{ typeof(Vector2), new ReadWriteBehavior(ReadVector2, WriteVector2) }
}.ToImmutableDictionary();
private static readonly Dictionary<Type, IReadWriteBehavior> TypeBehaviors
= new Dictionary<Type, IReadWriteBehavior>
{
{ typeof(Boolean), new ReadWriteBehavior<Boolean>(ReadBoolean, WriteBoolean) },
{ typeof(Byte), new ReadWriteBehavior<Byte>(ReadByte, WriteByte) },
{ typeof(UInt16), new ReadWriteBehavior<UInt16>(ReadUInt16, WriteUInt16) },
{ typeof(Int16), new ReadWriteBehavior<Int16>(ReadInt16, WriteInt16) },
{ typeof(UInt32), new ReadWriteBehavior<UInt32>(ReadUInt32, WriteUInt32) },
{ typeof(Int32), new ReadWriteBehavior<Int32>(ReadInt32, WriteInt32) },
{ typeof(UInt64), new ReadWriteBehavior<UInt64>(ReadUInt64, WriteUInt64) },
{ typeof(Int64), new ReadWriteBehavior<Int64>(ReadInt64, WriteInt64) },
{ typeof(Single), new ReadWriteBehavior<Single>(ReadSingle, WriteSingle) },
{ typeof(Double), new ReadWriteBehavior<Double>(ReadDouble, WriteDouble) },
{ typeof(String), new ReadWriteBehavior<String>(ReadString, WriteString) },
{ typeof(Identifier), new ReadWriteBehavior<Identifier>(ReadIdentifier, WriteIdentifier) },
{ typeof(AccountId), new ReadWriteBehavior<AccountId>(ReadAccountId, WriteAccountId) },
{ typeof(Color), new ReadWriteBehavior<Color>(ReadColor, WriteColor) },
{ typeof(Vector2), new ReadWriteBehavior<Vector2>(ReadVector2, WriteVector2) }
};
private static readonly ImmutableDictionary<Predicate<Type>, ReadWriteBehavior> TypePredicates = new Dictionary<Predicate<Type>, ReadWriteBehavior>
private static readonly ImmutableDictionary<Predicate<Type>, Func<Type, IReadWriteBehavior>> BehaviorFactories = new Dictionary<Predicate<Type>, Func<Type, IReadWriteBehavior>>
{
// Arrays
{ type => typeof(Array).IsAssignableFrom(type.BaseType), new ReadWriteBehavior(ReadArray, WriteArray) },
{ type => type.IsArray, CreateArrayBehavior },
// Nested INetSerializableStructs
{ type => typeof(INetSerializableStruct).IsAssignableFrom(type), new ReadWriteBehavior(ReadINetSerializableStruct, WriteINetSerializableStruct) },
{ type => typeof(INetSerializableStruct).IsAssignableFrom(type), CreateINetSerializableStructBehavior },
// Enums
{ type => type.IsEnum, new ReadWriteBehavior(ReadEnum, WriteEnum) },
{ type => type.IsEnum, CreateEnumBehavior },
// Nullable
{ type => Nullable.GetUnderlyingType(type) != null, new ReadWriteBehavior(ReadNullable, WriteNullable) },
{ type => Nullable.GetUnderlyingType(type) != null, CreateNullableStructBehavior },
// ImmutableArray
{ type => IsOfGenericType(type, typeof(ImmutableArray<>)), CreateImmutableArrayBehavior },
// Option
{ type => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Option<>), new ReadWriteBehavior(ReadOption, WriteOption) }
{ type => IsOfGenericType(type, typeof(Option<>)), CreateOptionBehavior }
}.ToImmutableDictionary();
private static readonly ReadWriteBehavior InvalidReadWriteBehavior = new ReadWriteBehavior(ReadInvalid, WriteInvalid);
private static readonly Dictionary<Type, MethodInfo> cachedSomeCreateMethods = new Dictionary<Type, MethodInfo>();
private static readonly Dictionary<Type, MethodInfo> cachedNoneCreateMethod = new Dictionary<Type, MethodInfo>();
private static void WriteInvalid(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) =>
throw new SerializationException($"Type {obj?.GetType()} cannot be serialized. Did you forget to implement {nameof(INetSerializableStruct)}?");
private static dynamic ReadInvalid(IReadMessage inc, Type type, NetworkSerialize attribute) => throw new SerializationException($"Type {type} cannot be deserialized. Did you forget to implement {nameof(INetSerializableStruct)}?");
private static void WriteOption(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg)
/// <param name="behaviorGenericParam">The type that the behavior handles</param>
/// <param name="funcGenericParam">The type that will be used as the generic parameter for the read/write methods</param>
/// <param name="readFunc">The read method.
/// It must have a generic parameter.
/// The return type must be such that if the generic parameter is replaced with funcGenericParam, you get behaviorGenericParam.</param>
/// <param name="writeFunc">The write method. The first parameter's type must be the same as readFunc's return type.</param>
/// <typeparam name="TDelegateBase">Ideally the least specific type possible, because it's replaced by behaviorGenericParam</typeparam>
/// <returns>A ReadWriteBehavior&lt;behaviorGenericParam&gt;</returns>
private static IReadWriteBehavior CreateBehavior<TDelegateBase>(Type behaviorGenericParam,
Type funcGenericParam,
ReadWriteBehavior<TDelegateBase>.ReadDelegate readFunc,
ReadWriteBehavior<TDelegateBase>.WriteDelegate writeFunc)
{
if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); }
var behaviorType = typeof(ReadWriteBehavior<>).MakeGenericType(behaviorGenericParam);
Type type = obj.GetType();
Type optionType = type.GetGenericTypeDefinition();
Type underlyingType = type.GetGenericArguments()[0];
var readDelegateType = typeof(ReadWriteBehavior<>.ReadDelegate).MakeGenericType(behaviorGenericParam);
var writeDelegateType = typeof(ReadWriteBehavior<>.WriteDelegate).MakeGenericType(behaviorGenericParam);
if (optionType == typeof(None<>))
var constructor = behaviorType.GetConstructor(new[]
{
msg.Write(false);
readDelegateType, writeDelegateType
});
return (constructor!.Invoke(new object[]
{
readFunc.Method.GetGenericMethodDefinition().MakeGenericMethod(funcGenericParam).CreateDelegate(readDelegateType),
writeFunc.Method.GetGenericMethodDefinition().MakeGenericMethod(funcGenericParam).CreateDelegate(writeDelegateType)
}) as IReadWriteBehavior)!;
}
private static IReadWriteBehavior CreateArrayBehavior(Type arrayType) =>
CreateBehavior(
arrayType,
arrayType.GetElementType()!,
ReadArray<object>,
WriteArray<object>);
private static IReadWriteBehavior CreateINetSerializableStructBehavior(Type structType) =>
CreateBehavior(
structType,
structType,
ReadINetSerializableStruct<INetSerializableStruct>,
WriteINetSerializableStruct<INetSerializableStruct>);
private static IReadWriteBehavior CreateEnumBehavior(Type enumType) =>
CreateBehavior(
enumType,
enumType,
ReadEnum<Enum>,
WriteEnum<Enum>);
private static IReadWriteBehavior CreateNullableStructBehavior(Type nullableType) =>
CreateBehavior(
nullableType,
Nullable.GetUnderlyingType(nullableType)!,
ReadNullable<int>,
WriteNullable<int>);
private static IReadWriteBehavior CreateOptionBehavior(Type optionType) =>
CreateBehavior(
optionType,
optionType.GetGenericArguments()[0],
ReadOption<object>,
WriteOption<object>);
private static IReadWriteBehavior CreateImmutableArrayBehavior(Type arrayType) =>
CreateBehavior(
arrayType,
arrayType.GetGenericArguments()[0],
ReadImmutableArray<object>,
WriteImmutableArray<object>);
private static ImmutableArray<T> ReadImmutableArray<T>(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) where T : notnull
{
return ReadArray<T>(inc, attribute, bitField).ToImmutableArray();
}
private static void WriteImmutableArray<T>(ImmutableArray<T> array, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) where T : notnull
{
ToolBox.ThrowIfNull(array);
WriteIReadOnlyCollection<T>(array, attribute, msg, bitField);
}
private static T[] ReadArray<T>(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) where T : notnull
{
int length = bitField.ReadInteger(0, attribute.ArrayMaxSize);
T[] array = new T[length];
if (!TryFindBehavior(out ReadWriteBehavior<T> behavior))
{
throw new InvalidOperationException($"Could not find suitable behavior for type {typeof(T)} in {nameof(ReadArray)}");
}
else if (optionType == typeof(Some<>))
for (int i = 0; i < length; i++)
{
msg.Write(true);
if (TryFindBehavior(underlyingType, out ReadWriteBehavior behavior))
{
behavior.WriteAction(obj.Value, attribute, msg);
}
array[i] = behavior.ReadActionDirect(inc, attribute, bitField);
}
else
return array;
}
private static void WriteArray<T>(T[] array, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) where T : notnull
{
ToolBox.ThrowIfNull(array);
WriteIReadOnlyCollection(array, attribute, msg, bitField);
}
private static void WriteIReadOnlyCollection<T>(IReadOnlyCollection<T> array, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) where T : notnull
{
bitField.WriteInteger(array.Count, 0, attribute.ArrayMaxSize);
if (!TryFindBehavior(out ReadWriteBehavior<T> behavior))
{
throw new ArgumentOutOfRangeException(nameof(obj), "Option type was neither None or Some");
throw new InvalidOperationException($"Could not find suitable behavior for type {typeof(T)} in {nameof(WriteArray)}");
}
foreach (T o in array)
{
behavior.WriteActionDirect(o, attribute, msg, bitField);
}
}
private static dynamic? ReadOption(IReadMessage inc, Type type, NetworkSerialize attribute)
private static T ReadINetSerializableStruct<T>(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) where T : INetSerializableStruct
{
Type underlyingType = type.GetGenericArguments()[0];
bool hasValue = inc.ReadBoolean();
if (!hasValue)
{
return GetCreateMethod(typeof(None<>), underlyingType, cachedNoneCreateMethod).Invoke(null, null);
}
if (TryFindBehavior(underlyingType, out ReadWriteBehavior behavior))
{
dynamic? value = behavior.ReadAction(inc, underlyingType, attribute);
return GetCreateMethod(typeof(Some<>), underlyingType, cachedSomeCreateMethods).Invoke(null, new[] { value });
}
throw new InvalidOperationException($"Could not find suitable behavior for type {underlyingType} in {nameof(ReadOption)}");
static MethodInfo GetCreateMethod(Type optionType, Type type, Dictionary<Type, MethodInfo> cache)
{
if (cache.TryGetValue(type, out MethodInfo? foundInfo))
{
return foundInfo;
}
Type genericType = optionType.MakeGenericType(type);
MethodInfo info = genericType.GetMethod("Create", BindingFlags.Static | BindingFlags.Public)!;
cache.Add(type, info);
return info;
}
return INetSerializableStruct.ReadInternal<T>(inc, bitField);
}
private static void WriteNullable(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg)
private static void WriteINetSerializableStruct<T>(T serializableStruct, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) where T : INetSerializableStruct
{
if (obj is { } notNull)
{
msg.Write(true);
if (TryFindBehavior(notNull.GetType(), out ReadWriteBehavior behavior))
{
// uh oh, something terrible has happened!
if (behavior.WriteAction == WriteNullable) { behavior = InvalidReadWriteBehavior; }
behavior.WriteAction(notNull, attribute, msg);
return;
}
}
msg.Write(false);
ToolBox.ThrowIfNull(serializableStruct);
serializableStruct.WriteInternal(msg, bitField);
}
private static dynamic? ReadNullable(IReadMessage inc, Type type, NetworkSerialize attribute)
private static T ReadEnum<T>(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) where T : Enum
{
if (!inc.ReadBoolean()) { return null; }
var type = typeof(T);
Type? underlyingType = Nullable.GetUnderlyingType(type);
if (underlyingType is null) { throw new InvalidOperationException($"Could not get the underlying type of {type} in {nameof(ReadNullable)}"); }
if (TryFindBehavior(underlyingType, out ReadWriteBehavior behavior))
{
// uh oh, something terrible has happened!
if (behavior.ReadAction == ReadNullable) { behavior = InvalidReadWriteBehavior; }
return behavior.ReadAction(inc, underlyingType, attribute);
}
throw new InvalidOperationException($"Could not find suitable behavior for type {underlyingType} in {nameof(ReadNullable)}");
}
private static void WriteEnum(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg)
{
if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); }
Range<int> range = GetEnumRange(obj.GetType());
msg.WriteRangedInteger(Convert.ChangeType(obj, obj.GetTypeCode()), range.Start, range.End);
}
private static dynamic ReadEnum(IReadMessage inc, Type type, NetworkSerialize attribute)
{
Range<int> range = GetEnumRange(type);
int enumIndex = inc.ReadRangedInteger(range.Start, range.End);
int enumIndex = bitField.ReadInteger(range.Start, range.End);
foreach (dynamic? e in Enum.GetValues(type))
foreach (T e in (T[])Enum.GetValues(type))
{
if (Convert.ChangeType(e, e!.GetTypeCode()) == enumIndex) { return e; }
if ((int)Convert.ChangeType(e, e.GetTypeCode()) == enumIndex) { return e; }
}
throw new InvalidOperationException($"An enum {type} with value {enumIndex} could not be found in {nameof(ReadEnum)}");
}
private static void WriteINetSerializableStruct(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg)
private static void WriteEnum<T>(T value, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) where T : Enum
{
if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); }
ToolBox.ThrowIfNull(value);
if (!(obj is INetSerializableStruct serializableStruct)) { throw new InvalidOperationException($"Object in {nameof(WriteINetSerializableStruct)} was {obj.GetType()} but expected {nameof(INetSerializableStruct)}"); }
serializableStruct.Write(msg);
Range<int> range = GetEnumRange(typeof(T));
bitField.WriteInteger((int)Convert.ChangeType(value, value.GetTypeCode()), range.Start, range.End);
}
private static dynamic ReadINetSerializableStruct(IReadMessage inc, Type type, NetworkSerialize attribute)
{
return INetSerializableStruct.ReadDynamic(type, inc);
}
private static void WriteDynamic(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg)
{
if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); }
msg.Write(obj);
}
private static dynamic ReadArray(IReadMessage inc, Type type, NetworkSerialize attribute)
{
Type? elementType = type.GetElementType();
if (elementType is null) { throw new InvalidOperationException($"Could not get the element type of {type} in {nameof(ReadArray)}"); }
int length = inc.ReadRangedInteger(0, attribute.ArrayMaxSize);
Array list = Array.CreateInstance(elementType, length);
for (int i = 0; i < length; i++)
private static T? ReadNullable<T>(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) where T : struct =>
ReadOption<T>(inc, attribute, bitField) switch
{
if (TryFindBehavior(elementType, out ReadWriteBehavior behavior))
{
list.SetValue(behavior.ReadAction(inc, elementType, attribute), i);
}
else
{
throw new InvalidOperationException($"Could not find suitable behavior for type {elementType} in {nameof(ReadArray)}");
}
Some<T> { Value: var value } => value,
None<T> _ => null,
_ => throw new ArgumentOutOfRangeException()
};
private static void WriteNullable<T>(T? value, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) where T : struct =>
WriteOption<T>(value.HasValue ? Option<T>.Some(value.Value) : Option<T>.None(), attribute, msg, bitField);
private static Option<T> ReadOption<T>(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) where T : notnull
{
bool hasValue = bitField.ReadBoolean();
if (!hasValue)
{
return Option<T>.None();
}
return list;
if (TryFindBehavior(out ReadWriteBehavior<T> behavior))
{
return Option<T>.Some(behavior.ReadActionDirect(inc, attribute, bitField));
}
throw new InvalidOperationException($"Could not find suitable behavior for type {typeof(T)} in {nameof(ReadOption)}");
}
private static void WriteArray(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg)
private static void WriteOption<T>(Option<T> option, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) where T : notnull
{
if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); }
ToolBox.ThrowIfNull(option);
if (!(obj is Array array)) { throw new InvalidOperationException($"Object in {nameof(WriteArray)} was {obj.GetType()} but expected {nameof(Array)}"); }
msg.WriteRangedInteger(array.Length, 0, attribute.ArrayMaxSize);
foreach (dynamic? o in array)
if (option.TryUnwrap(out T value))
{
if (TryFindBehavior(o!.GetType(), out ReadWriteBehavior behavior))
bitField.WriteBoolean(true);
if (TryFindBehavior(out ReadWriteBehavior<T> behavior))
{
behavior.WriteAction(o, attribute, msg);
behavior.WriteActionDirect(value, attribute, msg, bitField);
}
}
else
{
bitField.WriteBoolean(false);
}
}
private static dynamic ReadBoolean(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadBoolean();
private static bool ReadBoolean(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => bitField.ReadBoolean();
private static void WriteBoolean(bool b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { bitField.WriteBoolean(b); }
private static dynamic ReadByte(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadByte();
private static byte ReadByte(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadByte();
private static void WriteByte(byte b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.Write(b); }
private static dynamic ReadUInt16(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadUInt16();
private static ushort ReadUInt16(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadUInt16();
private static void WriteUInt16(ushort b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.Write(b); }
private static dynamic ReadInt16(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadInt16();
private static short ReadInt16(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadInt16();
private static void WriteInt16(short b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.Write(b); }
private static dynamic ReadUInt32(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadUInt32();
private static uint ReadUInt32(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadUInt32();
private static void WriteUInt32(uint b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.Write(b); }
private static dynamic ReadInt32(IReadMessage inc, Type type, NetworkSerialize attribute)
private static int ReadInt32(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField)
{
if (IsRanged(attribute.MinValueInt, attribute.MaxValueInt))
{
return inc.ReadRangedInteger(attribute.MinValueInt, attribute.MaxValueInt);
return bitField.ReadInteger(attribute.MinValueInt, attribute.MaxValueInt);
}
return inc.ReadInt32();
}
private static void WriteInt32(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg)
private static void WriteInt32(int i, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField)
{
if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); }
ToolBox.ThrowIfNull(i);
if (IsRanged(attribute.MinValueInt, attribute.MaxValueInt))
{
msg.WriteRangedInteger(obj, attribute.MinValueInt, attribute.MaxValueInt);
bitField.WriteInteger(i, attribute.MinValueInt, attribute.MaxValueInt);
return;
}
msg.Write(obj);
msg.Write(i);
}
private static dynamic ReadUInt64(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadUInt64();
private static ulong ReadUInt64(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadUInt64();
private static void WriteUInt64(ulong b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.Write(b); }
private static dynamic ReadInt64(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadInt64();
private static long ReadInt64(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadInt64();
private static void WriteInt64(long b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.Write(b); }
private static dynamic ReadSingle(IReadMessage inc, Type type, NetworkSerialize attribute)
private static float ReadSingle(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField)
{
if (IsRanged(attribute.MinValueFloat, attribute.MaxValueFloat))
{
return inc.ReadRangedSingle(attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits);
return bitField.ReadFloat(attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits);
}
return inc.ReadSingle();
}
private static void WriteSingle(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg)
private static void WriteSingle(float f, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField)
{
if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); }
ToolBox.ThrowIfNull(f);
if (IsRanged(attribute.MinValueFloat, attribute.MaxValueFloat))
{
msg.WriteRangedSingle(obj, attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits);
bitField.WriteFloat(f, attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits);
return;
}
msg.Write(obj);
msg.Write(f);
}
private static dynamic ReadDouble(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadDouble();
private static double ReadDouble(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadDouble();
private static void WriteDouble(double b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.Write(b); }
private static dynamic ReadString(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadString();
private static string ReadString(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadString();
private static void WriteString(string b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.Write(b); }
private static dynamic ReadIdentifier(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadIdentifier();
private static Identifier ReadIdentifier(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadIdentifier();
private static void WriteIdentifier(Identifier b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.Write(b); }
private static dynamic ReadColor(IReadMessage inc, Type type, NetworkSerialize attribute) => attribute.IncludeColorAlpha ? inc.ReadColorR8G8B8A8() : inc.ReadColorR8G8B8();
private static void WriteColor(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg)
private static AccountId ReadAccountId(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField)
{
if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); }
string str = inc.ReadString();
return AccountId.Parse(str).TryUnwrap(out var accountId)
? accountId
: throw new InvalidCastException($"Could not parse \"{str}\" as an {nameof(AccountId)}");
}
private static void WriteAccountId(AccountId accountId, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField)
{
msg.Write(accountId.StringRepresentation);
}
private static Color ReadColor(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => attribute.IncludeColorAlpha ? inc.ReadColorR8G8B8A8() : inc.ReadColorR8G8B8();
private static void WriteColor(Color color, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField)
{
ToolBox.ThrowIfNull(color);
if (attribute.IncludeColorAlpha)
{
msg.WriteColorR8G8B8A8(obj);
msg.WriteColorR8G8B8A8(color);
return;
}
msg.WriteColorR8G8B8(obj);
msg.WriteColorR8G8B8(color);
}
private static dynamic ReadVector2(IReadMessage inc, Type type, NetworkSerialize attribute)
private static Vector2 ReadVector2(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField)
{
float x;
float y;
if (IsRanged(attribute.MinValueFloat, attribute.MaxValueFloat))
{
x = inc.ReadRangedSingle(attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits);
y = inc.ReadRangedSingle(attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits);
}
else
{
x = inc.ReadSingle();
y = inc.ReadSingle();
}
float x = ReadSingle(inc, attribute, bitField);
float y = ReadSingle(inc, attribute, bitField);
return new Vector2(x, y);
}
private static void WriteVector2(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg)
private static void WriteVector2(Vector2 vector2, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField)
{
if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); }
ToolBox.ThrowIfNull(vector2);
var (x, y) = (Vector2)obj;
if (IsRanged(attribute.MinValueFloat, attribute.MaxValueFloat))
{
msg.WriteRangedSingle(x, attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits);
msg.WriteRangedSingle(y, attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits);
return;
}
msg.Write(x);
msg.Write(y);
var (x, y) = vector2;
WriteSingle(x, attribute, msg, bitField);
WriteSingle(y, attribute, msg, bitField);
}
private static bool IsRanged(float minValue, float maxValue) => minValue > float.MinValue || maxValue < float.MaxValue;
@@ -474,53 +516,71 @@ namespace Barotrauma
return new Range<int>(values.Min(), values.Max());
}
private static bool TryFindBehavior(Type type, out ReadWriteBehavior behavior)
private static bool TryFindBehavior<T>(out ReadWriteBehavior<T> behavior) where T : notnull
{
if (TypeBehaviors.TryGetValue(type, out behavior)) { return true; }
bool found = TryFindBehavior(typeof(T), out var bhvr);
behavior = (ReadWriteBehavior<T>)bhvr;
return found;
}
foreach (var (predicate, behavior2) in TypePredicates)
private static bool TryFindBehavior(Type type, out IReadWriteBehavior behavior)
{
if (TypeBehaviors.TryGetValue(type, out var outBehavior))
{
if (predicate(type))
{
behavior = behavior2;
return true;
}
behavior = outBehavior;
return true;
}
behavior = InvalidReadWriteBehavior;
foreach (var (predicate, factory) in BehaviorFactories)
{
if (!predicate(type)) { continue; }
behavior = factory(type);
TypeBehaviors.Add(type, behavior);
return true;
}
behavior = default!;
return false;
}
public static ImmutableArray<CachedReflectedVariable> GetPropertiesAndFields(Type type, Type baseClassType)
public static ImmutableArray<CachedReflectedVariable> GetPropertiesAndFields(Type type)
{
if (CachedVariables.TryGetValue(type, out var cached)) { return cached; }
List<CachedReflectedVariable> variables = new List<CachedReflectedVariable>();
IEnumerable<PropertyInfo> propertyInfos = type.GetProperties().Where(HasAttribute);
IEnumerable<FieldInfo> fieldInfos = type.GetFields().Where(HasAttribute);
IEnumerable<PropertyInfo> propertyInfos = type.GetProperties().Where(HasAttribute).Where(NotStatic);
IEnumerable<FieldInfo> fieldInfos = type.GetFields().Where(HasAttribute).Where(NotStatic);
foreach (PropertyInfo info in propertyInfos)
{
if (TryFindBehavior(info.PropertyType, out ReadWriteBehavior behavior))
if (info.SetMethod is null)
{
variables.Add(new CachedReflectedVariable(info, behavior, baseClassType));
//skip get-only properties, because it's
//useful to have them but their value
//cannot be set when reading a struct
continue;
}
if (TryFindBehavior(info.PropertyType, out IReadWriteBehavior behavior))
{
variables.Add(new CachedReflectedVariable(info, behavior, type));
}
else
{
throw new SerializationException($"Unable to serialize type \"{type}\".");
throw new Exception($"Unable to serialize type \"{type}\".");
}
}
foreach (FieldInfo info in fieldInfos)
{
if (TryFindBehavior(info.FieldType, out ReadWriteBehavior behavior))
if (TryFindBehavior(info.FieldType, out IReadWriteBehavior behavior))
{
variables.Add(new CachedReflectedVariable(info, behavior, baseClassType));
variables.Add(new CachedReflectedVariable(info, behavior, type));
}
else
{
throw new SerializationException($"Unable to serialize type \"{type}\".");
throw new Exception($"Unable to serialize type \"{type}\".");
}
}
@@ -528,7 +588,20 @@ namespace Barotrauma
CachedVariables.Add(type, array);
return array;
bool HasAttribute(MemberInfo info) => (info.GetCustomAttribute<NetworkSerialize>() ?? baseClassType.GetCustomAttribute<NetworkSerialize>()) != null;
bool HasAttribute(MemberInfo info) => (info.GetCustomAttribute<NetworkSerialize>() ?? type.GetCustomAttribute<NetworkSerialize>()) != null;
bool NotStatic(MemberInfo info)
=> info switch
{
PropertyInfo property => property.GetGetMethod() is { IsStatic: false },
FieldInfo field => !field.IsStatic,
_ => false
};
}
private static bool IsOfGenericType(Type type, Type comparedTo)
{
return type.IsGenericType && type.GetGenericTypeDefinition() == comparedTo;
}
}
@@ -575,13 +648,15 @@ namespace Barotrauma
/// <see cref="Single">float</see><br/>
/// <see cref="Double">double</see><br/>
/// <see cref="String">string</see><br/>
/// <see cref="Barotrauma.Networking.AccountId"/><br/>
/// <see cref="System.Collections.Immutable.ImmutableArray{T}"></see><br/>
/// <see cref="Microsoft.Xna.Framework.Color"/><br/>
/// <see cref="Microsoft.Xna.Framework.Vector2"/><br/>
/// In addition arrays, enums, <see cref="Nullable{T}"/> and <see cref="Option{T}"/> are supported.<br/>
/// Using <see cref="Nullable{T}"/> or <see cref="Option{T}"/> will make the field or property optional.
/// </remarks>
/// <seealso cref="NetworkSerialize"/>
public interface INetSerializableStruct
interface INetSerializableStruct
{
/// <summary>
/// Deserializes a network message into a struct.
@@ -608,21 +683,34 @@ namespace Barotrauma
/// <param name="inc">Incoming network message</param>
/// <typeparam name="T">Type of the struct that implements <see cref="INetSerializableStruct"/></typeparam>
/// <returns>A new struct of type T with fields and properties deserialized</returns>
public static T Read<T>(IReadMessage inc) where T : INetSerializableStruct => (T)ReadDynamic(typeof(T), inc);
public static dynamic ReadDynamic(Type type, IReadMessage inc)
public static T Read<T>(IReadMessage inc) where T : INetSerializableStruct
{
object? newObject = Activator.CreateInstance(type);
IReadableBitField bitField = new ReadOnlyBitField(inc);
return ReadInternal<T>(inc, bitField);
}
public static T ReadInternal<T>(IReadMessage inc, IReadableBitField bitField) where T : INetSerializableStruct
{
object? newObject = Activator.CreateInstance(typeof(T));
if (newObject is null) { return default!; }
var properties = NetSerializableProperties.GetPropertiesAndFields(type, type);
var properties = NetSerializableProperties.GetPropertiesAndFields(typeof(T));
foreach (NetSerializableProperties.CachedReflectedVariable property in properties)
{
NetworkSerialize attribute = property.Attribute;
property.SetValue(newObject, property.Behavior.ReadAction(inc, property.Type, attribute));
object? value = property.Behavior.ReadAction(inc, property.Attribute, bitField);
try
{
property.SetValue(newObject, value);
}
catch (Exception exception)
{
throw new Exception($"Failed to assign" +
$" {value ?? "[NULL]"} ({value?.GetType().Name ?? "[NULL]"})" +
$" to {typeof(T).Name}.{property.Name} ({property.Type.Name})", exception);
}
}
return newObject;
return (T)newObject;
}
/// <summary>
@@ -651,17 +739,26 @@ namespace Barotrauma
/// <param name="msg">Outgoing network message</param>
public void Write(IWriteMessage msg)
{
Type type = GetType();
var properties = NetSerializableProperties.GetPropertiesAndFields(type, type);
IWritableBitField bitField = new WriteOnlyBitField();
IWriteMessage structWriteMsg = new WriteOnlyMessage();
WriteInternal(structWriteMsg, bitField);
bitField.WriteToMessage(msg);
msg.Write(structWriteMsg.Buffer, 0, structWriteMsg.LengthBytes);
}
public void WriteInternal(IWriteMessage msg, IWritableBitField bitField)
{
var properties = NetSerializableProperties.GetPropertiesAndFields(GetType());
foreach (NetSerializableProperties.CachedReflectedVariable property in properties)
{
NetworkSerialize attribute = property.Attribute;
property.Behavior.WriteAction(property.GetValue(this), attribute, msg);
object? value = property.GetValue(this);
property.Behavior.WriteAction(value!, property.Attribute, msg, bitField);
}
}
}
public static class WriteOnlyMessageExtensions
static class WriteOnlyMessageExtensions
{
#if CLIENT
public static IWriteMessage WithHeader(this IWriteMessage msg, ClientPacketHeader header)
@@ -148,7 +148,6 @@ namespace Barotrauma.Networking
InvalidVersion,
MissingContentPackage,
IncompatibleContentPackage,
NotOnWhitelist,
ExcessiveDesyncOldEvent,
ExcessiveDesyncRemovedEvent,
SyncTimeout,
@@ -218,10 +217,7 @@ namespace Barotrauma.Networking
get { return gameStarted; }
}
public virtual List<Client> ConnectedClients
{
get { return null; }
}
public abstract IReadOnlyList<Client> ConnectedClients { get; }
public RespawnManager RespawnManager
{
@@ -268,19 +264,22 @@ namespace Barotrauma.Networking
{
retVal += "color:#ff9900;";
}
retVal += "metadata:" + (client.SteamID != 0 ? client.SteamID.ToString() : client.ID.ToString()) + "‖" + (name ?? client.Name).Replace("‖", "") + "‖end‖";
retVal += "metadata:" + (client.AccountId.TryUnwrap(out var accountId) ? accountId.ToString() : client.SessionId.ToString())
+ "‖" + (name ?? client.Name).Replace("‖", "") + "‖end‖";
return retVal;
}
public virtual void KickPlayer(string kickedName, string reason) { }
public abstract void KickPlayer(string kickedName, string reason);
public virtual void BanPlayer(string kickedName, string reason, bool range = false, TimeSpan? duration = null) { }
public abstract void BanPlayer(string kickedName, string reason, TimeSpan? duration = null);
public virtual void UnbanPlayer(string playerName, string playerIP) { }
public abstract void UnbanPlayer(string playerName);
public abstract void UnbanPlayer(Endpoint endpoint);
public virtual void Update(float deltaTime) { }
public virtual void Disconnect() { }
public virtual void Quit() { }
/// <summary>
/// Check if the two version are compatible (= if they can play together in multiplayer).
@@ -0,0 +1,24 @@
#nullable enable
namespace Barotrauma.Networking
{
abstract class AccountId
{
public abstract string StringRepresentation { get; }
public static Option<AccountId> Parse(string str)
=> ReflectionUtils.ParseDerived<AccountId>(str);
public abstract override bool Equals(object? obj);
public abstract override int GetHashCode();
public override string ToString() => StringRepresentation;
public static bool operator ==(AccountId a, AccountId b)
=> a.Equals(b);
public static bool operator !=(AccountId a, AccountId b)
=> !(a == b);
}
}
@@ -0,0 +1,121 @@
#nullable enable
using System;
namespace Barotrauma.Networking
{
sealed class SteamId : AccountId
{
public readonly UInt64 Value;
public override string StringRepresentation { get; }
/// Based on information found here: https://developer.valvesoftware.com/wiki/SteamID
/// ------------------------------------------------------------------------------------
/// A SteamID is a 64-bit value (16 hexadecimal digits) that's broken up as follows:
///
/// | a | b | c | d |
/// Most significant - | 01 | 1 | 00001 | 0546779D | - Least significant
///
/// a) 8 bits representing the universe the account belongs to.
/// b) 4 bits representing the type of account. Typically 1.
/// c) 20 bits representing the instance of the account. Typically 1.
/// d) 32 bits representing the account number.
///
/// The account number is additionally broken up as follows:
///
/// | e | f |
/// Most significant - | 0000010101000110011101111001110 | 1 | - Least significant
///
/// e) These are the 31 most significant bits of the account number.
/// f) This is the least significant bit of the account number, discriminated under the name Y for some reason.
///
/// Barotrauma supports two textual representations of SteamIDs:
/// 1. STEAM40: Given this name as it represents 40 of the 64 bits in the ID. The account type and instance both
/// have an implied value of 1. The format is "STEAM_{universe}:{Y}:{restOfAccountNumber}".
/// 2. STEAM64: If STEAM40 does not suffice to represent an ID (i.e. the account type or instance were different
/// from 1), we use "STEAM64_{fullId}" where fullId is the 64-bit decimal representation of the full
/// ID.
private const string steam64Prefix = "STEAM64_";
private const string steam40Prefix = "STEAM_";
private const UInt64 usualAccountInstance = 1;
private const UInt64 usualAccountType = 1;
static UInt64 ExtractBits(UInt64 id, int offset, int numberOfBits)
=> (id >> offset) & ((1ul << numberOfBits) - 1ul);
static UInt64 ExtractY(UInt64 id)
=> ExtractBits(id, offset: 0, numberOfBits: 1);
static UInt64 ExtractAccountNumberRemainder(UInt64 id)
=> ExtractBits(id, offset: 1, numberOfBits: 31);
static UInt64 ExtractAccountInstance(UInt64 id)
=> ExtractBits(id, offset: 32, numberOfBits: 20);
static UInt64 ExtractAccountType(UInt64 id)
=> ExtractBits(id, offset: 52, numberOfBits: 4);
static UInt64 ExtractUniverse(UInt64 id)
=> ExtractBits(id, offset: 56, numberOfBits: 8);
public SteamId(UInt64 value)
{
Value = value;
if (ExtractAccountInstance(Value) == usualAccountInstance
&& ExtractAccountType(Value) == usualAccountType)
{
UInt64 y = ExtractY(Value);
UInt64 accountNumberRemainder = ExtractAccountNumberRemainder(Value);
UInt64 universe = ExtractUniverse(Value);
StringRepresentation = $"{steam40Prefix}{universe}:{y}:{accountNumberRemainder}";
}
else
{
StringRepresentation = $"{steam64Prefix}{Value}";
}
}
public override string ToString() => StringRepresentation;
public new static Option<SteamId> Parse(string str)
{
if (str.IsNullOrWhiteSpace()) { return Option<SteamId>.None(); }
if (str.StartsWith(steam64Prefix, StringComparison.InvariantCultureIgnoreCase)) { str = str[steam64Prefix.Length..]; }
if (UInt64.TryParse(str, out UInt64 retVal) && ExtractAccountInstance(retVal) > 0)
{
return Option<SteamId>.Some(new SteamId(retVal));
}
if (!str.StartsWith(steam40Prefix, StringComparison.InvariantCultureIgnoreCase)) { return Option<SteamId>.None(); }
string[] split = str[steam40Prefix.Length..].Split(':');
if (split.Length != 3) { return Option<SteamId>.None(); }
if (!UInt64.TryParse(split[0], out UInt64 universe)) { return Option<SteamId>.None(); }
if (!UInt64.TryParse(split[1], out UInt64 y)) { return Option<SteamId>.None(); }
if (!UInt64.TryParse(split[2], out UInt64 accountNumber)) { return Option<SteamId>.None(); }
return Option<SteamId>.Some(
new SteamId((universe << 56)
| usualAccountType << 52
| usualAccountInstance << 32
| (accountNumber << 1)
| y));
}
public override bool Equals(object? obj)
=> obj switch
{
SteamId otherId => this == otherId,
_ => false
};
public override int GetHashCode()
=> Value.GetHashCode();
public static bool operator ==(SteamId a, SteamId b)
=> a.Value == b.Value;
public static bool operator !=(SteamId a, SteamId b)
=> !(a == b);
}
}
@@ -0,0 +1,50 @@
#nullable enable
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma.Networking
{
[NetworkSerialize]
readonly struct AccountInfo : INetSerializableStruct
{
public static readonly AccountInfo None = new AccountInfo(Option<AccountId>.None());
/// <summary>
/// The primary ID for a given user
/// </summary>
public readonly Option<AccountId> AccountId;
/// <summary>
/// Other user IDs that this user might be closely tied to,
/// such as the owner of the current copy of Barotrauma
/// </summary>
#warning TODO: make ImmutableArray once feature/inetserializablestruct-improvements gets merged to dev
public readonly AccountId[] OtherMatchingIds;
public AccountInfo(AccountId accountId, params AccountId[] otherIds) : this(Option<AccountId>.Some(accountId), otherIds) { }
public AccountInfo(Option<AccountId> accountId, params AccountId[] otherIds)
{
AccountId = accountId;
OtherMatchingIds = otherIds.Where(id => !accountId.ValueEquals(id)).ToArray();
}
public bool Matches(AccountId accountId)
=> AccountId.ValueEquals(accountId) || OtherMatchingIds.Contains(accountId);
public override bool Equals(object? obj)
=> obj switch
{
AccountInfo otherInfo => AccountId == otherInfo.AccountId && OtherMatchingIds.All(otherInfo.OtherMatchingIds.Contains),
_ => false
};
public override int GetHashCode()
=> AccountId.GetHashCode();
public static bool operator ==(AccountInfo a, AccountInfo b)
=> a.Equals(b);
public static bool operator !=(AccountInfo a, AccountInfo b) => !(a == b);
}
}
@@ -0,0 +1,26 @@
#nullable enable
namespace Barotrauma.Networking
{
abstract class Address
{
public abstract string StringRepresentation { get; }
public static Option<Address> Parse(string str)
=> ReflectionUtils.ParseDerived<Address>(str);
public abstract bool IsLocalHost { get; }
public abstract override bool Equals(object? obj);
public abstract override int GetHashCode();
public override string ToString() => StringRepresentation;
public static bool operator ==(Address a, Address b)
=> a.Equals(b);
public static bool operator !=(Address a, Address b)
=> !(a == b);
}
}
@@ -0,0 +1,69 @@
#nullable enable
using System;
using System.Linq;
using System.Net;
using System.Net.Sockets;
namespace Barotrauma.Networking
{
sealed class LidgrenAddress : Address
{
public readonly IPAddress NetAddress;
public override string StringRepresentation
=> NetAddress.ToString();
public override bool IsLocalHost => IPAddress.IsLoopback(NetAddress);
public LidgrenAddress(IPAddress netAddress)
{
NetAddress = netAddress;
}
public new static Option<LidgrenAddress> Parse(string endpointStr)
{
if (IPAddress.TryParse(endpointStr, out IPAddress? netEndpoint))
{
return Option<LidgrenAddress>.Some(new LidgrenAddress(netEndpoint!));
}
try
{
var resolvedAddresses = Dns.GetHostAddresses(endpointStr);
return resolvedAddresses.Any()
? Option<LidgrenAddress>.Some(new LidgrenAddress(resolvedAddresses.First()))
: Option<LidgrenAddress>.None();
}
catch (SocketException)
{
return Option<LidgrenAddress>.None();
}
catch (ArgumentOutOfRangeException)
{
return Option<LidgrenAddress>.None();
}
}
public override bool Equals(object? obj)
=> obj switch
{
LidgrenAddress otherAddress => this == otherAddress,
_ => false
};
public override int GetHashCode()
=> NetAddress.GetHashCode();
public static bool operator ==(LidgrenAddress a, LidgrenAddress b)
{
var addressA = a.NetAddress.MapToIPv6();
var addressB = b.NetAddress.MapToIPv6();
if (IPAddress.IsLoopback(addressA) && IPAddress.IsLoopback(addressB)) { return true; }
return addressA.Equals(addressB);
}
public static bool operator !=(LidgrenAddress a, LidgrenAddress b)
=> !(a == b);
}
}
@@ -0,0 +1,22 @@
#nullable enable
namespace Barotrauma.Networking
{
sealed class PipeAddress : Address
{
public override string StringRepresentation => "PIPE";
public override bool IsLocalHost => true;
public override bool Equals(object? obj)
=> obj is PipeAddress;
public override int GetHashCode() => 1;
public static bool operator ==(PipeAddress a, PipeAddress b)
=> true;
public static bool operator !=(PipeAddress a, PipeAddress b)
=> !(a == b);
}
}
@@ -0,0 +1,37 @@
#nullable enable
namespace Barotrauma.Networking
{
sealed class SteamP2PAddress : Address
{
public readonly SteamId SteamId;
public override string StringRepresentation => SteamId.StringRepresentation;
public override bool IsLocalHost => false;
public SteamP2PAddress(SteamId steamId)
{
SteamId = steamId;
}
public new static Option<SteamP2PAddress> Parse(string endpointStr)
=> SteamId.Parse(endpointStr).Select(steamId => new SteamP2PAddress(steamId));
public override bool Equals(object? obj)
=> obj switch
{
SteamP2PAddress otherAddress => this == otherAddress,
_ => false
};
public override int GetHashCode()
=> SteamId.GetHashCode();
public static bool operator ==(SteamP2PAddress a, SteamP2PAddress b)
=> a.SteamId == b.SteamId;
public static bool operator !=(SteamP2PAddress a, SteamP2PAddress b)
=> !(a == b);
}
}
@@ -0,0 +1,16 @@
#nullable enable
namespace Barotrauma.Networking
{
sealed class UnknownAddress : Address
{
public override string StringRepresentation => "Hidden";
public override bool IsLocalHost => false;
public override bool Equals(object? obj)
=> ReferenceEquals(obj, this);
public override int GetHashCode() => 1;
}
}
@@ -0,0 +1,33 @@
#nullable enable
namespace Barotrauma.Networking
{
abstract class Endpoint
{
public abstract string StringRepresentation { get; }
public abstract LocalizedString ServerTypeString { get; }
public readonly Address Address;
public Endpoint(Address address)
{
Address = address;
}
public abstract override bool Equals(object? obj);
public abstract override int GetHashCode();
public override string ToString() => StringRepresentation;
public static Option<Endpoint> Parse(string str)
=> ReflectionUtils.ParseDerived<Endpoint>(str);
public static bool operator ==(Endpoint a, Endpoint b)
=> a.Equals(b);
public static bool operator !=(Endpoint a, Endpoint b)
=> !(a == b);
}
}
@@ -0,0 +1,63 @@
#nullable enable
using System.Linq;
using System.Net;
namespace Barotrauma.Networking
{
sealed class LidgrenEndpoint : Endpoint
{
public readonly IPEndPoint NetEndpoint;
public int Port => NetEndpoint.Port;
public override string StringRepresentation
=> NetEndpoint.ToString();
public override LocalizedString ServerTypeString { get; } = TextManager.Get("DedicatedServer");
public LidgrenEndpoint(IPAddress address, int port) : this(new IPEndPoint(address, port)) { }
public LidgrenEndpoint(IPEndPoint netEndpoint) : base(new LidgrenAddress(netEndpoint.Address))
{
NetEndpoint = netEndpoint;
}
public new static Option<LidgrenEndpoint> Parse(string endpointStr)
{
if (IPEndPoint.TryParse(endpointStr, out IPEndPoint? netEndpoint))
{
return Option<LidgrenEndpoint>.Some(new LidgrenEndpoint(netEndpoint!));
}
if (endpointStr.Count(c => c == ':') == 1)
{
string[] split = endpointStr.Split(':');
string hostName = split[0];
if (LidgrenAddress.Parse(hostName).TryUnwrap(out var adr)
&& int.TryParse(split[1], out var port))
{
return Option<LidgrenEndpoint>.Some(new LidgrenEndpoint(adr.NetAddress, port));
}
}
return LidgrenAddress.Parse(endpointStr)
.Select(adr => new LidgrenEndpoint(adr.NetAddress, NetConfig.DefaultPort));
}
public override bool Equals(object? obj)
=> obj switch
{
LidgrenEndpoint otherEndpoint => this == otherEndpoint,
_ => false
};
public override int GetHashCode()
=> NetEndpoint.GetHashCode();
public static bool operator ==(LidgrenEndpoint a, LidgrenEndpoint b)
=> a.Address.Equals(b.Address) && a.Port == b.Port;
public static bool operator !=(LidgrenEndpoint a, LidgrenEndpoint b)
=> !(a == b);
}
}
@@ -0,0 +1,37 @@
#nullable enable
namespace Barotrauma.Networking
{
sealed class SteamP2PEndpoint : Endpoint
{
public readonly SteamId SteamId;
public override string StringRepresentation => SteamId.StringRepresentation;
public override LocalizedString ServerTypeString { get; } = TextManager.Get("SteamP2PServer");
public SteamP2PEndpoint(SteamId steamId) : base(new SteamP2PAddress(steamId))
{
SteamId = steamId;
}
public new static Option<SteamP2PEndpoint> Parse(string endpointStr)
=> SteamId.Parse(endpointStr).Select(steamId => new SteamP2PEndpoint(steamId));
public override bool Equals(object? obj)
=> obj switch
{
SteamP2PEndpoint otherEndpoint => this == otherEndpoint,
_ => false
};
public override int GetHashCode()
=> SteamId.GetHashCode();
public static bool operator ==(SteamP2PEndpoint a, SteamP2PEndpoint b)
=> a.SteamId == b.SteamId;
public static bool operator !=(SteamP2PEndpoint a, SteamP2PEndpoint b)
=> !(a == b);
}
}
@@ -4,11 +4,12 @@ using System.Text;
namespace Barotrauma.Networking
{
public interface IReadMessage
interface IReadMessage
{
bool ReadBoolean();
void ReadPadBits();
byte ReadByte();
byte PeekByte();
UInt16 ReadUInt16();
Int16 ReadInt16();
UInt32 ReadUInt32();
@@ -2,7 +2,7 @@
namespace Barotrauma.Networking
{
public interface IWriteMessage
interface IWriteMessage
{
void Write(bool val);
void WritePadBits();
@@ -11,7 +11,7 @@ namespace Barotrauma.Networking
{
public static class MsgConstants
{
public const int MTU = 1200;
public const int MTU = 1200; //TODO: determine dynamically
public const int CompressionThreshold = 1000;
public const int InitialBufferSize = 256;
public const int BufferOverAllocateAmount = 4;
@@ -254,6 +254,12 @@ namespace Barotrauma.Networking
return retval;
}
internal static byte PeekByte(byte[] buf, ref int bitPos)
{
byte retval = NetBitWriter.ReadByte(buf, 8, bitPos);
return retval;
}
internal static UInt16 ReadUInt16(byte[] buf, ref int bitPos)
{
uint retval = NetBitWriter.ReadUInt16(buf, 16, bitPos);
@@ -409,7 +415,7 @@ namespace Barotrauma.Networking
}
}
public class WriteOnlyMessage : IWriteMessage
class WriteOnlyMessage : IWriteMessage
{
private byte[] buf = new byte[MsgConstants.InitialBufferSize];
private int seekPos = 0;
@@ -602,9 +608,9 @@ namespace Barotrauma.Networking
}
}
public class ReadOnlyMessage : IReadMessage
class ReadOnlyMessage : IReadMessage
{
private byte[] buf;
private readonly byte[] buf;
private int seekPos = 0;
private int lengthBits = 0;
@@ -720,6 +726,11 @@ namespace Barotrauma.Networking
return MsgReader.ReadByte(buf, ref seekPos);
}
public byte PeekByte()
{
return MsgReader.PeekByte(buf, ref seekPos);
}
public UInt16 ReadUInt16()
{
return MsgReader.ReadUInt16(buf, ref seekPos);
@@ -801,7 +812,7 @@ namespace Barotrauma.Networking
}
}
public class ReadWriteMessage : IWriteMessage, IReadMessage
class ReadWriteMessage : IWriteMessage, IReadMessage
{
private byte[] buf;
private int seekPos = 0;
@@ -983,6 +994,11 @@ namespace Barotrauma.Networking
return MsgReader.ReadByte(buf, ref seekPos);
}
public byte PeekByte()
{
return MsgReader.PeekByte(buf, ref seekPos);
}
public UInt16 ReadUInt16()
{
return MsgReader.ReadUInt16(buf, ref seekPos);
@@ -1067,5 +1083,6 @@ namespace Barotrauma.Networking
{
throw new InvalidOperationException("ReadWriteMessages are not to be sent");
}
}
}
@@ -1,55 +1,14 @@
using System;
using System.Net;
using Lidgren.Network;
using Lidgren.Network;
namespace Barotrauma.Networking
{
public class LidgrenConnection : NetworkConnection
sealed class LidgrenConnection : NetworkConnection
{
public NetConnection NetConnection { get; private set; }
public readonly NetConnection NetConnection;
public IPEndPoint IPEndPoint => NetConnection.RemoteEndPoint;
public string IPString
public LidgrenConnection(NetConnection netConnection) : base(new LidgrenEndpoint(netConnection.RemoteEndPoint))
{
get
{
return IPEndPoint.Address.IsIPv4MappedToIPv6 ? IPEndPoint.Address.MapToIPv4NoThrow().ToString() : IPEndPoint.Address.ToString();
}
}
public UInt16 Port
{
get
{
return (UInt16)IPEndPoint.Port;
}
}
public LidgrenConnection(string name, NetConnection netConnection, UInt64 steamId)
{
Name = name;
NetConnection = netConnection;
SteamID = steamId;
EndPointString = IPString;
}
public override bool SetSteamIDIfUnknown(UInt64 id)
{
if (SteamID != 0) { return false; } //do not allow the SteamID to be set multiple times
SteamID = id;
return true;
}
public override bool EndpointMatches(string endPoint)
{
if (IPEndPoint?.Address == null) { return false; }
if (!IPAddress.TryParse(endPoint, out IPAddress addr)) { return false; }
IPAddress ip1 = IPEndPoint.Address.IsIPv4MappedToIPv6 ? IPEndPoint.Address.MapToIPv4() : IPEndPoint.Address;
IPAddress ip2 = addr.IsIPv4MappedToIPv6 ? addr.MapToIPv4() : addr;
return ip1.ToString() == ip2.ToString();
}
}
}
@@ -8,57 +8,37 @@ namespace Barotrauma.Networking
Disconnected = 0x2
}
public abstract class NetworkConnection
abstract class NetworkConnection
{
public const double TimeoutThreshold = 60.0; //full minute for timeout because loading screens can take quite a while
public const double TimeoutThresholdInGame = 10.0;
public string Name;
public AccountInfo AccountInfo { get; private set; } = AccountInfo.None;
public UInt64 SteamID
{
get;
protected set;
}
public UInt64 OwnerSteamID
{
get;
protected set;
}
public string EndPointString
{
get;
protected set;
}
public readonly Endpoint Endpoint;
[Obsolete("TODO: this doesn't belong in layer 1")]
public LanguageIdentifier Language
{
get; set;
}
public abstract bool EndpointMatches(string endPoint);
public NetworkConnection(Endpoint endpoint)
{
Endpoint = endpoint;
}
public bool EndpointMatches(Endpoint endPoint)
=> Endpoint == endPoint;
public NetworkConnectionStatus Status = NetworkConnectionStatus.Disconnected;
public virtual bool SetSteamIDIfUnknown(UInt64 id)
public void SetAccountInfo(AccountInfo newInfo)
{
//by default, don't allow setting the ID, this is only done
//with Lidgren connections since those are initialized before
//the SteamID can be known; it's set once the Steam auth ticket
//is received by the server.
return false;
}
public bool SetOwnerSteamIDIfUnknown(UInt64 id)
{
//we know that for both Lidgren and SteamP2P, the
//owner id isn't known until the auth ticket is
//processed, so this method is the same for both
if (OwnerSteamID != 0) { return false; }
OwnerSteamID = id;
return true;
AccountInfo = newInfo;
}
public sealed override string ToString()
=> Endpoint.StringRepresentation;
}
}
@@ -1,19 +1,33 @@
using Barotrauma.Steam;
#nullable enable
using System;
namespace Barotrauma.Networking
{
public class PipeConnection : NetworkConnection
sealed class PipeEndpoint : Endpoint
{
public PipeConnection(ulong steamId)
{
EndPointString = "PIPE";
SteamID = steamId;
}
public override string StringRepresentation => "PIPE";
public override LocalizedString ServerTypeString => throw new InvalidOperationException();
public override bool EndpointMatches(string endPoint)
public PipeEndpoint() : base(new PipeAddress()) { }
public override bool Equals(object? obj)
=> obj is PipeEndpoint;
public override int GetHashCode() => 1;
public static bool operator ==(PipeEndpoint a, PipeEndpoint b)
=> true;
public static bool operator !=(PipeEndpoint a, PipeEndpoint b)
=> !(a == b);
}
sealed class PipeConnection : NetworkConnection
{
public PipeConnection(AccountId accountId) : base(new PipeEndpoint())
{
return SteamManager.SteamIDStringToUInt64(endPoint) == SteamID || endPoint == "PIPE";
SetAccountInfo(new AccountInfo(Option<AccountId>.Some(accountId)));
}
}
}
@@ -1,18 +1,13 @@
using Barotrauma.Steam;
using System;
namespace Barotrauma.Networking
namespace Barotrauma.Networking
{
public class SteamP2PConnection : NetworkConnection
sealed class SteamP2PConnection : NetworkConnection
{
public double Timeout = 0.0;
public SteamP2PConnection(string name, UInt64 steamId)
public SteamP2PConnection(SteamId steamId) : this(new SteamP2PEndpoint(steamId)) { }
public SteamP2PConnection(SteamP2PEndpoint endpoint) : base(endpoint)
{
SteamID = steamId;
OwnerSteamID = 0;
EndPointString = SteamManager.SteamIDUInt64ToString(SteamID);
Name = name;
Heartbeat();
}
@@ -25,10 +20,5 @@ namespace Barotrauma.Networking
{
Timeout = TimeoutThreshold;
}
public override bool EndpointMatches(string endPoint)
{
return SteamManager.SteamIDStringToUInt64(endPoint) == SteamID;
}
}
}
@@ -269,6 +269,7 @@ namespace Barotrauma.Networking
#endif
}
}
respawnItems.Clear();
foreach (Structure wall in Structure.WallList)
{
@@ -70,27 +70,18 @@ namespace Barotrauma.Networking
public class SavedClientPermission
{
public readonly string EndPoint;
public readonly ulong SteamID;
public readonly Either<Address, AccountId> AddressOrAccountId;
public readonly string Name;
public HashSet<DebugConsole.Command> PermittedCommands;
public readonly ImmutableHashSet<DebugConsole.Command> PermittedCommands;
public ClientPermissions Permissions;
public readonly ClientPermissions Permissions;
public SavedClientPermission(string name, string endpoint, ClientPermissions permissions, HashSet<DebugConsole.Command> permittedCommands)
public SavedClientPermission(string name, Either<Address, AccountId> addressOrAccountId, ClientPermissions permissions, IEnumerable<DebugConsole.Command> permittedCommands)
{
this.Name = name;
this.EndPoint = endpoint;
this.AddressOrAccountId = addressOrAccountId;
this.Permissions = permissions;
this.PermittedCommands = permittedCommands;
}
public SavedClientPermission(string name, ulong steamID, ClientPermissions permissions, HashSet<DebugConsole.Command> permittedCommands)
{
this.Name = name;
this.SteamID = steamID;
this.Permissions = permissions;
this.PermittedCommands = permittedCommands;
this.PermittedCommands = permittedCommands.ToImmutableHashSet();
}
}
@@ -279,7 +270,6 @@ namespace Barotrauma.Networking
{
ServerLog = new ServerLog(serverName);
Whitelist = new WhiteList();
BanList = new BanList();
ExtraCargo = new Dictionary<ItemPrefab, int>();
@@ -398,8 +388,6 @@ namespace Barotrauma.Networking
public List<SavedClientPermission> ClientPermissions { get; private set; } = new List<SavedClientPermission>();
public WhiteList Whitelist { get; private set; }
[Serialize(20, IsPropertySaveable.Yes)]
public int TickRate
{
@@ -1,12 +1,8 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
namespace Barotrauma.Networking
{
public class VoipQueue : IDisposable
class VoipQueue : IDisposable
{
public const int BUFFER_COUNT = 8;
protected int[] bufferLengths;
@@ -8,7 +8,7 @@ namespace Barotrauma
{
public enum VoteState { None = 0, Started = 1, Running = 2, Passed = 3, Failed = 4 };
private IReadOnlyDictionary<T, int> GetVoteCounts<T>(VoteType voteType, List<Client> voters)
private IReadOnlyDictionary<T, int> GetVoteCounts<T>(VoteType voteType, IEnumerable<Client> voters)
{
Dictionary<T, int> voteList = new Dictionary<T, int>();
@@ -57,7 +57,7 @@ namespace Barotrauma
return selected;
}
public void ResetVotes(List<Client> connectedClients)
public void ResetVotes(IEnumerable<Client> connectedClients)
{
foreach (Client client in connectedClients)
{
@@ -19,7 +19,9 @@ namespace Barotrauma
public static float DisplayToRealWorldRatio = 1.0f / 100.0f;
public const float DisplayToSimRation = 100.0f;
public const float DisplayToSimRation = 100.0f;
public const float NeutralDensity = 10.0f;
public static bool TryParseCollisionCategory(string categoryName, out Category category)
{
@@ -369,7 +369,7 @@ namespace Barotrauma
float radius = ConvertUnits.ToSimUnits(colliderParams.Radius) * colliderParams.Ragdoll.LimbScale;
float height = ConvertUnits.ToSimUnits(colliderParams.Height) * colliderParams.Ragdoll.LimbScale;
float width = ConvertUnits.ToSimUnits(colliderParams.Width) * colliderParams.Ragdoll.LimbScale;
density = 10;
density = Physics.NeutralDensity;
CreateBody(width, height, radius, density, BodyType.Dynamic,
Physics.CollisionCharacter,
Physics.CollisionWall | Physics.CollisionLevel,
@@ -417,7 +417,7 @@ namespace Barotrauma
float radius = ConvertUnits.ToSimUnits(element.GetAttributeFloat("radius", 0.0f)) * scale;
float height = ConvertUnits.ToSimUnits(element.GetAttributeFloat("height", 0.0f)) * scale;
float width = ConvertUnits.ToSimUnits(element.GetAttributeFloat("width", 0.0f)) * scale;
density = Math.Max(forceDensity ?? element.GetAttributeFloat("density", 10.0f), MinDensity);
density = Math.Max(forceDensity ?? element.GetAttributeFloat("density", Physics.NeutralDensity), MinDensity);
Enum.TryParse(element.GetAttributeString("bodytype", "Dynamic"), out BodyType bodyType);
CreateBody(width, height, radius, density, bodyType, collisionCategory, collidesWith, findNewContacts);
_collisionCategories = collisionCategory;
@@ -394,25 +394,6 @@ namespace Barotrauma
return val;
}
public static UInt64 GetAttributeSteamID(this XElement element, string name, UInt64 defaultValue)
{
var attribute = element?.GetAttribute(name);
if (attribute == null) { return defaultValue; }
UInt64 val = defaultValue;
try
{
val = Steam.SteamManager.SteamIDStringToUInt64(attribute.Value);
}
catch (Exception e)
{
DebugConsole.ThrowError("Error in " + element + "! ", e);
}
return val;
}
public static int[] GetAttributeIntArray(this XElement element, string name, int[] defaultValue)
{
var attribute = element?.GetAttribute(name);
@@ -119,6 +119,8 @@ namespace Barotrauma
if (!HasRequiredConditions(currentTargets)) { return; }
if (Entity.Spawner != null && Entity.Spawner.IsInRemoveQueue(entity)) { return; }
switch (delayType)
{
case DelayTypes.Timer:
@@ -8,7 +8,6 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Networking;
namespace Barotrauma
{
@@ -158,11 +157,11 @@ namespace Barotrauma
/// </summary>
public readonly bool SpawnIfCantBeContained;
public readonly float Impulse;
public readonly float Rotation;
public readonly float RotationRad;
public readonly int Count;
public readonly float Spread;
public readonly SpawnRotationType RotationType;
public readonly float AimSpread;
public readonly float AimSpreadRad;
public readonly bool Equip;
public readonly float Condition;
@@ -198,14 +197,14 @@ namespace Barotrauma
SpawnIfInventoryFull = element.GetAttributeBool("spawnifinventoryfull", false);
SpawnIfCantBeContained = element.GetAttributeBool("spawnifcantbecontained", true);
Impulse = element.GetAttributeFloat("impulse", element.GetAttributeFloat("speed", 0.0f));
Impulse = element.GetAttributeFloat("impulse", element.GetAttributeFloat("launchimpulse", element.GetAttributeFloat("speed", 0.0f)));
Condition = MathHelper.Clamp(element.GetAttributeFloat("condition", 1.0f), 0.0f, 1.0f);
Rotation = element.GetAttributeFloat("rotation", 0.0f);
RotationRad = MathHelper.ToRadians(element.GetAttributeFloat("rotation", 0.0f));
Count = element.GetAttributeInt("count", 1);
Spread = element.GetAttributeFloat("spread", 0f);
AimSpread = element.GetAttributeFloat("aimspread", 0f);
AimSpreadRad = MathHelper.ToRadians(element.GetAttributeFloat("aimspread", 0f));
Equip = element.GetAttributeBool("equip", false);
string spawnTypeStr = element.GetAttributeString("spawnposition", "This");
@@ -213,7 +212,7 @@ namespace Barotrauma
{
DebugConsole.ThrowError("Error in StatusEffect config - \"" + spawnTypeStr + "\" is not a valid spawn position.");
}
string rotationTypeStr = element.GetAttributeString("rotationtype", Rotation != 0 ? "Fixed" : "Target");
string rotationTypeStr = element.GetAttributeString("rotationtype", RotationRad != 0 ? "Fixed" : "Target");
if (!Enum.TryParse(rotationTypeStr, ignoreCase: true, out RotationType))
{
DebugConsole.ThrowError("Error in StatusEffect config - \"" + rotationTypeStr + "\" is not a valid rotation type.");
@@ -1668,11 +1667,11 @@ namespace Barotrauma
Character.Controlled = newCharacter;
}
#elif SERVER
foreach (Client c in GameMain.Server.ConnectedClients)
/*foreach (Client c in GameMain.Server.ConnectedClients)
{
if (c.Character != target) { continue; }
GameMain.Server.SetClientCharacter(c, newCharacter);
}
}*/
#endif
}
if (characterSpawnInfo.RemovePreviousCharacter) { Entity.Spawner?.AddEntityToRemoveQueue(character); }
@@ -1705,92 +1704,101 @@ namespace Barotrauma
case ItemSpawnInfo.SpawnPositionType.This:
Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position + Rand.Vector(chosenItemSpawnInfo.Spread, Rand.RandSync.Unsynced), onSpawned: newItem =>
{
Item parentItem = entity as Item;
Projectile projectile = newItem.GetComponent<Projectile>();
if (projectile != null && user != null && sourceBody != null && entity != null)
if (entity != null)
{
var rope = newItem.GetComponent<Rope>();
if (rope != null && sourceBody.UserData is Limb sourceLimb)
if (rope != null && sourceBody != null && sourceBody.UserData is Limb sourceLimb)
{
rope.Attach(sourceLimb, newItem);
#if SERVER
newItem.CreateServerEvent(rope);
#endif
}
float spread = MathHelper.ToRadians(Rand.Range(-chosenItemSpawnInfo.AimSpread, chosenItemSpawnInfo.AimSpread));
var worldPos = sourceBody.Position;
float rotation = 0;
if (user.Submarine != null)
float spread = Rand.Range(-chosenItemSpawnInfo.AimSpreadRad, chosenItemSpawnInfo.AimSpreadRad);
float rotation = chosenItemSpawnInfo.RotationRad;
Vector2 worldPos;
if (sourceBody != null)
{
worldPos += user.Submarine.Position;
worldPos = sourceBody.Position;
if (user?.Submarine != null)
{
worldPos += user.Submarine.Position;
}
}
else
{
worldPos = entity.WorldPosition;
}
switch (chosenItemSpawnInfo.RotationType)
{
case ItemSpawnInfo.SpawnRotationType.Fixed:
rotation = sourceBody.TransformRotation(chosenItemSpawnInfo.Rotation);
if (sourceBody != null)
{
rotation = sourceBody.TransformRotation(chosenItemSpawnInfo.RotationRad);
}
else if (parentItem?.body != null)
{
rotation = parentItem.body.TransformRotation(chosenItemSpawnInfo.RotationRad);
}
break;
case ItemSpawnInfo.SpawnRotationType.Target:
rotation = MathUtils.VectorToAngle(entity.WorldPosition - worldPos);
break;
case ItemSpawnInfo.SpawnRotationType.Limb:
rotation = sourceBody.TransformedRotation;
if (sourceBody != null)
{
rotation = sourceBody.TransformedRotation;
}
break;
case ItemSpawnInfo.SpawnRotationType.Collider:
rotation = user.AnimController.Collider.Rotation + MathHelper.PiOver2;
if (parentItem?.body != null)
{
rotation = parentItem.body.Rotation;
}
else if (user != null)
{
rotation = user.AnimController.Collider.Rotation + MathHelper.PiOver2;
}
break;
case ItemSpawnInfo.SpawnRotationType.MainLimb:
rotation = user.AnimController.MainLimb.body.TransformedRotation;
if (user != null)
{
rotation = user.AnimController.MainLimb.body.TransformedRotation;
}
break;
case ItemSpawnInfo.SpawnRotationType.Random:
DebugConsole.ShowError("Random rotation is not supported for Projectiles.");
if (projectile != null)
{
DebugConsole.ShowError("Random rotation is not supported for Projectiles.");
}
else
{
rotation = Rand.Range(0f, MathHelper.TwoPi, Rand.RandSync.Unsynced);
}
break;
default:
throw new NotImplementedException("Projectile spawn rotation type not implemented: " + chosenItemSpawnInfo.RotationType);
throw new NotImplementedException("Item spawn rotation type not implemented: " + chosenItemSpawnInfo.RotationType);
}
rotation += MathHelper.ToRadians(chosenItemSpawnInfo.Rotation * user.AnimController.Dir);
projectile.Shoot(user, ConvertUnits.ToSimUnits(worldPos), ConvertUnits.ToSimUnits(worldPos), rotation + spread, ignoredBodies: user.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: true);
}
else
{
var body = newItem.body;
if (body != null)
if (user != null)
{
float rotation = MathHelper.ToRadians(chosenItemSpawnInfo.Rotation);
switch (chosenItemSpawnInfo.RotationType)
{
case ItemSpawnInfo.SpawnRotationType.Fixed:
if (sourceBody != null)
{
rotation = sourceBody.TransformRotation(chosenItemSpawnInfo.Rotation);
}
break;
case ItemSpawnInfo.SpawnRotationType.Limb:
if (sourceBody != null)
{
rotation += sourceBody.Rotation;
}
break;
case ItemSpawnInfo.SpawnRotationType.Collider:
if (entity is Character character)
{
rotation += character.AnimController.Collider.Rotation + MathHelper.PiOver2;
}
break;
case ItemSpawnInfo.SpawnRotationType.MainLimb:
if (entity is Character c)
{
rotation = c.AnimController.MainLimb.body.TransformedRotation;
}
break;
case ItemSpawnInfo.SpawnRotationType.Random:
rotation = Rand.Range(0f, MathHelper.TwoPi, Rand.RandSync.Unsynced);
break;
case ItemSpawnInfo.SpawnRotationType.Target:
break;
default:
throw new NotImplementedException("Spawn rotation type not implemented: " + chosenItemSpawnInfo.RotationType);
}
body.SetTransform(newItem.SimPosition, rotation);
body.ApplyLinearImpulse(Rand.Vector(1) * chosenItemSpawnInfo.Impulse);
rotation += chosenItemSpawnInfo.RotationRad * user.AnimController.Dir;
}
rotation += spread;
if (projectile != null)
{
projectile.Shoot(user,
ConvertUnits.ToSimUnits(worldPos),
ConvertUnits.ToSimUnits(worldPos),
rotation,
ignoredBodies: user?.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: true);
}
else if (newItem.body != null)
{
newItem.body.SetTransform(newItem.SimPosition, rotation);
Vector2 impulseDir = new Vector2(MathF.Cos(rotation), MathF.Sin(rotation));
newItem.body.ApplyLinearImpulse(impulseDir * chosenItemSpawnInfo.Impulse);
}
}
newItem.Condition = newItem.MaxCondition * chosenItemSpawnInfo.Condition;
@@ -2082,7 +2090,7 @@ namespace Barotrauma
if (!MathUtils.NearlyEqual(afflictionMultiplier, 1.0f))
{
return affliction.CreateMultiplied(afflictionMultiplier);
return affliction.CreateMultiplied(afflictionMultiplier, affliction.Probability);
}
return affliction;
}
@@ -2,6 +2,7 @@ using Steamworks.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Networking;
namespace Barotrauma.Steam
{
@@ -42,14 +43,14 @@ namespace Barotrauma.Steam
InitializeProjectSpecific();
}
public static ulong GetSteamID()
public static Option<SteamId> GetSteamId()
{
if (!IsInitialized || !Steamworks.SteamClient.IsValid)
{
return 0;
return Option<SteamId>.None();
}
return Steamworks.SteamClient.SteamId;
return Option<SteamId>.Some(new SteamId(Steamworks.SteamClient.SteamId));
}
public static bool IsFamilyShared()
@@ -249,37 +250,5 @@ namespace Barotrauma.Steam
return 0;
}
public static UInt64 SteamIDStringToUInt64(string str)
{
if (string.IsNullOrWhiteSpace(str)) { return 0; }
UInt64 retVal;
if (str.StartsWith("STEAM64_", StringComparison.InvariantCultureIgnoreCase)) { str = str.Substring(8); }
if (UInt64.TryParse(str, out retVal) && retVal > (1 << 52)) { return retVal; }
if (!str.StartsWith("STEAM_", StringComparison.InvariantCultureIgnoreCase)) { return 0; }
string[] split = str.Substring(6).Split(':');
if (split.Length != 3) { return 0; }
if (!UInt64.TryParse(split[0], out UInt64 universe)) { return 0; }
if (!UInt64.TryParse(split[1], out UInt64 y)) { return 0; }
if (!UInt64.TryParse(split[2], out UInt64 accountNumber)) { return 0; }
UInt64 accountInstance = 1; UInt64 accountType = 1;
return (universe << 56) | (accountType << 52) | (accountInstance << 32) | (accountNumber << 1) | y;
}
public static string SteamIDUInt64ToString(UInt64 uint64)
{
UInt64 y = uint64 & 0x1;
UInt64 accountNumber = (uint64 >> 1) & 0x7fffffff;
UInt64 universe = (uint64 >> 56) & 0xff;
string retVal = "STEAM_" + universe.ToString() + ":" + y.ToString() + ":" + accountNumber.ToString();
if (SteamIDStringToUInt64(retVal) != uint64) { return "STEAM64_" + uint64.ToString(); }
return retVal;
}
}
}
@@ -181,7 +181,7 @@ namespace Barotrauma
public static bool operator !=(string? a, RichString? b) => !(a == b);
}
public class StripRichTagsLString : LocalizedString
class StripRichTagsLString : LocalizedString
{
public readonly RichString RichStr;
@@ -1,8 +1,9 @@
#nullable enable
using System;
namespace Barotrauma
{
public abstract class Either<T, U>
public abstract class Either<T, U> where T : notnull where U : notnull
{
public static implicit operator Either<T, U>(T t) => new EitherT<T, U>(t);
public static implicit operator Either<T, U>(U u) => new EitherU<T, U>(u);
@@ -15,22 +16,32 @@ namespace Barotrauma
public abstract bool TryCast<V>(out V v);
public abstract override string ToString();
public abstract override string? ToString();
public abstract override bool Equals(object? obj);
public abstract override int GetHashCode();
public static bool operator ==(Either<T, U>? a, Either<T, U>? b)
=> a is null ? b is null : a.Equals(b);
public static bool operator !=(Either<T, U>? a, Either<T, U>? b)
=> !(a == b);
}
public sealed class EitherT<T, U> : Either<T, U>
public sealed class EitherT<T, U> : Either<T, U> where T : notnull where U : notnull
{
public readonly T Value;
public EitherT(T value) { Value = value; }
public override string ToString()
public override string? ToString()
{
return Value.ToString();
}
public override bool TryGet(out T t) { t = Value; return true; }
public override bool TryGet(out U u) { u = default; return false; }
public override bool TryGet(out U u) { u = default!; return false; }
public override bool TryCast<V>(out V v)
{
@@ -41,24 +52,34 @@ namespace Barotrauma
}
else
{
v = default;
v = default!;
return false;
}
}
public override bool Equals(object? obj)
=> obj switch
{
EitherT<T, U> other => Value.Equals(other.Value),
T value => Value.Equals(value),
_ => false
};
public override int GetHashCode() => Value.GetHashCode();
}
public sealed class EitherU<T, U> : Either<T, U>
public sealed class EitherU<T, U> : Either<T, U> where T : notnull where U : notnull
{
public readonly U Value;
public EitherU(U value) { Value = value; }
public override string ToString()
public override string? ToString()
{
return Value.ToString();
}
public override bool TryGet(out T t) { t = default; return false; }
public override bool TryGet(out T t) { t = default!; return false; }
public override bool TryGet(out U u) { u = Value; return true; }
public override bool TryCast<V>(out V v)
@@ -70,9 +91,19 @@ namespace Barotrauma
}
else
{
v = default;
v = default!;
return false;
}
}
public override bool Equals(object? obj)
=> obj switch
{
EitherU<T, U> other => Value.Equals(other.Value),
U value => Value.Equals(value),
_ => false
};
public override int GetHashCode() => Value.GetHashCode();
}
}
@@ -5,5 +5,13 @@ namespace Barotrauma
private None() { }
public static Option<T> Create() => new None<T>();
public override Option<T> Fallback(Option<T> fallback) => fallback;
public override T Fallback(T fallback) => fallback;
public override bool ValueEquals(T value) => false;
public override string ToString()
=> $"None<{typeof(T).Name}>";
}
}
@@ -1,3 +1,4 @@
#nullable enable
using System;
namespace Barotrauma
@@ -23,7 +24,7 @@ namespace Barotrauma
outValue = value;
return true;
case None<T> _:
outValue = default;
outValue = default!;
return false;
default:
throw new ArgumentOutOfRangeException();
@@ -37,5 +38,30 @@ namespace Barotrauma
None<T> _ => Option<TType>.None(),
_ => throw new ArgumentOutOfRangeException()
};
public abstract Option<T> Fallback(Option<T> fallback);
public abstract T Fallback(T fallback);
public abstract bool ValueEquals(T value);
public override bool Equals(object? obj)
=> obj switch
{
Some<T> { Value: var value } => this is Some<T> { Value: { } selfValue } && selfValue.Equals(value),
None<T> _ => IsNone(),
T value => this is Some<T> { Value: { } selfValue } && selfValue.Equals(value),
_ => false
};
public override int GetHashCode()
=> this is Some<T> { Value: { } value } ? value.GetHashCode() : 0;
public static bool operator ==(Option<T> a, Option<T> b)
=> a.Equals(b);
public static bool operator !=(Option<T> a, Option<T> b)
=> !(a == b);
public abstract override string ToString();
}
}
@@ -13,5 +13,13 @@ namespace Barotrauma
}
public static Option<T> Create(T value) => new Some<T>(value);
public override Option<T> Fallback(Option<T> fallback) => this;
public override T Fallback(T fallback) => Value;
public override bool ValueEquals(T value) => Value.Equals(value);
public override string ToString()
=> $"Some<{typeof(T).Name}>({Value})";
}
}
@@ -1,5 +1,7 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
@@ -7,14 +9,52 @@ namespace Barotrauma
{
public static class ReflectionUtils
{
private static Type[] cachedNonAbstractTypes;
private static readonly Dictionary<Assembly, ImmutableArray<Type>> cachedNonAbstractTypes
= new Dictionary<Assembly, ImmutableArray<Type>>();
public static IEnumerable<Type> GetDerivedNonAbstract<T>()
{
if (cachedNonAbstractTypes == null)
Assembly assembly = typeof(T).Assembly;
if (!cachedNonAbstractTypes.ContainsKey(assembly))
{
cachedNonAbstractTypes = Assembly.GetEntryAssembly().GetTypes().Where(t => !t.IsAbstract).ToArray();
cachedNonAbstractTypes[assembly] = assembly.GetTypes()
.Where(t => !t.IsAbstract).ToImmutableArray();
}
return cachedNonAbstractTypes.Where(t => t.IsSubclassOf(typeof(T)));
return cachedNonAbstractTypes[assembly].Where(t => t.IsSubclassOf(typeof(T)));
}
public static Option<T1> ParseDerived<T1>(string str)
{
static Option<T1> none() => Option<T1>.None();
var derivedTypes = GetDerivedNonAbstract<T1>();
Option<T1> parseOfType(Type t)
{
//every T1 type is expected to have a method with the following signature:
// public static Option<T> Parse(string str)
var parseFunc = t.GetMethod("Parse", BindingFlags.Public | BindingFlags.Static);
if (parseFunc is null) { return none(); }
var parameters = parseFunc.GetParameters();
if (parameters.Length != 1) { return none(); }
var returnType = parseFunc.ReturnType;
if (!returnType.IsConstructedGenericType) { return none(); }
if (returnType.GetGenericTypeDefinition() != typeof(Option<>)) { return none(); }
if (returnType.GenericTypeArguments[0] != t) { return none(); }
//some hacky business to convert from Option<T2> to Option<T1> when we only know T2 at runtime
static Option<T1> convert<T2>(Option<T2> option) where T2 : T1
=> option.Select(v => (T1)v);
Func<Option<T1>, Option<T1>> f = convert;
var constructedConverter = f.Method.GetGenericMethodDefinition().MakeGenericMethod(typeof(T1), t);
return constructedConverter.Invoke(null, new object?[] { parseFunc.Invoke(null, new object[] { str }) })
as Option<T1> ?? none();
}
return derivedTypes.Select(parseOfType).FirstOrDefault(t => t.IsSome()) ?? none();
}
}
}
@@ -4,8 +4,10 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
#if CLIENT
using Barotrauma.Networking;
using Barotrauma.Steam;
#endif
namespace Barotrauma.IO
{
@@ -26,14 +26,14 @@ namespace Barotrauma
}
}
public static partial class ToolBox
static partial class ToolBox
{
static internal class Epoch
internal static class Epoch
{
private static readonly DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
/// <summary>
/// Returns the current Unix Epoch (Coordinated Universal Time )
/// Returns the current Unix Epoch (Coordinated Universal Time)
/// </summary>
public static int NowUTC
{
@@ -712,5 +712,10 @@ namespace Barotrauma
return e;
}
public static void ThrowIfNull<T>(T o)
{
if (o is null) { throw new ArgumentNullException(); }
}
}
}