OBT/1.2.1(Summer update)

Sync with upstream
This commit is contained in:
NotAlwaysTrue
2026-06-16 22:17:29 +08:00
committed by GitHub
parent 59bc21973a
commit d109c8f827
85 changed files with 1377 additions and 497 deletions
@@ -524,8 +524,7 @@ namespace Barotrauma
{
UnlockAchievement(causeOfDeath.Killer, "killpoison".ToIdentifier());
}
else if (item.Prefab.Identifier == "nuclearshell" ||
item.Prefab.Identifier == "nucleardepthcharge")
else if (item.Prefab.Tags.Contains("nuclearexplosive"))
{
UnlockAchievement(causeOfDeath.Killer, "killnuke".ToIdentifier());
}
@@ -197,6 +197,18 @@ namespace Barotrauma
private readonly List<Body> myBodies;
#if SERVER
/// <summary>
/// How often the server can send messages about a limb targeting some attack target.
/// Mainly relevant for attacks with no cooldown, e.g. fractal guardian's steam cannons which run continuously over time (we can't send events every frame)
/// </summary>
private const double MinSetAttackTargetEventInterval = 0.5;
private IDamageable lastDamageTarget;
private Limb lastTargetLimb;
private Limb lastAttackLimb;
private double lastSetAttackTargetEventTime;
#endif
public LatchOntoAI LatchOntoAI { get; private set; }
public SwarmBehavior SwarmBehavior { get; private set; }
public PetBehavior PetBehavior { get; private set; }
@@ -2679,11 +2691,19 @@ namespace Barotrauma
if (!ActiveAttack.IsRunning)
{
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData(
AttackLimb,
damageTarget,
targetLimb,
SimPosition));
if (Timing.TotalTime > lastSetAttackTargetEventTime + MinSetAttackTargetEventInterval ||
damageTarget != lastDamageTarget || AttackLimb != lastAttackLimb || targetLimb != lastTargetLimb)
{
GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData(
AttackLimb,
damageTarget,
targetLimb,
SimPosition));
lastSetAttackTargetEventTime = Timing.TotalTime;
lastDamageTarget = damageTarget;
lastAttackLimb = AttackLimb;
lastTargetLimb = targetLimb;
}
#else
Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
#endif
@@ -46,7 +46,7 @@ namespace Barotrauma
private float respondToAttackTimer;
private const float RespondToAttackInterval = 1.0f;
private bool wasConscious;
private bool wasDead;
private bool freezeAI;
@@ -201,7 +201,7 @@ namespace Barotrauma
}
if (isIncapacitated) { return; }
wasConscious = true;
wasDead = false;
respondToAttackTimer -= deltaTime;
if (respondToAttackTimer <= 0.0f)
@@ -1256,14 +1256,15 @@ namespace Barotrauma
public override void OnAttacked(Character attacker, AttackResult attackResult)
{
// The attack incapacitated/killed the character: respond immediately to trigger nearby characters because the update loop no longer runs
if (wasConscious && (Character.IsIncapacitated || Character.Stun > 0.0f))
// If the character is incapacitated or dead, respond to the attack anyway to let other nearby characters react to it
// (But if the character is already dead, and was dead before this attack, don't react)
if (Character.IsDead && wasDead) { return; }
if (Character.IsIncapacitated || Character.Stun > 0.0f)
{
RespondToAttack(attacker, attackResult);
wasConscious = false;
wasDead = Character.IsDead;
return;
}
if (Character.IsDead) { return; }
if (attacker == null || Character.IsPlayer)
{
// The player characters need to "respond" to the attack always, because the update loop doesn't run for them.
@@ -1467,10 +1468,10 @@ namespace Barotrauma
otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull) ||
otherCharacter.CanSeeTarget(attacker, seeThroughWindows: true);
if (!isWitnessing)
{
if (Character.IsDead || Character.IsUnconscious || otherCharacter.TeamID != Character.TeamID)
{
if (Character.IsKnockedDown || otherCharacter.TeamID != Character.TeamID)
{
// Dead or in different team -> cannot report.
// Knocked down or in different team -> cannot report.
continue;
}
if (otherHumanAI.objectiveManager.HasOrders())
@@ -1494,6 +1495,14 @@ namespace Barotrauma
continue;
}
}
else if (!otherCharacter.IsSecurity)
{
//witnessed the attack as non-security, trigger security
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID))
{
TriggerSecurity(security.AIController as HumanAIController, attacker, DetermineCombatMode(security, cumulativeDamage, isWitnessing));
}
}
float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 3.0f, Rand.RandSync.Unsynced);
otherHumanAI.AddCombatObjective(combatMode, attacker, delay);
}
@@ -1926,12 +1935,12 @@ namespace Barotrauma
character.IsCriminal = true;
character.IsActingOffensively = true;
}
if (!TriggerSecurity(otherHumanAI, combatMode))
if (!TriggerSecurity(otherHumanAI, character, combatMode))
{
// Else call the others
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderBy(c => Vector2.DistanceSquared(character.WorldPosition, c.WorldPosition)))
{
if (!TriggerSecurity(security.AIController as HumanAIController, combatMode))
if (!TriggerSecurity(security.AIController as HumanAIController, character, combatMode))
{
// Only alert one guard at a time
return;
@@ -1941,25 +1950,25 @@ namespace Barotrauma
}
}
bool TriggerSecurity(HumanAIController humanAI, AIObjectiveCombat.CombatMode combatMode)
{
if (humanAI == null) { return false; }
if (!humanAI.Character.IsSecurity) { return false; }
if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; }
humanAI.AddCombatObjective(combatMode, character, delay: GetReactionTime(),
onCompleted: () =>
{
//if the target is arrested successfully, reset the damage accumulator
foreach (Character anyCharacter in Character.CharacterList)
}
private static bool TriggerSecurity(HumanAIController humanAI, Character targetCharacter, AIObjectiveCombat.CombatMode combatMode)
{
if (humanAI == null) { return false; }
if (!humanAI.Character.IsSecurity) { return false; }
if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; }
humanAI.AddCombatObjective(combatMode, targetCharacter, delay: GetReactionTime(),
onCompleted: () =>
{
//if the target is arrested successfully, reset the damage accumulator
foreach (Character anyCharacter in Character.CharacterList)
{
if (anyCharacter.AIController is HumanAIController anyAI)
{
if (anyCharacter.AIController is HumanAIController anyAI)
{
anyAI.structureDamageAccumulator?.Remove(character);
}
anyAI.structureDamageAccumulator?.Remove(targetCharacter);
}
});
return true;
}
}
});
return true;
}
public static void ItemTaken(Item item, Character thief)
@@ -133,7 +133,7 @@ namespace Barotrauma
bool operateExtinguisher = !moveCloser || (dist < extinguisher.Range * 1.2f && character.CanSeeTarget(targetHull));
if (operateExtinguisher)
{
character.CursorPosition = fs.Position;
character.CursorPosition = FarseerPhysics.ConvertUnits.ToDisplayUnits(Submarine.GetRelativeSimPositionFromWorldPosition(fs.WorldPosition, character.Submarine, fs.Submarine));
Vector2 fromCharacterToFireSource = fs.WorldPosition - character.WorldPosition;
character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, fromCharacterToFireSource.Length() / 2);
if (extinguisherItem.RequireAimToUse)
@@ -173,6 +173,16 @@ namespace Barotrauma
if (character.CanInteractWith(Item, out _, checkLinked: false))
{
waitTimer += deltaTime;
//if we're climbing upwards to the item, ensure the character stays within arm's length of it
//without this, the character can get stuck in a loop where the GoTo objective takes them close enough to the item,
//then the character shifts a bit downwards on the ladder and goes outside interaction range, and the GoTo objective kicks in again
if (character.IsClimbing &&
Item.WorldPosition.Y > character.WorldPosition.Y + FarseerPhysics.ConvertUnits.ToDisplayUnits(character.AnimController.ArmLength))
{
character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.UnitY);
}
if (waitTimer < WaitTimeBeforeRepair) { return; }
HumanAIController.FaceTarget(Item);
@@ -758,6 +758,11 @@ namespace Barotrauma
public float CoolDownTimer { get; set; }
public float CurrentRandomCoolDown { get; private set; }
public float SecondaryCoolDownTimer { get; set; }
/// <summary>
/// The attack is considered to be running from the moment it starts until the <see cref="AttackTimer"/> reaches the <see cref="Duration"/> of the attack, or until the attack lands successfully.
/// E.g. from the moment the monster decides to lunge itself towards the target until it hits a target or until it completes that lunge.
/// </summary>
public bool IsRunning { get; private set; }
public float AfterAttackTimer { get; set; }
@@ -997,6 +997,9 @@ namespace Barotrauma
public bool IsForceRagdolled;
public bool FollowCursor = true;
/// <summary>
/// Is the character currently dead, unconscious or paralyzed?
/// </summary>
public bool IsIncapacitated
{
get
@@ -1006,6 +1009,9 @@ namespace Barotrauma
}
}
/// <summary>
/// Is the character dead or below 0 vitality and not able to stay conscious?
/// </summary>
public bool IsUnconscious
{
get { return CharacterHealth.IsUnconscious; }
@@ -1673,7 +1679,8 @@ namespace Barotrauma
AnimController.FindHull(setInWater: true);
if (AnimController.CurrentHull != null) { Submarine = AnimController.CurrentHull.Submarine; }
IsContainable = prefab.ConfigElement.GetAttributeBool(nameof(IsContainable), def: Mass <= 30.0f);
//mass < 35 = husk chimera is the largest vanilla monster that can be contained by default
IsContainable = prefab.ConfigElement.GetAttributeBool(nameof(IsContainable), def: Mass < 35.0f);
CharacterList.Add(this);
@@ -2262,7 +2269,10 @@ namespace Barotrauma
{
Vector2 targetMovement = GetTargetMovement();
AnimController.TargetMovement = targetMovement;
AnimController.IgnorePlatforms = AnimController.TargetMovement.Y < -0.1f;
if (SelectedItem?.GetComponent<Controller>() is not { ControlCharacterPose: true })
{
AnimController.IgnorePlatforms = AnimController.TargetMovement.Y < -0.1f;
}
}
if (AnimController is HumanoidAnimController humanAnimController)
@@ -3508,9 +3518,11 @@ namespace Barotrauma
UpdateAttackers(deltaTime);
foreach (var characterTalent in characterTalents)
//use a for loop instead of foreach because talents can unlock other talents via StatusEffectAction (see #17328)
//this way we'll just add them to the end of the list without causing a collection was modified exception
for (int i = 0; i < characterTalents.Count; i++)
{
characterTalent.UpdateTalent(deltaTime);
characterTalents[i].UpdateTalent(deltaTime);
}
if (IsDead) { return; }
@@ -5801,6 +5813,12 @@ namespace Barotrauma
return info.UnlockedTalents.Contains(identifier);
}
public bool IsTalentLocked(Identifier talentIdentifier)
{
if (info == null) { return true; }
return Info.GetSavedStatValue(StatTypes.LockedTalents, talentIdentifier) >= 1;
}
public bool HasUnlockedAllTalents()
{
if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree))
@@ -1022,6 +1022,15 @@ namespace Barotrauma
partial void UpdateProjSpecific(float deltaTime);
#if SERVER
/// <summary>
/// How often the server can send messages about attacks being executed. Note that the timer is per-limb: if one limb executes an attack immediately after another, an network event can still be created.
/// Mainly relevant for attacks with no cooldown, e.g. fractal guardian's steam cannons which run continuously over time (we can't send events every frame)
/// </summary>
private const double MinExecuteAttackEventInterval = 0.5f;
private double lastExecuteAttackEventTime;
#endif
private readonly List<Body> contactBodies = new List<Body>();
/// <summary>
/// Returns true if the attack successfully hit something. If the distance is not given, it will be calculated.
@@ -1142,9 +1151,13 @@ namespace Barotrauma
ExecuteAttack(damageTarget, targetLimb, out attackResult);
}
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(character, new Character.ExecuteAttackEventData(
attackLimb: this, targetEntity: damageTarget, targetLimb: targetLimb,
targetSimPos: attackSimPos));
if (Timing.TotalTime > lastExecuteAttackEventTime + MinExecuteAttackEventInterval)
{
GameMain.NetworkMember.CreateEntityEvent(character, new Character.ExecuteAttackEventData(
attackLimb: this, targetEntity: damageTarget, targetLimb: targetLimb,
targetSimPos: attackSimPos));
lastExecuteAttackEventTime = Timing.TotalTime;
}
#endif
}
@@ -43,14 +43,14 @@ namespace Barotrauma.Abilities
{
foreach (Identifier identifier in option.TalentIdentifiers)
{
if (IsShowCaseTalent(identifier, option) || TalentTree.IsTalentLocked(identifier, characters)) { continue; }
if (IsShowCaseTalent(identifier, option) || Character.IsTalentLocked(identifier)) { continue; }
identifiers.Add(identifier);
}
foreach (var (_, value) in option.ShowCaseTalents)
{
var ids = value.Where(i => !TalentTree.IsTalentLocked(i, characters)).ToImmutableHashSet();
var ids = value.Where(i => !Character.IsTalentLocked(i)).ToImmutableHashSet();
if (ids.Count is 0) { continue; }
identifiers.Add(value.GetRandomUnsynced());
@@ -131,7 +131,7 @@ namespace Barotrauma
if (character.Info.GetTotalTalentPoints() - selectedTalents.Count <= 0) { return false; }
if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; }
if (IsTalentLocked(talentIdentifier, Character.GetFriendlyCrew(character))) { return false; }
if (character.IsTalentLocked(talentIdentifier)) { return false; }
if (character.Info.GetUnlockedTalentsInTree().Contains(talentIdentifier))
{
@@ -163,16 +163,6 @@ namespace Barotrauma
return false;
}
public static bool IsTalentLocked(Identifier talentIdentifier, IEnumerable<Character> characterList)
{
foreach (Character c in characterList)
{
if (c.Info.GetSavedStatValue(StatTypes.LockedTalents, talentIdentifier) >= 1) { return true; }
}
return false;
}
public static List<Identifier> CheckTalentSelection(Character controlledCharacter, IEnumerable<Identifier> selectedTalents)
{
List<Identifier> viableTalents = new List<Identifier>();
@@ -252,7 +252,7 @@ namespace Barotrauma
GameMain.NetworkMember.ShowNetStats = !GameMain.NetworkMember.ShowNetStats;
}));
commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team] [add to crew (true/false)]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null,
commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team] [add to crew (true/false)] [name]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null,
() =>
{
string[] creatureAndJobNames =
@@ -271,7 +271,7 @@ namespace Barotrauma
};
}, isCheat: true));
commands.Add(new Command("give|giveitem", "give|giveitem [itemname/itemidentifier] [amount] [condition]: Spawn an item in the inventory of the controlled character",
commands.Add(new Command("give|giveitem", "give|giveitem [itemname/itemidentifier] [amount] [condition] [quality]: Spawn an item in the inventory of the controlled character",
(string[] args) =>
{
if (Character.Controlled == null)
@@ -292,9 +292,12 @@ namespace Barotrauma
},
getValidArgs: () =>
{
return new string[][]
return new string[][]
{
GetItemNameOrIdParams().ToArray()
GetItemNameOrIdParams().ToArray(),
new string[] { "1" },
new string[] { "100" },
ItemQualityNames.ToArray()
};
}, isCheat: true));
@@ -311,7 +314,7 @@ namespace Barotrauma
};
}, isCheat: true));
commands.Add(new Command("spawnitem", "spawnitem [itemname/itemidentifier] [cursor/inventory/cargo/random/[name]] [amount] [condition]: Spawn an item at the position of the cursor, in the inventory of the controlled character, in the inventory of the client with the given name, or at a random spawnpoint if the location parameter is omitted or \"random\".",
commands.Add(new Command("spawnitem", "spawnitem [itemname/itemidentifier] [cursor/inventory/cargo/random/[name]] [amount] [condition] [quality]: Spawn an item at the position of the cursor, in the inventory of the controlled character, in the inventory of the client with the given name, or at a random spawnpoint if the location parameter is omitted or \"random\".",
(string[] args) =>
{
TrySpawnItem(args);
@@ -321,7 +324,10 @@ namespace Barotrauma
return new string[][]
{
GetItemNameOrIdParams().ToArray(),
GetSpawnPosParams().ToArray()
GetSpawnPosParams().ToArray(),
new string[] { "1" },
new string[] { "100" },
ItemQualityNames.ToArray()
};
}, isCheat: true));
@@ -1324,6 +1330,7 @@ namespace Barotrauma
}
else
{
if (GameMain.GameSession?.Map is Map map) { NewMessage("Map seed: " + map.Seed); }
NewMessage("Level seed: " + Level.Loaded.Seed);
NewMessage("Level generation params: " + Level.Loaded.GenerationParams.Identifier);
NewMessage("Adjacent locations: " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier()) + ", " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier()));
@@ -1333,17 +1340,29 @@ namespace Barotrauma
}
},null));
commands.Add(new Command("teleportsub", "teleportsub [start/end/endoutpost/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. The 'endoutpost' argument also automatically docks the sub with the outpost at the end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.",
commands.Add(new Command("teleportsub", "teleportsub [start/end/endoutpost/cursor] [submarine_team]: Teleport the submarine to the position of the cursor, or the start or end of the level. The 'endoutpost' argument also automatically docks the sub with the outpost at the end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.",
onExecute:(string[] args) =>
{
if (Submarine.MainSub == null) { return; }
Submarine submarineToTeleport = Submarine.MainSub;
if (args.Length > 1)
{
foreach (Submarine sub in Submarine.Loaded.Where(s => s.PhysicsBody.BodyType == FarseerPhysics.BodyType.Dynamic))
{
if ((sub.Info.Name + "_" + sub.TeamID) == args[1])
{
submarineToTeleport = sub;
break;
}
}
}
if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase))
{
#if SERVER
ThrowError("Cannot teleport the sub to the position of the cursor. Use \"start\" or \"end\", or execute the command as a client.");
#else
Submarine.MainSub.SetPosition(Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition));
submarineToTeleport.SetPosition(Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition));
#endif
}
else if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase))
@@ -1356,9 +1375,9 @@ namespace Barotrauma
Vector2 pos = Level.Loaded.StartPosition;
if (Level.Loaded.StartOutpost != null)
{
pos -= Vector2.UnitY * (Submarine.MainSub.Borders.Height + Level.Loaded.StartOutpost.Borders.Height) / 2;
pos -= Vector2.UnitY * (submarineToTeleport.Borders.Height + Level.Loaded.StartOutpost.Borders.Height) / 2;
}
Submarine.MainSub.SetPosition(pos);
submarineToTeleport.SetPosition(pos);
}
else if (args[0].Equals("end", StringComparison.OrdinalIgnoreCase))
{
@@ -1370,9 +1389,9 @@ namespace Barotrauma
Vector2 pos = Level.Loaded.EndPosition;
if (Level.Loaded.EndOutpost != null)
{
pos -= Vector2.UnitY * (Submarine.MainSub.Borders.Height + Level.Loaded.EndOutpost.Borders.Height) / 2;
pos -= Vector2.UnitY * (submarineToTeleport.Borders.Height + Level.Loaded.EndOutpost.Borders.Height) / 2;
}
Submarine.MainSub.SetPosition(pos);
submarineToTeleport.SetPosition(pos);
}
else if (args[0].Equals("endoutpost", StringComparison.OrdinalIgnoreCase))
{
@@ -1381,8 +1400,8 @@ namespace Barotrauma
NewMessage("Can't teleport the sub to the end outpost (no outpost at the end of the level).", Color.Red);
return;
}
Submarine.MainSub.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height);
var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Submarine.MainSub);
submarineToTeleport.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * submarineToTeleport.Borders.Height);
var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == submarineToTeleport);
var outpostDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Level.Loaded.EndOutpost);
if (submarineDockingPort != null && outpostDockingPort != null)
{
@@ -1394,7 +1413,8 @@ namespace Barotrauma
{
return new string[][]
{
new string[] { "start", "end", "endoutpost", "cursor" }
new string[] { "start", "end", "endoutpost", "cursor" },
ListAvailableSubmarines()
};
}, isCheat: true));
@@ -2569,7 +2589,17 @@ namespace Barotrauma
return locationNames.ToArray();
}
private static string[] ListAvailableSubmarines()
{
List<string> submarineNames = new();
foreach (var submarine in Submarine.Loaded.Where(s => s.PhysicsBody.BodyType == FarseerPhysics.BodyType.Dynamic))
{
submarineNames.Add(submarine.Info.Name + "_" + submarine.TeamID);
}
return submarineNames.ToArray();
}
private static bool TryFindTeleportPosition(string locationName, out Vector2 teleportPosition)
{
if (Submarine.MainSub is Submarine mainSub && string.Equals(locationName, "mainsub", StringComparison.InvariantCultureIgnoreCase))
@@ -2952,7 +2982,7 @@ namespace Barotrauma
isHuman = job != null || characterLowerCase == CharacterPrefab.HumanSpeciesName;
}
ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew);
ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew, out string renameCharacter);
if (usePreConfiguredNPC)
{
@@ -2983,6 +3013,14 @@ namespace Barotrauma
CharacterInfo characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant);
Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, spawnPosition, characterInfo, onSpawn: newCharacter =>
{
if (renameCharacter != null)
{
if (renameCharacter.Length > 31)
{
renameCharacter = renameCharacter.Substring(0, 32);
}
newCharacter.Info.Name = renameCharacter;
}
SetTeamAndCrew(newCharacter);
newCharacter.GiveJobItems(isPvPMode: GameMain.GameSession?.GameMode is PvPMode, spawnPoint);
newCharacter.GiveIdCardTags(spawnPoint);
@@ -3010,7 +3048,7 @@ namespace Barotrauma
}
}
void ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew)
void ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew, out string renameCharacter)
{
spawnPosition = Vector2.Zero;
spawnPoint = null;
@@ -3096,6 +3134,12 @@ namespace Barotrauma
ThrowError($"Could not parse the \"add to crew\" argument ({args[argIndex]}). Defaulting to {addToCrew}.");
}
}
argIndex++;
renameCharacter = null;
if (args.Length > argIndex)
{
renameCharacter = args[argIndex];
}
}
}
@@ -3103,6 +3147,7 @@ namespace Barotrauma
{
yield return "cursor";
yield return "inventory";
yield return "cargo";
#if SERVER
if (GameMain.Server != null)
@@ -3143,6 +3188,8 @@ namespace Barotrauma
}
}
private static ImmutableArray<string> ItemQualityNames = ["normal", "good", "excellent", "masterwork"];
private static void TrySpawnItem(string[] args)
{
try
@@ -3203,7 +3250,8 @@ namespace Barotrauma
int amount = 1;
int conditionPrc = 100;
int itemQuality = 0;
if (TryGetSpawnPosParam(out string spawnLocation, out int spawnLocationIndex))
{
switch (spawnLocation)
@@ -3223,7 +3271,7 @@ namespace Barotrauma
break;
default:
var matchingCharacter = FindMatchingCharacter(args.Skip(1).Take(1).ToArray());
if (matchingCharacter != null){ spawnInventory = matchingCharacter.Inventory; }
if (matchingCharacter != null) { spawnInventory = matchingCharacter.Inventory; }
break;
}
@@ -3232,10 +3280,21 @@ namespace Barotrauma
if (!int.TryParse(args[spawnLocationIndex + 1], NumberStyles.Any, CultureInfo.InvariantCulture, out amount)) { amount = 1; }
amount = Math.Min(amount, 100);
}
if (args.Length > spawnLocationIndex + 2)
{
if (!int.TryParse(args[^1], NumberStyles.Any, CultureInfo.InvariantCulture, out conditionPrc)) { conditionPrc = 100; }
if (!int.TryParse(args[spawnLocationIndex + 2], NumberStyles.Any, CultureInfo.InvariantCulture, out conditionPrc)) { conditionPrc = 100; }
}
if (args.Length > spawnLocationIndex + 3)
{
for (int i = 0; i <= Quality.MaxQuality; i++)
{
if (args[spawnLocationIndex + 3].ToLowerInvariant() == ItemQualityNames[i])
{
itemQuality = i;
}
}
}
}
@@ -3257,7 +3316,7 @@ namespace Barotrauma
}
else
{
Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, spawnPos.Value, condition: itemCondition);
Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, spawnPos.Value, condition: itemCondition, quality: itemQuality);
}
}
else if (spawnInventory != null)
@@ -3284,6 +3343,7 @@ namespace Barotrauma
}
item.Condition = item.Health * Math.Clamp(conditionPrc / 100f, 0f, 1f);
item.Quality = itemQuality;
}
}
}
@@ -31,12 +31,17 @@ namespace Barotrauma
Actions = new List<EventAction>();
foreach (var e in element.Elements())
{
if (e.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase))
if (e.NameAsIdentifier().Equals("statuseffect"))
{
DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". Status effect configured as a sub action. Please configure status effects as child elements of a StatusEffectAction.",
contentPackage: element.ContentPackage);
continue;
}
else if (e.NameAsIdentifier().Equals(nameof(OnRoundEndAction)))
{
DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". {nameof(OnRoundEndAction)} configured as a sub action. Please configure it as an action at the end of the event.",
contentPackage: element.ContentPackage);
}
var action = Instantiate(scriptedEvent, e);
if (action != null) { Actions.Add(action); }
}
@@ -123,7 +123,7 @@ namespace Barotrauma
AddTargetPredicate(
Tags.Traitor,
ScriptedEvent.TargetPredicate.EntityType.Character,
e => e is Character c && (c.IsPlayer || c.IsBot) && c.IsTraitor && !c.IsIncapacitated);
e => e is Character c && (c.IsPlayer || c.IsBot) && c.IsTraitor && !c.IsIncapacitated && CharacterTeamMatches(c));
}
private void TagNonTraitors()
@@ -131,7 +131,7 @@ namespace Barotrauma
AddTargetPredicate(
Tags.NonTraitor,
ScriptedEvent.TargetPredicate.EntityType.Character,
e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated);
e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated && CharacterTeamMatches(c));
}
private void TagNonTraitorPlayers()
@@ -139,7 +139,7 @@ namespace Barotrauma
AddTargetPredicate(
Tags.NonTraitorPlayer,
ScriptedEvent.TargetPredicate.EntityType.Character,
e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated);
e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated && CharacterTeamMatches(c));
}
private void TagBots(bool playerCrewOnly)
@@ -151,7 +151,8 @@ namespace Barotrauma
e is Character c &&
c.IsBot &&
(!c.IsIncapacitated || !IgnoreIncapacitatedCharacters) &&
(!playerCrewOnly || c.TeamID == CharacterTeamType.Team1));
(!playerCrewOnly || c.TeamID == CharacterTeamType.Team1) &&
CharacterTeamMatches(c));
}
private void TagCrew()
@@ -171,7 +172,7 @@ namespace Barotrauma
private void TagHumansByTag(Identifier tag)
{
AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab != null && c.HumanPrefab.GetTags().Contains(tag)));
AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab != null && c.HumanPrefab.GetTags().Contains(tag) && CharacterTeamMatches(c)));
}
private void TagHumansByJobIdentifier(Identifier jobIdentifier)
@@ -168,6 +168,9 @@ namespace Barotrauma.Items.Components
set { attachedByDefault = value; }
}
#if DEBUG
[Editable]
#endif
[Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at (in pixels, as an offset from the character's shoulder)."+
" For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards.")]
public Vector2 HoldPos
@@ -177,8 +180,10 @@ namespace Barotrauma.Items.Components
}
//the distance from the holding characters elbow to center of the physics body of the item
protected Vector2 holdPos;
#if DEBUG
[Editable]
#endif
[Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at when aiming (in pixels, as an offset from the character's shoulder)."+
" Works similarly as HoldPos, except that the position is rotated according to the direction the player is aiming at. For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards when aiming directly to the right.")]
public Vector2 AimPos
@@ -279,6 +284,9 @@ namespace Barotrauma.Items.Components
/// <summary>
/// For setting the handle positions using status effects
/// </summary>
#if DEBUG
[Editable]
#endif
public Vector2 Handle1
{
get { return ConvertUnits.ToDisplayUnits(handlePos[0]); }
@@ -299,6 +307,9 @@ namespace Barotrauma.Items.Components
/// <summary>
/// For setting the handle positions using status effects
/// </summary>
#if DEBUG
[Editable]
#endif
public Vector2 Handle2
{
get { return ConvertUnits.ToDisplayUnits(handlePos[1]); }
@@ -119,7 +119,7 @@ namespace Barotrauma.Items.Components
return OnPicked(picker, pickDroppedStack: true);
}
public virtual bool OnPicked(Character picker, bool pickDroppedStack)
public bool OnPicked(Character picker, bool pickDroppedStack, bool playSound = true)
{
//if the item has multiple Pickable components (e.g. Holdable and Wearable, check that we don't equip it in hands when the item is worn or vice versa)
if (item.GetComponents<Pickable>().Any())
@@ -156,7 +156,7 @@ namespace Barotrauma.Items.Components
ApplyStatusEffects(ActionType.OnPicked, 1.0f, picker);
#if CLIENT
if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) { SoundPlayer.PlayUISound(GUISoundType.PickItem); }
if (!GameMain.Instance.LoadingScreenOpen && playSound && picker == Character.Controlled) { SoundPlayer.PlayUISound(GUISoundType.PickItem); }
PlaySound(ActionType.OnPicked, picker);
#endif
if (pickDroppedStack)
@@ -164,7 +164,7 @@ namespace Barotrauma.Items.Components
foreach (var droppedItem in droppedStack)
{
if (droppedItem == item) { continue; }
droppedItem.GetComponent<Pickable>().OnPicked(picker, pickDroppedStack: false);
droppedItem.GetComponent<Pickable>().OnPicked(picker, pickDroppedStack: false, playSound: false);
}
}
return true;
@@ -42,6 +42,9 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, IsPropertySaveable.No, description: $"Should this item be removed if the linked character is null?")]
public bool RemoveItemIfCharacterNull { get; set; }
public Character? Character { get; private set; }
public bool DoesBleed => Character?.DoesBleed == true;
@@ -50,6 +53,8 @@ namespace Barotrauma.Items.Components
public LinkedControllerCharacterComponent(Item item, ContentXElement element) : base(item, element)
{
IsActive = true;
#if CLIENT
spriteOverrides = element.Elements()
.Where(static e => e.Name.LocalName.ToLowerInvariant() == "spriteoverride")
@@ -58,6 +63,16 @@ namespace Barotrauma.Items.Components
#endif
}
public override void Update(float deltaTime, Camera cam)
{
base.Update(deltaTime, cam);
if (RemoveItemIfCharacterNull && GameMain.NetworkMember is not { IsClient: true } && (Character == null || Character.Removed))
{
Entity.Spawner?.AddEntityToRemoveQueue(Item);
}
}
public void UpdateLinkedCharacter(Character? character)
{
Character = character;
@@ -525,7 +525,7 @@ namespace Barotrauma.Items.Components
return true;
}
if (containerToSpawnOnSelectedItem.Inventory.AllItems.Any(x => x.Prefab == spawnItemOnSelectedPrefab))
if (containerToSpawnOnSelectedItem.Inventory.AllItems.Any(item => item == spawnedItemOnSelected))
{
return true;
}
@@ -345,6 +345,8 @@ namespace Barotrauma.Items.Components
if (targetItem.AllowDeconstruct && allowRemove)
{
ApplyDeconstructionStatusEffects(targetItem, ActionType.OnDeconstructed, 1f);
//drop all items that are inside the deconstructed item
foreach (ItemContainer ic in targetItem.GetComponents<ItemContainer>())
{
@@ -480,6 +482,7 @@ namespace Barotrauma.Items.Components
// Move items again since the status effect could have spawned additional items in the character inventory
MoveItemsFromCharacterToOutput();
character.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null);
Entity.Spawner?.AddEntityToRemoveQueue(character);
}, 0.1f);
}
@@ -732,6 +732,7 @@ namespace Barotrauma.Items.Components
}
private readonly HashSet<Item> usedIngredients = new HashSet<Item>();
private readonly Dictionary<ItemPrefab, int> ingredientFlexibilityCache = new Dictionary<ItemPrefab, int>();
public bool MissingRequiredRecipe(FabricationRecipe fabricableItem, Character character)
{
@@ -786,10 +787,24 @@ namespace Barotrauma.Items.Components
//maintain a list of used ingredients so we don't end up considering the same item a suitable for multiple required ingredients
usedIngredients.Clear();
return fabricableItem.RequiredItems.All(requiredItem =>
// Items are considered more flexible if they can be used in many different requirements
ingredientFlexibilityCache.Clear();
foreach (var prefab in fabricableItem.RequiredItems.SelectMany(static r => r.ItemPrefabs))
{
ingredientFlexibilityCache[prefab] = fabricableItem.RequiredItems.Count(r => r.ItemPrefabs.Contains(prefab));
}
return fabricableItem.RequiredItems
// Match the most restrictive requirements to least restrictive first, while we still have items that we can use
.OrderBy(static r => r.ItemPrefabs.Count())
.ThenByDescending(static requiredItem => requiredItem.Amount)
.All(requiredItem =>
{
int availableItemsAmount = 0;
foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs)
foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs
// Fill in the least flexible and more abundant items first, so we don't end up using unique items first
.OrderBy(GetItemFlexibility)
.ThenByDescending(GetAvailableItemsCount))
{
if (!availableIngredients.TryGetValue(requiredPrefab.Identifier, out var availableItems)) { continue; }
@@ -811,6 +826,16 @@ namespace Barotrauma.Items.Components
return false;
});
int GetAvailableItemsCount(ItemPrefab itemPrefab)
{
return availableIngredients.TryGetValue(itemPrefab.Identifier, out var list) ? list.Count : 0;
}
int GetItemFlexibility(ItemPrefab itemPrefab)
{
return ingredientFlexibilityCache[itemPrefab];
}
}
private float GetRequiredTime(FabricationRecipe fabricableItem, Character user)
@@ -36,7 +36,7 @@ namespace Barotrauma.Items.Components
}
}
public LocalizedString? DisplayName { get; private set; }
public LocalizedString DisplayName { get; private set; }
private float supplyRatio = 1f;
public float SupplyRatio
@@ -80,6 +80,7 @@ namespace Barotrauma.Items.Components
SupplyRatio = element.GetAttributeFloat("ratio", SupplyRatio);
}
DisplayName = TextManager.Get(name).Fallback(name);
#if CLIENT
CreateGUI();
if (Screen.Selected is not { IsEditor: true })
@@ -252,7 +252,7 @@ namespace Barotrauma.Items.Components
{
PhysicsBody = new PhysicsBody(currentWidth, currentHeight, radius: 0.0f, density: 1.5f, BodyType.Static, Physics.CollisionWall, LevelTrigger.GetCollisionCategories(triggeredBy))
{
UserData = item
UserData = this
};
}
else
@@ -260,7 +260,7 @@ namespace Barotrauma.Items.Components
currentRadius = Math.Max(ConvertUnits.ToSimUnits(Radius * item.Scale), 0.01f);
PhysicsBody = new PhysicsBody(width: 0.0f, height: 0.0f, radius: currentRadius, density: 1.5f, BodyType.Static, Physics.CollisionWall, LevelTrigger.GetCollisionCategories(triggeredBy))
{
UserData = item
UserData = this
};
}
@@ -734,8 +734,8 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.No, description: "Hides the condition displayed in the item's tooltip.")]
public bool HideConditionInTooltip { get; set; }
[Serialize("", IsPropertySaveable.No, description: "If set, displays if the given fabrication recipe has been unlocked or not in the tooltip. The actual unlocking of the recipe should be handled in a status effect.")]
public Identifier UnlockedRecipeInToolTip { get; set; }
[Serialize("", IsPropertySaveable.No, description: "If set, the item's tooltip displays if the given fabrication recipe has been unlocked or not. The actual unlocking of the recipe should be handled in a status effect.")]
public Identifier[] UnlockedRecipeInToolTip { get; set; }
//if true and the item has trigger areas defined, characters need to be within the trigger to interact with the item
//if false, trigger areas define areas that can be used to highlight the item
@@ -332,17 +332,9 @@ namespace Barotrauma
void RunStateUnloaded_OnEnter(State<RunState> currentState)
{
Logger.LogMessage("LuaCs unloaded state entered");
if (PackageManagementService.IsAnyPackageRunning())
{
Logger.LogResults(PackageManagementService.StopRunningPackages());
}
if (PackageManagementService.IsAnyPackageLoaded())
{
DisposeLuaCsConfig();
Logger.LogResults(PackageManagementService.UnloadAllPackages());
}
Logger.LogResults(PackageManagementService.StopRunningPackages());
DisposeLuaCsConfig();
Logger.LogResults(PackageManagementService.UnloadAllPackages());
EventService.Reset();
ConfigService.Reset();
@@ -362,11 +354,7 @@ namespace Barotrauma
void RunStateLoadedNoExec_OnEnter(State<RunState> currentState)
{
Logger.LogMessage("LuaCs no execution state entered");
if (PackageManagementService.IsAnyPackageRunning())
{
Logger.LogResults(PackageManagementService.StopRunningPackages());
}
Logger.LogResults(PackageManagementService.StopRunningPackages());
if (!PackageManagementService.IsAnyPackageLoaded())
{
@@ -256,6 +256,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public Result<Assembly> CompileScriptAssembly([NotNull] string assemblyName,
bool compileWithInternalAccess,
ImmutableArray<SyntaxTree> syntaxTrees,
@@ -348,6 +349,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public FluentResults.Result<Assembly> LoadAssemblyFromFile(string assemblyFilePath,
ImmutableArray<string> additionalDependencyPaths)
{
@@ -434,6 +436,8 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public FluentResults.Result<Assembly> GetAssemblyByName(string assemblyName)
{
if (IsDisposed)
@@ -481,6 +485,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public FluentResults.Result<ImmutableArray<Type>> GetTypesInAssemblies()
{
if (IsDisposed)
@@ -501,6 +506,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public IEnumerable<Type> UnsafeGetTypesInAssemblies()
{
if (IsDisposed)
@@ -529,6 +535,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public Result<Type> GetTypeInAssemblies(string typeName)
{
if (IsDisposed)
@@ -557,13 +564,12 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
return; // we don't want to invoke events twice nor cause strong GC handles.
IsDisposed = true;
this.Unload();
this.DisposeInternal();
GC.SuppressFinalize(this);
}
~AssemblyLoader()
{
this.DisposeInternal();
this.Unload();
}
private void OnUnload(AssemblyLoadContext context)
@@ -578,8 +584,8 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
Thread.Sleep(1000/Timing.FixedUpdateRate-1);
}
var wf = new WeakReference<IAssemblyLoaderService>(this);
_onUnload?.Invoke(this);
this.DisposeInternal();
}
private void DisposeInternal()
@@ -590,6 +596,9 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
base.Unloading -= OnUnload;
this._dependencyResolvers.Clear();
this._loadedAssemblyData.Clear();
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
GC.WaitForFullGCComplete(10);
}
protected override Assembly Load(AssemblyName assemblyName)
@@ -658,6 +667,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
public readonly ImmutableArray<Type> Types;
public readonly ImmutableDictionary<string, Type> TypesByName;
[MethodImpl(MethodImplOptions.NoOptimization)]
public AssemblyData(Assembly assembly, byte[] assemblyImage)
{
Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
@@ -667,6 +677,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
TypesByName = Types.ToImmutableDictionary(type => type.FullName, type => type);
}
[MethodImpl(MethodImplOptions.NoOptimization)]
public AssemblyData(Assembly assembly, string path)
{
Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
@@ -694,6 +705,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
HashCode = AssemblyName.GetHashCode();
}
[MethodImpl(MethodImplOptions.NoOptimization)]
public AssemblyOrStringKey(string assemblyName)
{
if (assemblyName.IsNullOrWhiteSpace())
@@ -396,7 +396,7 @@ class LuaScriptManagementService : ILuaScriptManagementService, ILuaDataService,
typeof(ISettingList<ulong>),
typeof(ISettingList<long>),
typeof(ISettingList<float>),
typeof(ISettingList<double>),
typeof(ISettingList<double>)
];
Dictionary<string, Dictionary<string, object>> settingsTable = [];
@@ -420,9 +420,9 @@ class LuaScriptManagementService : ILuaScriptManagementService, ILuaDataService,
_script.Globals[keyPair.Key] = keyPair.Value;
}
UserData.RegisterType(typeof(ISettingRangeBase<int>));
#if CLIENT
UserData.RegisterType(typeof(ISettingControl));
_script.Globals["SettingControl"] = UserData.CreateStatic(typeof(ISettingControl));
#endif
new LuaConverters(this).RegisterLuaConverters();
@@ -43,7 +43,9 @@ internal class MainMenuPatch : ISystem, IEventScreenSelected
{
if (mainMenuUIAdded) { return; }
var textBlock = new GUITextBlock(new RectTransform(new Point(300, 30), screen.Frame.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(10, 10) }, "", Color.Red)
var textBlock = new GUITextBlock(
new RectTransform(new Point(300, 30), screen.Frame.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(10, 10) },
"", Color.Red, textAlignment: Alignment.TopLeft)
{
IgnoreLayoutGroups = false
};
@@ -23,6 +23,7 @@ public sealed partial class ModConfigFileParserService :
public ModConfigFileParserService(IStorageService storageService)
{
_storageService = storageService;
_storageService.UseCaching = false;
}
#region Dispose
@@ -45,6 +45,7 @@ public sealed class ModConfigService : IModConfigService
#if CLIENT
_stylesParserService = stylesParserService;
#endif
_storageService.UseCaching = false;
}
#region Dispose
@@ -194,7 +194,7 @@ public sealed class PackageManagementService : IPackageManagementService
IService.CheckDisposed(this);
var result = new FluentResults.Result();
var packages2 = packages.OrderBy(pkg => pkg.Name == "LuaCsForBarotrauma" ? 0 : 1) // always run lua cs first.
var packages2 = packages.OrderBy(pkg => pkg.Name == LuaCsSetup.PackageName ? 0 : 1) // always run lua cs first.
.ThenBy(packages.IndexOf)
.ToImmutableArray();
@@ -318,7 +318,7 @@ public sealed class PackageManagementService : IPackageManagementService
// get loading order. Note: packages not in the execution order list will load first.
var loadingOrderedPackages = _loadedPackages
.OrderBy(pkg => pkg.Key.Name == "LuaCsForBarotrauma" ? 0 : 1) // always run lua cs first.
.OrderBy(pkg => pkg.Key.Name == LuaCsSetup.PackageName ? 0 : 1) // always run lua cs first.
.ThenBy(pkg => executionOrder.IndexOf(pkg.Key))
.ToImmutableArray();
var loadOrderByPackage = loadingOrderedPackages.Select(p => p.Key).ToImmutableArray();
@@ -415,7 +415,9 @@ public sealed class PackageManagementService : IPackageManagementService
if (_loadedPackages.IsEmpty || _runningPackages.IsEmpty)
{
#if DEGUG
_logger.LogWarning($"{nameof(StopRunningPackages)}: No packages are currently executing.");
#endif
return FluentResults.Result.Ok();
}
@@ -10,6 +10,7 @@ using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Serialization;
using Barotrauma.Extensions;
using Barotrauma.IO;
@@ -88,7 +89,15 @@ public class PluginManagementService : IAssemblyManagementService
private ImmutableArray<MetadataReference> _baseMetadataReferences = ImmutableArray<MetadataReference>.Empty;
private ImmutableArray<MetadataReference> _baseMetadataReferencesNonPublicized = ImmutableArray<MetadataReference>.Empty;
private Thread _backgroundGCCleanupThread = null;
private long _backgroundGCWatchdogTicks = 0;
private static readonly int
GC_TASK_COMPLETION_TIMEOUT = 5000,
GC_BACKGND_MAXITERATIONS = 2,
GC_BACKGND_INTERVAL_MILLIS = 200,
GC_BACKGND_GENERATION_WAIT_MILLIS = 100;
private IEnumerable<MetadataReference> BaseMetadataReferences
{
@@ -174,6 +183,7 @@ public class PluginManagementService : IAssemblyManagementService
_pluginInjectorContainer?.Dispose();
_pluginInjectorContainer = null;
ReflectionUtils.ResetCache();
foreach (var loader in _assemblyLoaders)
{
try
@@ -184,14 +194,6 @@ public class PluginManagementService : IAssemblyManagementService
catch (Exception e)
{
_logger?.LogError($"Failed to dispose of {nameof(IAssemblyLoaderService)} for ContentPackage {loader.Key.Name}: \n{e.Message}");
if (loader.Value.Assemblies.Any())
{
foreach (var ass in loader.Value.Assemblies)
{
_logger?.LogWarning($"{nameof(PluginManagementService)}: Fallback manual unsubscription of assemblies: {ass.GetName()}");
ReflectionUtils.RemoveAssemblyFromCache(ass);
}
}
}
}
_assemblyLoaders.Clear();
@@ -222,6 +224,7 @@ public class PluginManagementService : IAssemblyManagementService
private IEventService _pluginEventService;
private Lazy<ILuaPatcher> _pluginLuaPatcherService;
private Func<IConsoleCommandsService> _consoleCommandServiceFactory;
private readonly IConsoleCommandsService _internalConsoleCommandsService;
private ILuaCsInfoProvider _luaCsInfoProvider;
private readonly ConcurrentDictionary<ContentPackage, IAssemblyLoaderService> _assemblyLoaders = new();
private readonly ConcurrentDictionary<Type, ContentPackage> _pluginPackageLookup = new();
@@ -251,6 +254,21 @@ public class PluginManagementService : IAssemblyManagementService
_pluginLuaPatcherService = pluginLuaPatcherService;
_consoleCommandServiceFactory = consoleCommandServiceFactory;
_luaCsInfoProvider = luaCsInfoProvider;
_internalConsoleCommandsService = consoleCommandServiceFactory.Invoke();
RegisterCommands(_internalConsoleCommandsService);
}
private void RegisterCommands(IConsoleCommandsService cmdService)
{
cmdService.RegisterCommand("plugin_forcerungc", "Forces the GC to run", cmds =>
{
_logger.LogMessage("Forcing GC run.");
Task.Factory.StartNew(async () =>
{
await RunGC(true, false);
});
});
}
private ServiceContainer CreatePluginServiceContainer()
@@ -317,11 +335,13 @@ public class PluginManagementService : IAssemblyManagementService
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public bool TryGetPackageForPlugin<TPlugin>(out ContentPackage ownerPackage)
{
return _pluginPackageLookup.TryGetValue(typeof(TPlugin), out ownerPackage);
}
[MethodImpl(MethodImplOptions.NoInlining)]
public Type GetType(string typeName, bool isByRefType = false, bool includeInterfaces = false,
bool includeDefaultContext = true)
{
@@ -370,6 +390,7 @@ public class PluginManagementService : IAssemblyManagementService
return null;
}
[MethodImpl(MethodImplOptions.NoOptimization)]
public FluentResults.Result ActivatePluginInstances(ImmutableArray<ContentPackage> executionOrder, bool excludeAlreadyRunningPackages = true)
{
if (executionOrder.IsDefaultOrEmpty)
@@ -488,6 +509,7 @@ public class PluginManagementService : IAssemblyManagementService
return results;
// helper
[MethodImpl(MethodImplOptions.NoOptimization)]
FluentResults.Result PluginInitRunner(IAssemblyPlugin plugin, Action<IAssemblyPlugin> action)
{
try
@@ -502,7 +524,7 @@ public class PluginManagementService : IAssemblyManagementService
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public FluentResults.Result LoadAssemblyResources(ImmutableArray<IAssemblyResourceInfo> resources)
{
if (resources.IsDefaultOrEmpty)
@@ -705,7 +727,7 @@ public class PluginManagementService : IAssemblyManagementService
{
builder.AddRange(BaseMetadataReferencesWithBarotrauma);
foreach (var loaderService in _assemblyLoaders
.Where(asl => !asl.Key.Name.Equals("LuaCsForBarotrauma", StringComparison.InvariantCultureIgnoreCase))
.Where(asl => !asl.Key.Name.Equals(LuaCsSetup.PackageName, StringComparison.InvariantCultureIgnoreCase))
.ToImmutableArray())
{
builder.AddRange(loaderService.Value.AssemblyReferences.Where(ar => ar is not null));
@@ -732,7 +754,7 @@ public class PluginManagementService : IAssemblyManagementService
.Replace(" Barotrauma.Networking.Client.ClientList", " ModUtils.Client.ClientList")
.Replace("ItemPrefab.GetItemPrefab", "ModUtils.ItemPrefab.GetItemPrefab");
}
private IntPtr OnAssemblyLoaderResolvingUnmanaged(Assembly callerAssembly, string targetAssemblyName)
{
Guard.IsNull(callerAssembly, nameof(callerAssembly));
@@ -805,21 +827,58 @@ public class PluginManagementService : IAssemblyManagementService
{
_eventService?.Value?.PublishEvent<IEventAssemblyUnloading>(sub => sub.OnAssemblyUnloading(assembly));
}
_unloadingAssemblyLoaders.Add(loader, loader.OwnerPackage);
}
[MethodImpl(MethodImplOptions.NoOptimization)]
public FluentResults.Result UnloadManagedAssemblies()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_assemblyLoaders.Count == 0)
{
return FluentResults.Result.Ok();
}
var results = new FluentResults.Result();
results.WithReasons(UnsafeDisposeManagedTypeInstances().Reasons);
if (!_pluginInstances.IsEmpty)
{
foreach (var instance in _pluginInstances.SelectMany(kvp => kvp.Value))
{
try
{
instance.Dispose();
}
catch (Exception e)
{
results.WithError(new ExceptionalError(e));
continue;
}
}
_pluginInstances.Clear();
}
if (_pluginEventService is not null)
{
_eventService.Value.RemoveDispatcherEventService(_pluginEventService);
try
{
_pluginEventService.Dispose();
}
catch (Exception e)
{
results.WithError(new ExceptionalError(e));
}
_pluginEventService = null;
}
try
{
_pluginInjectorContainer?.Dispose();
}
catch (Exception e)
{
results.WithError(new ExceptionalError(e));
}
_pluginInjectorContainer = null;
ReflectionUtils.ResetCache();
foreach (var loaderService in _assemblyLoaders)
@@ -827,7 +886,6 @@ public class PluginManagementService : IAssemblyManagementService
try
{
loaderService.Value.Dispose();
_unloadingAssemblyLoaders.Add(loaderService.Value, loaderService.Key);
}
catch (Exception e)
{
@@ -837,38 +895,7 @@ public class PluginManagementService : IAssemblyManagementService
_assemblyLoaders.Clear();
_storageService.PurgeCache();
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true);
#if DEBUG
// Print still loaded assembly load ctx after giving some time
CoroutineManager.Invoke(() =>
{
if (!_unloadingAssemblyLoaders.Any())
{
return;
}
StringBuilder sb = new StringBuilder();
sb.AppendLine("The following ContentPackages have not unloaded their assemblies:");
foreach (var kvp in _unloadingAssemblyLoaders.ToImmutableArray())
{
sb.AppendLine($"- '{kvp.Value.Name}'");
}
// Use DebugConsole in case logger is null by the time this executes.
if (_logger is null)
{
DebugConsole.LogError(sb.ToString());
}
else
{
_logger.LogWarning(sb.ToString());
}
}, 3.0f);
#endif
_pluginPackageLookup.Clear();
// clear native libraries
if (_loadedNativeLibraries.Any())
@@ -888,41 +915,109 @@ public class PluginManagementService : IAssemblyManagementService
_loadedNativeLibraries.Clear();
}
Task.Factory.StartNew(async () =>
{
await RunGC(true, false);
});
return results;
}
private FluentResults.Result UnsafeDisposeManagedTypeInstances()
private void SafeLogUnloadingPackages()
{
var results = new FluentResults.Result();
if (!_pluginInstances.IsEmpty)
if (!_unloadingAssemblyLoaders.Any())
{
foreach (var instance in _pluginInstances.SelectMany(kvp => kvp.Value))
return;
}
StringBuilder sb = new StringBuilder();
sb.AppendLine("The following ContentPackages have not unloaded their assemblies:");
foreach (var kvp in _unloadingAssemblyLoaders.ToImmutableArray())
{
sb.AppendLine($"- '{kvp.Value.Name}'");
}
// Use DebugConsole in case logger is null by the time this executes.
if (_logger is null)
{
DebugConsole.Log(sb.ToString());
}
else
{
_logger.LogWarning(sb.ToString());
}
}
private void GCCleanupTask(TaskCompletionSource<bool> completionSuccess)
{
GC.RegisterForFullGCNotification(1, 1);
try
{
for (int iter = 0; iter < GC_BACKGND_MAXITERATIONS; iter++)
{
try
int maxGen = GC.MaxGeneration;
for (int currGen = 0; currGen < maxGen; currGen++)
{
instance.Dispose();
}
catch (Exception e)
{
results.WithError(new ExceptionalError(e));
continue;
GC.Collect(currGen, GCCollectionMode.Forced, false, false); // marking pass
GC.WaitForFullGCComplete(GC_BACKGND_GENERATION_WAIT_MILLIS);
GC.Collect(currGen); // generation cleanup
}
Thread.Sleep(GC_BACKGND_INTERVAL_MILLIS);
}
completionSuccess.SetResult(true);
}
if (_pluginEventService is not null)
catch (ThreadInterruptedException tie)
{
_eventService.Value.RemoveDispatcherEventService(_pluginEventService);
_pluginEventService = null;
completionSuccess.SetResult(false);
}
catch (Exception e)
{
completionSuccess.SetException(e);
}
finally
{
GC.CancelFullGCNotification();
}
}
private async Task RunGC(bool logResults, bool runOnMainThread)
{
var gcCompletionSuccess = new TaskCompletionSource<bool>();
if (runOnMainThread)
{
GCCleanupTask(gcCompletionSuccess);
if (logResults)
{
SafeLogUnloadingPackages();
}
return;
}
_pluginInjectorContainer = null;
_pluginInstances.Clear();
_pluginPackageLookup.Clear();
return results;
var gcThread = new Thread(() =>
{
GCCleanupTask(gcCompletionSuccess);
}) { IsBackground = true };
gcThread.Start();
try
{
await gcCompletionSuccess.Task.WaitAsync(TimeSpan.FromMilliseconds(GC_TASK_COMPLETION_TIMEOUT));
}
catch (TimeoutException te)
{
_logger.LogError($"{nameof(RunGC)}: The GC task thread has timed out.");
gcThread.Interrupt();
gcThread.Join();
}
if (logResults)
{
SafeLogUnloadingPackages();
}
}
public Result<Assembly> GetLoadedAssembly(OneOf<AssemblyName, string> assemblyName, in Guid[] excludedContexts)
@@ -2971,11 +2971,39 @@ namespace Barotrauma
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");
$" Total value: {GetTotalLevelResourceValue()} 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");
$" Total value: {GetTotalAbyssResourceValue()} mk");
}
int GetTotalLevelResourceValue()
{
int value = 0;
foreach (var pathPoint in PathPoints)
{
foreach (var clusterLocation in pathPoint.ClusterLocations)
{
foreach (var resource in clusterLocation.Resources)
{
value += resource.Prefab.DefaultPrice?.Price ?? 0;
}
}
}
return value;
}
int GetTotalAbyssResourceValue()
{
int value = 0;
foreach (var clusterLocation in AbyssResources)
{
foreach (var resource in clusterLocation.Resources)
{
value += resource.Prefab.DefaultPrice?.Price ?? 0;
}
}
return value;
}
#endif
@@ -3204,7 +3232,6 @@ namespace Barotrauma
}
}
/// <param name="rotation">Used by clients to set the rotation for the resources</param>
public List<Item> GenerateMissionResources(ItemPrefab prefab, int requiredAmount, PositionType positionType, IEnumerable<Cave> targetCaves = null)
{
var allValidLocations = GetAllValidClusterLocations();
@@ -5150,6 +5177,7 @@ namespace Barotrauma
renderer.Dispose();
renderer = null;
}
backgroundCreatureManager?.Clear();
#endif
if (LevelObjectManager != null)
@@ -11,7 +11,7 @@ namespace Barotrauma
partial class LevelObject : ISpatialEntity, IDamageable, ISerializableEntity
{
public readonly LevelObjectPrefab Prefab;
public Vector3 Position;
public Vector3 Position { get; set; }
public float NetworkUpdateTimer;
@@ -457,7 +457,7 @@ namespace Barotrauma
if (newObject.NeedsUpdate) { updateableObjects.Add(newObject); }
//add some variance to the Z position to prevent z-fighting
//(based on the x and y position of the object, scaled to be visually insignificant)
newObject.Position.Z += (minX + minY) % 100.0f * 0.00001f;
newObject.Position += new Vector3(0, 0, (minX + minY) % 100.0f * 0.00001f);
int xStart = (int)Math.Floor(minX / GridSize);
int xEnd = (int)Math.Floor(maxX / GridSize);
@@ -348,11 +348,22 @@ namespace Barotrauma
{
price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated, includeSaved: false));
price *= 1f - characters.Max(static c => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, Tags.StatIdentifierTargetAll));
price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, tag)));
price *= 1f - characters.Max(c => GetStatValuesForItem(c, item, StatTypes.StoreBuyMultiplierAffiliated));
}
price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplier, includeSaved: false));
price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplier, tag)));
price *= 1f - characters.Max(c => GetStatValuesForItem(c, item, StatTypes.StoreBuyMultiplier));
}
static float GetStatValuesForItem(Character character, ItemPrefab item, StatTypes statType)
{
float statValueSum = 0.0f;
foreach (Identifier itemTag in item.Tags)
{
statValueSum += character.Info.GetSavedStatValue(statType, itemTag);
}
return statValueSum;
}
// Price should never go below 1 mk
return Math.Max((int)price, 1);
}
@@ -487,7 +487,19 @@ namespace Barotrauma
var portrait = new Sprite(subElement, lazyLoad: true);
if (portrait != null)
{
#if CLIENT
if (!File.Exists(portrait.FilePath))
{
DebugConsole.ThrowError($"Error in location type \"{Identifier}\": cannot find the location portrait \"{portrait.FilePath}\".");
}
else
{
portraitsList.Add(portrait);
}
#elif SERVER
// Add without checking the path, since servers don't parse the file path of the sprite
portraitsList.Add(portrait);
#endif
}
break;
case "store":
@@ -261,15 +261,7 @@ namespace Barotrauma
}
}
foreach (var endLocation in EndLocations)
{
if (endLocation.Type?.ForceLocationName is { IsEmpty: false })
{
endLocation.ForceName(endLocation.Type.ForceLocationName);
}
}
AssignEndLocationLevelData();
AssignEndLocationLevelData(campaign);
//backwards compatibility: if locations go out of bounds (map saved with different generation parameters before width/height were included in the xml)
float maxX = Locations.Select(l => l.MapPosition.X).Max();
@@ -973,13 +965,43 @@ namespace Barotrauma
previousToEndLocation.Connections.Add(endConnection);
endLocation.Connections.Add(endConnection);
AssignEndLocationLevelData();
AssignEndLocationLevelData(campaign);
}
private void AssignEndLocationLevelData()
/// <summary>
/// Assigns the correct outpost generation parameters to the end locations. Also checks and ensures that all of them are correctly assigned to the end biome, and have a location type that can be generated in the end biome.
/// Strangely shaped custom maps may sometimes generate in a way that there aren't enough locations in the last biome to assign as the end locations, and we may end up choosing locations in the second-to-last biome instead - let's correct that here.
/// </summary>
/// <param name="campaign"></param>
/// <exception cref="InvalidOperationException"></exception>
private void AssignEndLocationLevelData(CampaignMode campaign)
{
Biome endBiome = Biome.Prefabs.OrderBy(p => p.UintIdentifier).FirstOrDefault(b => b.IsEndBiome) ?? throw new InvalidOperationException("Could not find an end biome to assign to the end locations.");
LocationType endLocationType =
LocationType.Prefabs
.OrderBy(p => p.UintIdentifier)
.FirstOrDefault(IsSuitableEndLocationType)
?? throw new InvalidOperationException("Could not find an a location type to assign to the end locations.");
bool IsSuitableEndLocationType(LocationType lt)
{
return lt.AreaSettings.Any(s =>
s.Commonness > 0 &&
(s.MatchesBiome(endBiome.Identifier) || s.MatchesZone(generationParams.DifficultyZones)));
}
for (int i = 0; i < endLocations.Count; i++)
{
if (endLocations[i].Biome != endBiome)
{
endLocations[i].Biome = endBiome;
endLocations[i].LevelData = new LevelData(endLocations[i], this, endLocations[i].LevelData.Difficulty);
}
endLocations[i].ChangeType(campaign: campaign, endLocationType);
if (endLocationType.ForceLocationName is { IsEmpty: false })
{
endLocations[i].ForceName(endLocationType.ForceLocationName);
}
endLocations[i].LevelData.ReassignGenerationParams(Seed);
var outpostParams = OutpostGenerationParams.OutpostParams.FirstOrDefault(p => p.ForceToEndLocationIndex == i);
if (outpostParams != null)
@@ -275,7 +275,7 @@ namespace Barotrauma
{
CreateStairBodies();
}
else if (HasBody)
else if (Prefab.Body)
{
CreateSections();
UpdateSections();
@@ -346,7 +346,7 @@ namespace Barotrauma
{
Rectangle oldRect = Rect;
base.Rect = value;
if (HasBody)
if (Prefab.Body)
{
CreateSections();
UpdateSections();
@@ -668,7 +668,7 @@ namespace Barotrauma
{
prevSections = Sections.ToArray();
}
if (!HasBody)
if (!Prefab.Body)
{
if (FlippedX && IsHorizontal)
{
@@ -685,7 +685,7 @@ namespace Barotrauma
xsections = 1;
ysections = 1;
}
Sections = new WallSection[xsections];
Sections = new WallSection[Math.Max(xsections, ysections)];
}
else
{
@@ -1635,7 +1635,7 @@ namespace Barotrauma
CreateStairBodies();
}
if (HasBody)
if (Prefab.Body)
{
CreateSections();
UpdateSections();
@@ -1663,7 +1663,7 @@ namespace Barotrauma
CreateStairBodies();
}
if (HasBody)
if (Prefab.Body)
{
CreateSections();
UpdateSections();
@@ -29,6 +29,31 @@ namespace Barotrauma
partial class SubmarineInfo : IDisposable
{
public static HashSet<string> SubmarinePathsWithRemoteStorage { get; set; } = [];
public bool SaveToRemoteStorage
{
get
{
if (FilePath == null) { return false; }
return SubmarinePathsWithRemoteStorage.Contains(FilePath.CleanUpPathCrossPlatform(correctFilenameCase: false));
}
set
{
if (FilePath == null) { return; }
if (value)
{
SubmarinePathsWithRemoteStorage.Add(FilePath.CleanUpPathCrossPlatform(correctFilenameCase: false));
}
else
{
SubmarinePathsWithRemoteStorage.Remove(FilePath.CleanUpPathCrossPlatform(correctFilenameCase: false));
}
}
}
private static List<SubmarineInfo> savedSubmarines = new List<SubmarineInfo>();
public static IEnumerable<SubmarineInfo> SavedSubmarines => savedSubmarines;
@@ -197,6 +222,8 @@ namespace Barotrauma
set;
}
public bool IsFromRemoteStorage;
/// <summary>
/// When enabled, the <see cref="SubmarineElement">XML element is not loaded</see> until it is accessed.
/// </summary>
@@ -562,6 +562,19 @@ namespace Barotrauma
IgnoredHints.Init(currentConfigDoc.Root.GetChildElement("ignoredhints"));
DebugConsoleMapping.Init(currentConfigDoc.Root.GetChildElement("debugconsolemapping"));
CompletedTutorials.Init(currentConfigDoc.Root.GetChildElement("tutorials"));
var submarineSettings = currentConfigDoc.Root.GetChildElement("submarinesettings");
if (submarineSettings != null)
{
SubmarineInfo.SubmarinePathsWithRemoteStorage.Clear();
foreach (XElement subElement in submarineSettings.Elements("SubmarineWithRemoteStorage"))
{
string path = subElement.GetAttributeString("path", "");
if (!path.IsNullOrEmpty())
{
SubmarineInfo.SubmarinePathsWithRemoteStorage.Add(path);
}
}
}
#endif
}
else
@@ -689,7 +702,14 @@ namespace Barotrauma
XElement tutorialsElement = new XElement("tutorials"); root.Add(tutorialsElement);
CompletedTutorials.Instance.SaveTo(tutorialsElement);
XElement submarineSettings = new XElement("submarinesettings"); root.Add(submarineSettings);
SubmarineInfo.SubmarinePathsWithRemoteStorage.ForEach(path =>
{
submarineSettings.Add(new XElement("SubmarineWithRemoteStorage", new XAttribute("path", path)));
});
XElement keyMappingElement = new XElement("keymapping",
currentConfig.KeyMap.Bindings.Select(kvp
=> new XAttribute(kvp.Key.ToString(), kvp.Value.ToString())));
@@ -1786,7 +1786,7 @@ namespace Barotrauma
offset *= item.Scale;
if (item.FlippedX) { offset.X *= -1; }
if (item.FlippedY) { offset.Y *= -1; }
offset = Vector2.Transform(offset, Matrix.CreateRotationZ(-item.RotationRad));
offset = Vector2.Transform(offset, Matrix.CreateRotationZ(item.body?.Rotation ?? -item.RotationRad));
}
}
@@ -0,0 +1,105 @@
#nullable enable
using Barotrauma.IO;
using Microsoft.Xna.Framework;
using Steamworks;
using System;
using System.Diagnostics.CodeAnalysis;
namespace Barotrauma.Steam;
internal static partial class RemoteStorageHelper
{
public static readonly Color SteamColor = Color.DodgerBlue;
public static readonly string DebugPrefix = $"‖color:{SteamColor.ToStringHex()}‖[Remote Storage]‖end‖";
/// <summary>Attempts to read a file from remote storage into a byte array.</summary>
/// <param name="remoteFile">The remote file to read from.</param>
/// <param name="bytes">The bytes read from the remote file. Returns <see langword="null"/> if the operation failed.</param>
/// <returns>
/// <see langword="true"/> if the operation was successful.<br/>
/// <see langword="false"/> if the operation failed.
/// </returns>
public static bool TryRead(this SteamRemoteStorage.RemoteFile remoteFile, [NotNullWhen(returnValue: true)] out byte[]? bytes, bool logError = true)
{
bytes = SteamRemoteStorage.FileRead(remoteFile.Filename);
bool success = bytes != null;
if (logError && !success)
{
DebugConsole.ThrowError($"{DebugPrefix} Failed to read file \"{remoteFile.Filename}\" from remote storage: operation failed.");
}
return success;
}
/// <summary>Attempts to write a file to remote storage.</summary>
/// <param name="localPath">The path of the local file to read from.</param>
/// <param name="saveAs">The name of the remote file to write to. If <see langword="null"/>, the file name of <paramref name="localPath"/> is used.</param>
/// <param name="allowOverwrite">If <see langword="true"/>, overwriting existing remote files is allowed.</param>
/// <returns>
/// <see langword="true"/> if the operation was successful.<br/>
/// <see langword="false"/> if the operation failed.
/// </returns>
public static bool TryWrite(string localPath, string? saveAs = null, bool allowOverwrite = false, bool logError = true)
{
string fileName = saveAs ?? Path.GetFileName(localPath);
if (!allowOverwrite && SteamRemoteStorage.FileExists(fileName))
{
if (logError)
{
DebugConsole.ThrowError($"{DebugPrefix} Failed to write file \"{fileName}\" to remote storage: file already exists.");
}
return false;
}
byte[] data;
try
{
data = File.ReadAllBytes(localPath);
}
catch (Exception exception)
{
if (logError)
{
DebugConsole.ThrowError($"{DebugPrefix} Failed to read file \"{fileName}\" while writing to remote storage: {exception}");
}
return false;
}
bool success = SteamRemoteStorage.FileWrite(fileName, data);
if (logError && !success)
{
DebugConsole.ThrowError($"{DebugPrefix} Failed to write file \"{fileName}\" to remote storage: operation failed.");
}
return success;
}
/// <summary>Attempts to delete a file from remote storage.</summary>
/// <param name="fileName">The name of the remote file to delete.</param>
/// <returns>
/// <see langword="true"/> if the operation was successful.<br/>
/// <see langword="false"/> if the operation failed.
/// </returns>
public static bool TryDelete(string fileName, bool logError = true)
{
bool success = SteamRemoteStorage.FileDelete(fileName);
if (logError && !success)
{
DebugConsole.ThrowError($"{DebugPrefix} Failed to delete file \"{fileName}\" from remote storage: operation failed.");
}
return success;
}
/// <summary>Checks if a file is stored remotely.</summary>
/// <param name="fileName">The name of the remote file to check.</param>
/// <returns>
/// <see langword="true"/> if the file is stored.<br/>
/// <see langword="false"/> if the file is not stored or the operation failed.
/// </returns>
public static bool IsStored(string fileName) => SteamRemoteStorage.FileExists(fileName);
}