Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs
Juan Pablo Arce 4206f6db42 Unstable 0.17.3.0
2022-03-22 14:44:56 -03:00

539 lines
21 KiB
C#

using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Networking;
#if SERVER
using Barotrauma.Networking;
#endif
namespace Barotrauma
{
class PurchasedItem
{
public ItemPrefab ItemPrefab { get; }
public int Quantity { get; set; }
public bool? IsStoreComponentEnabled { get; set; }
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;
}
}
class SoldItem
{
public ItemPrefab ItemPrefab { get; }
public ushort ID { get; private set; }
public bool Removed { get; set; }
public byte SellerID { get; }
public SellOrigin Origin { get; }
public enum SellOrigin
{
Character,
Submarine
}
public SoldItem(ItemPrefab itemPrefab, ushort id, bool removed, byte sellerId, SellOrigin origin)
{
ItemPrefab = itemPrefab;
ID = id;
Removed = removed;
SellerID = sellerId;
Origin = origin;
}
public void SetItemId(ushort id)
{
if (ID != Entity.NullEntityID)
{
DebugConsole.ShowError("Error setting SoldItem.ID: ID has already been set and should not be changed.");
return;
}
ID = id;
}
}
partial class CargoManager
{
private class SoldEntity
{
public enum SellStatus
{
/// <summary>
/// Entity sold in SP. Or, entity sold by client and confirmed by server in MP.
/// </summary>
Confirmed,
/// <summary>
/// Entity sold by client in MP. Client has received at least one update from server after selling, but this entity wasn't yet confirmed.
/// </summary>
Unconfirmed,
/// <summary>
/// Entity sold by client in MP. Client hasn't yet received an update from server after selling.
/// </summary>
Local
}
public Item Item { get; private set; }
public ItemPrefab ItemPrefab { get; }
public SellStatus Status { get; set; }
public SoldEntity(Item item, SellStatus status)
{
Item = item;
ItemPrefab = item?.Prefab;
Status = status;
}
public SoldEntity(ItemPrefab itemPrefab, SellStatus status)
{
ItemPrefab = itemPrefab;
Status = status;
}
public void SetItem(Item item)
{
if (Item != null)
{
DebugConsole.ShowError($"Trying to set SoldEntity.Item, but it's already set!\n{Environment.StackTrace.CleanupStackTrace()}");
return;
}
Item = item;
}
}
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>();
private readonly CampaignMode campaign;
private Location Location => campaign?.Map?.CurrentLocation;
public Action OnItemsInBuyCrateChanged;
public Action OnItemsInSellCrateChanged;
public Action OnItemsInSellFromSubCrateChanged;
public Action OnPurchasedItemsChanged;
public Action OnSoldItemsChanged;
public CargoManager(CampaignMode campaign)
{
this.campaign = campaign;
}
public void ClearItemsInBuyCrate()
{
ItemsInBuyCrate.Clear();
OnItemsInBuyCrateChanged?.Invoke();
}
public void ClearItemsInSellCrate()
{
ItemsInSellCrate.Clear();
OnItemsInSellCrateChanged?.Invoke();
}
public void ClearItemsInSellFromSubCrate()
{
ItemsInSellFromSubCrate.Clear();
OnItemsInSellFromSubCrateChanged?.Invoke();
}
public void SetPurchasedItems(List<PurchasedItem> items)
{
PurchasedItems.Clear();
PurchasedItems.AddRange(items);
OnPurchasedItemsChanged?.Invoke();
}
public void ModifyItemQuantityInBuyCrate(ItemPrefab itemPrefab, int changeInQuantity, Client client = null)
{
var itemInCrate = ItemsInBuyCrate.Find(i => i.ItemPrefab == itemPrefab);
if (itemInCrate != null)
{
itemInCrate.Quantity += changeInQuantity;
if (itemInCrate.Quantity < 1)
{
ItemsInBuyCrate.Remove(itemInCrate);
}
}
else if (changeInQuantity > 0)
{
itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity, client);
ItemsInBuyCrate.Add(itemInCrate);
}
OnItemsInBuyCrateChanged?.Invoke();
}
public void ModifyItemQuantityInSubSellCrate(ItemPrefab itemPrefab, int changeInQuantity, Client client = null)
{
var itemInCrate = ItemsInSellFromSubCrate.Find(i => i.ItemPrefab == itemPrefab);
if (itemInCrate != null)
{
itemInCrate.Quantity += changeInQuantity;
if (itemInCrate.Quantity < 1)
{
ItemsInSellFromSubCrate.Remove(itemInCrate);
}
}
else if (changeInQuantity > 0)
{
itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity, client);
ItemsInSellFromSubCrate.Add(itemInCrate);
}
OnItemsInSellFromSubCrateChanged?.Invoke();
}
public void PurchaseItems(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));
foreach (PurchasedItem item in itemsToPurchase)
{
// Add to the purchased items
var purchasedItem = PurchasedItems.Find(pi => pi.ItemPrefab == item.ItemPrefab);
if (purchasedItem != null)
{
purchasedItem.Quantity += item.Quantity;
}
else
{
purchasedItem = new PurchasedItem(item.ItemPrefab, item.Quantity, client);
PurchasedItems.Add(purchasedItem);
}
// Exchange money
var itemValue = item.Quantity * buyValues[item.ItemPrefab];
campaign.GetWallet(client).TryDeduct(itemValue);
GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value);
Location.StoreCurrentBalance += itemValue;
if (removeFromCrate)
{
// Remove from the shopping crate
var crateItem = ItemsInBuyCrate.Find(pi => pi.ItemPrefab == item.ItemPrefab);
if (crateItem != null)
{
crateItem.Quantity -= item.Quantity;
if (crateItem.Quantity < 1) { ItemsInBuyCrate.Remove(crateItem); }
}
}
}
OnPurchasedItemsChanged?.Invoke();
}
public Dictionary<ItemPrefab, int> GetBuyValuesAtCurrentLocation(IEnumerable<ItemPrefab> items)
{
var buyValues = new Dictionary<ItemPrefab, int>();
foreach (var item in items)
{
if (item == null) { continue; }
if (!buyValues.ContainsKey(item))
{
var buyValue = Location?.GetAdjustedItemBuyPrice(item) ?? 0;
buyValues.Add(item, buyValue);
}
}
return buyValues;
}
public Dictionary<ItemPrefab, int> GetSellValuesAtCurrentLocation(IEnumerable<ItemPrefab> items)
{
var sellValues = new Dictionary<ItemPrefab, int>();
foreach (var item in items)
{
if (item == null) { continue; }
if (!sellValues.ContainsKey(item))
{
var sellValue = Location?.GetAdjustedItemSellPrice(item) ?? 0;
sellValues.Add(item, sellValue);
}
}
return sellValues;
}
public void CreatePurchasedItems()
{
CreateItems(PurchasedItems, Submarine.MainSub);
OnPurchasedItemsChanged?.Invoke();
}
private Dictionary<ItemPrefab, int> UndeterminedSoldEntities { get; } = new Dictionary<ItemPrefab, int>();
public IEnumerable<Item> GetSellableItemsFromSub()
{
if (Submarine.MainSub == null) { return new List<Item>(); }
var confirmedSoldEntities = Enumerable.Empty<SoldEntity>();
UndeterminedSoldEntities.Clear();
#if CLIENT
confirmedSoldEntities = GetConfirmedSoldEntities();
foreach (var soldEntity in SoldEntities)
{
if (soldEntity.Item != null) { continue; }
if (UndeterminedSoldEntities.TryGetValue(soldEntity.ItemPrefab, out int count))
{
UndeterminedSoldEntities[soldEntity.ItemPrefab] = count + 1;
}
else
{
UndeterminedSoldEntities.Add(soldEntity.ItemPrefab, 1);
}
}
#endif
return Submarine.MainSub.GetItems(true).FindAll(item =>
{
if (!IsItemSellable(item, confirmedSoldEntities)) { return false; }
if (item.GetRootInventoryOwner() is Character) { return false; }
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; }
return true;
}).Distinct();
static bool ItemAndAllContainersInteractable(Item item)
{
do
{
if (!item.IsPlayerTeamInteractable) { return false; }
item = item.Container;
} while (item != null);
return true;
}
}
private bool IsItemSellable(Item item, IEnumerable<SoldEntity> confirmedItems)
{
if (item.Removed) { return false; }
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 (UndeterminedSoldEntities.TryGetValue(item.Prefab, out int count))
{
int newCount = count - 1;
if (newCount > 0)
{
UndeterminedSoldEntities[item.Prefab] = newCount;
}
else
{
UndeterminedSoldEntities.Remove(item.Prefab);
}
return false;
}
if (item.OwnInventory?.Container is ItemContainer itemContainer)
{
var containedItems = item.ContainedItems;
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; }
}
return true;
}
public static void CreateItems(List<PurchasedItem> itemsToSpawn, Submarine sub)
{
if (itemsToSpawn.Count == 0) { return; }
WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, sub);
if (wp == null)
{
DebugConsole.ThrowError("The submarine must have a waypoint marked as Cargo for bought items to be placed correctly!");
return;
}
Hull cargoRoom = Hull.FindHull(wp.WorldPosition);
if (cargoRoom == null)
{
DebugConsole.ThrowError("A waypoint marked as Cargo must be placed inside a room!");
return;
}
if (sub == Submarine.MainSub)
{
#if CLIENT
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)
{
ChatMessage msg = ChatMessage.Create("",
TextManager.ContainsTag(cargoRoom.RoomName) ? $"CargoSpawnNotification~[roomname]=§{cargoRoom.RoomName}" : $"CargoSpawnNotification~[roomname]={cargoRoom.RoomName}",
ChatMessageType.ServerMessageBoxInGame, null);
msg.IconStyle = "StoreShoppingCrateIcon";
GameMain.Server.SendDirectChatMessage(msg, client);
}
#endif
}
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);
#if SERVER
Entity.Spawner?.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
#endif
(itemContainer?.Item ?? item).CampaignInteractionType = CampaignMode.InteractionType.Cargo;
static void itemSpawned(Item item)
{
Submarine sub = item.Submarine ?? item.GetRootContainer()?.Submarine;
if (sub != null)
{
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
{
wifiComponent.TeamID = sub.TeamID;
}
}
}
}
}
itemsToSpawn.Clear();
}
public static Vector2 GetCargoPos(Hull hull, ItemPrefab itemPrefab)
{
float floorPos = hull.Rect.Y - hull.Rect.Height;
Vector2 position = new Vector2(
hull.Rect.Width > 40 ? Rand.Range(hull.Rect.X + 20f, hull.Rect.Right - 20f) : hull.Rect.Center.X,
floorPos);
//check where the actual floor structure is in case the bottom of the hull extends below it
if (Submarine.PickBody(
ConvertUnits.ToSimUnits(new Vector2(position.X, hull.Rect.Y - hull.Rect.Height / 2)),
ConvertUnits.ToSimUnits(position),
collisionCategory: Physics.CollisionWall) != null)
{
float floorStructurePos = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition.Y);
if (floorStructurePos > floorPos)
{
floorPos = floorStructurePos;
}
}
position.Y = floorPos + itemPrefab.Size.Y / 2;
return position;
}
public void SavePurchasedItems(XElement parentElement)
{
var itemsElement = new XElement("cargo");
foreach (PurchasedItem item in PurchasedItems)
{
if (item?.ItemPrefab == null) { continue; }
itemsElement.Add(new XElement("item",
new XAttribute("id", item.ItemPrefab.Identifier),
new XAttribute("qty", item.Quantity),
new XAttribute("buyer", item.BuyerCharacterInfoId)));
}
parentElement.Add(itemsElement);
}
public void LoadPurchasedItems(XElement element)
{
var purchasedItems = new List<PurchasedItem>();
if (element != null)
{
foreach (XElement itemElement in element.GetChildElements("item"))
{
string id = itemElement.GetAttributeString("id", null);
if (string.IsNullOrWhiteSpace(id)) { continue; }
var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == id);
if (prefab == null) { continue; }
int qty = itemElement.GetAttributeInt("qty", 0);
int buyerId = itemElement.GetAttributeInt("buyer", 0);
purchasedItems.Add(new PurchasedItem(prefab, qty, buyerId));
}
}
SetPurchasedItems(purchasedItems);
}
}
}