Merge remote-tracking branch 'upstream/master' into develop

This commit is contained in:
EvilFactory
2024-06-18 12:19:13 -03:00
263 changed files with 7788 additions and 2849 deletions
@@ -533,7 +533,7 @@ namespace Barotrauma
.Distinct();
public static IEnumerable<Item> FilterCargoCrates(IEnumerable<Item> items, Func<Item, bool> conditional = null)
=> items.Where(it => it.HasTag(Tags.Crate) && !it.NonInteractable && !it.NonPlayerTeamInteractable && !it.HiddenInGame && !it.Removed && (conditional == null || conditional(it)));
=> items.Where(it => it.HasTag(Tags.Crate) && !it.NonInteractable && !it.NonPlayerTeamInteractable && !it.IsHidden && !it.Removed && (conditional == null || conditional(it)));
public static IEnumerable<ItemContainer> FindReusableCargoContainers(IEnumerable<Submarine> subs, IEnumerable<Hull> cargoRooms = null) =>
FilterCargoCrates(Item.ItemList, it => subs.Contains(it.Submarine) && !it.HasTag(Tags.CargoMissionItem) && (cargoRooms == null || cargoRooms.Contains(it.CurrentHull)))
@@ -685,7 +685,14 @@ namespace Barotrauma
var idCard = item.GetComponent<IdCard>();
if (cargoManager != null && idCard != null && purchased.BuyerCharacterInfoIdentifier != 0)
{
cargoManager.purchasedIDCards.Add((purchased, idCard));
if (purchased.DeliverImmediately)
{
InitPurchasedIDCard(purchased, idCard);
}
else
{
cargoManager.purchasedIDCards.Add((purchased, idCard));
}
}
Submarine sub = item.Submarine ?? item.RootContainer?.Submarine;
@@ -703,18 +710,23 @@ namespace Barotrauma
{
foreach ((PurchasedItem purchased, IdCard idCard) in purchasedIDCards)
{
if (idCard != null && purchased.BuyerCharacterInfoIdentifier != 0)
{
var owner = Character.CharacterList.Find(c => c.Info?.GetIdentifier() == purchased.BuyerCharacterInfoIdentifier);
if (owner?.Info != null)
{
var mainSubSpawnPoints = WayPoint.SelectCrewSpawnPoints(new List<CharacterInfo>() { owner.Info }, Submarine.MainSub);
idCard.Initialize(mainSubSpawnPoints.FirstOrDefault(), owner);
}
}
InitPurchasedIDCard(purchased, idCard);
}
}
private static void InitPurchasedIDCard(PurchasedItem purchased, IdCard idCard)
{
if (idCard != null && purchased.BuyerCharacterInfoIdentifier != 0)
{
var owner = Character.CharacterList.Find(c => c.Info?.GetIdentifier() == purchased.BuyerCharacterInfoIdentifier);
if (owner?.Info != null)
{
var mainSubSpawnPoints = WayPoint.SelectCrewSpawnPoints(new List<CharacterInfo>() { owner.Info }, Submarine.MainSub);
idCard.Initialize(mainSubSpawnPoints.FirstOrDefault(), owner);
}
}
}
public static Vector2 GetCargoPos(Hull hull, ItemPrefab itemPrefab)
{
float floorPos = hull.Rect.Y - hull.Rect.Height;
@@ -99,14 +99,20 @@ namespace Barotrauma
{
if (order.Identifier == Tags.DeconstructThis)
{
Item.DeconstructItems.Add(item);
foreach (var stackedItem in item.GetStackedItems())
{
Item.DeconstructItems.Add(stackedItem);
}
#if CLIENT
HintManager.OnItemMarkedForDeconstruction(order.OrderGiver);
#endif
}
else
{
Item.DeconstructItems.Remove(item);
foreach (var stackedItem in item.GetStackedItems())
{
Item.DeconstructItems.Remove(stackedItem);
}
}
}
}
@@ -180,6 +186,17 @@ namespace Barotrauma
DebugConsole.ThrowError("Tried to add a dead character to CrewManager!\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
if (character.Info == null)
{
if (character.Prefab.ContentPackage == GameMain.VanillaContent)
{
DebugConsole.ThrowError($"Added a character with no {nameof(CharacterInfo)} to the crew." + Environment.StackTrace.CleanupStackTrace());
}
else
{
DebugConsole.ThrowError($"Added add a character with no {nameof(CharacterInfo)} to the crew. This may lead to issues: consider adding {nameof(CharacterPrefab.HasCharacterInfo)}=\"True\" to the character config.");
}
}
if (!characters.Contains(character))
{
@@ -275,11 +292,7 @@ namespace Barotrauma
if (Level.Loaded != null && Level.Loaded.ShouldSpawnCrewInsideOutpost())
{
spawnWaypoints = WayPoint.WayPointList.FindAll(wp =>
wp.SpawnType == SpawnType.Human &&
wp.Submarine == Level.Loaded.StartOutpost &&
wp.CurrentHull != null &&
wp.CurrentHull.OutpostModuleTags.Contains("airlock".ToIdentifier()));
spawnWaypoints = GetOutpostSpawnpoints();
while (spawnWaypoints.Count > characterInfos.Count)
{
spawnWaypoints.RemoveAt(Rand.Int(spawnWaypoints.Count));
@@ -301,46 +314,8 @@ namespace Barotrauma
var info = characterInfos[i];
info.TeamID = CharacterTeamType.Team1;
Character character = Character.Create(info, spawnWaypoints[i].WorldPosition, info.Name);
if (character.Info != null)
{
if (!character.Info.StartItemsGiven && character.Info.InventoryData != null)
{
DebugConsole.AddWarning($"Error when initializing a round: character \"{character.Name}\" has not been given their initial items but has saved inventory data. Using the saved inventory data instead of giving the character new items.");
}
if (character.Info.InventoryData != null)
{
character.SpawnInventoryItems(character.Inventory, character.Info.InventoryData.FromPackage(null));
}
else if (!character.Info.StartItemsGiven)
{
character.GiveJobItems(mainSubWaypoints[i]);
foreach (Item item in character.Inventory.AllItems)
{
//if the character is loaded from a human prefab with preconfigured items, its ID card gets assigned to the sub it spawns in
//we don't want that in this case, the crew's cards shouldn't be submarine-specific
var idCard = item.GetComponent<Items.Components.IdCard>();
if (idCard != null)
{
idCard.SubmarineSpecificID = 0;
}
}
}
if (character.Info.HealthData != null)
{
CharacterInfo.ApplyHealthData(character, character.Info.HealthData);
}
InitializeCharacter(character, mainSubWaypoints[i], spawnWaypoints[i]);
character.LoadTalents();
character.GiveIdCardTags(mainSubWaypoints[i]);
character.GiveIdCardTags(spawnWaypoints[i]);
character.Info.StartItemsGiven = true;
if (character.Info.OrderData != null)
{
character.Info.ApplyOrderData();
}
}
AddCharacter(character, sortCrewList: false);
#if CLIENT
if (IsSinglePlayer && (Character.Controlled == null || character.Info.LastControlled)) { Character.Controlled = character; }
@@ -355,6 +330,61 @@ namespace Barotrauma
conversationTimer = IsSinglePlayer ? Rand.Range(5.0f, 10.0f) : Rand.Range(45.0f, 60.0f);
}
/// <summary>
/// Returns the potential crew spawnpositions for the crew in the loaded outpost
/// </summary>
public List<WayPoint> GetOutpostSpawnpoints()
{
return WayPoint.WayPointList.FindAll(wp =>
wp.SpawnType == SpawnType.Human &&
wp.Submarine == Level.Loaded.StartOutpost &&
wp.CurrentHull != null &&
wp.CurrentHull.OutpostModuleTags.Contains("airlock".ToIdentifier()));
}
public void InitializeCharacter(Character character, WayPoint mainSubWaypoint, WayPoint spawnWaypoint)
{
if (character.Info != null)
{
if (!character.Info.StartItemsGiven && character.Info.InventoryData != null)
{
DebugConsole.AddWarning($"Error when initializing a round: character \"{character.Name}\" has not been given their initial items but has saved inventory data. Using the saved inventory data instead of giving the character new items.");
}
if (character.Info.InventoryData != null)
{
character.SpawnInventoryItems(character.Inventory, character.Info.InventoryData.FromPackage(null));
}
else if (!character.Info.StartItemsGiven)
{
character.GiveJobItems(mainSubWaypoint);
foreach (Item item in character.Inventory.AllItems)
{
//if the character is loaded from a human prefab with preconfigured items, its ID card gets assigned to the sub it spawns in
//we don't want that in this case, the crew's cards shouldn't be submarine-specific
var idCard = item.GetComponent<Items.Components.IdCard>();
if (idCard != null)
{
idCard.SubmarineSpecificID = 0;
}
}
}
if (character.Info.HealthData != null)
{
CharacterInfo.ApplyHealthData(character, character.Info.HealthData);
}
character.LoadTalents();
character.GiveIdCardTags(mainSubWaypoint);
character.GiveIdCardTags(spawnWaypoint);
character.Info.StartItemsGiven = true;
if (character.Info.OrderData != null)
{
character.Info.ApplyOrderData();
}
}
}
public void RenameCharacter(CharacterInfo characterInfo, string newName)
{
int identifier = characterInfo.GetIdentifierUsingOriginalName();
@@ -53,7 +53,7 @@ namespace Barotrauma
internal struct NetWalletSetSalaryUpdate : INetSerializableStruct
{
[NetworkSerialize]
public ushort Target;
public Option<ushort> Target;
[NetworkSerialize(MinValueInt = 0, MaxValueInt = 100)]
public int NewRewardDistribution;
@@ -117,13 +117,13 @@ namespace Barotrauma
public override int Balance
{
get => 0;
set => new InvalidOperationException("Tried to set the balance on an invalid wallet");
set => throw new InvalidOperationException("Tried to set the balance on an invalid wallet");
}
public override int RewardDistribution
{
get => 0;
set => new InvalidOperationException("Tried to set the reward distribution on an invalid wallet");
set => throw new InvalidOperationException("Tried to set the reward distribution on an invalid wallet");
}
}
@@ -134,7 +134,7 @@ namespace Barotrauma
public const string LowerCaseSaveElementName = "wallet";
private const string AttributeNameBalance = "balance",
AttrubuteNameRewardDistribution = "rewarddistribution",
AttributeNameRewardDistribution = "rewarddistribution",
SaveElementName = "Wallet";
public readonly Option<Character> Owner;
@@ -152,7 +152,15 @@ namespace Barotrauma
public virtual int RewardDistribution
{
get => rewardDistribution;
set => rewardDistribution = ClampRewardDistribution(value);
set
{
rewardDistribution = ClampRewardDistribution(value);
if (Owner.TryUnwrap(out var character) && character.Info is { } info)
{
info.LastRewardDistribution = Option.Some(rewardDistribution);
}
}
}
public Wallet(Option<Character> owner)
@@ -163,12 +171,14 @@ namespace Barotrauma
public Wallet(Option<Character> owner, XElement element): this(owner)
{
balance = ClampBalance(element.GetAttributeInt(AttributeNameBalance, 0));
rewardDistribution = ClampBalance(element.GetAttributeInt(AttrubuteNameRewardDistribution, 0));
rewardDistribution = ClampBalance(element.GetAttributeInt(AttributeNameRewardDistribution, 0));
}
public XElement Save()
{
XElement element = new XElement(SaveElementName, new XAttribute(AttributeNameBalance, Balance), new XAttribute(AttrubuteNameRewardDistribution, RewardDistribution));
XElement element = new XElement(SaveElementName,
new XAttribute(AttributeNameBalance, Balance),
new XAttribute(AttributeNameRewardDistribution, RewardDistribution));
return element;
}
@@ -195,6 +205,11 @@ namespace Barotrauma
SettingsChanged(balanceChanged: Option<int>.Some(-price), rewardChanged: Option<int>.None());
}
/// <summary>
/// Sets how much salary the wallet owner should receive from mission rewards.
/// Bank's salary determines the default salary for new characters.
/// </summary>
/// <param name="value"></param>
public void SetRewardDistribution(int value)
{
int oldValue = RewardDistribution;
@@ -347,6 +347,10 @@ namespace Barotrauma
/// </summary>
public event Action BeforeLevelLoading;
/// <summary>
/// Triggers when saving and quitting mid-round (as in, not just transferring to a new level). Automatically cleared after triggering -> no need to unregister
/// </summary>
public event Action OnSaveAndQuit;
public override void AddExtraMissions(LevelData levelData)
{
@@ -472,7 +476,7 @@ namespace Barotrauma
var missionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t == automaticMission.MissionTag)).OrderBy(m => m.UintIdentifier);
if (missionPrefabs.Any())
{
var missionPrefab = ToolBox.SelectWeightedRandom(missionPrefabs, p => (float)p.Commonness, rand);
var missionPrefab = ToolBox.SelectWeightedRandom(missionPrefabs, p => p.Commonness, rand);
if (missionPrefab.Type == MissionType.Pirate && Missions.Any(m => m.Prefab.Type == MissionType.Pirate))
{
continue;
@@ -515,8 +519,8 @@ namespace Barotrauma
if (endLevelMissionPrefabs.Any())
{
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
var endLevelMissionPrefab = ToolBox.SelectWeightedRandom(endLevelMissionPrefabs, p => (float)p.Commonness, rand);
if (!Missions.Any(m => m.Prefab.Type == endLevelMissionPrefab.Type))
var endLevelMissionPrefab = ToolBox.SelectWeightedRandom(endLevelMissionPrefabs, p => p.Commonness, rand);
if (Missions.All(m => m.Prefab.Type != endLevelMissionPrefab.Type))
{
if (levelData.Type == LevelData.LevelType.LocationConnection)
{
@@ -913,6 +917,20 @@ namespace Barotrauma
}
}
/// <summary>
/// Handles updating store stock, registering event history and relocating items (i.e. things that need to be done when saving and quitting mid-round)
/// </summary>
public void HandleSaveAndQuit()
{
OnSaveAndQuit?.Invoke();
OnSaveAndQuit = null;
if (Level.IsLoadedFriendlyOutpost)
{
UpdateStoreStock();
}
GameMain.GameSession.EventManager?.RegisterEventHistory(registerFinishedOnly: true);
}
/// <summary>
/// Updates store stock before saving the game
/// </summary>
@@ -1368,13 +1386,13 @@ namespace Barotrauma
{
if (item.Removed) { continue; }
if (item.NonInteractable || item.NonPlayerTeamInteractable) { continue; }
if (item.HiddenInGame) { continue; }
if (item.IsHidden) { continue; }
if (!connectedSubs.Contains(item.Submarine)) { continue; }
if (item.Prefab.DontTransferBetweenSubs) { continue; }
if (AnyParentInventoryDisableTransfer(item)) { continue; }
var rootOwner = item.GetRootInventoryOwner();
if (rootOwner is Character) { continue; }
if (rootOwner is Item ownerItem && (ownerItem.NonInteractable || item.NonPlayerTeamInteractable || ownerItem.HiddenInGame)) { continue; }
if (rootOwner is Item ownerItem && (ownerItem.NonInteractable || item.NonPlayerTeamInteractable || ownerItem.IsHidden)) { continue; }
if (item.GetComponent<Door>() != null) { continue; }
if (item.Components.None(c => c is Pickable)) { continue; }
if (item.Components.Any(c => c is Pickable p && p.IsAttached)) { continue; }
@@ -29,7 +29,7 @@ namespace Barotrauma
: base(preset)
{
Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation };
var mission = Mission.LoadRandom(locations, seed, false, missionType);
var mission = Mission.LoadRandom(locations, seed, requireCorrectLocationType: false, missionType, difficultyLevel: GameMain.NetworkMember.ServerSettings.SelectedLevelDifficulty);
if (mission != null)
{
missions.Add(mission);
@@ -231,6 +231,12 @@ namespace Barotrauma
{
campaign.Bank.Deduct(selectedSub.Price);
campaign.Bank.Balance = Math.Max(campaign.Bank.Balance, 0);
#if SERVER
if (GameMain.Server?.ServerSettings?.NewCampaignDefaultSalary is { } salary)
{
campaign.Bank.SetRewardDistribution((int)Math.Round(salary, digits: 0));
}
#endif
}
return campaign;
}
@@ -970,6 +976,8 @@ namespace Barotrauma
{
ToggleTabMenu();
}
DeathPrompt?.Close();
DeathPrompt.CloseBotPanel();
GUI.PreventPauseMenuToggle = true;
@@ -42,16 +42,25 @@ namespace Barotrauma
{
AvailableCharacters.ForEach(c => c.Remove());
AvailableCharacters.Clear();
foreach (var missingJob in location.Type.GetHireablesMissingFromCrew())
{
AddCharacter(missingJob);
amount--;
}
for (int i = 0; i < amount; i++)
{
JobPrefab job = location.Type.GetRandomHireable();
if (job == null) { return; }
var variant = Rand.Range(0, job.Variants, Rand.RandSync.ServerAndClient);
AvailableCharacters.Add(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant));
AddCharacter(location.Type.GetRandomHireable());
}
if (location.Faction != null) { GenerateFactionCharacters(location.Faction.Prefab); }
if (location.SecondaryFaction != null) { GenerateFactionCharacters(location.SecondaryFaction.Prefab); }
void AddCharacter(JobPrefab job)
{
if (job == null) { return; }
int variant = Rand.Range(0, job.Variants, Rand.RandSync.ServerAndClient);
AvailableCharacters.Add(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant));
}
}
private void GenerateFactionCharacters(FactionPrefab faction)
@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System;
using System.Collections.Generic;
@@ -125,7 +125,7 @@ namespace Barotrauma
public NetCrewMember(CharacterInfo info)
{
CharacterInfoID = info.GetIdentifierUsingOriginalName();
CharacterInfoID = info.ID;
Afflictions = ImmutableArray<NetAffliction>.Empty;
}
@@ -138,7 +138,7 @@ namespace Barotrauma
{
foreach (CharacterInfo info in crew)
{
if (info.GetIdentifierUsingOriginalName() == CharacterInfoID)
if (info.ID == CharacterInfoID)
{
return info;
}
@@ -159,6 +159,14 @@ namespace Barotrauma
private readonly CampaignMode? campaign;
/// <summary>
/// Characters whose afflictions have changed (processed periodically, refreshing the UI client-side, sending updates to clients server-side
/// </summary>
private readonly HashSet<Character> charactersWithAfflictionChanges = new HashSet<Character>();
private float processAfflictionChangesTimer;
private const float ProcessAfflictionChangesInterval = 1.0f;
public MedicalClinic(CampaignMode campaign)
{
this.campaign = campaign;
@@ -305,35 +313,8 @@ namespace Barotrauma
private void OnAfflictionCountChangedPrivate(Character character)
{
if (character is not { CharacterHealth: { } health, Info: { } info }) { return; }
ImmutableArray<NetAffliction> afflictions = GetAllAfflictions(health);
#if CLIENT
if (GameMain.NetworkMember is null)
{
ui?.UpdateAfflictions(new NetCrewMember(info, afflictions));
}
ui?.UpdateCrewPanel();
#elif SERVER
foreach (AfflictionSubscriber sub in afflictionSubscribers.ToList())
{
if (sub.Expiry < DateTimeOffset.Now)
{
afflictionSubscribers.Remove(sub);
continue;
}
if (sub.Target == info)
{
ServerSend(new NetCrewMember(info, afflictions),
header: NetworkHeader.AFFLICTION_UPDATE,
deliveryMethod: DeliveryMethod.Unreliable,
targetClient: sub.Subscriber);
}
}
#endif
if (character?.Info == null) { return; }
charactersWithAfflictionChanges.Add(character);
}
public int GetTotalCost() => PendingHeals.SelectMany(static h => h.Afflictions).Aggregate(0, static (current, affliction) => current + affliction.Price);
@@ -295,7 +295,7 @@ namespace Barotrauma
/// <summary>
/// Purchases an item swap and handles logic for deducting the credit.
/// </summary>
public void PurchaseItemSwap(Item itemToRemove, ItemPrefab itemToInstall, bool force = false, Client? client = null)
public void PurchaseItemSwap(Item itemToRemove, ItemPrefab itemToInstall, bool isNetworkMessage = false, Client? client = null)
{
if (!CanUpgradeSub())
{
@@ -343,12 +343,14 @@ namespace Barotrauma
price = itemToInstall.SwappableItem.GetPrice(Campaign.Map?.CurrentLocation) * linkedItems.Count;
}
if (force)
if (isNetworkMessage)
{
price = 0;
}
if (Campaign.TryPurchase(client, price))
//do not try to purchase if this is a network message (if the server is telling us that an item swap was purchased)
//we want to do the purchase no matter what, and the server handles deducting the money
if (isNetworkMessage || Campaign.TryPurchase(client, price))
{
PurchasedItemSwaps.RemoveAll(p => linkedItems.Contains(p.ItemToRemove));
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
@@ -433,8 +435,7 @@ namespace Barotrauma
{
if (itemToCancel.PendingItemSwap == null)
{
var replacement = MapEntityPrefab.Find("", swappableItem.ReplacementOnUninstall) as ItemPrefab;
if (replacement == null)
if (MapEntityPrefab.FindByIdentifier(swappableItem.ReplacementOnUninstall) is not ItemPrefab replacement)
{
DebugConsole.ThrowError($"Failed to uninstall item \"{itemToCancel.Name}\". Could not find the replacement item \"{swappableItem.ReplacementOnUninstall}\".");
return;
@@ -786,11 +787,10 @@ namespace Barotrauma
private void LoadPendingUpgrades(XElement? element, bool isSingleplayer = true)
{
if (!(element is { HasElements: true })) { return; }
if (element is not { HasElements: true }) { return; }
List<PurchasedUpgrade> pendingUpgrades = new List<PurchasedUpgrade>();
// ReSharper disable once LoopCanBeConvertedToQuery
foreach (XElement upgrade in element.Elements())
{
Identifier categoryIdentifier = upgrade.GetAttributeIdentifier("category", Identifier.Empty);