This commit is contained in:
Evil Factory
2022-06-15 13:26:49 -03:00
410 changed files with 11140 additions and 5815 deletions
@@ -1,74 +1,141 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using System;
using System.Collections.Generic;
using System.Linq;
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;
/// <summary>
/// If we are spawning in an area where difficulty should not be a factor, assume difficulty is at the exact "middle"
/// </summary>
public const float DefaultDifficultyModifier = 0f;
public static void PlaceIfNeeded()
public static void SpawnItems(Identifier? startItemSet = null)
{
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; }
for (int i = 0; i < Submarine.MainSubs.Length; i++)
//player has more than one sub = we must have given the start items already
bool startItemsGiven = GameMain.GameSession?.OwnedSubmarines != null && GameMain.GameSession.OwnedSubmarines.Count > 1;
if (!startItemsGiven)
{
if (Submarine.MainSubs[i] == null || Submarine.MainSubs[i].Info.InitialSuppliesSpawned) { continue; }
List<Submarine> subs = new List<Submarine>() { Submarine.MainSubs[i] };
subs.AddRange(Submarine.MainSubs[i].DockedTo.Where(d => !d.Info.IsOutpost));
Place(subs);
subs.ForEach(s => s.Info.InitialSuppliesSpawned = true);
for (int i = 0; i < Submarine.MainSubs.Length; i++)
{
var sub = Submarine.MainSubs[i];
if (sub == null || sub.Info.InitialSuppliesSpawned || !sub.Info.IsPlayer) { continue; }
//1st pass: items defined in the start item set, only spawned in the main sub (not drones/shuttles or other linked subs)
SpawnStartItems(sub, startItemSet);
//2nd pass: items defined using preferred containers, spawned in the main sub and all the linked subs (drones, shuttles etc)
var subs = sub.GetConnectedSubs().Where(s => s.TeamID == sub.TeamID);
CreateAndPlace(subs);
subs.ForEach(s => s.Info.InitialSuppliesSpawned = true);
}
}
float difficultyModifier = GetLevelDifficultyModifier();
//spawn items in wrecks, beacon stations and pirate subs
foreach (var sub in Submarine.Loaded)
{
if (sub.Info.Type == SubmarineType.Player ||
sub.Info.Type == SubmarineType.Outpost ||
sub.Info.Type == SubmarineType.OutpostModule ||
sub.Info.Type == SubmarineType.EnemySubmarine)
sub.Info.Type == SubmarineType.OutpostModule)
{
continue;
}
Place(sub.ToEnumerable(), difficultyModifier: difficultyModifier);
if (sub.Info.InitialSuppliesSpawned) { continue; }
CreateAndPlace(sub.ToEnumerable());
sub.Info.InitialSuppliesSpawned = true;
}
if (Level.Loaded?.StartOutpost != null && Level.Loaded.Type == LevelData.LevelType.Outpost)
{
Rand.SetSyncedSeed(ToolBox.StringToInt(Level.Loaded.StartOutpost.Info.Name));
Place(Level.Loaded.StartOutpost.ToEnumerable());
var sub = Level.Loaded.StartOutpost;
if (!sub.Info.InitialSuppliesSpawned)
{
Rand.SetSyncedSeed(ToolBox.StringToInt(sub.Info.Name));
CreateAndPlace(sub.ToEnumerable());
sub.Info.InitialSuppliesSpawned = true;
}
}
}
private const float MaxDifficultyModifier = 0.2f;
/// <summary>
/// Spawn probability of loot is modified by difficulty, -20% less loot at 0% difficulty and +20% loot at 100% difficulty.
/// </summary>
private static float GetLevelDifficultyModifier()
{
return Math.Clamp(Level.Loaded?.Difficulty is float difficulty ? (difficulty / 100f) * (MaxDifficultyModifier * 2) - MaxDifficultyModifier : DefaultDifficultyModifier, -MaxDifficultyModifier, MaxDifficultyModifier);
}
public static void RegenerateLoot(Submarine sub, ItemContainer regeneratedContainer)
{
// Level difficulty currently doesn't affect regenerated loot for the sake of simplicity
Place(sub.ToEnumerable(), regeneratedContainer: regeneratedContainer);
CreateAndPlace(sub.ToEnumerable(), regeneratedContainer: regeneratedContainer);
}
private static void Place(IEnumerable<Submarine> subs, ItemContainer regeneratedContainer = null, float difficultyModifier = DefaultDifficultyModifier)
public static Identifier DefaultStartItemSet = new Identifier("normal");
/// <summary>
/// Spawns the items defined in the start item set in the specified sub.
/// </summary>
private static void SpawnStartItems(Submarine sub, Identifier? startItemSet)
{
Identifier setIdentifier = startItemSet ?? DefaultStartItemSet;
if (!StartItemSet.Sets.TryGet(setIdentifier, out StartItemSet itemSet))
{
DebugConsole.AddWarning($"Couldn't find a start item set matching the identifier \"{setIdentifier}\"!");
if (!StartItemSet.Sets.TryGet(DefaultStartItemSet, out StartItemSet defaultSet))
{
DebugConsole.ThrowError($"Couldn't find the default start item set \"{DefaultStartItemSet}\"!");
return;
}
itemSet = defaultSet;
}
WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, sub);
ISpatialEntity initialSpawnPos;
if (wp?.CurrentHull == null)
{
var spawnHull = Hull.HullList.Where(h => h.Submarine == sub && !h.IsWetRoom).GetRandomUnsynced();
if (spawnHull == null)
{
DebugConsole.AddWarning($"Failed to spawn start items in the sub. No cargo waypoint or dry hulls found to spawn the items in.");
return;
}
initialSpawnPos = spawnHull;
}
else
{
initialSpawnPos = wp;
}
var newItems = new List<Item>();
foreach (var startItem in itemSet.Items)
{
if (!ItemPrefab.Prefabs.TryGet(startItem.Item, out ItemPrefab itemPrefab))
{
DebugConsole.AddWarning($"Cannot find a start item with with the identifier \"{startItem.Item}\"");
continue;
}
for (int i = 0; i < startItem.Amount; i++)
{
var item = new Item(itemPrefab, initialSpawnPos.Position, sub, callOnItemLoaded: false);
// Is this necessary?
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
{
wifiComponent.TeamID = sub.TeamID;
}
newItems.Add(item);
}
}
var cargoContainers = new List<ItemContainer>();
foreach (var item in newItems)
{
#if SERVER
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
#endif
foreach (ItemComponent ic in item.Components)
{
ic.OnItemLoaded();
}
var container = sub.FindContainerFor(item, onlyPrimary: true);
if (container == null)
{
var cargoContainer = CargoManager.GetOrCreateCargoContainerFor(item.Prefab, initialSpawnPos, ref cargoContainers);
container = cargoContainer?.Item;
}
container?.OwnInventory.TryPutItem(item, user: null);
}
}
private static void CreateAndPlace(IEnumerable<Submarine> subs, ItemContainer regeneratedContainer = null)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
@@ -76,7 +143,7 @@ namespace Barotrauma
return;
}
List<Item> spawnedItems = new List<Item>(100);
List<Item> itemsToSpawn = new List<Item>(100);
int itemCountApprox = MapEntityPrefab.List.Count() / 3;
var containers = new List<ItemContainer>(70 + 30 * subs.Count());
@@ -100,11 +167,11 @@ namespace Barotrauma
containers.Shuffle(Rand.RandSync.ServerAndClient);
}
foreach (ItemPrefab ip in ItemPrefab.Prefabs)
var itemPrefabs = ItemPrefab.Prefabs.OrderBy(p => p.UintIdentifier);
foreach (ItemPrefab ip in itemPrefabs)
{
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)))
if (ip.PreferredContainers.None()) { continue; }
if (ip.ConfigElement.Elements().Any(e => string.Equals(e.Name.ToString(), typeof(ItemContainer).Name.ToString(), StringComparison.OrdinalIgnoreCase)) && itemPrefabs.Any(ip2 => CanSpawnIn(ip2, ip)))
{
prefabsItemsCanSpawnIn.Add(ip);
}
@@ -141,9 +208,9 @@ namespace Barotrauma
{
var subNames = subs.Select(s => s.Info.Name).ToList();
DebugConsole.NewMessage($"Automatically placed items in { string.Join(", ", subNames) }:");
foreach (string itemName in spawnedItems.Select(it => it.Name).Distinct())
foreach (string itemName in itemsToSpawn.Select(it => it.Name).Distinct())
{
DebugConsole.NewMessage(" - " + itemName + " x" + spawnedItems.Count(it => it.Name == itemName));
DebugConsole.NewMessage(" - " + itemName + " x" + itemsToSpawn.Count(it => it.Name == itemName));
}
}
@@ -153,24 +220,28 @@ namespace Barotrauma
{
foreach (Location.TakenItem takenItem in GameMain.GameSession.StartLocation.TakenItems)
{
var matchingItem = spawnedItems.Find(it => takenItem.Matches(it));
var matchingItem = itemsToSpawn.Find(it => takenItem.Matches(it));
if (matchingItem == null) { continue; }
var containedItems = spawnedItems.FindAll(it => it.ParentInventory?.Owner == matchingItem);
if (OutputDebugInfo)
{
DebugConsole.NewMessage($"Removing the stolen item: {matchingItem.Prefab.Identifier} ({matchingItem.ID})");
}
var containedItems = itemsToSpawn.FindAll(it => it.ParentInventory?.Owner == matchingItem);
matchingItem.Remove();
spawnedItems.Remove(matchingItem);
itemsToSpawn.Remove(matchingItem);
foreach (Item containedItem in containedItems)
{
containedItem.Remove();
spawnedItems.Remove(containedItem);
itemsToSpawn.Remove(containedItem);
}
}
}
foreach (Item spawnedItem in spawnedItems)
foreach (Item item in itemsToSpawn)
{
#if SERVER
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(spawnedItem));
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
#endif
foreach (ItemComponent ic in spawnedItem.Components)
foreach (ItemComponent ic in item.Components)
{
ic.OnItemLoaded();
}
@@ -186,9 +257,12 @@ namespace Barotrauma
return false;
}
bool success = false;
bool isCampaign = GameMain.GameSession?.GameMode is CampaignMode;
foreach (PreferredContainer preferredContainer in itemPrefab.PreferredContainers)
{
if (preferredContainer.SpawnProbability <= 0.0f || preferredContainer.MaxAmount <= 0) { continue; }
if (preferredContainer.CampaignOnly && !isCampaign) { continue; }
if (preferredContainer.NotCampaign && isCampaign) { continue; }
if (preferredContainer.SpawnProbability <= 0.0f || preferredContainer.MaxAmount <= 0 && preferredContainer.Amount <= 0) { continue; }
validContainers = GetValidContainers(preferredContainer, containers, validContainers, primary: true);
if (validContainers.None())
{
@@ -196,10 +270,10 @@ namespace Barotrauma
}
foreach (var validContainer in validContainers)
{
var newItems = SpawnItem(itemPrefab, containers, validContainer, difficultyModifier);
var newItems = CreateItems(itemPrefab, containers, validContainer);
if (newItems.Any())
{
spawnedItems.AddRange(newItems);
itemsToSpawn.AddRange(newItems);
success = true;
}
}
@@ -238,16 +312,20 @@ namespace Barotrauma
(3, 0.0f),
};
private static List<Item> SpawnItem(ItemPrefab itemPrefab, List<ItemContainer> containers, KeyValuePair<ItemContainer, PreferredContainer> validContainer, float difficultyModifier)
private static List<Item> CreateItems(ItemPrefab itemPrefab, List<ItemContainer> containers, KeyValuePair<ItemContainer, PreferredContainer> validContainer)
{
List<Item> spawnedItems = new List<Item>();
if (Rand.Value(Rand.RandSync.ServerAndClient) > validContainer.Value.SpawnProbability * (1f + difficultyModifier)) { return spawnedItems; }
List<Item> newItems = new List<Item>();
if (Rand.Value(Rand.RandSync.ServerAndClient) > validContainer.Value.SpawnProbability) { return newItems; }
// Don't add dangerously reactive materials in thalamus wrecks
if (validContainer.Key.Item.Submarine.WreckAI != null && itemPrefab.Tags.Contains("explodesinwater"))
{
return spawnedItems;
return newItems;
}
int amount = validContainer.Value.Amount;
if (amount == 0)
{
amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1, Rand.RandSync.ServerAndClient);
}
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))
@@ -255,14 +333,12 @@ namespace Barotrauma
containers.Remove(validContainer.Key);
break;
}
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.ServerAndClient);
qualityCommonnesses.Select(q => q.commonness).ToList(), 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, callOnItemLoaded: false)
{
@@ -277,11 +353,11 @@ namespace Barotrauma
{
wifiComponent.TeamID = validContainer.Key.Item.Submarine.TeamID;
}
spawnedItems.Add(item);
newItems.Add(item);
validContainer.Key.Inventory.TryPutItem(item, null, createNetworkEvent: false);
containers.AddRange(item.GetComponents<ItemContainer>());
}
return spawnedItems;
return newItems;
}
}
}
@@ -22,14 +22,14 @@ namespace Barotrauma
public int Quantity { get; set; }
public bool? IsStoreComponentEnabled { get; set; }
public readonly int BuyerCharacterInfoId;
public readonly int BuyerCharacterInfoIdentifier;
public PurchasedItem(ItemPrefab itemPrefab, int quantity, int buyerCharacterInfoId)
{
ItemPrefabIdentifier = itemPrefab.Identifier;
Quantity = quantity;
IsStoreComponentEnabled = null;
BuyerCharacterInfoId = buyerCharacterInfoId;
BuyerCharacterInfoIdentifier = buyerCharacterInfoId;
}
#if CLIENT
@@ -44,7 +44,7 @@ namespace Barotrauma
ItemPrefabIdentifier = itemPrefabId;
Quantity = quantity;
IsStoreComponentEnabled = null;
BuyerCharacterInfoId = buyer?.Character?.Info?.ID ?? Character.Controlled?.Info?.ID ?? 0;
BuyerCharacterInfoIdentifier = buyer?.Character?.Info?.GetIdentifier() ?? Character.Controlled?.Info?.GetIdentifier() ?? 0;
}
public override string ToString()
@@ -284,11 +284,10 @@ namespace Barotrauma
foreach (PurchasedItem item in newItems)
{
int itemValue = item.Quantity * buyValues[item.ItemPrefab];
GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value);
sb.Append($"\n - {item.ItemPrefab.Name} x{item.Quantity}");
price += itemValue;
}
GameServer.Log($"{NetworkMember.ClientLogName(client, client?.Name ?? "Unknown")} purchased {newItems.Count} item(s) for {TextManager.FormatCurrency(price)}{sb.ToString()}", ServerLog.MessageType.Money);
}
#endif
@@ -317,7 +316,10 @@ namespace Barotrauma
// Exchange money
int itemValue = item.Quantity * buyValues[item.ItemPrefab];
campaign.TryPurchase(client, itemValue);
GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value);
if (GameMain.IsSingleplayer)
{
GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value);
}
store.Balance += itemValue;
if (removeFromCrate)
{
@@ -368,12 +370,13 @@ namespace Barotrauma
public void CreatePurchasedItems()
{
purchasedIDCards.Clear();
var items = new List<PurchasedItem>();
foreach (var storeSpecificItems in PurchasedItems)
{
items.AddRange(storeSpecificItems.Value);
}
CreateItems(items, Submarine.MainSub);
CreateItems(items, Submarine.MainSub, this);
PurchasedItems.Clear();
OnPurchasedItemsChanged?.Invoke();
}
@@ -407,7 +410,7 @@ namespace Barotrauma
if (!item.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { return false; }
if (!item.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { return false; }
if (!ItemAndAllContainersInteractable(item)) { return false; }
if (item.GetRootContainer() is Item rootContainer && rootContainer.HasTag("donttakeitems")) { return false; }
if (item.GetRootContainer() is Item rootContainer && rootContainer.HasTag("dontsellitems")) { return false; }
return true;
}).Distinct();
@@ -428,7 +431,7 @@ namespace Barotrauma
if (!item.Prefab.CanBeSold) { return false; }
if (item.SpawnedInCurrentOutpost) { return false; }
if (!item.Prefab.AllowSellingWhenBroken && item.ConditionPercentage < 90.0f) { return false; }
if (confirmedItems.Any(ci => ci.Item == item)) { return false; }
if (confirmedItems != null && confirmedItems.Any(ci => ci.Item == item)) { return false; }
if (UndeterminedSoldEntities.TryGetValue(item.Prefab, out int count))
{
int newCount = count - 1;
@@ -448,13 +451,58 @@ namespace Barotrauma
if (containedItems.None()) { return true; }
// Allow selling the item if contained items are unsellable and set to be removed on deconstruct
if (itemContainer.RemoveContainedItemsOnDeconstruct && containedItems.All(it => !it.Prefab.CanBeSold)) { return true; }
// Otherwise there must be no contained items or the contained items must be confirmed as sold
if (!containedItems.All(it => confirmedItems.Any(ci => ci.Item == it))) { return false; }
if (confirmedItems != null)
{
// Otherwise there must be no contained items or the contained items must be confirmed as sold
if (!containedItems.All(it => confirmedItems.Any(ci => ci.Item == it))) { return false; }
}
}
return true;
}
public static void CreateItems(List<PurchasedItem> itemsToSpawn, Submarine sub)
public static ItemContainer GetOrCreateCargoContainerFor(ItemPrefab item, ISpatialEntity cargoRoomOrSpawnPoint, ref List<ItemContainer> availableContainers)
{
ItemContainer itemContainer = null;
if (!string.IsNullOrEmpty(item.CargoContainerIdentifier))
{
itemContainer = availableContainers.Find(ac =>
ac.Inventory.CanBePut(item) &&
(ac.Item.Prefab.Identifier == item.CargoContainerIdentifier ||
ac.Item.Prefab.Tags.Contains(item.CargoContainerIdentifier)));
if (itemContainer == null)
{
ItemPrefab containerPrefab = ItemPrefab.Prefabs.Find(ep =>
ep.Identifier == item.CargoContainerIdentifier ||
(ep.Tags != null && ep.Tags.Contains(item.CargoContainerIdentifier)));
if (containerPrefab == null)
{
DebugConsole.AddWarning($"CargoManager: could not find the item prefab for container {item.CargoContainerIdentifier}!");
return null;
}
Vector2 containerPosition = cargoRoomOrSpawnPoint is Hull cargoRoom ? GetCargoPos(cargoRoom, containerPrefab) : cargoRoomOrSpawnPoint.Position;
Item containerItem = new Item(containerPrefab, containerPosition, cargoRoomOrSpawnPoint.Submarine);
itemContainer = containerItem.GetComponent<ItemContainer>();
if (itemContainer == null)
{
DebugConsole.AddWarning($"CargoManager: No ItemContainer component found in {containerItem.Prefab.Identifier}!");
return null;
}
availableContainers.Add(itemContainer);
#if SERVER
if (GameMain.Server != null)
{
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(itemContainer.Item));
}
#endif
}
}
return itemContainer;
}
public static void CreateItems(List<PurchasedItem> itemsToSpawn, Submarine sub, CargoManager cargoManager)
{
if (itemsToSpawn.Count == 0) { return; }
@@ -496,60 +544,26 @@ namespace Barotrauma
}
List<ItemContainer> availableContainers = new List<ItemContainer>();
ItemPrefab containerPrefab = null;
foreach (PurchasedItem pi in itemsToSpawn)
{
Vector2 position = GetCargoPos(cargoRoom, pi.ItemPrefab);
for (int i = 0; i < pi.Quantity; i++)
{
ItemContainer itemContainer = null;
if (!string.IsNullOrEmpty(pi.ItemPrefab.CargoContainerIdentifier))
{
itemContainer = availableContainers.Find(ac =>
ac.Inventory.CanBePut(pi.ItemPrefab) &&
(ac.Item.Prefab.Identifier == pi.ItemPrefab.CargoContainerIdentifier ||
ac.Item.Prefab.Tags.Contains(pi.ItemPrefab.CargoContainerIdentifier.ToLowerInvariant())));
if (itemContainer == null)
{
containerPrefab = ItemPrefab.Prefabs.Find(ep =>
ep.Identifier == pi.ItemPrefab.CargoContainerIdentifier ||
(ep.Tags != null && ep.Tags.Contains(pi.ItemPrefab.CargoContainerIdentifier.ToLowerInvariant())));
if (containerPrefab == null)
{
DebugConsole.ThrowError("Cargo spawning failed - could not find the item prefab for container \"" + pi.ItemPrefab.CargoContainerIdentifier + "\"!");
continue;
}
Vector2 containerPosition = GetCargoPos(cargoRoom, containerPrefab);
Item containerItem = new Item(containerPrefab, containerPosition, wp.Submarine);
itemContainer = containerItem.GetComponent<ItemContainer>();
if (itemContainer == null)
{
DebugConsole.ThrowError("Cargo spawning failed - container \"" + containerItem.Name + "\" does not have an ItemContainer component!");
continue;
}
availableContainers.Add(itemContainer);
#if SERVER
if (GameMain.Server != null)
{
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(itemContainer.Item));
}
#endif
}
}
var item = new Item(pi.ItemPrefab, position, wp.Submarine);
itemContainer?.Inventory.TryPutItem(item, null);
itemSpawned(item);
var itemContainer = GetOrCreateCargoContainerFor(pi.ItemPrefab, cargoRoom, ref availableContainers);
itemContainer?.Inventory.TryPutItem(item, null);
var idCard = item.GetComponent<IdCard>();
if (cargoManager != null && idCard != null && pi.BuyerCharacterInfoIdentifier != 0)
{
cargoManager.purchasedIDCards.Add((pi, idCard));
}
itemSpawned(pi, item);
#if SERVER
Entity.Spawner?.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
#endif
(itemContainer?.Item ?? item).CampaignInteractionType = CampaignMode.InteractionType.Cargo;
static void itemSpawned(Item item)
static void itemSpawned(PurchasedItem purchased, Item item)
{
Submarine sub = item.Submarine ?? item.GetRootContainer()?.Submarine;
if (sub != null)
@@ -565,6 +579,23 @@ namespace Barotrauma
itemsToSpawn.Clear();
}
private readonly List<(PurchasedItem purchaseInfo, IdCard idCard)> purchasedIDCards = new List<(PurchasedItem purchaseInfo, IdCard idCard)>();
public void InitPurchasedIDCards()
{
foreach ((PurchasedItem purchased, IdCard idCard) in purchasedIDCards)
{
if (idCard != null && purchased.BuyerCharacterInfoIdentifier != 0)
{
var owner = Character.CharacterList.Find(c => c.Info?.GetIdentifier() == purchased.BuyerCharacterInfoIdentifier);
if (owner?.Info != null)
{
var mainSubSpawnPoints = WayPoint.SelectCrewSpawnPoints(new List<CharacterInfo>() { owner.Info }, Submarine.MainSub);
idCard.Initialize(mainSubSpawnPoints.FirstOrDefault(), owner);
}
}
}
}
public static Vector2 GetCargoPos(Hull hull, ItemPrefab itemPrefab)
{
float floorPos = hull.Rect.Y - hull.Rect.Height;
@@ -603,7 +634,7 @@ namespace Barotrauma
new XAttribute("id", item.ItemPrefab.Identifier),
new XAttribute("qty", item.Quantity),
new XAttribute("storeid", storeSpecificItems.Key),
new XAttribute("buyer", item.BuyerCharacterInfoId)));
new XAttribute("buyer", item.BuyerCharacterInfoIdentifier)));
}
}
parentElement.Add(itemsElement);
@@ -51,8 +51,6 @@ namespace Barotrauma
public ReadyCheck ActiveReadyCheck;
public XElement ActiveOrdersElement { get; set; }
public CrewManager(bool isSinglePlayer)
{
IsSinglePlayer = isSinglePlayer;
@@ -493,9 +491,8 @@ namespace Barotrauma
partial void UpdateProjectSpecific(float deltaTime);
private void SaveActiveOrders(XElement parentElement)
public void SaveActiveOrders(XElement element)
{
ActiveOrdersElement = new XElement("activeorders");
// Only save orders with no fade out time (e.g. ignore orders)
var ordersToSave = new List<Order>();
foreach (var activeOrder in ActiveOrders)
@@ -504,14 +501,13 @@ namespace Barotrauma
if (order == null || activeOrder.FadeOutTime.HasValue) { continue; }
ordersToSave.Add(order.WithManualPriority(CharacterInfo.HighestManualOrderPriority));
}
CharacterInfo.SaveOrders(ActiveOrdersElement, ordersToSave.ToArray());
parentElement?.Add(ActiveOrdersElement);
CharacterInfo.SaveOrders(element, ordersToSave.ToArray());
}
public void LoadActiveOrders()
public void LoadActiveOrders(XElement element)
{
if (ActiveOrdersElement == null) { return; }
foreach (var orderInfo in CharacterInfo.LoadOrders(ActiveOrdersElement))
if (element == null) { return; }
foreach (var orderInfo in CharacterInfo.LoadOrders(element))
{
IIgnorable ignoreTarget = null;
if (orderInfo.IsIgnoreOrder)
@@ -1,4 +1,5 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework;
@@ -9,63 +10,6 @@ using System.Xml.Linq;
namespace Barotrauma
{
internal struct CampaignSettings
{
public static CampaignSettings Empty => new CampaignSettings();
// Anything that uses this field I wasn't sure if actually needed the proper campaign settings to be passed down
public static CampaignSettings Unsure => Empty;
public bool RadiationEnabled { get; set; }
public int TotalMaxMissionCount => MaxMissionCount + GetAddedMissionCount();
private int maxMissionCount;
public int MaxMissionCount
{
get { return maxMissionCount; }
set { maxMissionCount = MathHelper.Clamp(value, MinMissionCountLimit, MaxMissionCountLimit); }
}
public const int DefaultMaxMissionCount = 2;
public const int MaxMissionCountLimit = 10;
public const int MinMissionCountLimit = 1;
public CampaignSettings(IReadMessage inc)
{
maxMissionCount = DefaultMaxMissionCount;
RadiationEnabled = inc.ReadBoolean();
MaxMissionCount = inc.ReadRangedInteger(MinMissionCountLimit, MaxMissionCountLimit);
}
public CampaignSettings(XElement element)
{
maxMissionCount = DefaultMaxMissionCount;
RadiationEnabled = element.GetAttributeBool(nameof(RadiationEnabled).ToLowerInvariant(), true);
MaxMissionCount = element.GetAttributeInt(nameof(MaxMissionCount).ToLowerInvariant(), DefaultMaxMissionCount);
}
public void Serialize(IWriteMessage msg)
{
msg.Write(RadiationEnabled);
msg.WriteRangedInteger(MaxMissionCount, MinMissionCountLimit, MaxMissionCountLimit);
}
public int GetAddedMissionCount()
{
int count = 0;
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
{
count += (int)character.GetStatValue(StatTypes.ExtraMissionCount);
}
return count;
}
public XElement Save()
{
return new XElement(nameof(CampaignSettings), new XAttribute(nameof(RadiationEnabled).ToLowerInvariant(), RadiationEnabled), new XAttribute(nameof(MaxMissionCount).ToLowerInvariant(), MaxMissionCount));
}
}
abstract partial class CampaignMode : GameMode
{
[NetworkSerialize]
@@ -107,6 +51,8 @@ namespace Barotrauma
protected XElement petsElement;
protected XElement ActiveOrdersElement { get; set; }
public CampaignSettings Settings;
private readonly List<Mission> extraMissions = new List<Mission>();
@@ -146,9 +92,8 @@ namespace Barotrauma
//key = dialog flag, double = Timing.TotalTime when the line was last said
private readonly Dictionary<string, double> dialogLastSpoken = new Dictionary<string, double>();
public bool PurchasedHullRepairs, PurchasedLostShuttles, PurchasedItemRepairs;
public SubmarineInfo PendingSubmarineSwitch;
public bool TransferItemsOnSubSwitch { get; set; }
protected Map map;
public Map Map
@@ -186,12 +131,16 @@ namespace Barotrauma
protected set;
}
protected CampaignMode(GameModePreset preset)
public virtual bool PurchasedHullRepairs { get; set; }
public virtual bool PurchasedLostShuttles { get; set; }
public virtual bool PurchasedItemRepairs { get; set; }
protected CampaignMode(GameModePreset preset, CampaignSettings settings)
: base(preset)
{
Bank = new Wallet(Option<Character>.None())
{
Balance = InitialMoney
Balance = settings.InitialMoney
};
CargoManager = new CargoManager(this);
@@ -558,6 +507,8 @@ namespace Barotrauma
}
}
public TransitionType GetAvailableTransition() => GetAvailableTransition(out _, out _);
/// <summary>
/// Which submarine is at a position where it can leave the level and enter another one (if any).
/// </summary>
@@ -593,6 +544,7 @@ namespace Barotrauma
if (Level.Loaded.StartOutpost == null)
{
Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID);
if (closestSub == null) { return null; }
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
}
else
@@ -726,7 +678,6 @@ namespace Barotrauma
}
}
public void EndCampaign()
{
foreach (Character c in Character.CharacterList)
@@ -738,13 +689,16 @@ namespace Barotrauma
}
foreach (LocationConnection connection in Map.Connections)
{
connection.Difficulty = MathHelper.Lerp(connection.Difficulty, 100.0f, 0.25f);
connection.LevelData.Difficulty = connection.Difficulty;
connection.LevelData.IsBeaconActive = false;
connection.Difficulty = connection.Biome.MaxDifficulty;
connection.LevelData = new LevelData(connection)
{
IsBeaconActive = false
};
connection.LevelData.HasHuntingGrounds = connection.LevelData.OriginallyHadHuntingGrounds;
}
foreach (Location location in Map.Locations)
{
location.LevelData = new LevelData(location, location.Biome.MaxDifficulty);
location.Reset();
}
Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation));
@@ -868,7 +822,7 @@ namespace Barotrauma
const float MaxDist = 3000.0f;
const float MinDist = 2500.0f;
if (!Level.IsLoadedOutpost) { return; }
if (!Level.IsLoadedFriendlyOutpost) { return; }
Rectangle worldBorders = Submarine.MainSub.GetDockedBorders();
worldBorders.Location += Submarine.MainSub.WorldPosition.ToPoint();
@@ -1032,5 +986,190 @@ namespace Barotrauma
}
}
protected void LeaveUnconnectedSubs(Submarine leavingSub)
{
if (leavingSub != Submarine.MainSub && !leavingSub.DockedTo.Contains(Submarine.MainSub))
{
Submarine.MainSub = leavingSub;
GameMain.GameSession.Submarine = leavingSub;
GameMain.GameSession.SubmarineInfo = leavingSub.Info;
leavingSub.Info.FilePath = System.IO.Path.Combine(SaveUtil.TempPath, leavingSub.Info.Name + ".sub");
var subsToLeaveBehind = GetSubsToLeaveBehind(leavingSub);
GameMain.GameSession.OwnedSubmarines.Add(leavingSub.Info);
foreach (Submarine sub in subsToLeaveBehind)
{
GameMain.GameSession.OwnedSubmarines.RemoveAll(s => s != leavingSub.Info && s.Name == sub.Info.Name);
MapEntity.mapEntityList.RemoveAll(e => e.Submarine == sub && e is LinkedSubmarine);
LinkedSubmarine.CreateDummy(leavingSub, sub);
}
}
}
public SubmarineInfo SwitchSubs()
{
if (TransferItemsOnSubSwitch)
{
TransferItemsBetweenSubs();
}
RefreshOwnedSubmarines();
PendingSubmarineSwitch = null;
return GameMain.GameSession.SubmarineInfo;
}
/// <summary>
/// Also serializes the current sub.
/// </summary>
protected void TransferItemsBetweenSubs()
{
Submarine currentSub = GameMain.GameSession.Submarine;
if (currentSub == null || currentSub.Removed)
{
DebugConsole.ThrowError("Cannot transfer items between subs, because the current sub is null or removed!");
return;
}
var itemsToTransfer = new List<(Item item, Item container)>();
if (PendingSubmarineSwitch != null)
{
var connectedSubs = currentSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player).ToHashSet();
// Remove items from the old sub
foreach (Item item in Item.ItemList)
{
if (item.Removed) { continue; }
if (item.NonInteractable) { continue; }
if (item.HiddenInGame) { continue; }
if (!connectedSubs.Contains(item.Submarine)) { continue; }
if (item.Prefab.DontTransferBetweenSubs) { continue; }
if (item.GetRootInventoryOwner() is Character) { continue; }
if (item.GetComponent<Holdable>() == null && item.GetComponent<Wearable>() == null && item.GetComponent<Projectile>() == null) { continue; }
if (item.Components.Any(c => c is Holdable h && h.Attached)) { continue; }
if (item.Components.Any(c => c is Wire w && w.Connections.Any(c => c != null))) { continue; }
itemsToTransfer.Add((item, item.Container));
item.Submarine = null;
}
foreach (var (item, container) in itemsToTransfer)
{
if (container?.Submarine != null)
{
// Drop the item if it's not inside another item set to be transferred.
item.Drop(null, createNetworkEvent: false, setTransform: false);
}
}
}
// Serialize the current sub
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(currentSub);
if (PendingSubmarineSwitch != null && itemsToTransfer.Any())
{
// Load the new sub
var newSub = new Submarine(PendingSubmarineSwitch);
var connectedSubs = newSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player).ToHashSet();
// Move the transferred items
List<ItemContainer> availableContainers = Item.ItemList
.Where(it => connectedSubs.Contains(it.Submarine) && it.HasTag("crate") && !it.NonInteractable && !it.HiddenInGame && !it.Removed)
.Select(it => it.GetComponent<ItemContainer>())
.Where(c => c != null)
.ToList();
foreach (var (item, oldContainer) in itemsToTransfer)
{
Item newContainer = null;
item.Submarine = newSub;
if (item.Container == null)
{
newContainer = newSub.FindContainerFor(item, onlyPrimary: true, checkTransferConditions: true, allowConnectedSubs: true);
}
if (item.Container == null && (newContainer == null || !newContainer.OwnInventory.TryPutItem(item, user: null, createNetworkEvent: false)))
{
WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, newSub);
Hull spawnHull = wp?.CurrentHull ?? Hull.HullList.Where(h => h.Submarine == newSub && !h.IsWetRoom).GetRandomUnsynced();
if (spawnHull == null)
{
DebugConsole.AddWarning($"Failed to transfer items between subs. No cargo waypoint or dry hulls found in the new sub.");
return;
}
if (spawnHull != null)
{
var cargoContainer = CargoManager.GetOrCreateCargoContainerFor(item.Prefab, spawnHull, ref availableContainers);
if (cargoContainer == null || !cargoContainer.Inventory.TryPutItem(item, user: null, createNetworkEvent: false))
{
Vector2 simPos = ConvertUnits.ToSimUnits(CargoManager.GetCargoPos(spawnHull, item.Prefab));
item.SetTransform(simPos, 0.0f, findNewHull: false, setPrevTransform: false);
}
}
else
{
DebugConsole.AddWarning($"Failed to transfer item {item.Prefab.Identifier} ({item.ID}), because no cargo spawn point could be found!");
}
}
string newContainerName = newContainer == null ? "(null)" : $"{newContainer.Prefab.Identifier} ({newContainer.Tags})";
string msg = "Item transfer log error.";
if (oldContainer != null)
{
if (newContainer == null && oldContainer == item.Container)
{
msg = $"Transferred {item.Prefab.Identifier} ({item.ID}) contained inside {oldContainer.Prefab.Identifier} ({oldContainer.ID})";
}
else
{
msg = $"Transferred {item.Prefab.Identifier} ({item.ID}) from {oldContainer.Prefab.Identifier} ({oldContainer.Tags}) to {newContainerName}";
}
}
else
{
msg = $"Transferred {item.Prefab.Identifier} ({item.ID}) to {newContainerName}";
}
#if DEBUG
DebugConsole.NewMessage(msg);
#else
DebugConsole.Log(msg);
#endif
}
// Serialize the new sub
PendingSubmarineSwitch = new SubmarineInfo(newSub);
}
}
protected void RefreshOwnedSubmarines()
{
if (PendingSubmarineSwitch != null)
{
SubmarineInfo previousSub = GameMain.GameSession.SubmarineInfo;
GameMain.GameSession.SubmarineInfo = PendingSubmarineSwitch;
for (int i = 0; i < GameMain.GameSession.OwnedSubmarines.Count; i++)
{
if (GameMain.GameSession.OwnedSubmarines[i].Name == previousSub.Name)
{
GameMain.GameSession.OwnedSubmarines[i] = previousSub;
break;
}
}
}
}
public void SavePets(XElement parentElement = null)
{
petsElement = new XElement("pets");
PetBehavior.SavePets(petsElement);
parentElement?.Add(petsElement);
}
public void LoadPets()
{
if (petsElement != null)
{
PetBehavior.LoadPets(petsElement);
}
}
public void SaveActiveOrders(XElement parentElement = null)
{
ActiveOrdersElement = new XElement("activeorders");
CrewManager?.SaveActiveOrders(ActiveOrdersElement);
parentElement?.Add(ActiveOrdersElement);
}
public void LoadActiveOrders()
{
CrewManager?.LoadActiveOrders(ActiveOrdersElement);
}
}
}
@@ -0,0 +1,92 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Xml.Linq;
namespace Barotrauma
{
internal static class CampaignModePresets
{
public static readonly ImmutableArray<CampaignSettings> List;
public static readonly ImmutableDictionary<Identifier, CampaignSettingDefinitions> Definitions;
private static readonly string fileListPath = Path.Combine("Data", "campaignsettings.xml");
static CampaignModePresets()
{
if (!File.Exists(fileListPath) || !(XMLExtensions.TryLoadXml(fileListPath)?.Root is { } docRoot))
{
List = ImmutableArray<CampaignSettings>.Empty;
return;
}
List<CampaignSettings> list = new List<CampaignSettings>();
Dictionary<Identifier, CampaignSettingDefinitions> definitions = new Dictionary<Identifier, CampaignSettingDefinitions>();
foreach (XElement element in docRoot.Elements())
{
Identifier name = element.NameAsIdentifier();
if (name == CampaignSettings.LowerCaseSaveElementName)
{
list.Add(new CampaignSettings(element));
}
else if (name == nameof(CampaignSettingDefinitions))
{
foreach (XElement subElement in element.Elements())
{
definitions.Add(subElement.NameAsIdentifier(), new CampaignSettingDefinitions(subElement));
}
}
}
List = list.ToImmutableArray();
Definitions = definitions.ToImmutableDictionary();
}
}
internal readonly struct CampaignSettingDefinitions
{
// Definitely not the best way to do this
private readonly ImmutableDictionary<Identifier, Either<int, float>> values;
public CampaignSettingDefinitions(XElement element)
{
var definitions = new Dictionary<Identifier, Either<int, float>>();
foreach (XAttribute attribute in element.Attributes())
{
Identifier name = attribute.NameAsIdentifier();
if (attribute.Value.Contains('.'))
{
definitions.Add(name, element.GetAttributeFloat(name.Value, 0));
}
else
{
definitions.Add(name, element.GetAttributeInt(name.Value, 0));
}
}
values = definitions.ToImmutableDictionary();
}
public float GetFloat(Identifier identifier)
{
float range = 0;
if (!values.TryGetValue(identifier, out Either<int, float> value) || !value.TryGet(out range))
{
DebugConsole.ThrowError($"CampaignSettings: Can't find value for {identifier}");
}
return range;
}
public int GetInt(Identifier identifier)
{
int integer = 0;
if (!values.TryGetValue(identifier, out Either<int, float> value) || !value.TryGet(out integer))
{
DebugConsole.ThrowError($"CampaignSettings: Can't find value for {identifier}");
}
return integer;
}
}
}
@@ -0,0 +1,114 @@
#nullable enable
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma
{
internal class CampaignSettings : INetSerializableStruct, ISerializableEntity
{
public static CampaignSettings Empty => new CampaignSettings(element: null);
public string Name => "CampaignSettings";
public const string LowerCaseSaveElementName = "campaignsettings";
[Serialize("", IsPropertySaveable.Yes)]
public string PresetName { get; set; } = string.Empty;
[Serialize(false, IsPropertySaveable.Yes), NetworkSerialize]
public bool RadiationEnabled { get; set; }
private int maxMissionCount;
[Serialize(DefaultMaxMissionCount, IsPropertySaveable.Yes), NetworkSerialize(MinValueInt = MinMissionCountLimit, MaxValueInt = MaxMissionCountLimit)]
public int MaxMissionCount
{
get => maxMissionCount;
set => maxMissionCount = MathHelper.Clamp(value, MinMissionCountLimit, MaxMissionCountLimit);
}
public int TotalMaxMissionCount => MaxMissionCount + GetAddedMissionCount();
[Serialize(StartingBalanceAmount.Medium, IsPropertySaveable.Yes), NetworkSerialize]
public StartingBalanceAmount StartingBalanceAmount { get; set; }
[Serialize(GameDifficulty.Medium, IsPropertySaveable.Yes), NetworkSerialize]
public GameDifficulty Difficulty { get; set; }
[Serialize("normal", IsPropertySaveable.Yes), NetworkSerialize]
public Identifier StartItemSet { get; set; }
public int InitialMoney
{
get
{
if (CampaignModePresets.Definitions.TryGetValue(nameof(StartingBalanceAmount).ToIdentifier(), out var definition))
{
return definition.GetInt(StartingBalanceAmount.ToIdentifier());
}
return 8000;
}
}
public float ExtraEventManagerDifficulty
{
get
{
if (CampaignModePresets.Definitions.TryGetValue(nameof(ExtraEventManagerDifficulty).ToIdentifier(), out var definition))
{
return definition.GetFloat(Difficulty.ToIdentifier());
}
return 0;
}
}
public float LevelDifficultyMultiplier
{
get
{
if (CampaignModePresets.Definitions.TryGetValue(nameof(LevelDifficultyMultiplier).ToIdentifier(), out var definition))
{
return definition.GetFloat(Difficulty.ToIdentifier());
}
return 1.0f;
}
}
public const int DefaultMaxMissionCount = 2;
public const int MaxMissionCountLimit = 10;
public const int MinMissionCountLimit = 1;
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
// required for INetSerializableStruct
public CampaignSettings()
{
SerializableProperties = SerializableProperty.GetProperties(this);
}
public CampaignSettings(XElement? element = null)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
public XElement Save()
{
XElement saveElement = new XElement(LowerCaseSaveElementName);
SerializableProperty.SerializeProperties(this, saveElement, saveIfDefault: true);
return saveElement;
}
private static int GetAddedMissionCount()
{
int count = 0;
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
{
count += (int)character.GetStatValue(StatTypes.ExtraMissionCount);
}
return count;
}
}
}
@@ -29,7 +29,11 @@ namespace Barotrauma
: base(preset)
{
Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation };
missions.Add(Mission.LoadRandom(locations, seed, false, missionType));
var mission = Mission.LoadRandom(locations, seed, false, missionType);
if (mission != null)
{
missions.Add(mission);
}
}
protected static IEnumerable<MissionPrefab> ValidateMissionPrefabs(IEnumerable<MissionPrefab> missionPrefabs, Dictionary<MissionType, Type> missionClasses)
@@ -12,19 +12,60 @@ namespace Barotrauma
{
public const int MinimumInitialMoney = 500;
private UInt16 lastUpdateID;
public UInt16 LastUpdateID
[Flags]
public enum NetFlags : UInt16
{
get
{
#if SERVER
if (GameMain.Server != null && lastUpdateID < 1) { lastUpdateID++; }
#endif
return lastUpdateID;
}
set { lastUpdateID = value; }
Misc = 0x1,
MapAndMissions = 0x2,
UpgradeManager = 0x4,
SubList = 0x8,
ItemsInBuyCrate = 0x10,
ItemsInSellFromSubCrate = 0x20,
PurchasedItems = 0x80,
SoldItems = 0x100,
Reputation = 0x200,
CharacterInfo = 0x800
}
private readonly Dictionary<NetFlags, UInt16> lastUpdateID;
public UInt16 GetLastUpdateIdForFlag(NetFlags flag)
{
if (!ValidateFlag(flag)) { return 0; }
return lastUpdateID[flag];
}
public void SetLastUpdateIdForFlag(NetFlags flag, UInt16 id)
{
if (!ValidateFlag(flag)) { return; }
lastUpdateID[flag] = id;
}
public void IncrementLastUpdateIdForFlag(NetFlags flag)
{
if (!ValidateFlag(flag)) { return; }
if (!lastUpdateID.ContainsKey(flag)) { lastUpdateID[flag] = 0; }
lastUpdateID[flag]++;
}
public void IncrementAllLastUpdateIds()
{
foreach (NetFlags flag in Enum.GetValues(typeof(NetFlags)))
{
if (!lastUpdateID.ContainsKey(flag)) { lastUpdateID[flag] = 0; }
lastUpdateID[flag]++;
}
}
private bool ValidateFlag(NetFlags flag)
{
if (MathHelper.IsPowerOfTwo((int)flag)) { return true; }
#if DEBUG
throw new InvalidOperationException($"\"{flag}\" is not a valid campaign update flag.");
#else
return false;
#endif
}
private UInt16 lastSaveID;
public UInt16 LastSaveID
{
@@ -35,11 +76,11 @@ namespace Barotrauma
#endif
return lastSaveID;
}
set
set
{
#if SERVER
//trigger a campaign update to notify the clients of the changed save ID
lastUpdateID++;
IncrementLastUpdateIdForFlag(NetFlags.Misc);
#endif
lastSaveID = value;
}
@@ -52,23 +93,33 @@ namespace Barotrauma
get; set;
}
private MultiPlayerCampaign() : base(GameModePreset.MultiPlayerCampaign)
private MultiPlayerCampaign(CampaignSettings settings) : base(GameModePreset.MultiPlayerCampaign, settings)
{
currentCampaignID++;
lastUpdateID = new Dictionary<NetFlags, ushort>();
foreach (NetFlags flag in Enum.GetValues(typeof(NetFlags)))
{
#if SERVER
//server starts from a higher ID to ensure we send the initial state
lastUpdateID[flag] = 1;
#else
lastUpdateID[flag] = 0;
#endif
}
CampaignID = currentCampaignID;
CampaignMetadata = new CampaignMetadata(this);
UpgradeManager = new UpgradeManager(this);
InitCampaignData();
}
public static MultiPlayerCampaign StartNew(string mapSeed, SubmarineInfo selectedSub, CampaignSettings settings)
public static MultiPlayerCampaign StartNew(string mapSeed, CampaignSettings settings)
{
MultiPlayerCampaign campaign = new MultiPlayerCampaign();
MultiPlayerCampaign campaign = new MultiPlayerCampaign(settings);
//only the server generates the map, the clients load it from a save file
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
campaign.map = new Map(campaign, mapSeed, settings);
campaign.Settings = settings;
campaign.map = new Map(campaign, mapSeed);
}
campaign.InitProjSpecific();
return campaign;
@@ -76,7 +127,7 @@ namespace Barotrauma
public static MultiPlayerCampaign LoadNew(XElement element)
{
MultiPlayerCampaign campaign = new MultiPlayerCampaign();
MultiPlayerCampaign campaign = new MultiPlayerCampaign(CampaignSettings.Empty);
campaign.Load(element);
campaign.InitProjSpecific();
campaign.IsFirstRound = false;
@@ -124,18 +175,17 @@ namespace Barotrauma
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "campaignsettings":
case CampaignSettings.LowerCaseSaveElementName:
Settings = new CampaignSettings(subElement);
#if CLIENT
GameMain.NetworkMember.ServerSettings.MaxMissionCount = Settings.MaxMissionCount;
GameMain.NetworkMember.ServerSettings.RadiationEnabled = Settings.RadiationEnabled;
GameMain.NetworkMember.ServerSettings.CampaignSettings = Settings;
#endif
break;
case "map":
if (map == null)
{
//map not created yet, loading this campaign for the first time
map = Map.Load(this, subElement, Settings);
map = Map.Load(this, subElement);
}
else
{
@@ -155,7 +205,7 @@ namespace Barotrauma
case "bots" when GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer:
CrewManager.HasBots = subElement.GetAttributeBool("hasbots", false);
CrewManager.AddCharacterElements(subElement);
CrewManager.ActiveOrdersElement = subElement.GetChildElement("activeorders");
ActiveOrdersElement = subElement.GetChildElement("activeorders");
break;
case "cargo":
CargoManager?.LoadPurchasedItems(subElement);
@@ -72,7 +72,7 @@ namespace Barotrauma
get
{
if (Map != null) { return Map.CurrentLocation; }
if (dummyLocations == null) { CreateDummyLocations(); }
if (dummyLocations == null) { dummyLocations = CreateDummyLocations(LevelData?.Seed ?? string.Empty); }
if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); }
return dummyLocations[0];
}
@@ -83,7 +83,7 @@ namespace Barotrauma
get
{
if (Map != null) { return Map.SelectedLocation; }
if (dummyLocations == null) { CreateDummyLocations(); }
if (dummyLocations == null) { dummyLocations = CreateDummyLocations(LevelData?.Seed ?? string.Empty); }
if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); }
return dummyLocations[1];
}
@@ -207,7 +207,7 @@ namespace Barotrauma
}
else if (gameModePreset.GameModeType == typeof(MultiPlayerCampaign))
{
var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings);
var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), settings);
if (selectedSub != null)
{
campaign.Bank.Deduct(selectedSub.Price);
@@ -218,7 +218,7 @@ namespace Barotrauma
#if CLIENT
else if (gameModePreset.GameModeType == typeof(SinglePlayerCampaign))
{
var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings);
var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), settings);
if (selectedSub != null)
{
campaign.Bank.TryDeduct(selectedSub.Price);
@@ -245,25 +245,15 @@ namespace Barotrauma
}
}
private void CreateDummyLocations(LocationType? forceLocationType = null)
public static Location[] CreateDummyLocations(string seed, LocationType? forceLocationType = null)
{
dummyLocations = new Location[2];
string seed = "";
if (GameMain.GameSession != null && GameMain.GameSession.Level != null)
{
seed = GameMain.GameSession.Level.Seed;
}
else if (GameMain.NetLobbyScreen != null)
{
seed = GameMain.NetLobbyScreen.LevelSeed;
}
var dummyLocations = new Location[2];
MTRandom rand = new MTRandom(ToolBox.StringToInt(seed));
for (int i = 0; i < 2; i++)
{
dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null, rand, requireOutpost: true, forceLocationType: forceLocationType);
}
return dummyLocations;
}
public void LoadPreviousSave()
@@ -275,7 +265,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, Client? client = null)
public void SwitchSubmarine(SubmarineInfo newSubmarine, bool transferItems, int cost, Client? client = null)
{
if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name))
{
@@ -293,15 +283,13 @@ namespace Barotrauma
}
}
}
if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && cost > 0)
{
Campaign!.TryPurchase(client, cost);
}
GameAnalyticsManager.AddMoneySpentEvent(cost, GameAnalyticsManager.MoneySink.SubmarineSwitch, newSubmarine.Name);
Campaign!.PendingSubmarineSwitch = newSubmarine;
return newSubmarine;
Campaign!.TransferItemsOnSubSwitch = transferItems;
}
public void PurchaseSubmarine(SubmarineInfo newSubmarine, Client? client = null)
@@ -312,6 +300,9 @@ namespace Barotrauma
{
GameAnalyticsManager.AddMoneySpentEvent(newSubmarine.Price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name);
OwnedSubmarines.Add(newSubmarine);
#if SERVER
(Campaign as MultiPlayerCampaign)?.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.SubList);
#endif
}
}
@@ -348,7 +339,7 @@ namespace Barotrauma
!missionPrefab.AllowedConnectionTypes.Any())
{
LocationType? locationType = LocationType.Prefabs.FirstOrDefault(lt => missionPrefab.AllowedLocationTypes.Any(m => m == lt.Identifier));
CreateDummyLocations(locationType);
dummyLocations = CreateDummyLocations(levelSeed, locationType);
randomLevel = LevelData.CreateRandom(levelSeed, difficulty, levelGenerationParams, requireOutpost: true);
break;
}
@@ -433,7 +424,7 @@ namespace Barotrauma
Level? level = null;
if (levelData != null)
{
level = Level.Generate(levelData, mirrorLevel, startOutpost, endOutpost);
level = Level.Generate(levelData, mirrorLevel, StartLocation, EndLocation, startOutpost, endOutpost);
}
InitializeLevel(level);
@@ -602,10 +593,13 @@ namespace Barotrauma
{
//only place items and corpses here in single player
//the server does this after loading the respawn shuttle
Level?.SpawnNPCs();
Level?.SpawnCorpses();
Level?.PrepareBeaconStation();
AutoItemPlacer.PlaceIfNeeded();
if (Level != null)
{
Level.SpawnNPCs();
Level.SpawnCorpses();
Level.PrepareBeaconStation();
}
AutoItemPlacer.SpawnItems(Campaign?.Settings.StartItemSet);
}
if (GameMode is MultiPlayerCampaign mpCampaign)
{
@@ -630,8 +624,6 @@ namespace Barotrauma
return;
}
var originalSubPos = Submarine.WorldPosition;
if (level.StartOutpost != null)
{
//start by placing the sub below the outpost
@@ -714,7 +706,7 @@ namespace Barotrauma
if (!ls.LoadSub || ls.Sub.DockedTo.Contains(Submarine)) { continue; }
if (Submarine.Info.LeftBehindDockingPortIDs.Contains(ls.OriginalLinkedToID)) { continue; }
if (ls.Sub.Info.SubmarineElement.Attribute("location") != null) { continue; }
ls.Sub.SetPosition(ls.Sub.WorldPosition + (Submarine.WorldPosition - originalSubPos));
ls.SetPositionRelativeToMainSub();
}
}
@@ -846,6 +838,11 @@ namespace Barotrauma
{
GUI.TogglePauseMenu();
}
if (IsTabMenuOpen)
{
ToggleTabMenu();
}
GUI.PreventPauseMenuToggle = true;
if (!(GameMode is TestGameMode) && Screen.Selected == GameMain.GameScreen && RoundSummary != null)
@@ -1082,8 +1079,16 @@ namespace Barotrauma
rootElement.Add(new XAttribute("savetime", ToolBox.Epoch.NowLocal));
rootElement.Add(new XAttribute("version", GameMain.Version));
var submarineInfo = Campaign?.PendingSubmarineSwitch ?? SubmarineInfo;
rootElement.Add(new XAttribute("submarine", submarineInfo == null ? "" : submarineInfo.Name));
if (Submarine?.Info != null && !Submarine.Removed && Campaign != null)
{
bool hasNewPendingSub = Campaign.PendingSubmarineSwitch != null &&
Campaign.PendingSubmarineSwitch.MD5Hash.StringRepresentation != Submarine.Info.MD5Hash.StringRepresentation;
if (hasNewPendingSub)
{
Campaign.SwitchSubs();
}
}
rootElement.Add(new XAttribute("submarine", SubmarineInfo == null ? "" : SubmarineInfo.Name));
if (OwnedSubmarines != null)
{
List<string> ownedSubmarineNames = new List<string>();
@@ -13,12 +13,26 @@ namespace Barotrauma
internal partial class ReadyCheck
{
private readonly float endTime;
private float time;
private readonly DateTime endTime;
private readonly DateTime startTime;
public readonly Dictionary<byte, ReadyStatus> Clients;
public bool IsFinished = false;
public ReadyCheck(List<byte> clients, float duration = 30)
public ReadyCheck(List<byte> clients, DateTime startTime, DateTime endTime)
: this(clients)
{
this.startTime = startTime;
this.endTime = endTime;
}
public ReadyCheck(List<byte> clients, float duration)
: this(clients)
{
startTime = DateTime.Now;
endTime = startTime + new TimeSpan(0, 0, 0, 0, (int)(duration * 1000));
}
private ReadyCheck(List<byte> clients)
{
Clients = new Dictionary<byte, ReadyStatus>();
foreach (byte client in clients)
@@ -27,24 +41,17 @@ namespace Barotrauma
Clients.Add(client, ReadyStatus.Unanswered);
}
time = duration;
endTime = duration;
#if CLIENT
lastSecond = (int) Math.Ceiling(duration);
#endif
}
partial void EndReadyCheck();
public void Update(float deltaTime)
{
if (time > 0)
if (DateTime.Now < endTime)
{
#if CLIENT
UpdateBar();
#endif
time -= deltaTime;
return;
}