Merge branch 'dev' of https://github.com/Regalis11/Barotrauma.git into unstable-tests

This commit is contained in:
Evil Factory
2022-04-08 12:52:28 -03:00
990 changed files with 44338 additions and 38589 deletions
@@ -6,6 +6,10 @@ using Barotrauma.Extensions;
namespace Barotrauma
{
#warning TODO: This class needs some changes:
// - We shouldn't be iterating over MapEntityPrefab.List. It has no guarantee of any sort of order and becomes entirely unpredictable once you start adding mods.
// - Note: iterating over ItemPrefab.Prefabs would also be incorrect. Sorting by UintIdentifier is necessary for determinism.
// - SpawnItems and SpawnItem are named incorrectly.
static class AutoItemPlacer
{
public static bool OutputDebugInfo = false;
@@ -76,8 +80,8 @@ namespace Barotrauma
int itemCountApprox = MapEntityPrefab.List.Count() / 3;
var containers = new List<ItemContainer>(70 + 30 * subs.Count());
var prefabsWithContainer = new List<ItemPrefab>(itemCountApprox / 3);
var prefabsWithoutContainer = new List<ItemPrefab>(itemCountApprox);
var prefabsItemsCanSpawnIn = new List<ItemPrefab>(itemCountApprox / 3);
var singlePrefabs = new List<ItemPrefab>(itemCountApprox);
var removals = new List<ItemPrefab>();
// generate loot only for a specific container if defined
@@ -93,42 +97,45 @@ namespace Barotrauma
if (item.GetRootInventoryOwner() is Character) { continue; }
containers.AddRange(item.GetComponents<ItemContainer>());
}
containers.Shuffle(Rand.RandSync.Server);
containers.Shuffle(Rand.RandSync.ServerAndClient);
}
foreach (MapEntityPrefab prefab in MapEntityPrefab.List)
foreach (ItemPrefab ip in ItemPrefab.Prefabs)
{
if (!(prefab is ItemPrefab ip)) { continue; }
if (ip.ConfigElement.Elements().Any(e => string.Equals(e.Name.ToString(), typeof(ItemContainer).Name.ToString(), StringComparison.OrdinalIgnoreCase)))
if (!ip.PreferredContainers.Any()) { continue; }
if (ip.ConfigElement.Elements().Any(e => string.Equals(e.Name.ToString(), typeof(ItemContainer).Name.ToString(), StringComparison.OrdinalIgnoreCase)) &&
ItemPrefab.Prefabs.Any(ip2 => CanSpawnIn(ip2, ip)))
{
prefabsWithContainer.Add(ip);
prefabsItemsCanSpawnIn.Add(ip);
}
else
{
prefabsWithoutContainer.Add(ip);
singlePrefabs.Add(ip);
}
}
var validContainers = new Dictionary<ItemContainer, PreferredContainer>();
prefabsWithContainer.Shuffle(Rand.RandSync.Server);
// Spawn items that have an ItemContainer component first so we can fill them up with items if needed (oxygen tanks inside the spawned diving masks, etc)
for (int i = 0; i < prefabsWithContainer.Count; i++)
bool CanSpawnIn(ItemPrefab item, ItemPrefab container)
{
var itemPrefab = prefabsWithContainer[i];
if (itemPrefab == null) { continue; }
if (SpawnItems(itemPrefab))
foreach (var preferredContainer in item.PreferredContainers)
{
removals.Add(itemPrefab);
if (ItemPrefab.IsContainerPreferred(preferredContainer.Primary, container.Identifier.ToEnumerable().Union(container.Tags))) { return true; }
}
return false;
}
// Remove containers that we successfully spawned items into so that they are not counted in in the second pass.
removals.ForEach(i => prefabsWithContainer.Remove(i));
// Another pass for items with containers because also they can spawn inside other items (like smg magazine)
prefabsWithContainer.ForEach(i => SpawnItems(i));
// Spawn items that don't have containers last
prefabsWithoutContainer.Shuffle(Rand.RandSync.Server);
prefabsWithoutContainer.ForEach(i => SpawnItems(i));
var validContainers = new Dictionary<ItemContainer, PreferredContainer>();
prefabsItemsCanSpawnIn.Shuffle(Rand.RandSync.ServerAndClient);
// Spawn items that other items can spawn in first so we can fill them up with items if needed (oxygen tanks inside the spawned diving masks, etc)
for (int i = 0; i < prefabsItemsCanSpawnIn.Count; i++)
{
var itemPrefab = prefabsItemsCanSpawnIn[i];
if (itemPrefab == null) { continue; }
SpawnItems(itemPrefab);
}
// Spawn items that nothing can spawn in last
singlePrefabs.Shuffle(Rand.RandSync.ServerAndClient);
singlePrefabs.ForEach(i => SpawnItems(i));
if (OutputDebugInfo)
{
@@ -158,12 +165,17 @@ namespace Barotrauma
}
}
}
#if SERVER
foreach (Item spawnedItem in spawnedItems)
{
Entity.Spawner.CreateNetworkEvent(spawnedItem, remove: false);
}
#if SERVER
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(spawnedItem));
#endif
foreach (ItemComponent ic in spawnedItem.Components)
{
ic.OnItemLoaded();
}
}
bool SpawnItems(ItemPrefab itemPrefab)
{
if (itemPrefab == null)
@@ -229,13 +241,13 @@ namespace Barotrauma
private static List<Item> SpawnItem(ItemPrefab itemPrefab, List<ItemContainer> containers, KeyValuePair<ItemContainer, PreferredContainer> validContainer, float difficultyModifier)
{
List<Item> spawnedItems = new List<Item>();
if (Rand.Value(Rand.RandSync.Server) > validContainer.Value.SpawnProbability * (1f + difficultyModifier)) { return spawnedItems; }
if (Rand.Value(Rand.RandSync.ServerAndClient) > validContainer.Value.SpawnProbability * (1f + difficultyModifier)) { return spawnedItems; }
// Don't add dangerously reactive materials in thalamus wrecks
if (validContainer.Key.Item.Submarine.WreckAI != null && itemPrefab.Tags.Contains("explodesinwater"))
{
return spawnedItems;
}
int amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1, Rand.RandSync.Server);
int amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1, Rand.RandSync.ServerAndClient);
for (int i = 0; i < amount; i++)
{
if (validContainer.Key.Inventory.IsFull(takeStacksIntoAccount: true))
@@ -244,15 +256,15 @@ namespace Barotrauma
break;
}
var existingItem = validContainer.Key.Inventory.AllItems.FirstOrDefault(it => it.prefab == itemPrefab);
var existingItem = validContainer.Key.Inventory.AllItems.FirstOrDefault(it => it.Prefab == itemPrefab);
int quality =
existingItem?.Quality ??
ToolBox.SelectWeightedRandom(
qualityCommonnesses.Select(q => q.quality).ToList(),
qualityCommonnesses.Select(q => q.commonness).ToList(),
Rand.RandSync.Server);
Rand.RandSync.ServerAndClient);
if (!validContainer.Key.Inventory.CanBePut(itemPrefab, quality: quality)) { break; }
var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine)
var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine, callOnItemLoaded: false)
{
SpawnedInCurrentOutpost = validContainer.Key.Item.SpawnedInCurrentOutpost,
AllowStealing = validContainer.Key.Item.AllowStealing,
@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Networking;
#if SERVER
using Barotrauma.Networking;
#endif
@@ -18,11 +19,31 @@ namespace Barotrauma
public int Quantity { get; set; }
public bool? IsStoreComponentEnabled { get; set; }
public PurchasedItem(ItemPrefab itemPrefab, int quantity)
public readonly int BuyerCharacterInfoId;
public PurchasedItem(ItemPrefab itemPrefab, int quantity, int buyerCharacterInfoId)
{
ItemPrefab = itemPrefab;
Quantity = quantity;
IsStoreComponentEnabled = null;
BuyerCharacterInfoId = buyerCharacterInfoId;
}
#if CLIENT
public PurchasedItem(ItemPrefab itemPrefab, int quantity)
: this(itemPrefab, quantity, buyer: null) { }
#endif
public PurchasedItem(ItemPrefab itemPrefab, int quantity, Client buyer)
{
ItemPrefab = itemPrefab;
Quantity = quantity;
IsStoreComponentEnabled = null;
BuyerCharacterInfoId = buyer?.Character?.Info?.ID ?? Character.Controlled?.Info?.ID ?? 0;
}
public override string ToString()
{
return $"{ItemPrefab.Name} ({Quantity})";
}
}
@@ -110,11 +131,11 @@ namespace Barotrauma
public const int MaxQuantity = 100;
public List<PurchasedItem> ItemsInBuyCrate { get; } = new List<PurchasedItem>();
public List<PurchasedItem> ItemsInSellCrate { get; } = new List<PurchasedItem>();
public List<PurchasedItem> ItemsInSellFromSubCrate { get; } = new List<PurchasedItem>();
public List<PurchasedItem> PurchasedItems { get; } = new List<PurchasedItem>();
public List<SoldItem> SoldItems { get; } = new List<SoldItem>();
public Dictionary<Identifier, List<PurchasedItem>> ItemsInBuyCrate { get; } = new Dictionary<Identifier, List<PurchasedItem>>();
public Dictionary<Identifier, List<PurchasedItem>> ItemsInSellCrate { get; } = new Dictionary<Identifier, List<PurchasedItem>>();
public Dictionary<Identifier, List<PurchasedItem>> ItemsInSellFromSubCrate { get; } = new Dictionary<Identifier, List<PurchasedItem>>();
public Dictionary<Identifier, List<PurchasedItem>> PurchasedItems { get; } = new Dictionary<Identifier, List<PurchasedItem>>();
public Dictionary<Identifier, List<SoldItem>> SoldItems { get; } = new Dictionary<Identifier, List<SoldItem>>();
private readonly CampaignMode campaign;
@@ -131,6 +152,60 @@ namespace Barotrauma
this.campaign = campaign;
}
private List<T> GetItems<T>(Identifier identifier, Dictionary<Identifier, List<T>> items, bool create = false)
{
if (items.TryGetValue(identifier, out var storeSpecificItems) && storeSpecificItems != null)
{
return storeSpecificItems;
}
else if (create)
{
storeSpecificItems = new List<T>();
items.Add(identifier, storeSpecificItems);
return storeSpecificItems;
}
else
{
return new List<T>();
}
}
public List<PurchasedItem> GetBuyCrateItems(Identifier identifier, bool create = false) => GetItems(identifier, ItemsInBuyCrate, create);
public List<PurchasedItem> GetBuyCrateItems(Location.StoreInfo store, bool create = false) => GetBuyCrateItems(store?.Identifier ?? Identifier.Empty, create);
public PurchasedItem GetBuyCrateItem(Identifier identifier, ItemPrefab prefab) => GetBuyCrateItems(identifier)?.FirstOrDefault(i => i.ItemPrefab == prefab);
public PurchasedItem GetBuyCrateItem(Location.StoreInfo store, ItemPrefab prefab) => GetBuyCrateItem(store?.Identifier ?? Identifier.Empty, prefab);
public List<PurchasedItem> GetSellCrateItems(Identifier identifier, bool create = false) => GetItems(identifier, ItemsInSellCrate, create);
public List<PurchasedItem> GetSellCrateItems(Location.StoreInfo store, bool create = false) => GetSellCrateItems(store?.Identifier ?? Identifier.Empty, create);
public PurchasedItem GetSellCrateItem(Identifier identifier, ItemPrefab prefab) => GetSellCrateItems(identifier)?.FirstOrDefault(i => i.ItemPrefab == prefab);
public PurchasedItem GetSellCrateItem(Location.StoreInfo store, ItemPrefab prefab) => GetSellCrateItem(store?.Identifier ?? Identifier.Empty, prefab);
public List<PurchasedItem> GetSubCrateItems(Identifier identifier, bool create = false) => GetItems(identifier, ItemsInSellFromSubCrate, create);
public List<PurchasedItem> GetSubCrateItems(Location.StoreInfo store, bool create = false) => GetSubCrateItems(store?.Identifier ?? Identifier.Empty, create);
public PurchasedItem GetSubCrateItem(Identifier identifier, ItemPrefab prefab) => GetSubCrateItems(identifier)?.FirstOrDefault(i => i.ItemPrefab == prefab);
public PurchasedItem GetSubCrateItem(Location.StoreInfo store, ItemPrefab prefab) => GetSubCrateItem(store?.Identifier ?? Identifier.Empty, prefab);
public List<PurchasedItem> GetPurchasedItems(Identifier identifier, bool create = false) => GetItems(identifier, PurchasedItems, create);
public List<PurchasedItem> GetPurchasedItems(Location.StoreInfo store, bool create = false) => GetPurchasedItems(store?.Identifier ?? Identifier.Empty, create);
public PurchasedItem GetPurchasedItem(Identifier identifier, ItemPrefab prefab) => GetPurchasedItems(identifier)?.FirstOrDefault(i => i.ItemPrefab == prefab);
public PurchasedItem GetPurchasedItem(Location.StoreInfo store, ItemPrefab prefab) => GetPurchasedItem(store?.Identifier ?? Identifier.Empty, prefab);
public List<SoldItem> GetSoldItems(Identifier identifier, bool create = false) => GetItems(identifier, SoldItems, create);
public List<SoldItem> GetSoldItems(Location.StoreInfo store, bool create = false) => GetSoldItems(store?.Identifier ?? Identifier.Empty, create);
public void ClearItemsInBuyCrate()
{
ItemsInBuyCrate.Clear();
@@ -149,115 +224,118 @@ namespace Barotrauma
OnItemsInSellFromSubCrateChanged?.Invoke();
}
public void SetPurchasedItems(List<PurchasedItem> items)
public void SetPurchasedItems(Dictionary<Identifier, List<PurchasedItem>> purchasedItems)
{
if (purchasedItems.Count == 0 && PurchasedItems.Count == 0) { return; }
PurchasedItems.Clear();
PurchasedItems.AddRange(items);
foreach (var entry in purchasedItems)
{
PurchasedItems.Add(entry.Key, entry.Value);
}
OnPurchasedItemsChanged?.Invoke();
}
public void ModifyItemQuantityInBuyCrate(ItemPrefab itemPrefab, int changeInQuantity)
public void ModifyItemQuantityInBuyCrate(Identifier storeIdentifier, ItemPrefab itemPrefab, int changeInQuantity, Client client = null)
{
var itemInCrate = ItemsInBuyCrate.Find(i => i.ItemPrefab == itemPrefab);
if (itemInCrate != null)
if (GetBuyCrateItem(storeIdentifier, itemPrefab) is { } item)
{
itemInCrate.Quantity += changeInQuantity;
if (itemInCrate.Quantity < 1)
item.Quantity += changeInQuantity;
if (item.Quantity < 1)
{
ItemsInBuyCrate.Remove(itemInCrate);
}
}
else if(changeInQuantity > 0)
{
itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity);
ItemsInBuyCrate.Add(itemInCrate);
}
OnItemsInBuyCrateChanged?.Invoke();
}
public void ModifyItemQuantityInSubSellCrate(ItemPrefab itemPrefab, int changeInQuantity)
{
var itemInCrate = ItemsInSellFromSubCrate.Find(i => i.ItemPrefab == itemPrefab);
if (itemInCrate != null)
{
itemInCrate.Quantity += changeInQuantity;
if (itemInCrate.Quantity < 1)
{
ItemsInSellFromSubCrate.Remove(itemInCrate);
GetBuyCrateItems(storeIdentifier, create: true).Remove(item);
}
}
else if (changeInQuantity > 0)
{
itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity);
ItemsInSellFromSubCrate.Add(itemInCrate);
GetBuyCrateItems(storeIdentifier, create: true).Add(new PurchasedItem(itemPrefab, changeInQuantity, client));
}
OnItemsInBuyCrateChanged?.Invoke();
}
public void ModifyItemQuantityInSubSellCrate(Identifier storeIdentifier, ItemPrefab itemPrefab, int changeInQuantity, Client client = null)
{
if (GetSubCrateItem(storeIdentifier, itemPrefab) is { } item)
{
item.Quantity += changeInQuantity;
if (item.Quantity < 1)
{
GetSubCrateItems(storeIdentifier)?.Remove(item);
}
}
else if (changeInQuantity > 0)
{
GetSubCrateItems(storeIdentifier, create: true).Add(new PurchasedItem(itemPrefab, changeInQuantity, client));
}
OnItemsInSellFromSubCrateChanged?.Invoke();
}
public void PurchaseItems(List<PurchasedItem> itemsToPurchase, bool removeFromCrate)
public void PurchaseItems(Identifier storeIdentifier, List<PurchasedItem> itemsToPurchase, bool removeFromCrate, Client client = null)
{
// Check all the prices before starting the transaction
// to make sure the modifiers stay the same for the whole transaction
Dictionary<ItemPrefab, int> buyValues = GetBuyValuesAtCurrentLocation(itemsToPurchase.Select(i => i.ItemPrefab));
var store = Location.GetStore(storeIdentifier);
if (store == null) { return; }
var itemsPurchasedFromStore = GetPurchasedItems(storeIdentifier, create: true);
// Check all the prices before starting the transaction to make sure the modifiers stay the same for the whole transaction
var buyValues = GetBuyValuesAtCurrentLocation(storeIdentifier, itemsToPurchase.Select(i => i.ItemPrefab));
var itemsInStoreCrate = GetBuyCrateItems(storeIdentifier, create: true);
foreach (PurchasedItem item in itemsToPurchase)
{
// Add to the purchased items
var purchasedItem = PurchasedItems.Find(pi => pi.ItemPrefab == item.ItemPrefab);
var purchasedItem = itemsPurchasedFromStore.Find(pi => pi.ItemPrefab == item.ItemPrefab);
if (purchasedItem != null)
{
purchasedItem.Quantity += item.Quantity;
}
else
{
purchasedItem = new PurchasedItem(item.ItemPrefab, item.Quantity);
PurchasedItems.Add(purchasedItem);
purchasedItem = new PurchasedItem(item.ItemPrefab, item.Quantity, client);
itemsPurchasedFromStore.Add(purchasedItem);
}
// Exchange money
var itemValue = item.Quantity * buyValues[item.ItemPrefab];
campaign.Money -= itemValue;
GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier);
Location.StoreCurrentBalance += itemValue;
int itemValue = item.Quantity * buyValues[item.ItemPrefab];
campaign.GetWallet(client).TryDeduct(itemValue);
GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value);
store.Balance += itemValue;
if (removeFromCrate)
{
// Remove from the shopping crate
var crateItem = ItemsInBuyCrate.Find(pi => pi.ItemPrefab == item.ItemPrefab);
if (crateItem != null)
if (itemsInStoreCrate.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } crateItem)
{
crateItem.Quantity -= item.Quantity;
if (crateItem.Quantity < 1) { ItemsInBuyCrate.Remove(crateItem); }
if (crateItem.Quantity < 1) { itemsInStoreCrate.Remove(crateItem); }
}
}
}
OnPurchasedItemsChanged?.Invoke();
}
public Dictionary<ItemPrefab, int> GetBuyValuesAtCurrentLocation(IEnumerable<ItemPrefab> items)
public Dictionary<ItemPrefab, int> GetBuyValuesAtCurrentLocation(Identifier storeIdentifier, IEnumerable<ItemPrefab> items)
{
var buyValues = new Dictionary<ItemPrefab, int>();
var store = Location?.GetStore(storeIdentifier);
if (store == null) { return buyValues; }
foreach (var item in items)
{
if (item == null) { continue; }
if (!buyValues.ContainsKey(item))
{
var buyValue = Location?.GetAdjustedItemBuyPrice(item) ?? 0;
int buyValue = store?.GetAdjustedItemBuyPrice(item) ?? 0;
buyValues.Add(item, buyValue);
}
}
return buyValues;
}
public Dictionary<ItemPrefab, int> GetSellValuesAtCurrentLocation(IEnumerable<ItemPrefab> items)
public Dictionary<ItemPrefab, int> GetSellValuesAtCurrentLocation(Identifier storeIdentifier, IEnumerable<ItemPrefab> items)
{
var sellValues = new Dictionary<ItemPrefab, int>();
var store = Location?.GetStore(storeIdentifier);
if (store == null) { return sellValues; }
foreach (var item in items)
{
if (item == null) { continue; }
if (!sellValues.ContainsKey(item))
{
var sellValue = Location?.GetAdjustedItemSellPrice(item) ?? 0;
int sellValue = store?.GetAdjustedItemSellPrice(item) ?? 0;
sellValues.Add(item, sellValue);
}
}
@@ -266,7 +344,13 @@ namespace Barotrauma
public void CreatePurchasedItems()
{
CreateItems(PurchasedItems, Submarine.MainSub);
var items = new List<PurchasedItem>();
foreach (var storeSpecificItems in PurchasedItems)
{
items.AddRange(storeSpecificItems.Value);
}
CreateItems(items, Submarine.MainSub);
PurchasedItems.Clear();
OnPurchasedItemsChanged?.Invoke();
}
@@ -367,7 +451,14 @@ namespace Barotrauma
if (sub == Submarine.MainSub)
{
#if CLIENT
new GUIMessageBox("", TextManager.GetWithVariable("CargoSpawnNotification", "[roomname]", cargoRoom.DisplayName, true), new string[0], type: GUIMessageBox.Type.InGame, iconStyle: "StoreShoppingCrateIcon");
new GUIMessageBox("",
TextManager.GetWithVariable("CargoSpawnNotification",
"[roomname]",
cargoRoom.DisplayName,
FormatCapitals.Yes),
Array.Empty<LocalizedString>(),
type: GUIMessageBox.Type.InGame,
iconStyle: "StoreShoppingCrateIcon");
#else
foreach (Client client in GameMain.Server.ConnectedClients)
{
@@ -420,7 +511,7 @@ namespace Barotrauma
#if SERVER
if (GameMain.Server != null)
{
Entity.Spawner.CreateNetworkEvent(itemContainer.Item, false);
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(itemContainer.Item));
}
#endif
}
@@ -431,7 +522,7 @@ namespace Barotrauma
itemSpawned(item);
#if SERVER
Entity.Spawner?.CreateNetworkEvent(item, false);
Entity.Spawner?.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
#endif
(itemContainer?.Item ?? item).CampaignInteractionType = CampaignMode.InteractionType.Cargo;
static void itemSpawned(Item item)
@@ -479,29 +570,41 @@ namespace Barotrauma
public void SavePurchasedItems(XElement parentElement)
{
var itemsElement = new XElement("cargo");
foreach (PurchasedItem item in PurchasedItems)
foreach (var storeSpecificItems in PurchasedItems)
{
if (item?.ItemPrefab == null) { continue; }
itemsElement.Add(new XElement("item",
new XAttribute("id", item.ItemPrefab.Identifier),
new XAttribute("qty", item.Quantity)));
foreach (var item in storeSpecificItems.Value)
{
if (item?.ItemPrefab == null) { continue; }
itemsElement.Add(new XElement("item",
new XAttribute("id", item.ItemPrefab.Identifier),
new XAttribute("qty", item.Quantity),
new XAttribute("storeid", storeSpecificItems.Key),
new XAttribute("buyer", item.BuyerCharacterInfoId)));
}
}
parentElement.Add(itemsElement);
}
public void LoadPurchasedItems(XElement element)
{
var purchasedItems = new List<PurchasedItem>();
var purchasedItems = new Dictionary<Identifier, List<PurchasedItem>>();
if (element != null)
{
foreach (XElement itemElement in element.GetChildElements("item"))
{
var id = itemElement.GetAttributeString("id", null);
if (string.IsNullOrWhiteSpace(id)) { continue; }
var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == id);
string prefabId = itemElement.GetAttributeString("id", null);
if (string.IsNullOrWhiteSpace(prefabId)) { continue; }
var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == prefabId);
if (prefab == null) { continue; }
var qty = itemElement.GetAttributeInt("qty", 0);
purchasedItems.Add(new PurchasedItem(prefab, qty));
int qty = itemElement.GetAttributeInt("qty", 0);
Identifier storeId = itemElement.GetAttributeIdentifier("storeid", "merchant");
int buyerId = itemElement.GetAttributeInt("buyer", 0);
if (!purchasedItems.TryGetValue(storeId, out var storeItems))
{
storeItems = new List<PurchasedItem>();
purchasedItems.Add(storeId, storeItems);
}
storeItems.Add(new PurchasedItem(prefab, qty, buyerId));
}
}
SetPurchasedItems(purchasedItems);
@@ -20,13 +20,33 @@ namespace Barotrauma
private readonly List<CharacterInfo> characterInfos = new List<CharacterInfo>();
private readonly List<Character> characters = new List<Character>();
public IEnumerable<Character> GetCharacters()
{
return characters;
}
public IEnumerable<CharacterInfo> GetCharacterInfos()
{
return characterInfos;
}
private Character welcomeMessageNPC;
public List<CharacterInfo> CharacterInfos => characterInfos;
public bool HasBots { get; set; }
public List<Pair<Order, float?>> ActiveOrders { get; } = new List<Pair<Order, float?>>();
public class ActiveOrder
{
public readonly Order Order;
public float? FadeOutTime;
public ActiveOrder(Order order, float? fadeOutTime)
{
Order = order;
FadeOutTime = fadeOutTime;
}
}
public List<ActiveOrder> ActiveOrders { get; } = new List<ActiveOrder>();
public bool IsSinglePlayer { get; private set; }
public ReadyCheck ActiveReadyCheck;
@@ -54,16 +74,16 @@ namespace Barotrauma
// Ignore orders work a bit differently since the "unignore" order counters the "ignore" order
var isUnignoreOrder = order.Identifier == "unignorethis";
var orderPrefab = !isUnignoreOrder ? order.Prefab : Order.GetPrefab("ignorethis");
Pair<Order, float?> existingOrder = ActiveOrders.Find(o =>
o.First.Prefab == orderPrefab && MatchesTarget(o.First.TargetEntity, order.TargetEntity) &&
(o.First.TargetType != Order.OrderTargetType.WallSection || o.First.WallSectionIndex == order.WallSectionIndex));
var orderPrefab = !isUnignoreOrder ? order.Prefab : OrderPrefab.Prefabs["ignorethis"];
ActiveOrder existingOrder = ActiveOrders.Find(o =>
o.Order.Prefab == orderPrefab && MatchesTarget(o.Order.TargetEntity, order.TargetEntity) &&
(o.Order.TargetType != Order.OrderTargetType.WallSection || o.Order.WallSectionIndex == order.WallSectionIndex));
if (existingOrder != null)
{
if (!isUnignoreOrder)
{
existingOrder.Second = fadeOutTime;
existingOrder.FadeOutTime = fadeOutTime;
return false;
}
else
@@ -74,7 +94,7 @@ namespace Barotrauma
}
else if (!isUnignoreOrder)
{
ActiveOrders.Add(new Pair<Order, float?>(order, fadeOutTime));
ActiveOrders.Add(new ActiveOrder(order, fadeOutTime));
#if CLIENT
HintManager.OnActiveOrderAdded(order);
#endif
@@ -96,7 +116,7 @@ namespace Barotrauma
public void AddCharacterElements(XElement element)
{
foreach (XElement characterElement in element.Elements())
foreach (var characterElement in element.Elements())
{
if (!characterElement.Name.ToString().Equals("character", StringComparison.OrdinalIgnoreCase)) { continue; }
CharacterInfo characterInfo = new CharacterInfo(characterElement);
@@ -105,7 +125,7 @@ namespace Barotrauma
characterInfo.CrewListIndex = characterElement.GetAttributeInt("crewlistindex", -1);
#endif
characterInfos.Add(characterInfo);
foreach (XElement subElement in characterElement.Elements())
foreach (var subElement in characterElement.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
@@ -205,7 +225,7 @@ namespace Barotrauma
wp.SpawnType == SpawnType.Human &&
wp.Submarine == Level.Loaded.StartOutpost &&
wp.CurrentHull != null &&
wp.CurrentHull.OutpostModuleTags.Contains("airlock"));
wp.CurrentHull.OutpostModuleTags.Contains("airlock".ToIdentifier()));
while (spawnWaypoints.Count > characterInfos.Count)
{
spawnWaypoints.RemoveAt(Rand.Int(spawnWaypoints.Count));
@@ -236,7 +256,7 @@ namespace Barotrauma
}
if (character.Info.InventoryData != null)
{
character.SpawnInventoryItems(character.Inventory, character.Info.InventoryData);
character.SpawnInventoryItems(character.Inventory, character.Info.InventoryData.FromPackage(null));
}
else if (!character.Info.StartItemsGiven)
{
@@ -301,12 +321,12 @@ namespace Barotrauma
public void Update(float deltaTime)
{
foreach (Pair<Order, float?> order in ActiveOrders)
foreach (ActiveOrder order in ActiveOrders)
{
if (order.Second.HasValue) { order.Second -= deltaTime; }
if (order.FadeOutTime.HasValue) { order.FadeOutTime -= deltaTime; }
}
ActiveOrders.RemoveAll(o => (o.Second.HasValue && o.Second <= 0.0f) ||
(o.First.TargetEntity != null && o.First.TargetEntity.Removed));
ActiveOrders.RemoveAll(o => (o.FadeOutTime.HasValue && o.FadeOutTime <= 0.0f) ||
(o.Order.TargetEntity != null && o.Order.TargetEntity.Removed));
UpdateConversations(deltaTime);
UpdateProjectSpecific(deltaTime);
@@ -357,20 +377,20 @@ namespace Barotrauma
if (player.TeamID != npc.TeamID && !player.IsIncapacitated && player.CurrentHull == npc.CurrentHull)
{
List<Character> availableSpeakers = new List<Character>() { npc, player };
List<string> dialogFlags = new List<string>() { "OutpostNPC", "EnterOutpost" };
List<Identifier> dialogFlags = new List<Identifier>() { "OutpostNPC".ToIdentifier(), "EnterOutpost".ToIdentifier() };
if (GameMain.GameSession?.GameMode is CampaignMode campaignMode)
{
if (campaignMode.Map?.CurrentLocation?.Type?.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase) ?? false)
if (campaignMode.Map?.CurrentLocation?.Type?.Identifier == "abandoned")
{
if (npc.TeamID == CharacterTeamType.None)
{
dialogFlags.Remove("OutpostNPC");
dialogFlags.Add("Bandit");
dialogFlags.Remove("OutpostNPC".ToIdentifier());
dialogFlags.Add("Bandit".ToIdentifier());
}
else if (npc.TeamID == CharacterTeamType.FriendlyNPC)
{
dialogFlags.Remove("OutpostNPC");
dialogFlags.Add("Hostage");
dialogFlags.Remove("OutpostNPC".ToIdentifier());
dialogFlags.Add("Hostage".ToIdentifier());
}
}
else if (campaignMode.Map?.CurrentLocation?.Reputation != null)
@@ -381,11 +401,11 @@ namespace Barotrauma
campaignMode.Map.CurrentLocation.Reputation.Value);
if (normalizedReputation < 0.2f)
{
dialogFlags.Add("LowReputation");
dialogFlags.Add("LowReputation".ToIdentifier());
}
else if (normalizedReputation > 0.8f)
{
dialogFlags.Add("HighReputation");
dialogFlags.Add("HighReputation".ToIdentifier());
}
}
}
@@ -451,13 +471,18 @@ namespace Barotrauma
// Prioritize those who are on the same submarine as the controlled character
.OrderByDescending(c => Character.Controlled == null || c.Submarine == Character.Controlled.Submarine)
// Prioritize those who are already ordered to operate the device
.ThenByDescending(c => order.Category == OrderCategory.Operate && c.CurrentOrders.Any(o => o.Order != null && o.Order.Identifier == order.Identifier && o.Order.TargetEntity == order.TargetEntity))
.ThenByDescending(c
=> order.Category == OrderCategory.Operate
&& c.CurrentOrders.Any(o
=> o != null
&& o.Identifier == order.Identifier
&& o.TargetEntity == order.TargetEntity))
// Prioritize those with the appropriate job for the order
.ThenByDescending(c => order.HasAppropriateJob(c))
.ThenByDescending(order.HasAppropriateJob)
// Prioritize those who don't yet have the same order (which allows quick-assigning the order to different characters)
.ThenByDescending(c => c.CurrentOrders.None(o => o.Order != null && o.Order.Identifier == order.Identifier))
.ThenByDescending(c => c.CurrentOrders.None(o => o != null && o.Identifier == order.Identifier))
// Prioritize those with the preferred job for the order
.ThenByDescending(c => order.HasPreferredJob(c))
.ThenByDescending(order.HasPreferredJob)
// Prioritize bots over player-controlled characters
.ThenByDescending(c => c.IsBot)
// Prioritize those with a lower current objective priority
@@ -472,12 +497,12 @@ namespace Barotrauma
{
ActiveOrdersElement = new XElement("activeorders");
// Only save orders with no fade out time (e.g. ignore orders)
var ordersToSave = new List<OrderInfo>();
var ordersToSave = new List<Order>();
foreach (var activeOrder in ActiveOrders)
{
var order = activeOrder?.First;
if (order == null || activeOrder.Second.HasValue) { continue; }
ordersToSave.Add(new OrderInfo(order, null, CharacterInfo.HighestManualOrderPriority));
var order = activeOrder?.Order;
if (order == null || activeOrder.FadeOutTime.HasValue) { continue; }
ordersToSave.Add(order.WithManualPriority(CharacterInfo.HighestManualOrderPriority));
}
CharacterInfo.SaveOrders(ActiveOrdersElement, ordersToSave.ToArray());
parentElement?.Add(ActiveOrdersElement);
@@ -489,22 +514,22 @@ namespace Barotrauma
foreach (var orderInfo in CharacterInfo.LoadOrders(ActiveOrdersElement))
{
IIgnorable ignoreTarget = null;
if (orderInfo.Order.IsIgnoreOrder)
if (orderInfo.IsIgnoreOrder)
{
switch (orderInfo.Order.TargetType)
switch (orderInfo.TargetType)
{
case Order.OrderTargetType.Entity:
ignoreTarget = orderInfo.Order.TargetEntity as IIgnorable;
ignoreTarget = orderInfo.TargetEntity as IIgnorable;
break;
case Order.OrderTargetType.WallSection when orderInfo.Order.TargetEntity is Structure s && orderInfo.Order.WallSectionIndex.HasValue:
ignoreTarget = s.GetSection(orderInfo.Order.WallSectionIndex.Value) as IIgnorable;
case Order.OrderTargetType.WallSection when orderInfo.TargetEntity is Structure s && orderInfo.WallSectionIndex.HasValue:
ignoreTarget = s.GetSection(orderInfo.WallSectionIndex.Value);
break;
default:
DebugConsole.ThrowError("Error loading an ignore order - can't find a proper ignore target");
continue;
}
}
if (orderInfo.Order.TargetEntity == null || (orderInfo.Order.IsIgnoreOrder && ignoreTarget == null))
if (orderInfo.TargetEntity == null || (orderInfo.IsIgnoreOrder && ignoreTarget == null))
{
// The order target doesn't exist anymore, just discard the loaded order
continue;
@@ -513,7 +538,7 @@ namespace Barotrauma
{
ignoreTarget.OrderedToBeIgnored = true;
}
AddOrder(orderInfo.Order, null);
AddOrder(orderInfo, null);
}
}
}
@@ -10,7 +10,7 @@ namespace Barotrauma
{
public CampaignMode Campaign { get; }
private readonly Dictionary<string, object> data = new Dictionary<string, object>();
private readonly Dictionary<Identifier, object> data = new Dictionary<Identifier, object>();
public CampaignMetadata(CampaignMode campaign)
{
@@ -21,15 +21,15 @@ namespace Barotrauma
{
Campaign = campaign;
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
if (string.Equals(subElement.Name.ToString(), "data", StringComparison.InvariantCultureIgnoreCase))
{
string identifier = subElement.GetAttributeString("key", string.Empty).ToLowerInvariant();
Identifier identifier = subElement.GetAttributeIdentifier("key", Identifier.Empty);
string value = subElement.GetAttributeString("value", string.Empty);
string valueType = subElement.GetAttributeString("type", string.Empty);
if (string.IsNullOrWhiteSpace(identifier) || string.IsNullOrWhiteSpace(value) || string.IsNullOrWhiteSpace(valueType))
if (identifier.IsEmpty || string.IsNullOrWhiteSpace(value) || string.IsNullOrWhiteSpace(valueType))
{
DebugConsole.ThrowError("Unable to load value because one or more of the required attributes are empty.\n" +
$"key: \"{identifier}\", value: \"{value}\", type: \"{valueType}\"");
@@ -43,28 +43,20 @@ namespace Barotrauma
DebugConsole.ThrowError($"Type for {identifier} not found ({valueType}).");
continue;
}
if (type == typeof(float))
else if (type == typeof(Identifier))
{
if (!float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out float floatValue))
{
DebugConsole.ThrowError($"Error in campaign metadata: could not parse \"{value}\" as a float.");
continue;
}
data.Add(identifier, floatValue);
data.Add(identifier, value.ToIdentifier());
}
else
{
data.Add(identifier, Convert.ChangeType(value, type));
data.Add(identifier, Convert.ChangeType(value, type, NumberFormatInfo.InvariantInfo));
}
}
}
}
public void SetValue(string identifier, object value)
public void SetValue(Identifier identifier, object value)
{
identifier = identifier.ToLowerInvariant();
DebugConsole.Log($"Set the value \"{identifier}\" to {value}");
if (!data.ContainsKey(identifier))
@@ -76,33 +68,32 @@ namespace Barotrauma
data[identifier] = value;
}
public float GetFloat(string identifier, float? defaultValue = null)
public float GetFloat(Identifier identifier, float? defaultValue = null)
{
return (float)GetTypeOrDefault(identifier, typeof(float), defaultValue ?? 0f);
}
public int GetInt(string identifier, int? defaultValue = null)
public int GetInt(Identifier identifier, int? defaultValue = null)
{
return (int)GetTypeOrDefault(identifier, typeof(int), defaultValue ?? 0);
}
public bool GetBoolean(string identifier, bool? defaultValue = null)
public bool GetBoolean(Identifier identifier, bool? defaultValue = null)
{
return (bool)GetTypeOrDefault(identifier, typeof(bool), defaultValue ?? false);
}
public string GetString(string identifier, string? defaultValue = null)
public string GetString(Identifier identifier, string? defaultValue = null)
{
return (string)GetTypeOrDefault(identifier, typeof(string), defaultValue ?? string.Empty);
}
public bool HasKey(string identifier)
public bool HasKey(Identifier identifier)
{
identifier = identifier.ToLowerInvariant();
return data.ContainsKey(identifier);
}
private object GetTypeOrDefault(string identifier, Type type, object defaultValue)
private object GetTypeOrDefault(Identifier identifier, Type type, object defaultValue)
{
object? value = GetValue(identifier);
@@ -122,7 +113,7 @@ namespace Barotrauma
return defaultValue;
}
public object? GetValue(string identifier)
public object? GetValue(Identifier identifier)
{
return data.ContainsKey(identifier) ? data[identifier] : null;
}
@@ -133,16 +124,16 @@ namespace Barotrauma
foreach (var (key, value) in data)
{
string valueStr = value?.ToString() ?? "";
if (value?.GetType() == typeof(float))
string valueStr = value.ToString() ?? throw new NullReferenceException();
if (value is float f)
{
valueStr = ((float)value).ToString("G", CultureInfo.InvariantCulture);
valueStr = f.ToString("G", CultureInfo.InvariantCulture);
}
element.Add(new XElement("Data",
new XAttribute("key", key),
new XAttribute("value", valueStr),
new XAttribute("type", value?.GetType())));
new XAttribute("type", value.GetType())));
}
#if DEBUG
DebugConsole.Log(element.ToString());
@@ -1,8 +1,6 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
using System;
namespace Barotrauma
{
@@ -18,16 +16,16 @@ namespace Barotrauma
}
}
internal class FactionPrefab : IDisposable
internal class FactionPrefab : Prefab
{
public static List<FactionPrefab> Prefabs { get; set; }
public readonly static PrefabCollection<FactionPrefab> Prefabs = new PrefabCollection<FactionPrefab>();
public string Name { get; }
public LocalizedString Name { get; }
public string Description { get; }
public string ShortDescription { get; }
public LocalizedString Description { get; }
public LocalizedString ShortDescription { get; }
public string Identifier { get; }
public int MenuOrder { get; }
/// <summary>
/// How low the reputation can drop on this faction
@@ -52,17 +50,17 @@ namespace Barotrauma
public Color IconColor { get; }
#endif
private FactionPrefab(XElement element)
public FactionPrefab(ContentXElement element, FactionsFile file) : base(file, element.GetAttributeIdentifier("identifier", string.Empty))
{
Identifier = element.GetAttributeString("identifier", string.Empty);
MenuOrder = element.GetAttributeInt("menuorder", 0);
MinReputation = element.GetAttributeInt("minreputation", -100);
MaxReputation = element.GetAttributeInt("maxreputation", 100);
InitialReputation = element.GetAttributeInt("initialreputation", 0);
Name = element.GetAttributeString("name", null) ?? TextManager.Get($"faction.{Identifier}", returnNull: true) ?? "Unnamed";
Description = element.GetAttributeString("description", null) ?? TextManager.Get($"faction.{Identifier}.description", returnNull: true) ?? "";
ShortDescription = element.GetAttributeString("shortdescription", null) ?? TextManager.Get($"faction.{Identifier}.shortdescription", returnNull: true) ?? "";
Name = element.GetAttributeString("name", null) ?? TextManager.Get($"faction.{Identifier}").Fallback("Unnamed");
Description = element.GetAttributeString("description", null) ?? TextManager.Get($"faction.{Identifier}.description").Fallback("");
ShortDescription = element.GetAttributeString("shortdescription", null) ?? TextManager.Get($"faction.{Identifier}.shortdescription").Fallback("");
#if CLIENT
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
if (subElement.Name.ToString().Equals("icon", StringComparison.OrdinalIgnoreCase))
@@ -78,64 +76,12 @@ namespace Barotrauma
#endif
}
public static void LoadFactions()
{
Prefabs?.ForEach(set => set.Dispose());
Prefabs = new List<FactionPrefab>();
IEnumerable<ContentFile> files = GameMain.Instance.GetFilesOfType(ContentType.Factions);
foreach (ContentFile file in files)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
XElement? rootElement = doc?.Root;
if (doc == null || rootElement == null) { continue; }
if (doc.Root.IsOverride())
{
Prefabs.Clear();
DebugConsole.NewMessage($"Overriding all factions with '{file.Path}'", Color.Yellow);
}
foreach (XElement element in rootElement.Elements())
{
bool isOverride = element.IsOverride();
XElement sourceElement = isOverride ? element.FirstElement() : element;
string elementName = sourceElement.Name.ToString().ToLowerInvariant();
string identifier = sourceElement.GetAttributeString("identifier", null);
if (string.IsNullOrWhiteSpace(identifier))
{
DebugConsole.ThrowError($"No identifier defined for the faction config '{elementName}' in file '{file.Path}'");
continue;
}
var existingParams = Prefabs.Find(set => set.Identifier == identifier);
if (existingParams != null)
{
if (isOverride)
{
DebugConsole.NewMessage($"Overriding faction config '{identifier}' using the file '{file.Path}'", Color.Yellow);
Prefabs.Remove(existingParams);
}
else
{
DebugConsole.ThrowError($"Duplicate faction config: '{identifier}' defined in {elementName} of '{file.Path}'");
continue;
}
}
Prefabs.Add(new FactionPrefab(element));
}
}
}
public void Dispose()
public override void Dispose()
{
#if CLIENT
Icon?.Remove();
Icon = null;
#endif
GC.SuppressFinalize(this);
}
}
}
@@ -13,13 +13,13 @@ namespace Barotrauma
public const float MinReputationLossPerStolenItem = 0.5f;
public const float MaxReputationLossPerStolenItem = 10.0f;
public string Identifier { get; }
public Identifier Identifier { get; }
public int MinReputation { get; }
public int MaxReputation { get; }
public int InitialReputation { get; }
public CampaignMetadata Metadata { get; }
private readonly string metaDataIdentifier;
private readonly Identifier metaDataIdentifier;
/// <summary>
/// Reputation value normalized to the range of 0-1
@@ -46,8 +46,8 @@ namespace Barotrauma
if (increase != 0 && Character.Controlled != null)
{
Character.Controlled.AddMessage(
TextManager.GetWithVariable("reputationgainnotification", "[reputationname]", Location?.Name ?? Faction.Prefab.Name),
increase > 0 ? GUI.Style.Green : GUI.Style.Red,
TextManager.GetWithVariable("reputationgainnotification", "[reputationname]", Location?.Name ?? Faction.Prefab.Name).Value,
increase > 0 ? GUIStyle.Green : GUIStyle.Red,
playSound: true, Identifier, increase, lifetime: 5.0f);
}
#endif
@@ -64,7 +64,7 @@ namespace Barotrauma
if (reputationChange > 0f)
{
float reputationGainMultiplier = 1f;
foreach (Character character in GameSession.GetSessionCrewCharacters())
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
{
reputationGainMultiplier += character.GetStatValue(StatTypes.ReputationGainMultiplier);
}
@@ -80,23 +80,23 @@ namespace Barotrauma
public readonly Location Location;
public Reputation(CampaignMetadata metadata, Location location, string identifier, int minReputation, int maxReputation, int initialReputation)
public Reputation(CampaignMetadata metadata, Location location, Identifier identifier, int minReputation, int maxReputation, int initialReputation)
: this(metadata, null, location, identifier, minReputation, maxReputation, initialReputation)
{
}
public Reputation(CampaignMetadata metadata, Faction faction, int minReputation, int maxReputation, int initialReputation)
: this(metadata, faction, null, $"faction.{faction.Prefab.Identifier}", minReputation, maxReputation, initialReputation)
: this(metadata, faction, null, $"faction.{faction.Prefab.Identifier}".ToIdentifier(), minReputation, maxReputation, initialReputation)
{
}
private Reputation(CampaignMetadata metadata, Faction faction, Location location, string identifier, int minReputation, int maxReputation, int initialReputation)
private Reputation(CampaignMetadata metadata, Faction faction, Location location, Identifier identifier, int minReputation, int maxReputation, int initialReputation)
{
System.Diagnostics.Debug.Assert(metadata != null);
System.Diagnostics.Debug.Assert(faction != null || location != null);
Metadata = metadata;
Identifier = identifier.ToLowerInvariant();
metaDataIdentifier = $"reputation.{Identifier}";
Identifier = identifier;
metaDataIdentifier = $"reputation.{Identifier}".ToIdentifier();
MinReputation = minReputation;
MaxReputation = maxReputation;
InitialReputation = initialReputation;
@@ -104,12 +104,12 @@ namespace Barotrauma
Location = location;
}
public string GetReputationName()
public LocalizedString GetReputationName()
{
return GetReputationName(NormalizedValue);
}
public static string GetReputationName(float normalizedValue)
public static LocalizedString GetReputationName(float normalizedValue)
{
if (normalizedValue < HostileThreshold)
{
@@ -135,36 +135,36 @@ namespace Barotrauma
{
if (normalizedValue < HostileThreshold)
{
return GUI.Style.ColorReputationVeryLow;
return GUIStyle.ColorReputationVeryLow;
}
else if (normalizedValue < 0.4f)
{
return GUI.Style.ColorReputationLow;
return GUIStyle.ColorReputationLow;
}
else if (normalizedValue < 0.6f)
{
return GUI.Style.ColorReputationNeutral;
return GUIStyle.ColorReputationNeutral;
}
else if (normalizedValue < 0.8f)
{
return GUI.Style.ColorReputationHigh;
return GUIStyle.ColorReputationHigh;
}
return GUI.Style.ColorReputationVeryHigh;
return GUIStyle.ColorReputationVeryHigh;
}
public string GetFormattedReputationText(bool addColorTags = false)
public LocalizedString GetFormattedReputationText(bool addColorTags = false)
{
return GetFormattedReputationText(NormalizedValue, Value, addColorTags);
}
public static string GetFormattedReputationText(float normalizedValue, float value, bool addColorTags = false)
public static LocalizedString GetFormattedReputationText(float normalizedValue, float value, bool addColorTags = false)
{
string reputationName = GetReputationName(normalizedValue);
string formattedReputation = TextManager.GetWithVariables("reputationformat",
new string[] { "[reputationname]", "[reputationvalue]" },
new string[] { reputationName, ((int)Math.Round(value)).ToString() });
LocalizedString reputationName = GetReputationName(normalizedValue);
LocalizedString formattedReputation = TextManager.GetWithVariables("reputationformat",
("[reputationname]", reputationName),
("[reputationvalue]", ((int)Math.Round(value)).ToString()));
if (addColorTags)
{
formattedReputation = $"‖color:{XMLExtensions.ColorToString(GetReputationColor(normalizedValue))}‖{formattedReputation}‖end‖";
formattedReputation = $"‖color:{XMLExtensions.ToStringHex(GetReputationColor(normalizedValue))}‖{formattedReputation}‖end‖";
}
return formattedReputation;
}
@@ -0,0 +1,225 @@
using System;
using System.Xml.Linq;
using Barotrauma.Networking;
namespace Barotrauma
{
internal readonly struct WalletChangedEvent
{
public readonly Option<Character> Owner;
public readonly Wallet Wallet;
public readonly WalletInfo Info;
public readonly WalletChangedData ChangedData;
public WalletChangedEvent(Wallet wallet, WalletChangedData changedData, WalletInfo info)
{
Wallet = wallet;
Info = info;
ChangedData = changedData;
Owner = wallet.Owner;
}
}
[NetworkSerialize]
internal struct WalletInfo : INetSerializableStruct
{
public int RewardDistribution;
public int Balance;
}
/// <summary>
/// Network message for the server to update wallet values to clients
/// </summary>
internal struct NetWalletUpdate : INetSerializableStruct
{
[NetworkSerialize(ArrayMaxSize = NetConfig.MaxPlayers + 1)]
public NetWalletTransaction[] Transactions;
}
/// <summary>
/// Network message for the client to transfer money between wallets
/// </summary>
[NetworkSerialize]
internal struct NetWalletTransfer : INetSerializableStruct
{
public Option<ushort> Sender;
public Option<ushort> Receiver;
public int Amount;
}
/// <summary>
/// Network message for the client to set the salary of someone
/// </summary>
internal struct NetWalletSetSalaryUpdate : INetSerializableStruct
{
[NetworkSerialize]
public ushort Target;
[NetworkSerialize(MinValueInt = 0, MaxValueInt = 100)]
public int NewRewardDistribution;
}
/// <summary>
/// Represents the difference in balance and salary when a wallet gets updated
/// Not really used right now but could be used for notifications when receiving funds similar to how talents do it
/// </summary>
[NetworkSerialize]
internal struct WalletChangedData : INetSerializableStruct
{
public Option<int> RewardDistributionChanged;
public Option<int> BalanceChanged;
public WalletChangedData MergeInto(WalletChangedData other)
{
other.BalanceChanged = AddOptionalInt(other.BalanceChanged, BalanceChanged);
other.RewardDistributionChanged = AddOptionalInt(other.RewardDistributionChanged, RewardDistributionChanged);
return other;
static Option<int> AddOptionalInt(Option<int> a, Option<int> b)
{
return a switch
{
Some<int> some1 => b switch
{
Some<int> some2 => Option<int>.Some(some1.Value + some2.Value),
None<int> _ => Option<int>.Some(some1.Value),
_ => throw new ArgumentOutOfRangeException(nameof(b))
},
None<int> _ => b switch
{
Some<int> some1 => Option<int>.Some(some1.Value),
None<int> _ => Option<int>.None(),
_ => throw new ArgumentOutOfRangeException(nameof(b))
},
_ => throw new ArgumentOutOfRangeException(nameof(a))
};
}
}
}
/// <summary>
/// Represents an update that changed the amount of money or salary of the wallet
/// </summary>
[NetworkSerialize]
internal struct NetWalletTransaction : INetSerializableStruct
{
public Option<ushort> CharacterID;
public WalletChangedData ChangedData;
public WalletInfo Info;
}
// ReSharper disable ValueParameterNotUsed
internal sealed class InvalidWallet : Wallet
{
public InvalidWallet(): base(Option<Character>.None()) { }
public override int Balance
{
get => 0;
set => 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");
}
}
internal partial class Wallet
{
public static readonly Wallet Invalid = new InvalidWallet();
public const string LowerCaseSaveElementName = "wallet";
private const string AttributeNameBalance = "balance",
AttrubuteNameRewardDistribution = "rewarddistribution",
SaveElementName = "Wallet";
public readonly Option<Character> Owner;
private int balance;
public virtual int Balance
{
get => balance;
set => balance = ClampBalance(value);
}
private int rewardDistribution;
public virtual int RewardDistribution
{
get => rewardDistribution;
set => rewardDistribution = ClampRewardDistribution(value);
}
public Wallet(Option<Character> owner)
{
Owner = owner;
}
public Wallet(Option<Character> owner, XElement element): this(owner)
{
balance = ClampBalance(element.GetAttributeInt(AttributeNameBalance, 0));
rewardDistribution = ClampBalance(element.GetAttributeInt(AttrubuteNameRewardDistribution, 0));
}
public XElement Save()
{
XElement element = new XElement(SaveElementName, new XAttribute(AttributeNameBalance, Balance), new XAttribute(AttrubuteNameRewardDistribution, RewardDistribution));
return element;
}
public bool TryDeduct(int price)
{
if (!CanAfford(price)) { return false; }
Deduct(price);
return true;
}
public bool CanAfford(int price) => Balance >= price;
public void Refund(int price) => Give(price);
public void Give(int amount)
{
Balance += amount;
SettingsChanged(balanceChanged: Option<int>.Some(amount), rewardChanged: Option<int>.None());
}
public void Deduct(int price)
{
Balance -= price;
SettingsChanged(balanceChanged: Option<int>.Some(-price), rewardChanged: Option<int>.None());
}
public void SetRewardDistribution(int value)
{
int oldValue = RewardDistribution;
RewardDistribution = value;
SettingsChanged(balanceChanged: Option<int>.None(), rewardChanged: Option<int>.Some(RewardDistribution - oldValue));
}
public WalletInfo CreateWalletInfo()
{
return new WalletInfo
{
Balance = Balance,
RewardDistribution = RewardDistribution
};
}
public string GetOwnerLogName() => Owner switch
{
Some<Character> { Value: var character } => character.Name,
None<Character> _ => "the bank",
_ => throw new ArgumentOutOfRangeException(nameof(Owner))
};
partial void SettingsChanged(Option<int> balanceChanged, Option<int> rewardChanged);
private static int ClampBalance(int value) => Math.Clamp(value, 0, CampaignMode.MaxMoney);
private static int ClampRewardDistribution(int value) => Math.Clamp(value, 0, 100);
}
}
@@ -53,7 +53,7 @@ namespace Barotrauma
public int GetAddedMissionCount()
{
int count = 0;
foreach (Character character in GameSession.GetSessionCrewCharacters())
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
{
count += (int)character.GetStatValue(StatTypes.ExtraMissionCount);
}
@@ -68,7 +68,16 @@ namespace Barotrauma
abstract partial class CampaignMode : GameMode
{
const int MaxMoney = int.MaxValue / 2; //about 1 billion
[NetworkSerialize]
public struct SaveInfo : INetSerializableStruct
{
public string FilePath;
public int SaveTime;
public string SubmarineName;
public string[] EnabledContentPackageNames;
}
public const int MaxMoney = int.MaxValue / 2; //about 1 billion
public const int InitialMoney = 8500;
//duration of the cinematic + credits at the end of the campaign
@@ -102,6 +111,8 @@ namespace Barotrauma
private readonly List<Mission> extraMissions = new List<Mission>();
public readonly NamedEvent<WalletChangedEvent> OnMoneyChanged = new NamedEvent<WalletChangedEvent>();
public enum TransitionType
{
None,
@@ -167,12 +178,7 @@ namespace Barotrauma
}
}
private int money;
public int Money
{
get { return money; }
set { money = MathHelper.Clamp(value, 0, MaxMoney); }
}
public Wallet Bank;
public LevelData NextLevel
{
@@ -183,9 +189,42 @@ namespace Barotrauma
protected CampaignMode(GameModePreset preset)
: base(preset)
{
Money = InitialMoney;
Bank = new Wallet(Option<Character>.None())
{
Balance = InitialMoney
};
CargoManager = new CargoManager(this);
MedicalClinic = new MedicalClinic(this);
Identifier messageIdentifier = new Identifier("money");
#if CLIENT
OnMoneyChanged.RegisterOverwriteExisting(new Identifier("CampaignMoneyChangeNotification"), e =>
{
if (!(e.ChangedData.BalanceChanged is Some<int> { Value: var changed })) { return; }
bool isGain = changed > 0;
Color clr = isGain ? GUIStyle.Yellow : GUIStyle.Red;
switch (e.Owner)
{
case Some<Character> { Value: var owner}:
owner.AddMessage(FormatMessage(), clr, playSound: Character.Controlled == owner, messageIdentifier, changed);
break;
case None<Character> _ when IsSinglePlayer:
Character.Controlled?.AddMessage(FormatMessage(), clr, playSound: true, messageIdentifier, changed);
break;
}
string FormatMessage() => TextManager.GetWithVariable(isGain ? "moneygainformat" : "moneyloseformat", "[money]", TextManager.FormatCurrency(Math.Abs(changed))).ToString();
});
#endif
}
public virtual Wallet GetWallet(Client client = null)
{
return Bank;
}
/// <summary>
@@ -200,7 +239,7 @@ namespace Barotrauma
{
return Level.Loaded.EndLocation;
}
return Level.Loaded?.StartLocation ?? Map.CurrentLocation;
return Level.Loaded?.StartLocation ?? Map.CurrentLocation;
}
public List<Submarine> GetSubsToLeaveBehind(Submarine leavingSub)
@@ -255,8 +294,6 @@ namespace Barotrauma
PurchasedLostShuttles = false;
var connectedSubs = Submarine.MainSub.GetConnectedSubs();
wasDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost);
ResetTalentData();
}
public void InitCampaignData()
@@ -304,11 +341,11 @@ namespace Barotrauma
if (levelData.HasBeaconStation && !levelData.IsBeaconActive)
{
var beaconMissionPrefabs = MissionPrefab.List.FindAll(m => m.Tags.Any(t => t.Equals("beaconnoreward", StringComparison.OrdinalIgnoreCase)));
var beaconMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t.Equals("beaconnoreward", StringComparison.OrdinalIgnoreCase)));
if (beaconMissionPrefabs.Any())
{
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
var beaconMissionPrefab = ToolBox.SelectWeightedRandom(beaconMissionPrefabs, beaconMissionPrefabs.Select(p => (float)p.Commonness).ToList(), rand);
var beaconMissionPrefab = ToolBox.SelectWeightedRandom(beaconMissionPrefabs, p => (float)p.Commonness, rand);
if (!Missions.Any(m => m.Prefab.Type == beaconMissionPrefab.Type))
{
extraMissions.Add(beaconMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub));
@@ -317,15 +354,32 @@ namespace Barotrauma
}
if (levelData.HasHuntingGrounds)
{
var huntingGroundsMissionPrefabs = MissionPrefab.List.FindAll(m => m.Tags.Any(t => t.Equals("huntinggroundsnoreward", StringComparison.OrdinalIgnoreCase)));
var huntingGroundsMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase)));
if (!huntingGroundsMissionPrefabs.Any())
{
DebugConsole.AddWarning("Could not find a hunting grounds mission for the level. No mission with the tag \"huntinggroundsnoreward\" found.");
DebugConsole.AddWarning("Could not find a hunting grounds mission for the level. No mission with the tag \"huntinggrounds\" found.");
}
else
{
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(huntingGroundsMissionPrefabs, huntingGroundsMissionPrefabs.Select(p => (float)Math.Max(p.Commonness, 0.1f)).ToList(), rand);
// Adjust the prefab commonness based on the difficulty tag
var prefabs = huntingGroundsMissionPrefabs.ToList();
var weights = prefabs.Select(p => (float)Math.Max(p.Commonness, 1)).ToList();
for (int i = 0; i < prefabs.Count; i++)
{
var prefab = prefabs[i];
var weight = weights[i];
if (prefab.Tags.Contains("easy"))
{
weight *= MathHelper.Lerp(0.2f, 2f, MathUtils.InverseLerp(80, LevelData.HuntingGroundsDifficultyThreshold, levelData.Difficulty));
}
else if (prefab.Tags.Contains("hard"))
{
weight *= MathHelper.Lerp(0.5f, 1.5f, MathUtils.InverseLerp(LevelData.HuntingGroundsDifficultyThreshold + 10, 80, levelData.Difficulty));
}
weights[i] = weight;
}
var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(prefabs, weights, rand);
if (!Missions.Any(m => m.Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase))))
{
extraMissions.Add(huntingGroundsMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub));
@@ -592,22 +646,22 @@ namespace Barotrauma
if (map != null && CargoManager != null)
{
map.CurrentLocation.RegisterTakenItems(takenItems);
map.CurrentLocation.AddToStock(CargoManager.SoldItems);
map.CurrentLocation.AddStock(CargoManager.SoldItems);
CargoManager.ClearSoldItemsProjSpecific();
map.CurrentLocation.RemoveFromStock(CargoManager.PurchasedItems);
map.CurrentLocation.RemoveStock(CargoManager.PurchasedItems);
}
if (GameMain.NetworkMember == null)
{
CargoManager.ClearItemsInBuyCrate();
CargoManager.ClearItemsInSellCrate();
CargoManager.ClearItemsInSellFromSubCrate();
CargoManager?.ClearItemsInBuyCrate();
CargoManager?.ClearItemsInSellCrate();
CargoManager?.ClearItemsInSellFromSubCrate();
}
else
{
if (GameMain.NetworkMember.IsServer)
{
CargoManager?.ClearItemsInBuyCrate();
// TODO: CargoManager?.ClearItemsInSellFromSubCrate();
CargoManager?.ClearItemsInSellFromSubCrate();
}
else if (GameMain.NetworkMember.IsClient)
{
@@ -692,32 +746,31 @@ namespace Barotrauma
if (CampaignMetadata != null)
{
int loops = CampaignMetadata.GetInt("campaign.endings", 0);
CampaignMetadata.SetValue("campaign.endings", loops + 1);
int loops = CampaignMetadata.GetInt("campaign.endings".ToIdentifier(), 0);
CampaignMetadata.SetValue("campaign.endings".ToIdentifier(), loops + 1);
}
GameAnalyticsManager.AddProgressionEvent(
GameAnalyticsManager.ProgressionStatus.Complete,
Preset?.Identifier ?? "none");
Preset?.Identifier.Value ?? "none");
string eventId = "FinishCampaign:";
GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"));
GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0));
GameAnalyticsManager.AddDesignEvent(eventId + "Money", Money);
GameAnalyticsManager.AddDesignEvent(eventId + "Money", Bank.Balance);
GameAnalyticsManager.AddDesignEvent(eventId + "Playtime", TotalPlayTime);
GameAnalyticsManager.AddDesignEvent(eventId + "PassedLevels", TotalPassedLevels);
}
protected virtual void EndCampaignProjSpecific() { }
public bool TryHireCharacter(Location location, CharacterInfo characterInfo)
public bool TryHireCharacter(Location location, CharacterInfo characterInfo, Client client = null)
{
if (characterInfo == null) { return false; }
if (Money < characterInfo.Salary) { return false; }
if (!GetWallet(client).TryDeduct(characterInfo.Salary)) { return false; }
characterInfo.IsNewHire = true;
location.RemoveHireableCharacter(characterInfo);
CrewManager.AddCharacterInfo(characterInfo);
Money -= characterInfo.Salary;
GameAnalyticsManager.AddMoneySpentEvent(characterInfo.Salary, GameAnalyticsManager.MoneySink.Crew, characterInfo.Job?.Prefab.Identifier ?? "unknown");
GameAnalyticsManager.AddMoneySpentEvent(characterInfo.Salary, GameAnalyticsManager.MoneySink.Crew, characterInfo.Job?.Prefab.Identifier.Value ?? "unknown");
return true;
}
@@ -740,8 +793,8 @@ namespace Barotrauma
HumanAIController humanAI = npc.AIController as HumanAIController;
if (humanAI == null) { yield return CoroutineStatus.Success; }
var waitOrder = Order.PrefabList.Find(o => o.Identifier.Equals("wait", StringComparison.OrdinalIgnoreCase));
humanAI.SetForcedOrder(waitOrder, string.Empty, null);
var waitOrder = OrderPrefab.Prefabs["wait"].CreateInstance(OrderPrefab.OrderTargetType.Entity);
humanAI.SetForcedOrder(waitOrder);
var waitObjective = humanAI.ObjectiveManager.ForcedOrder;
humanAI.FaceTarget(interactor);
@@ -769,6 +822,11 @@ namespace Barotrauma
public void AssignNPCMenuInteraction(Character character, InteractionType interactionType)
{
character.CampaignInteractionType = interactionType;
if (character.CampaignInteractionType == InteractionType.Store &&
character.HumanPrefab is { Identifier: var merchantId })
{
character.MerchantIdentifier = merchantId;
}
character.DisableHealthWindow =
interactionType != InteractionType.None &&
interactionType != InteractionType.Examine &&
@@ -782,7 +840,7 @@ namespace Barotrauma
character.SetCustomInteract(
NPCInteract,
#if CLIENT
hudText: TextManager.GetWithVariable("CampaignInteraction." + interactionType, "[key]", GameMain.Config.KeyBindText(InputType.Use)));
hudText: TextManager.GetWithVariable("CampaignInteraction." + interactionType, "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use)));
#else
hudText: TextManager.Get("CampaignInteraction." + interactionType));
#endif
@@ -855,8 +913,8 @@ namespace Barotrauma
{
GameMain.Server.SendDirectChatMessage(Networking.ChatMessage.Create(
TextManager.Get("RadioAnnouncerName"),
TextManager.Get("TooFarFromOutpostWarning"), Networking.ChatMessageType.Default, null), c);
TextManager.Get("RadioAnnouncerName").Value,
TextManager.Get("TooFarFromOutpostWarning").Value, Networking.ChatMessageType.Default, null), c);
}
#endif
}
@@ -905,7 +963,7 @@ namespace Barotrauma
public void LogState()
{
DebugConsole.NewMessage("********* CAMPAIGN STATUS *********", Color.White);
DebugConsole.NewMessage(" Money: " + Money, Color.White);
DebugConsole.NewMessage(" Money: " + Bank.Balance, Color.White);
DebugConsole.NewMessage(" Current location: " + map.CurrentLocation.Name, Color.White);
DebugConsole.NewMessage(" Available destinations: ", Color.White);
@@ -959,13 +1017,5 @@ namespace Barotrauma
}
}
// Talent relevant data, only stored for the duration of the mission
private void ResetTalentData()
{
CrewHasDied = false;
}
public bool CrewHasDied { get; set; }
}
}
@@ -26,33 +26,6 @@ namespace Barotrauma
private XElement itemData;
private XElement healthData;
public XElement OrderData { get; private set; }
public void Refresh(Character character)
{
healthData = new XElement("health");
character.CharacterHealth.Save(healthData);
if (character.Inventory != null)
{
itemData = new XElement("inventory");
Character.SaveInventory(character.Inventory, itemData);
}
OrderData = new XElement("orders");
CharacterInfo.SaveOrderData(character.Info, OrderData);
}
public XElement Save()
{
XElement element = new XElement("CharacterCampaignData",
new XAttribute("name", Name),
new XAttribute("endpoint", ClientEndPoint),
new XAttribute("steamid", SteamID));
CharacterInfo?.Save(element);
if (itemData != null) { element.Add(itemData); }
if (healthData != null) { element.Add(healthData); }
if (OrderData != null) { element.Add(OrderData); }
return element;
}
public XElement WalletData;
}
}
@@ -27,7 +27,7 @@ namespace Barotrauma
get { return preset.IsSinglePlayer; }
}
public string Name
public LocalizedString Name
{
get { return preset.Name; }
}
@@ -19,20 +19,20 @@ namespace Barotrauma
public readonly Type GameModeType;
public readonly string Name;
public readonly string Description;
public readonly LocalizedString Name;
public readonly LocalizedString Description;
public readonly string Identifier;
public readonly Identifier Identifier;
public readonly bool IsSinglePlayer;
//are clients allowed to vote for this gamemode
public readonly bool Votable;
public GameModePreset(string identifier, Type type, bool isSinglePlayer = false, bool votable = true)
public GameModePreset(Identifier identifier, Type type, bool isSinglePlayer = false, bool votable = true)
{
Name = TextManager.Get("GameMode." + identifier);
Description = TextManager.Get("GameModeDescription." + identifier, returnNull: true) ?? "";
Description = TextManager.Get("GameModeDescription." + identifier).Fallback("");
Identifier = identifier;
GameModeType = type;
@@ -46,15 +46,15 @@ namespace Barotrauma
public static void Init()
{
#if CLIENT
Tutorial = new GameModePreset("tutorial", typeof(TutorialMode), true);
DevSandbox = new GameModePreset("devsandbox", typeof(GameMode), true);
SinglePlayerCampaign = new GameModePreset("singleplayercampaign", typeof(SinglePlayerCampaign), true);
TestMode = new GameModePreset("testmode", typeof(TestGameMode), true);
Tutorial = new GameModePreset("tutorial".ToIdentifier(), typeof(TutorialMode), true);
DevSandbox = new GameModePreset("devsandbox".ToIdentifier(), typeof(GameMode), true);
SinglePlayerCampaign = new GameModePreset("singleplayercampaign".ToIdentifier(), typeof(SinglePlayerCampaign), true);
TestMode = new GameModePreset("testmode".ToIdentifier(), typeof(TestGameMode), true);
#endif
Sandbox = new GameModePreset("sandbox", typeof(GameMode), false);
Mission = new GameModePreset("mission", typeof(CoOpMode), false);
PvP = new GameModePreset("pvp", typeof(PvPMode), false);
MultiPlayerCampaign = new GameModePreset("multiplayercampaign", typeof(MultiPlayerCampaign), false, false);
Sandbox = new GameModePreset("sandbox".ToIdentifier(), typeof(GameMode), false);
Mission = new GameModePreset("mission".ToIdentifier(), typeof(CoOpMode), false);
PvP = new GameModePreset("pvp".ToIdentifier(), typeof(PvPMode), false);
MultiPlayerCampaign = new GameModePreset("multiplayercampaign".ToIdentifier(), typeof(MultiPlayerCampaign), false, false);
}
}
}
@@ -1,4 +1,5 @@
using Barotrauma.IO;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -99,7 +100,6 @@ namespace Barotrauma
/// </summary>
private void Load(XElement element)
{
Money = element.GetAttributeInt("money", 0);
PurchasedLostShuttles = element.GetAttributeBool("purchasedlostshuttles", false);
PurchasedHullRepairs = element.GetAttributeBool("purchasedhullrepairs", false);
PurchasedItemRepairs = element.GetAttributeBool("purchaseditemrepairs", false);
@@ -120,7 +120,7 @@ namespace Barotrauma
#endif
}
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
@@ -166,6 +166,9 @@ namespace Barotrauma
case "stats":
LoadStats(subElement);
break;
case Wallet.LowerCaseSaveElementName:
Bank = new Wallet(Option<Character>.None(), subElement);
break;
#if SERVER
case "savedexperiencepoints":
foreach (XElement savedExp in subElement.Elements())
@@ -177,6 +180,15 @@ namespace Barotrauma
}
}
int oldMoney = element.GetAttributeInt("money", 0);
if (oldMoney > 0)
{
Bank = new Wallet(Option<Character>.None())
{
Balance = oldMoney
};
}
CampaignMetadata ??= new CampaignMetadata(this);
UpgradeManager ??= new UpgradeManager(this);
@@ -192,7 +204,7 @@ namespace Barotrauma
{
var characterDataDoc = XMLExtensions.TryLoadXml(characterDataPath);
if (characterDataDoc?.Root == null) { return; }
foreach (XElement subElement in characterDataDoc.Root.Elements())
foreach (var subElement in characterDataDoc.Root.Elements())
{
characterData.Add(new CharacterCampaignData(subElement));
}
@@ -200,7 +212,6 @@ namespace Barotrauma
#endif
}
public static List<SubmarineInfo> GetCampaignSubs()
{
bool isSubmarineVisible(SubmarineInfo s)
@@ -231,5 +242,78 @@ namespace Barotrauma
return availableSubs;
}
private static void WriteItems(IWriteMessage msg, Dictionary<Identifier, List<PurchasedItem>> purchasedItems)
{
msg.Write((byte)purchasedItems.Count);
foreach (var storeItems in purchasedItems)
{
msg.Write(storeItems.Key);
msg.Write((UInt16)storeItems.Value.Count);
foreach (var item in storeItems.Value)
{
msg.Write(item.ItemPrefab.Identifier);
msg.WriteRangedInteger(item.Quantity, 0, CargoManager.MaxQuantity);
}
}
}
private static Dictionary<Identifier, List<PurchasedItem>> ReadPurchasedItems(IReadMessage msg, Client sender)
{
var items = new Dictionary<Identifier, List<PurchasedItem>>();
byte storeCount = msg.ReadByte();
for (int i = 0; i < storeCount; i++)
{
Identifier storeId = msg.ReadIdentifier();
items.Add(storeId, new List<PurchasedItem>());
UInt16 itemCount = msg.ReadUInt16();
for (int j = 0; j < itemCount; j++)
{
Identifier itemId = msg.ReadIdentifier();
int quantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity);
items[storeId].Add(new PurchasedItem(ItemPrefab.Prefabs[itemId], quantity, sender));
}
}
return items;
}
private static void WriteItems(IWriteMessage msg, Dictionary<Identifier, List<SoldItem>> soldItems)
{
msg.Write((byte)soldItems.Count);
foreach (var storeItems in soldItems)
{
msg.Write(storeItems.Key);
msg.Write((UInt16)storeItems.Value.Count);
foreach (var item in storeItems.Value)
{
msg.Write(item.ItemPrefab.Identifier);
msg.Write((UInt16)item.ID);
msg.Write(item.Removed);
msg.Write(item.SellerID);
msg.Write((byte)item.Origin);
}
}
}
private static Dictionary<Identifier, List<SoldItem>> ReadSoldItems(IReadMessage msg)
{
var soldItems = new Dictionary<Identifier, List<SoldItem>>();
byte storeCount = msg.ReadByte();
for (int i = 0; i < storeCount; i++)
{
Identifier storeId = msg.ReadIdentifier();
soldItems.Add(storeId, new List<SoldItem>());
UInt16 itemCount = msg.ReadUInt16();
for (int j = 0; j < storeCount; j++)
{
Identifier prefabId = msg.ReadIdentifier();
UInt16 itemId = msg.ReadUInt16();
bool removed = msg.ReadBoolean();
byte sellerId = msg.ReadByte();
byte origin = msg.ReadByte();
soldItems[storeId].Add(new SoldItem(ItemPrefab.Prefabs[prefabId], itemId, removed, sellerId, (SoldItem.SellOrigin)origin));
}
}
return soldItems;
}
}
}
@@ -1,10 +1,14 @@
using Barotrauma.IO;
#nullable enable
using Barotrauma.IO;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Networking;
namespace Barotrauma
{
@@ -14,11 +18,11 @@ namespace Barotrauma
public readonly EventManager EventManager;
public GameMode GameMode;
public GameMode? GameMode;
//two locations used as the start and end in the MP mode
private Location[] dummyLocations;
public CrewManager CrewManager;
private Location[]? dummyLocations;
public CrewManager? CrewManager;
public double RoundStartTime;
@@ -27,18 +31,22 @@ namespace Barotrauma
private readonly List<Mission> missions = new List<Mission>();
public IEnumerable<Mission> Missions { get { return missions; } }
private readonly HashSet<Character> casualties = new HashSet<Character>();
public IEnumerable<Character> Casualties { get { return casualties; } }
public CharacterTeamType? WinningTeam;
public bool IsRunning { get; private set; }
public bool RoundEnding { get; private set; }
public Level Level { get; private set; }
public LevelData LevelData { get; private set; }
public Level? Level { get; private set; }
public LevelData? LevelData { get; private set; }
public bool MirrorLevel { get; private set; }
public Map Map
public Map? Map
{
get
{
@@ -46,7 +54,7 @@ namespace Barotrauma
}
}
public CampaignMode Campaign
public CampaignMode? Campaign
{
get
{
@@ -61,6 +69,7 @@ namespace Barotrauma
{
if (Map != null) { return Map.CurrentLocation; }
if (dummyLocations == null) { CreateDummyLocations(); }
if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); }
return dummyLocations[0];
}
}
@@ -71,6 +80,7 @@ namespace Barotrauma
{
if (Map != null) { return Map.SelectedLocation; }
if (dummyLocations == null) { CreateDummyLocations(); }
if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); }
return dummyLocations[1];
}
}
@@ -79,29 +89,16 @@ namespace Barotrauma
public List<SubmarineInfo> OwnedSubmarines = new List<SubmarineInfo>();
public Submarine Submarine { get; set; }
public Submarine? Submarine { get; set; }
public string SavePath { get; set; }
public string? SavePath { get; set; }
partial void InitProjSpecific();
private GameSession(SubmarineInfo submarineInfo, List<SubmarineInfo> ownedSubmarines = null)
private GameSession(SubmarineInfo submarineInfo)
{
InitProjSpecific();
SubmarineInfo = submarineInfo;
#if CLIENT
if (ownedSubmarines == null && GameMode is MultiPlayerCampaign && GameMain.NetLobbyScreen.ServerOwnedSubmarines != null)
{
ownedSubmarines = GameMain.NetLobbyScreen.ServerOwnedSubmarines;
}
#endif
OwnedSubmarines = ownedSubmarines ?? new List<SubmarineInfo>();
if (!OwnedSubmarines.Any(s => s.Name == submarineInfo.Name))
{
OwnedSubmarines.Add(submarineInfo);
}
GameMain.GameSession = this;
EventManager = new EventManager();
}
@@ -109,34 +106,37 @@ namespace Barotrauma
/// <summary>
/// Start a new GameSession. Will be saved to the specified save path (if playing a game mode that can be saved).
/// </summary>
public GameSession(SubmarineInfo submarineInfo, string savePath, GameModePreset gameModePreset, CampaignSettings settings, string seed = null, MissionType missionType = MissionType.None)
public GameSession(SubmarineInfo submarineInfo, string savePath, GameModePreset gameModePreset, CampaignSettings settings, string? seed = null, MissionType missionType = MissionType.None)
: this(submarineInfo)
{
this.SavePath = savePath;
CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer);
CrewManager = new CrewManager(gameModePreset.IsSinglePlayer);
GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, settings, missionType: missionType);
InitOwnedSubs(submarineInfo);
}
/// <summary>
/// Start a new GameSession with a specific pre-selected mission.
/// </summary>
public GameSession(SubmarineInfo submarineInfo, GameModePreset gameModePreset, string seed = null, IEnumerable<MissionPrefab> missionPrefabs = null)
public GameSession(SubmarineInfo submarineInfo, GameModePreset gameModePreset, string? seed = null, IEnumerable<MissionPrefab>? missionPrefabs = null)
: this(submarineInfo)
{
CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer);
CrewManager = new CrewManager(gameModePreset.IsSinglePlayer);
GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, CampaignSettings.Empty, missionPrefabs: missionPrefabs);
InitOwnedSubs(submarineInfo);
}
/// <summary>
/// Load a game session from the specified XML document. The session will be saved to the specified path.
/// </summary>
public GameSession(SubmarineInfo submarineInfo, List<SubmarineInfo> ownedSubmarines, XDocument doc, string saveFile) : this(submarineInfo, ownedSubmarines)
public GameSession(SubmarineInfo submarineInfo, List<SubmarineInfo> ownedSubmarines, XDocument doc, string saveFile) : this(submarineInfo)
{
this.SavePath = saveFile;
GameMain.GameSession = this;
XElement rootElement = doc.Root ?? throw new NullReferenceException("Game session XML element is invalid: document is null.");
//selectedSub.Name = doc.Root.GetAttributeString("submarine", selectedSub.Name);
foreach (XElement subElement in doc.Root.Elements())
foreach (var subElement in rootElement.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
@@ -147,15 +147,17 @@ namespace Barotrauma
var campaign = SinglePlayerCampaign.Load(subElement);
campaign.LoadNewLevel();
GameMode = campaign;
InitOwnedSubs(submarineInfo, ownedSubmarines);
break;
#endif
case "multiplayercampaign":
CrewManager = new CrewManager(false);
var mpCampaign = MultiPlayerCampaign.LoadNew(subElement);
GameMode = mpCampaign;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
mpCampaign.LoadNewLevel();
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
mpCampaign.LoadNewLevel();
InitOwnedSubs(submarineInfo, ownedSubmarines);
//save to ensure the campaign ID in the save file matches the one that got assigned to this campaign instance
SaveUtil.SaveGame(saveFile);
}
@@ -164,7 +166,16 @@ namespace Barotrauma
}
}
private GameMode InstantiateGameMode(GameModePreset gameModePreset, string seed, SubmarineInfo selectedSub, CampaignSettings settings, IEnumerable<MissionPrefab> missionPrefabs = null, MissionType missionType = MissionType.None)
private void InitOwnedSubs(SubmarineInfo submarineInfo, List<SubmarineInfo>? ownedSubmarines = null)
{
OwnedSubmarines = ownedSubmarines ?? new List<SubmarineInfo>();
if (submarineInfo != null && !OwnedSubmarines.Any(s => s.Name == submarineInfo.Name))
{
OwnedSubmarines.Add(submarineInfo);
}
}
private GameMode InstantiateGameMode(GameModePreset gameModePreset, string? seed, SubmarineInfo selectedSub, CampaignSettings settings, IEnumerable<MissionPrefab>? missionPrefabs = null, MissionType missionType = MissionType.None)
{
if (gameModePreset.GameModeType == typeof(CoOpMode) || gameModePreset.GameModeType == typeof(PvPMode))
{
@@ -193,9 +204,10 @@ namespace Barotrauma
else if (gameModePreset.GameModeType == typeof(MultiPlayerCampaign))
{
var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings);
if (campaign != null && selectedSub != null)
if (selectedSub != null)
{
campaign.Money = Math.Max(MultiPlayerCampaign.MinimumInitialMoney, campaign.Money - selectedSub.Price);
campaign.Bank.TryDeduct(selectedSub.Price);
campaign.Bank.Balance = Math.Max(campaign.Bank.Balance, MultiPlayerCampaign.MinimumInitialMoney);
}
return campaign;
}
@@ -203,9 +215,10 @@ namespace Barotrauma
else if (gameModePreset.GameModeType == typeof(SinglePlayerCampaign))
{
var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings);
if (campaign != null && selectedSub != null)
if (selectedSub != null)
{
campaign.Money = Math.Max(SinglePlayerCampaign.MinimumInitialMoney, campaign.Money - selectedSub.Price);
campaign.Bank.TryDeduct(selectedSub.Price);
campaign.Bank.Balance = Math.Max(campaign.Bank.Balance, MultiPlayerCampaign.MinimumInitialMoney);
}
return campaign;
}
@@ -258,7 +271,7 @@ namespace Barotrauma
/// <summary>
/// Switch to another submarine. The sub is loaded when the next round starts.
/// </summary>
public SubmarineInfo SwitchSubmarine(SubmarineInfo newSubmarine, int cost)
public SubmarineInfo SwitchSubmarine(SubmarineInfo newSubmarine, int cost, Client? client = null)
{
if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name))
{
@@ -277,20 +290,22 @@ namespace Barotrauma
}
}
Campaign.Money -= cost;
if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && cost > 0)
{
Campaign!.GetWallet(client).TryDeduct(cost);
}
GameAnalyticsManager.AddMoneySpentEvent(cost, GameAnalyticsManager.MoneySink.SubmarineSwitch, newSubmarine.Name);
((CampaignMode)GameMode).PendingSubmarineSwitch = newSubmarine;
Campaign!.PendingSubmarineSwitch = newSubmarine;
return newSubmarine;
}
public void PurchaseSubmarine(SubmarineInfo newSubmarine)
public void PurchaseSubmarine(SubmarineInfo newSubmarine, Client? client = null)
{
if (Campaign is null) { return; }
if (Campaign.Money < newSubmarine.Price) { return; }
if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && !Campaign.GetWallet(client).TryDeduct(newSubmarine.Price)) { return; }
if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name))
{
Campaign.Money -= newSubmarine.Price;
GameAnalyticsManager.AddMoneySpentEvent(newSubmarine.Price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name);
OwnedSubmarines.Add(newSubmarine);
}
@@ -317,9 +332,10 @@ namespace Barotrauma
return isRadiated;
}
public void StartRound(string levelSeed, float? difficulty = null, LevelGenerationParams levelGenerationParams = null)
public void StartRound(string levelSeed, float? difficulty = null, LevelGenerationParams? levelGenerationParams = null)
{
LevelData randomLevel = null;
if (GameMode == null) { return; }
LevelData? randomLevel = null;
foreach (Mission mission in Missions.Union(GameMode.Missions))
{
MissionPrefab missionPrefab = mission.Prefab;
@@ -327,7 +343,7 @@ namespace Barotrauma
missionPrefab.AllowedLocationTypes.Any() &&
!missionPrefab.AllowedConnectionTypes.Any())
{
LocationType locationType = LocationType.List.FirstOrDefault(lt => missionPrefab.AllowedLocationTypes.Any(m => m.Equals(lt.Identifier, StringComparison.OrdinalIgnoreCase)));
LocationType? locationType = LocationType.Prefabs.FirstOrDefault(lt => missionPrefab.AllowedLocationTypes.Any(m => m == lt.Identifier));
CreateDummyLocations(locationType);
randomLevel = LevelData.CreateRandom(levelSeed, difficulty, levelGenerationParams, requireOutpost: true);
break;
@@ -337,8 +353,10 @@ namespace Barotrauma
StartRound(randomLevel);
}
public void StartRound(LevelData levelData, bool mirrorLevel = false, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null)
public void StartRound(LevelData? levelData, bool mirrorLevel = false, SubmarineInfo? startOutpost = null, SubmarineInfo? endOutpost = null)
{
AfflictionPrefab.LoadAllEffects();
MirrorLevel = mirrorLevel;
if (SubmarineInfo == null)
{
@@ -375,7 +393,7 @@ namespace Barotrauma
}
}
foreach (Mission mission in GameMode.Missions)
foreach (Mission mission in GameMode!.Missions)
{
// setting level for missions that may involve difficulty-related submarine creation
mission.SetLevel(levelData);
@@ -392,18 +410,20 @@ namespace Barotrauma
if (GameMain.NetworkMember?.ServerSettings?.LockAllDefaultWires ?? false)
{
foreach (Item item in Item.ItemList)
List<Item> items = new List<Item>();
items.AddRange(Submarine.MainSubs[0].GetItems(alsoFromConnectedSubs: true));
if (Submarine.MainSubs[1] != null)
{
if (item.Submarine == Submarine.MainSubs[0] ||
(Submarine.MainSubs[1] != null && item.Submarine == Submarine.MainSubs[1]))
{
Wire wire = item.GetComponent<Wire>();
if (wire != null && !wire.NoAutoLock && wire.Connections.Any(c => c != null)) { wire.Locked = true; }
}
items.AddRange(Submarine.MainSubs[1].GetItems(alsoFromConnectedSubs: true));
}
foreach (Item item in items)
{
Wire wire = item.GetComponent<Wire>();
if (wire != null && !wire.NoAutoLock && wire.Connections.Any(c => c != null)) { wire.Locked = true; }
}
}
Level level = null;
Level? level = null;
if (levelData != null)
{
level = Level.Generate(levelData, mirrorLevel, startOutpost, endOutpost);
@@ -411,13 +431,18 @@ namespace Barotrauma
InitializeLevel(level);
//Clear out the cached grids and force update
Powered.Grids.Clear();
casualties.Clear();
GameAnalyticsManager.AddProgressionEvent(
GameAnalyticsManager.ProgressionStatus.Start,
GameMode?.Preset?.Identifier ?? "none");
GameMode?.Preset?.Identifier.Value ?? "none");
string eventId = "StartRound:" + (GameMode?.Preset?.Identifier ?? "none") + ":";
string eventId = "StartRound:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":";
GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"));
GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Preset?.Identifier ?? "none"));
GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Preset?.Identifier.Value ?? "none"));
GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0));
foreach (Mission mission in missions)
{
@@ -425,12 +450,12 @@ namespace Barotrauma
}
if (Level.Loaded != null)
{
string levelId = Level.Loaded.Type == LevelData.LevelType.Outpost ?
Identifier levelId = (Level.Loaded.Type == LevelData.LevelType.Outpost ?
Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier :
Level.Loaded.GenerationParams?.Identifier;
GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + Level.Loaded.Type.ToString() + ":" + (levelId ?? "null"));
Level.Loaded.GenerationParams?.Identifier) ?? "null".ToIdentifier();
GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + Level.Loaded.Type.ToString() + ":" + levelId);
}
GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none"));
GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"));
#if CLIENT
if (GameMode is TutorialMode tutorialMode)
{
@@ -452,16 +477,16 @@ namespace Barotrauma
{
GameAnalyticsManager.AddDesignEvent(eventId + "Radiation:Disabled");
}
bool firstTimeInBiome = Map != null && !Map.Connections.Any(c => c.Passed && c.Biome == LevelData.Biome);
bool firstTimeInBiome = Map != null && !Map.Connections.Any(c => c.Passed && c.Biome == LevelData!.Biome);
if (firstTimeInBiome)
{
GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none") + "Discovered:Playtime", campaignMode.TotalPlayTime);
GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none") + "Discovered:PassedLevels", campaignMode.TotalPassedLevels);
GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none") + "Discovered:Playtime", campaignMode.TotalPlayTime);
GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none") + "Discovered:PassedLevels", campaignMode.TotalPassedLevels);
}
}
#if CLIENT
if (GameMode is CampaignMode) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); }
if (GameMode is CampaignMode && levelData != null) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); }
var existingRoundSummary = GUIMessageBox.MessageBoxes.Find(mb => mb.UserData is RoundSummary)?.UserData as RoundSummary;
if (existingRoundSummary?.ContinueButton != null)
@@ -469,12 +494,12 @@ namespace Barotrauma
existingRoundSummary.ContinueButton.Visible = true;
}
RoundSummary = new RoundSummary(Submarine.Info, GameMode, Missions, StartLocation, EndLocation);
RoundSummary = new RoundSummary(GameMode, Missions, StartLocation, EndLocation);
if (!(GameMode is TutorialMode) && !(GameMode is TestGameMode))
{
GUI.AddMessage("", Color.Transparent, 3.0f, playSound: false);
if (EndLocation != null)
if (EndLocation != null && levelData != null)
{
GUI.AddMessage(levelData.Biome.DisplayName, Color.Lerp(Color.CadetBlue, Color.DarkRed, levelData.Difficulty / 100.0f), 5.0f, playSound: false);
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Destination"), EndLocation.Name), Color.CadetBlue, playSound: false);
@@ -505,7 +530,7 @@ namespace Barotrauma
#endif
}
private void InitializeLevel(Level level)
private void InitializeLevel(Level? level)
{
//make sure no status effects have been carried on from the next round
//(they should be stopped in EndRound, this is a safeguard against cases where the round is ended ungracefully)
@@ -516,7 +541,7 @@ namespace Barotrauma
GameMain.LightManager.LosEnabled = GameMain.Client == null || GameMain.Client.CharacterInfo != null;
#endif
if (GameMain.LightManager.LosEnabled) { GameMain.LightManager.LosAlpha = 1f; }
if (GameMain.Client == null) { GameMain.LightManager.LosMode = GameMain.Config.LosMode; }
if (GameMain.Client == null) { GameMain.LightManager.LosMode = GameSettings.CurrentConfig.Graphics.LosMode; }
#endif
LevelData = level?.LevelData;
Level = level;
@@ -533,28 +558,28 @@ namespace Barotrauma
Entity.Spawner = new EntitySpawner();
missions.Clear();
GameMode.AddExtraMissions(LevelData);
missions.AddRange(GameMode.Missions);
GameMode.Start();
foreach (Mission mission in missions)
if (GameMode != null && Submarine != null)
{
int prevEntityCount = Entity.GetEntities().Count();
mission.Start(Level.Loaded);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Entity.GetEntities().Count() != prevEntityCount)
missions.Clear();
GameMode.AddExtraMissions(LevelData);
missions.AddRange(GameMode.Missions);
GameMode.Start();
foreach (Mission mission in missions)
{
DebugConsole.ThrowError(
$"Entity count has changed after starting a mission ({mission.Prefab.Identifier}) as a client. " +
"The clients should not instantiate entities themselves when starting the mission," +
" but instead the server should inform the client of the spawned entities using Mission.ServerWriteInitial.");
int prevEntityCount = Entity.GetEntities().Count();
mission.Start(Level.Loaded);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Entity.GetEntities().Count() != prevEntityCount)
{
DebugConsole.ThrowError(
$"Entity count has changed after starting a mission ({mission.Prefab.Identifier}) as a client. " +
"The clients should not instantiate entities themselves when starting the mission," +
" but instead the server should inform the client of the spawned entities using Mission.ServerWriteInitial.");
}
}
}
EventManager?.StartRound(Level.Loaded);
SteamAchievementManager.OnStartRound();
EventManager?.StartRound(Level.Loaded);
SteamAchievementManager.OnStartRound();
if (GameMode != null)
{
GameMode.ShowStartMessage();
if (GameMain.NetworkMember == null)
@@ -573,7 +598,7 @@ namespace Barotrauma
}
}
GameMain.Config.RecentlyEncounteredCreatures.Clear();
CreatureMetrics.Instance.RecentlyEncountered.Clear();
GameMain.GameScreen.Cam.Position = Character.Controlled?.WorldPosition ?? Submarine.MainSub.WorldPosition;
RoundStartTime = Timing.TotalTime;
@@ -581,11 +606,11 @@ namespace Barotrauma
IsRunning = true;
}
public void PlaceSubAtStart(Level level)
public void PlaceSubAtStart(Level? level)
{
if (level == null)
if (level == null || Submarine == null)
{
Submarine.MainSub.SetPosition(Vector2.Zero);
Submarine?.SetPosition(Vector2.Zero);
return;
}
@@ -603,7 +628,7 @@ namespace Barotrauma
//find the port that's the nearest to the outpost and dock if one is found
float closestDistance = 0.0f;
DockingPort myPort = null, outPostPort = null;
DockingPort? myPort = null, outPostPort = null;
foreach (DockingPort port in DockingPort.List)
{
if (port.IsHorizontal || port.Docked) { continue; }
@@ -689,7 +714,7 @@ namespace Barotrauma
UpdateProjSpecific(deltaTime);
}
public Mission GetMission(int index)
public Mission? GetMission(int index)
{
if (index < 0 || index >= missions.Count) { return null; }
return missions[index];
@@ -700,12 +725,13 @@ namespace Barotrauma
return missions.IndexOf(mission);
}
public void EnforceMissionOrder(List<string> missionIdentifiers)
public void EnforceMissionOrder(List<Identifier> missionIdentifiers)
{
List<Mission> sortedMissions = new List<Mission>();
foreach (string missionId in missionIdentifiers)
foreach (Identifier missionId in missionIdentifiers)
{
var matchingMission = missions.Find(m => m.Prefab.Identifier == missionId);
if (matchingMission == null) { continue; }
sortedMissions.Add(matchingMission);
missions.Remove(matchingMission);
}
@@ -714,36 +740,57 @@ namespace Barotrauma
partial void UpdateProjSpecific(float deltaTime);
public static IEnumerable<Character> GetSessionCrewCharacters()
/// <summary>
/// Returns a list of crew characters currently in the game with a given filter.
/// </summary>
/// <param name="type">Character type filter</param>
/// <returns></returns>
/// <remarks>
/// In singleplayer mode the CharacterType.Player returns the currently controlled player.
/// </remarks>
public static ImmutableHashSet<Character> GetSessionCrewCharacters(CharacterType type)
{
LuaResult result = new LuaResult(GameMain.Lua.hook.Call("gameSession.getSessionCrewCharacters"));
if (!(GameMain.GameSession.CrewManager is { } crewManager)) { return ImmutableHashSet<Character>.Empty; }
if (!result.IsNull())
{
return result.DynValue().ToObject<IEnumerable<Character>>();
}
IEnumerable<Character> players;
IEnumerable<Character> bots;
HashSet<Character> characters = new HashSet<Character>();
#if SERVER
return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null && !c.IsDead);
#else
if (GameMain.GameSession == null) { return Enumerable.Empty<Character>(); }
return GameMain.GameSession.CrewManager.GetCharacters().Where(c => c?.Info != null && !c.IsDead);
#endif
players = GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null && !c.IsDead);
bots = crewManager.GetCharacters().Where(c => !c.IsRemotePlayer);
#elif CLIENT
players = crewManager.GetCharacters().Where(c => c.IsPlayer);
bots = crewManager.GetCharacters().Where(c => c.IsBot);
#endif
if (type.HasFlag(CharacterType.Bot))
{
foreach (Character bot in bots) { characters.Add(bot); }
}
if (type.HasFlag(CharacterType.Player))
{
foreach (Character player in players) { characters.Add(player); }
}
return characters.ToImmutableHashSet();
}
public void EndRound(string endMessage, List<TraitorMissionResult> traitorResults = null, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None)
public void EndRound(string endMessage, List<TraitorMissionResult>? traitorResults = null, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None)
{
RoundEnding = true;
#if CLIENT
GameMain.Lua.hook.Call("roundEnd");
#endif
//Clear the grids to allow for garbage collection
Powered.Grids.Clear();
try
{
IEnumerable<Character> crewCharacters = GetSessionCrewCharacters();
ImmutableHashSet<Character> crewCharacters = GetSessionCrewCharacters(CharacterType.Both);
int prevMoney = (GameMode as CampaignMode)?.Money ?? 0;
int prevMoney = GetAmountOfMoney(crewCharacters);
foreach (Mission mission in missions)
{
@@ -764,7 +811,6 @@ namespace Barotrauma
character.CheckTalents(AbilityEffectType.OnAnyMissionCompleted);
}
}
if (missions.All(m => m.Completed))
{
foreach (Character character in crewCharacters)
@@ -799,23 +845,24 @@ namespace Barotrauma
GameMode?.End(transitionType);
EventManager?.EndRound();
StatusEffect.StopAll();
AfflictionPrefab.ClearAllEffects();
IsRunning = false;
#if CLIENT
bool success = CrewManager.GetCharacters().Any(c => !c.IsDead);
bool success = CrewManager!.GetCharacters().Any(c => !c.IsDead);
#else
bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead);
#endif
double roundDuration = Timing.TotalTime - RoundStartTime;
GameAnalyticsManager.AddProgressionEvent(
success ? GameAnalyticsManager.ProgressionStatus.Complete : GameAnalyticsManager.ProgressionStatus.Fail,
GameMode?.Name ?? "none",
GameMode?.Name?.Value ?? "none",
roundDuration);
string eventId = "EndRound:" + (GameMode?.Preset?.Identifier ?? "none") + ":";
string eventId = "EndRound:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":";
LogEndRoundStats(eventId);
if (GameMode is CampaignMode campaignMode)
{
GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", campaignMode.Money - prevMoney);
GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", GetAmountOfMoney(crewCharacters) - prevMoney);
campaignMode.TotalPlayTime += roundDuration;
}
#if CLIENT
@@ -827,13 +874,24 @@ namespace Barotrauma
{
RoundEnding = false;
}
int GetAmountOfMoney(IEnumerable<Character> crew)
{
if (!(GameMode is CampaignMode campaign)) { return 0; }
return GameMain.NetworkMember switch
{
null => campaign.Bank.Balance,
_ => crew.Sum(c => c.Wallet.Balance) + campaign.Bank.Balance
};
}
}
public void LogEndRoundStats(string eventId)
{
double roundDuration = Timing.TotalTime - RoundStartTime;
GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), roundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name ?? "none"), roundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name.Value ?? "none"), roundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0), roundDuration);
foreach (Mission mission in missions)
{
@@ -841,11 +899,11 @@ namespace Barotrauma
}
if (Level.Loaded != null)
{
string levelId = Level.Loaded.Type == LevelData.LevelType.Outpost ?
Identifier levelId = (Level.Loaded.Type == LevelData.LevelType.Outpost ?
Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier :
Level.Loaded.GenerationParams?.Identifier;
GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + (levelId ?? "null")), roundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none"), roundDuration);
Level.Loaded.GenerationParams?.Identifier) ?? "null".ToIdentifier();
GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + levelId), roundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"), roundDuration);
}
if (Submarine.MainSub != null)
@@ -874,7 +932,7 @@ namespace Barotrauma
}
}
foreach (Character c in GetSessionCrewCharacters())
foreach (Character c in GetSessionCrewCharacters(CharacterType.Both))
{
foreach (var itemSelectedDuration in c.ItemSelectedDurations)
{
@@ -887,7 +945,7 @@ namespace Barotrauma
{
characterType = "Player";
}
GameAnalyticsManager.AddDesignEvent("TimeSpentOnDevices:" + (GameMode?.Preset?.Identifier ?? "none") + ":" + characterType + ":" + (c.Info?.Job?.Prefab.Identifier ?? "NoJob") + ":" + itemSelectedDuration.Key.Identifier, itemSelectedDuration.Value);
GameAnalyticsManager.AddDesignEvent("TimeSpentOnDevices:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":" + characterType + ":" + (c.Info?.Job?.Prefab.Identifier.Value ?? "NoJob") + ":" + itemSelectedDuration.Key.Identifier, itemSelectedDuration.Value);
}
}
#if CLIENT
@@ -907,37 +965,42 @@ namespace Barotrauma
public void KillCharacter(Character character)
{
if (CrewManager != null && CrewManager.GetCharacters().Contains(character))
{
casualties.Add(character);
}
#if CLIENT
CrewManager.KillCharacter(character);
CrewManager?.KillCharacter(character);
#endif
}
public void ReviveCharacter(Character character)
{
casualties.Remove(character);
#if CLIENT
CrewManager.ReviveCharacter(character);
CrewManager?.ReviveCharacter(character);
#endif
}
public static bool IsCompatibleWithEnabledContentPackages(IList<string> contentPackagePaths, out string errorMsg)
public static bool IsCompatibleWithEnabledContentPackages(IList<string> contentPackageNames, out LocalizedString errorMsg)
{
errorMsg = "";
//no known content packages, must be an older save file
if (!contentPackagePaths.Any()) { return true; }
if (!contentPackageNames.Any()) { return true; }
List<string> missingPackages = new List<string>();
foreach (string packagePath in contentPackagePaths)
foreach (string packageName in contentPackageNames)
{
if (!GameMain.Config.AllEnabledPackages.Any(cp => cp.Path == packagePath))
if (!ContentPackageManager.EnabledPackages.All.Any(cp => cp.NameMatches(packageName)))
{
missingPackages.Add(packagePath);
missingPackages.Add(packageName);
}
}
List<string> excessPackages = new List<string>();
foreach (ContentPackage cp in GameMain.Config.AllEnabledPackages)
foreach (ContentPackage cp in ContentPackageManager.EnabledPackages.All)
{
if (!cp.HasMultiplayerIncompatibleContent) { continue; }
if (!contentPackagePaths.Any(p => p == cp.Path))
if (!cp.HasMultiplayerSyncedContent) { continue; }
if (!contentPackageNames.Any(p => cp.NameMatches(p)))
{
excessPackages.Add(cp.Name);
}
@@ -946,10 +1009,10 @@ namespace Barotrauma
bool orderMismatch = false;
if (missingPackages.Count == 0 && missingPackages.Count == 0)
{
var enabledPackages = GameMain.Config.AllEnabledPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).ToList();
for (int i = 0; i < contentPackagePaths.Count && i < enabledPackages.Count; i++)
var enabledPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent).ToImmutableArray();
for (int i = 0; i < contentPackageNames.Count && i < enabledPackages.Length; i++)
{
if (contentPackagePaths[i] != enabledPackages[i].Path)
if (!enabledPackages[i].NameMatches(contentPackageNames[i]))
{
orderMismatch = true;
break;
@@ -969,18 +1032,18 @@ namespace Barotrauma
}
if (excessPackages.Count == 1)
{
if (!string.IsNullOrEmpty(errorMsg)) { errorMsg += "\n"; }
if (!errorMsg.IsNullOrEmpty()) { errorMsg += "\n"; }
errorMsg += TextManager.GetWithVariable("campaignmode.incompatiblecontentpackage", "[incompatiblecontentpackage]", excessPackages[0]);
}
else if (excessPackages.Count > 1)
{
if (!string.IsNullOrEmpty(errorMsg)) { errorMsg += "\n"; }
if (!errorMsg.IsNullOrEmpty()) { errorMsg += "\n"; }
errorMsg += TextManager.GetWithVariable("campaignmode.incompatiblecontentpackages", "[incompatiblecontentpackages]", string.Join(", ", excessPackages));
}
if (orderMismatch)
{
if (!string.IsNullOrEmpty(errorMsg)) { errorMsg += "\n"; }
errorMsg += TextManager.GetWithVariable("campaignmode.contentpackageordermismatch", "[loadorder]", string.Join(", ", contentPackagePaths));
if (!errorMsg.IsNullOrEmpty()) { errorMsg += "\n"; }
errorMsg += TextManager.GetWithVariable("campaignmode.contentpackageordermismatch", "[loadorder]", string.Join(", ", contentPackageNames));
}
return false;
@@ -994,24 +1057,25 @@ namespace Barotrauma
}
XDocument doc = new XDocument(new XElement("Gamesession"));
XElement rootElement = doc.Root ?? throw new NullReferenceException("Game session XML element is invalid: document is null.");
doc.Root.Add(new XAttribute("savetime", ToolBox.Epoch.NowLocal));
doc.Root.Add(new XAttribute("version", GameMain.Version));
rootElement.Add(new XAttribute("savetime", ToolBox.Epoch.NowLocal));
rootElement.Add(new XAttribute("version", GameMain.Version));
var submarineInfo = Campaign?.PendingSubmarineSwitch ?? SubmarineInfo;
doc.Root.Add(new XAttribute("submarine", submarineInfo == null ? "" : submarineInfo.Name));
rootElement.Add(new XAttribute("submarine", submarineInfo == null ? "" : submarineInfo.Name));
if (OwnedSubmarines != null)
{
List<string> ownedSubmarineNames = new List<string>();
var ownedSubsElement = new XElement("ownedsubmarines");
doc.Root.Add(ownedSubsElement);
rootElement.Add(ownedSubsElement);
foreach (var ownedSub in OwnedSubmarines)
{
ownedSubsElement.Add(new XElement("sub", new XAttribute("name", ownedSub.Name)));
}
}
doc.Root.Add(new XAttribute("mapseed", Map.Seed));
doc.Root.Add(new XAttribute("selectedcontentpackages",
string.Join("|", GameMain.Config.AllEnabledPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).Select(cp => cp.Path))));
if (Map != null) { rootElement.Add(new XAttribute("mapseed", Map.Seed)); }
rootElement.Add(new XAttribute("selectedcontentpackagenames",
string.Join("|", ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent).Select(cp => cp.Name.Replace("|", @"\|")))));
((CampaignMode)GameMode).Save(doc.Root);
@@ -1020,7 +1084,7 @@ namespace Barotrauma
/*public void Load(XElement saveElement)
{
foreach (XElement subElement in saveElement.Elements())
foreach (var subElement in saveElement.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
@@ -29,8 +29,8 @@ namespace Barotrauma
JobPrefab job = location.Type.GetRandomHireable();
if (job == null) { return; }
var variant = Rand.Range(0, job.Variants, Rand.RandSync.Server);
AvailableCharacters.Add(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: job, variant: variant));
var variant = Rand.Range(0, job.Variants, Rand.RandSync.ServerAndClient);
AvailableCharacters.Add(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant));
}
}
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Networking;
namespace Barotrauma
{
@@ -63,7 +64,7 @@ namespace Barotrauma
public struct NetAffliction : INetSerializableStruct
{
[NetworkSerialize]
public string Identifier;
public Identifier Identifier;
[NetworkSerialize]
public ushort Strength;
@@ -116,7 +117,7 @@ namespace Barotrauma
foreach (AfflictionPrefab prefab in AfflictionPrefab.List)
{
if (prefab.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase))
if (prefab.Identifier == Identifier)
{
cachedPrefab = prefab;
return prefab;
@@ -128,7 +129,7 @@ namespace Barotrauma
set
{
cachedPrefab = value;
Identifier = value?.Identifier ?? string.Empty;
Identifier = value?.Identifier ?? Identifier.Empty;
Strength = 0;
Price = 0;
}
@@ -136,12 +137,12 @@ namespace Barotrauma
public readonly bool AfflictionEquals(AfflictionPrefab prefab)
{
return prefab.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase);
return prefab.Identifier == Identifier;
}
public readonly bool AfflictionEquals(NetAffliction affliction)
{
return affliction.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase);
return affliction.Identifier == Identifier;
}
}
@@ -177,17 +178,20 @@ namespace Barotrauma
}
}
public readonly List<NetCrewMember> PendingHeals = new List<NetCrewMember>();
public Action? OnUpdate;
private readonly CampaignMode? campaign;
public MedicalClinic(CampaignMode campaign)
{
this.campaign = campaign;
#if CLIENT
campaign.OnMoneyChanged.RegisterOverwriteExisting(nameof(MedicalClinic).ToIdentifier(), OnMoneyChanged);
#endif
}
public readonly List<NetCrewMember> PendingHeals = new List<NetCrewMember>();
public Action? OnUpdate;
private static bool IsOutpostInCombat()
{
if (!(Level.Loaded is { Type: LevelData.LevelType.Outpost })) { return false; }
@@ -203,14 +207,13 @@ namespace Barotrauma
return false;
}
private HealRequestResult HealAllPending(bool force = false)
private HealRequestResult HealAllPending(bool force = false, Client? client = null)
{
int totalCost = GetTotalCost();
if (!force)
{
if (GetMoney() < totalCost) { return HealRequestResult.InsufficientFunds; }
if (IsOutpostInCombat()) { return HealRequestResult.Refused; }
if (!GetWallet(client).TryDeduct(totalCost)) { return HealRequestResult.InsufficientFunds; }
}
ImmutableArray<CharacterInfo> crew = GetCrewCharacters();
@@ -221,15 +224,10 @@ namespace Barotrauma
foreach (NetAffliction affliction in crewMember.Afflictions)
{
health.ReduceAffliction(null, affliction.Identifier, affliction.Prefab?.MaxStrength ?? affliction.Strength);
health.ReduceAfflictionOnAllLimbs(affliction.Identifier, affliction.Prefab?.MaxStrength ?? affliction.Strength);
}
}
if (campaign != null)
{
campaign.Money -= totalCost;
}
ClearPendingHeals();
return HealRequestResult.Success;
@@ -316,7 +314,10 @@ namespace Barotrauma
private int GetAdjustedPrice(int price) => campaign?.Map?.CurrentLocation is { Type: { HasOutpost: true } } currentLocation ? currentLocation.GetAdjustedHealCost(price) : int.MaxValue;
public int GetMoney() => campaign?.Money ?? 0;
public Wallet GetWallet(Client? c = null)
{
return campaign?.GetWallet(c) ?? Wallet.Invalid;
}
public static ImmutableArray<CharacterInfo> GetCrewCharacters()
{
@@ -333,21 +334,21 @@ namespace Barotrauma
#if DEBUG && CLIENT
private static readonly CharacterInfo[] TestInfos =
{
new CharacterInfo("human"),
new CharacterInfo("human"),
new CharacterInfo("human"),
new CharacterInfo("human"),
new CharacterInfo("human"),
new CharacterInfo("human"),
new CharacterInfo("human")
new CharacterInfo(CharacterPrefab.HumanSpeciesName),
new CharacterInfo(CharacterPrefab.HumanSpeciesName),
new CharacterInfo(CharacterPrefab.HumanSpeciesName),
new CharacterInfo(CharacterPrefab.HumanSpeciesName),
new CharacterInfo(CharacterPrefab.HumanSpeciesName),
new CharacterInfo(CharacterPrefab.HumanSpeciesName),
new CharacterInfo(CharacterPrefab.HumanSpeciesName)
};
private static readonly NetAffliction[] TestAfflictions =
{
new NetAffliction { Identifier = "internaldamage", Strength = 80, Price = 10 },
new NetAffliction { Identifier = "blunttrauma", Strength = 50, Price = 10 },
new NetAffliction { Identifier = "lacerations", Strength = 20, Price = 10 },
new NetAffliction { Identifier = "burn", Strength = 10, Price = 10 }
new NetAffliction { Identifier = "internaldamage".ToIdentifier(), Strength = 80, Price = 10 },
new NetAffliction { Identifier = "blunttrauma".ToIdentifier(), Strength = 50, Price = 10 },
new NetAffliction { Identifier = "lacerations".ToIdentifier(), Strength = 20, Price = 10 },
new NetAffliction { Identifier = "burn".ToIdentifier(), Strength = 10, Price = 10 }
};
#endif
}
@@ -3,6 +3,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Networking;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma
@@ -96,6 +98,8 @@ namespace Barotrauma
public UpgradeManager(CampaignMode campaign)
{
UpgradeCategory.Categories.ForEach(c => c.DeterminePrefabsThatAllowUpgrades());
DebugConsole.Log("Created brand new upgrade manager.");
Campaign = campaign;
}
@@ -112,7 +116,7 @@ namespace Barotrauma
}
else
{
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
@@ -158,7 +162,7 @@ namespace Barotrauma
price += item.Prefab.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation);
}
//refund the current item
if (replacement != item.prefab)
if (replacement != ((MapEntity)item).Prefab)
{
price -= item.Prefab.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation);
}
@@ -175,7 +179,7 @@ namespace Barotrauma
/// Purchased upgrades are temporarily stored in <see cref="PendingUpgrades"/> and they are applied
/// after the next round starts similarly how items are spawned in the stowage room after the round starts.
/// </remarks>
public void PurchaseUpgrade(UpgradePrefab prefab, UpgradeCategory category, bool force = false)
public void PurchaseUpgrade(UpgradePrefab prefab, UpgradeCategory category, bool force = false, Client? client = null)
{
if (!CanUpgradeSub())
{
@@ -212,25 +216,24 @@ namespace Barotrauma
price = 0;
}
if (Campaign.Money >= price)
if (Campaign.GetWallet(client).TryDeduct(price))
{
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
// only make the NPC speak if more than 5 minutes have passed since the last purchased service
if (lastUpgradeSpeak == DateTime.MinValue || lastUpgradeSpeak.AddMinutes(5) < DateTime.Now)
{
UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased"), Campaign.IsSinglePlayer);
UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased").Value, Campaign.IsSinglePlayer);
lastUpgradeSpeak = DateTime.Now;
}
}
Campaign.Money -= price;
GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineUpgrade, prefab.Identifier);
GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineUpgrade, prefab.Identifier.Value);
PurchasedUpgrade? upgrade = FindMatchingUpgrade(prefab, category);
#if CLIENT
DebugLog($"CLIENT: Purchased level {GetUpgradeLevel(prefab, category) + 1} {category.Name}.{prefab.Name} for {price}", GUI.Style.Orange);
DebugLog($"CLIENT: Purchased level {GetUpgradeLevel(prefab, category) + 1} {category.Name}.{prefab.Name} for {price}", GUIStyle.Orange);
#endif
if (upgrade == null)
@@ -250,14 +253,14 @@ namespace Barotrauma
else
{
DebugConsole.ThrowError("Tried to purchase an upgrade with insufficient funds, the transaction has not been completed.\n" +
$"Upgrade: {prefab.Name}, Cost: {price}, Have: {Campaign.Money}");
$"Upgrade: {prefab.Name}, Cost: {price}, Have: {Campaign.GetWallet(client).Balance}");
}
}
/// <summary>
/// Purchases an item swap and handles logic for deducting the credit.
/// </summary>
public void PurchaseItemSwap(Item itemToRemove, ItemPrefab itemToInstall, bool force = false)
public void PurchaseItemSwap(Item itemToRemove, ItemPrefab itemToInstall, bool force = false, Client? client = null)
{
if (!CanUpgradeSub())
{
@@ -285,7 +288,7 @@ namespace Barotrauma
return;
}
if (itemToRemove.prefab == itemToInstall)
if (((MapEntity)itemToRemove).Prefab == itemToInstall)
{
DebugConsole.ThrowError($"Failed to swap item \"{itemToRemove.Name}\" (trying to swap with the same item!).");
return;
@@ -310,7 +313,7 @@ namespace Barotrauma
price = 0;
}
if (Campaign.Money >= price)
if (Campaign.GetWallet(client).TryDeduct(price))
{
PurchasedItemSwaps.RemoveAll(p => linkedItems.Contains(p.ItemToRemove));
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
@@ -318,13 +321,12 @@ namespace Barotrauma
// only make the NPC speak if more than 5 minutes have passed since the last purchased service
if (lastUpgradeSpeak == DateTime.MinValue || lastUpgradeSpeak.AddMinutes(5) < DateTime.Now)
{
UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased"), Campaign.IsSinglePlayer);
UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased").Value, Campaign.IsSinglePlayer);
lastUpgradeSpeak = DateTime.Now;
}
}
Campaign.Money -= price;
GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineWeapon, itemToInstall.Identifier);
GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineWeapon, itemToInstall.Identifier.Value);
foreach (Item itemToSwap in linkedItems)
{
@@ -352,7 +354,7 @@ namespace Barotrauma
else
{
DebugConsole.ThrowError("Tried to swap an item with insufficient funds, the transaction has not been completed.\n" +
$"Item to remove: {itemToRemove.Name}, Item to install: {itemToInstall.Name}, Cost: {price}, Have: {Campaign.Money}");
$"Item to remove: {itemToRemove.Name}, Item to install: {itemToInstall.Name}, Cost: {price}, Have: {Campaign.GetWallet(client).Balance}");
}
}
@@ -367,7 +369,7 @@ namespace Barotrauma
return;
}
if (itemToRemove?.PendingItemSwap == null && string.IsNullOrEmpty(itemToRemove?.Prefab.SwappableItem?.ReplacementOnUninstall))
if (itemToRemove?.PendingItemSwap == null && (itemToRemove?.Prefab.SwappableItem?.ReplacementOnUninstall.IsEmpty ?? true))
{
DebugConsole.ThrowError($"Cannot uninstall item \"{itemToRemove?.Name}\" (no replacement item configured).");
return;
@@ -385,7 +387,7 @@ namespace Barotrauma
// only make the NPC speak if more than 5 minutes have passed since the last purchased service
if (lastUpgradeSpeak == DateTime.MinValue || lastUpgradeSpeak.AddMinutes(5) < DateTime.Now)
{
UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased"), Campaign.IsSinglePlayer);
UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased").Value, Campaign.IsSinglePlayer);
lastUpgradeSpeak = DateTime.Now;
}
}
@@ -429,7 +431,7 @@ namespace Barotrauma
{
if (!(secondLinkedEntity is Item linkedItem) || linkedItem == item) { continue; }
if (linkedItem.AllowSwapping &&
linkedItem.Prefab.SwappableItem != null && (linkedItem.Prefab.SwappableItem.CanBeBought || item.Prefab.SwappableItem.ReplacementOnUninstall == linkedItem.prefab.Identifier) &&
linkedItem.Prefab.SwappableItem != null && (linkedItem.Prefab.SwappableItem.CanBeBought || item.Prefab.SwappableItem.ReplacementOnUninstall == ((MapEntity)linkedItem).Prefab.Identifier) &&
linkedItem.Prefab.SwappableItem.SwapIdentifier.Equals(item.Prefab.SwappableItem.SwapIdentifier, StringComparison.OrdinalIgnoreCase))
{
linkedItems.Add(linkedItem);
@@ -543,7 +545,7 @@ namespace Barotrauma
if (upgrade == null || upgrade.Level != level || isOverMax)
{
DebugLog($"{wall.prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}. Fixing...");
DebugLog($"{((MapEntity)wall).Prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}. Fixing...");
FixUpgradeOnItem(wall, prefab, level);
}
}
@@ -572,7 +574,7 @@ namespace Barotrauma
if (upgrade == null || upgrade.Level != level || isOverMax)
{
DebugLog($"{item.prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}{(isOverMax ? " (Over max level!)" : string.Empty)}. Fixing...");
DebugLog($"{((MapEntity)item).Prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}{(isOverMax ? " (Over max level!)" : string.Empty)}. Fixing...");
FixUpgradeOnItem(item, prefab, level);
}
}
@@ -598,7 +600,7 @@ namespace Barotrauma
/// <param name="submarine"></param>
/// <param name="level"></param>
/// <returns>New level that was applied, -1 if no upgrades were applied.</returns>
private static int BuyUpgrade(UpgradePrefab prefab, UpgradeCategory category, Submarine submarine, int level = 1, Submarine parentSub = null)
private static int BuyUpgrade(UpgradePrefab prefab, UpgradeCategory category, Submarine submarine, int level = 1, Submarine? parentSub = null)
{
int? newLevel = null;
if (category.IsWallUpgrade)
@@ -753,13 +755,13 @@ namespace Barotrauma
// ReSharper disable once LoopCanBeConvertedToQuery
foreach (XElement upgrade in element.Elements())
{
string? categoryIdentifier = upgrade.GetAttributeString("category", null);
Identifier categoryIdentifier = upgrade.GetAttributeIdentifier("category", Identifier.Empty);
UpgradeCategory? category = UpgradeCategory.Find(categoryIdentifier);
if (string.IsNullOrWhiteSpace(categoryIdentifier) || category == null) { continue; }
if (categoryIdentifier.IsEmpty || category == null) { continue; }
string? prefabIdentifier = upgrade.GetAttributeString("prefab", null);
Identifier prefabIdentifier = upgrade.GetAttributeIdentifier("prefab", Identifier.Empty);
UpgradePrefab? prefab = UpgradePrefab.Find(prefabIdentifier);
if (string.IsNullOrWhiteSpace(prefabIdentifier) || prefab == null) { continue; }
if (prefabIdentifier.IsEmpty || prefab == null) { continue; }
int level = upgrade.GetAttributeInt("level", -1);
if (level < 0) { continue; }
@@ -814,6 +816,6 @@ namespace Barotrauma
private PurchasedUpgrade? FindMatchingUpgrade(UpgradePrefab prefab, UpgradeCategory category) => PendingUpgrades.Find(u => u.Prefab == prefab && u.Category == category);
private static string FormatIdentifier(UpgradePrefab prefab, UpgradeCategory category) => $"upgrade.{category.Identifier}.{prefab.Identifier}";
private static Identifier FormatIdentifier(UpgradePrefab prefab, UpgradeCategory category) => $"upgrade.{category.Identifier}.{prefab.Identifier}".ToIdentifier();
}
}