using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; namespace Barotrauma { partial class CargoMission : Mission { private readonly ContentXElement itemConfig; private readonly List items = new List(); private readonly Dictionary parentInventoryIDs = new Dictionary(); private readonly Dictionary inventorySlotIndices = new Dictionary(); private readonly Dictionary parentItemContainerIndices = new Dictionary(); /// /// Percentage of items (0.0 - 1.0) needed to be delivered to complete the mission. /// private float requiredDeliveryAmount; private readonly List<(ContentXElement element, ItemContainer container)> itemsToSpawn = new List<(ContentXElement element, ItemContainer container)>(); private int? rewardPerCrate; private int calculatedReward; private int maxItemCount; private Submarine currentSub; private SubmarineInfo nextRoundSubInfo; private readonly List previouslySelectedMissions = new List(); public override LocalizedString Description { get { if ((GameMain.GameSession?.Campaign?.PendingSubmarineSwitch ?? Submarine.MainSub?.Info) != nextRoundSubInfo) { string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(Submarine.MainSub))}‖end‖"; if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); } } return description; } } public CargoMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { this.currentSub = sub; this.nextRoundSubInfo = sub?.Info; itemConfig = prefab.ConfigElement.GetChildElement("Items"); requiredDeliveryAmount = Math.Min(prefab.ConfigElement.GetAttributeFloat("requireddeliveryamount", 0.98f), 1.0f); //this can get called between rounds when the client receives a campaign save //don't attempt to determine cargo if the sub hasn't been fully loaded if (sub == null || sub.Loading || sub.Removed || Submarine.Unloading || !Submarine.Loaded.Contains(sub)) { return; } DetermineCargo(); } private void DetermineCargo() { if (this.currentSub == null || itemConfig == null) { calculatedReward = Prefab.Reward; return; } itemsToSpawn.Clear(); maxItemCount = 0; foreach (var subElement in itemConfig.Elements()) { int maxCount = subElement.GetAttributeInt("maxcount", 10); maxItemCount += maxCount; } var pendingSubInfo = GameMain.GameSession?.Campaign?.PendingSubmarineSwitch; if (pendingSubInfo != null && pendingSubInfo != currentSub.Info) { //if we've got a submarine switch pending, calculate the amount of cargo based on it's cargo capacity //TODO: this isn't guaranteed to be accurate, because we don't take existing items in the new sub's cargo containers //or items that might get transferred in them into account maxItemCount = Math.Min(maxItemCount, pendingSubInfo.CargoCapacity); previouslySelectedMissions.Clear(); if (GameMain.GameSession?.StartLocation?.SelectedMissions != null) { bool isPriorMission = true; foreach (Mission mission in GameMain.GameSession.StartLocation.SelectedMissions) { if (mission is not CargoMission otherMission) { continue; } if (mission == this) { isPriorMission = false; } previouslySelectedMissions.Add(otherMission); if (!isPriorMission) { continue; } maxItemCount -= otherMission.itemsToSpawn.Count; } } for (int i = 0; i < maxItemCount; i++) { foreach (var subElement in itemConfig.Elements()) { int maxCount = subElement.GetAttributeInt("maxcount", 10); if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { continue; } // For logging purposes FindItemPrefab(subElement); while (itemsToSpawn.Count < maxItemCount) { itemsToSpawn.Add((subElement, null)); if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { break; } } } } maxItemCount = Math.Max(0, maxItemCount); nextRoundSubInfo = pendingSubInfo; } else { List<(ItemContainer container, int freeSlots)> containers = currentSub.GetCargoContainers(); containers.Sort((c1, c2) => { return c2.container.Capacity.CompareTo(c1.container.Capacity); }); previouslySelectedMissions.Clear(); if (GameMain.GameSession?.StartLocation?.SelectedMissions != null) { bool isPriorMission = true; foreach (Mission mission in GameMain.GameSession.StartLocation.SelectedMissions) { if (mission is not CargoMission otherMission) { continue; } if (mission == this) { isPriorMission = false; } previouslySelectedMissions.Add(otherMission); if (!isPriorMission) { continue; } foreach (var (element, container) in otherMission.itemsToSpawn) { for (int i = 0; i < containers.Count; i++) { if (containers[i].container == container) { containers[i] = (containers[i].container, containers[i].freeSlots - 1); break; } } } } } for (int i = 0; i < containers.Count; i++) { foreach (var subElement in itemConfig.Elements()) { int maxCount = subElement.GetAttributeInt("maxcount", 10); if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { continue; } ItemPrefab itemPrefab = FindItemPrefab(subElement); while (containers[i].freeSlots > 0 && containers[i].container.Inventory.CanBePut(itemPrefab)) { containers[i] = (containers[i].container, containers[i].freeSlots - 1); itemsToSpawn.Add((subElement, containers[i].container)); if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { break; } } } } } if (!itemsToSpawn.Any()) { itemsToSpawn.Add((itemConfig.Elements().First(), null)); } // Calculate the current total reward, since it might differ from the // prefab total reward depending on the current actual crate count. calculatedReward = 0; bool crateValuesUniform = true; int? prevCrateReward = null; foreach (var (element, container) in itemsToSpawn) { int currentCrateReward = element.GetAttributeInt("reward", 0); calculatedReward += currentCrateReward; // Apparently crates can have varying values, so we need to check // here if that is the case, stopping checks on the first discrepancy if (crateValuesUniform) { if (prevCrateReward.HasValue) { if (prevCrateReward.Value != currentCrateReward) { crateValuesUniform = false; } } prevCrateReward = currentCrateReward; } } if (crateValuesUniform) { // If rewardPerCrate is set, it will be displayed in the client UI as eg. "123 mk x 5" rewardPerCrate = calculatedReward / itemsToSpawn.Count; } else { // If rewardPerCrate is null, the client UI will display just the total reward rewardPerCrate = null; } // Apply the mission reward campaign setting multiplier to the per-crate price, too if (GameMain.GameSession?.Campaign is CampaignMode campaign && rewardPerCrate is int confirmedRewardPerCrate) { rewardPerCrate = (int)Math.Round(confirmedRewardPerCrate * campaign.Settings.MissionRewardMultiplier); } string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(currentSub))}‖end‖"; if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); } } public override int GetBaseReward(Submarine sub) { // If we are not at the location of the mission, skip the calculation of the reward if (GameMain.GameSession?.StartLocation != Locations[0]) { return calculatedReward; } bool missionsChanged = false; if (GameMain.GameSession?.StartLocation?.SelectedMissions != null) { List currentMissions = GameMain.GameSession.StartLocation.SelectedMissions.Where(m => m is CargoMission).ToList(); if (currentMissions.Count != previouslySelectedMissions.Count) { missionsChanged = true; } else { for (int i = 0; i < previouslySelectedMissions.Count; i++) { if (previouslySelectedMissions[i] != currentMissions[i]) { missionsChanged = true; break; } } } } var pendingSubInfo = GameMain.GameSession?.Campaign?.PendingSubmarineSwitch; if (pendingSubInfo != null && nextRoundSubInfo != pendingSubInfo) { this.nextRoundSubInfo = pendingSubInfo; DetermineCargo(); } else if (sub != this.currentSub || missionsChanged) { this.currentSub = sub; this.nextRoundSubInfo = sub?.Info; DetermineCargo(); } return calculatedReward; } private void InitItems() { this.currentSub = Submarine.MainSub; DetermineCargo(); items.Clear(); parentInventoryIDs.Clear(); parentItemContainerIndices.Clear(); inventorySlotIndices.Clear(); if (itemConfig == null) { DebugConsole.ThrowError("Failed to initialize items for cargo mission (itemConfig == null)", contentPackage: Prefab.ContentPackage); return; } foreach (var (element, container) in itemsToSpawn) { LoadItemAsChild(element, container?.Item); } if (requiredDeliveryAmount <= 0.0f) { requiredDeliveryAmount = 1.0f; } } private void LoadItemAsChild(ContentXElement element, Item parent) { ItemPrefab itemPrefab = FindItemPrefab(element); Vector2? position = GetCargoSpawnPosition(itemPrefab, out Submarine cargoRoomSub); if (!position.HasValue) { return; } var item = new Item(itemPrefab, position.Value, cargoRoomSub) { SpawnedInCurrentOutpost = true, AllowStealing = false }; item.AddTag(Tags.CargoMissionItem); item.AddTag(Prefab.Identifier); foreach (var tag in Prefab.Tags) { item.AddTag(tag); } item.FindHull(); items.Add(item); if (parent?.GetComponent() != null) { parentInventoryIDs.Add(item, parent.ID); parentItemContainerIndices.Add(item, (byte)parent.GetComponentIndex(parent.GetComponent())); parent.Combine(item, user: null); inventorySlotIndices.Add(item, item.ParentInventory?.FindIndex(item) ?? -1); } foreach (var subElement in element.Elements()) { int amount = subElement.GetAttributeInt("amount", 1); for (int i = 0; i < amount; i++) { LoadItemAsChild(subElement, item); } } } protected override void StartMissionSpecific(Level level) { items.Clear(); parentInventoryIDs.Clear(); if (!IsClient) { InitItems(); } } protected override bool DetermineCompleted() { if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit) { int deliveredItemCount = items.Count(it => IsItemDelivered(it)); if (deliveredItemCount / (float)items.Count >= requiredDeliveryAmount) { return true; } } return false; } protected override void EndMissionSpecific(bool completed) { foreach (Item item in items) { if (!item.Removed) { item.Remove(); } } items.Clear(); failed = !completed; } private static bool IsItemDelivered(Item item) { if (item.Removed || item.Condition <= 0.0f || Submarine.MainSub == null) { return false; } var submarine = item.Submarine ?? item.RootContainer?.Submarine; return submarine == Submarine.MainSub || Submarine.MainSub.GetConnectedSubs().Contains(submarine); } } }