using Barotrauma; using Barotrauma.Abilities; using Barotrauma.Extensions; using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; using MoreLevelContent.Shared; using MoreLevelContent.Shared.Utils; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Xml.Linq; namespace MoreLevelContent.Items { // Shared internal partial class SimpleStore : Powered, IServerSerializable, IClientSerializable { private ImmutableDictionary fabricationRecipes; //this is not readonly because tutorials fuck this up!!!! private const int MaxAmountToFabricate = 99; private FabricationRecipe fabricatedItem; private float timeUntilReady; private float requiredTime; private string savedFabricatedItem; private float savedTimeUntilReady, savedRequiredTime; private readonly Dictionary> availableIngredients = new Dictionary>(); const float RefreshIngredientsInterval = 1.0f; private float refreshIngredientsTimer; private bool hasPower; private Character user; private ItemContainer inputContainer, outputContainer; [Editable(MinValueFloat = 0.1f, MaxValueFloat = 1000), Serialize(1.0f, IsPropertySaveable.Yes)] public float FabricationSpeed { get; set; } [Serialize(1.0f, IsPropertySaveable.Yes)] public float SkillRequirementMultiplier { get; set; } private int amountToFabricate; [Serialize(1, IsPropertySaveable.Yes)] public int AmountToFabricate { get { return amountToFabricate; } set { amountToFabricate = MathHelper.Clamp(value, 1, MaxAmountToFabricate); } } private int amountRemaining; private const float TinkeringSpeedIncrease = 2.5f; private enum SimpleStoreState { Active = 1, Paused = 2, Stopped = 0 } private SimpleStoreState state; private SimpleStoreState State { get { return state; } set { if (state == value) { return; } state = value; #if SERVER serverEventId++; item.CreateServerEvent(this); #endif } } public ItemContainer InputContainer { get { return inputContainer; } } public ItemContainer OutputContainer { get { return outputContainer; } } private float progressState; private readonly Dictionary fabricationLimits = new Dictionary(); public Action OnItemFabricated; public SimpleStore(Item item, ContentXElement element) : base(item, element) { var fabricationRecipes = new List<(uint id, FabricationRecipe fabricationRecipe)>(); int maxSellables = element.GetAttributeInt("maxsellables", 8); foreach (var subElement in element.GetChildElements("sellableitem")) { var itemToFab = subElement.GetAttributeIdentifier("identifier", ""); if (itemToFab.IsEmpty) { Log.Debug("Sellable item has no identifier"); continue; } FabricationRecipe recipe = new FabricationRecipe(subElement, itemToFab); fabricationRecipes.Add((recipe.RecipeHash, recipe)); if (recipe.FabricationLimitMax >= 0) { fabricationLimits.Add(recipe.RecipeHash, Rand.Range(recipe.FabricationLimitMin, recipe.FabricationLimitMax + 1)); } } foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) { foreach (FabricationRecipe recipe in itemPrefab.FabricationRecipes.Values) { // Skip items that don't get fabb'd here if (recipe.SuitableFabricatorIdentifiers.Length > 0) { if (!recipe.SuitableFabricatorIdentifiers.Any(i => item.Prefab.Identifier == i || item.HasTag(i))) { continue; } } //the errors below may be caused by a mod overriding a base item instead of this one, log the package of the base item in that case var packageToLog = itemPrefab.GetParentModPackageOrThisPackage(); bool recipeInvalid = false; foreach (var requiredItem in recipe.RequiredItems) { if (requiredItem.ItemPrefabs.None()) { DebugConsole.ThrowError($"Error in the fabrication recipe for \"{itemPrefab.Name}\". Could not find the ingredient \"{requiredItem}\".", contentPackage: packageToLog); recipeInvalid = true; } } if (recipeInvalid) { continue; } // if (fabricationRecipes.TryGetValue(recipe.RecipeHash, out var duplicateRecipe)) // { // DebugConsole.ThrowError($"Error in the fabrication recipe for \"{itemPrefab.Name}\". Duplicate recipe in \"{duplicateRecipe.TargetItem.Identifier}\".", // contentPackage: packageToLog); // continue; // } fabricationRecipes.Add((recipe.RecipeHash, recipe)); if (recipe.FabricationLimitMax >= 0) { fabricationLimits.Add(recipe.RecipeHash, Rand.Range(recipe.FabricationLimitMin, recipe.FabricationLimitMax + 1)); } } } // When loaded into a level, remove some of the items until we only have if (Level.Loaded != null) { Random rnd = MLCUtils.GetLevelRandom(); fabricationRecipes = fabricationRecipes.OrderBy(r => r.id).ToList(); while (fabricationRecipes.Count > maxSellables) { _ = fabricationRecipes.Remove(fabricationRecipes.GetRandom(rnd)); } } this.fabricationRecipes = fabricationRecipes.ToImmutableDictionary(); state = SimpleStoreState.Stopped; } public override void OnItemLoaded() { base.OnItemLoaded(); var containers = item.GetComponents().ToList(); if (containers.Count < 2) { DebugConsole.ThrowError("Error in item \"" + item.Name + "\": Fabricators must have two ItemContainer components!"); return; } inputContainer = containers[0]; outputContainer = containers[1]; foreach (var recipe in fabricationRecipes.Values) { if (recipe.RequiredItems.Length > inputContainer.Capacity) { DebugConsole.ThrowErrorLocalized("Error in item \"" + item.Name + "\": There's not enough room in the input inventory for the ingredients of \"" + recipe.TargetItem.Name + "\"!"); } } OnItemLoadedProjSpecific(); } partial void OnItemLoadedProjSpecific(); public override bool Select(Character character) { SelectProjSpecific(character); return base.Select(character); } partial void SelectProjSpecific(Character character); public override bool Pick(Character picker) { return picker != null; } partial void CreateRecipes(); private void StartFabricating(FabricationRecipe selectedItem, Character user, bool addToServerLog = true) { if (selectedItem == null) { return; } if (!outputContainer.Inventory.CanProbablyBePut(selectedItem.TargetItem, selectedItem.OutCondition * selectedItem.TargetItem.Health)) { return; } IsActive = true; this.user = user; fabricatedItem = selectedItem; RefreshAvailableIngredients(); #if CLIENT itemList.Enabled = false; if (amountInput != null) { amountInput.Enabled = false; } RefreshActivateButtonText(); #endif bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; if (!isClient) { MoveIngredientsToInputContainer(selectedItem); } requiredTime = GetRequiredTime(fabricatedItem, user); timeUntilReady = requiredTime; inputContainer.Inventory.Locked = true; outputContainer.Inventory.Locked = true; if (GameMain.NetworkMember?.IsServer ?? true) { State = SimpleStoreState.Active; } #if SERVER if (user != null && addToServerLog && selectedItem.RequiredMoney == 0) { if (selectedItem.RequiredMoney > 0) { GameServer.Log($"{GameServer.CharacterLogName(user)} bought {selectedItem.DisplayName.Value} for {selectedItem.RequiredMoney} mk from {item.Name}", ServerLog.MessageType.Money); } } #endif } private void CancelFabricating(Character user = null) { IsActive = false; this.user = null; currPowerConsumption = 0.0f; hasSoundPlayed = false; progressState = 0.0f; timeUntilReady = 0.0f; UpdateRequiredTimeProjSpecific(); inputContainer.Inventory.Locked = false; outputContainer.Inventory.Locked = false; if (GameMain.NetworkMember?.IsServer ?? true) { State = SimpleStoreState.Stopped; } if (fabricatedItem == null) { return; } #if SERVER if (user != null) { GameServer.Log(GameServer.CharacterLogName(user) + " cancelled the fabrication of " + fabricatedItem.DisplayName.Value + " in " + item.Name, ServerLog.MessageType.ItemInteraction); } #elif CLIENT itemList.Enabled = true; if (amountInput != null) { amountInput.Enabled = amountTextMax.Enabled; } RefreshActivateButtonText(); #endif fabricatedItem = null; } public override void Update(float deltaTime, Camera cam) { if (refreshIngredientsTimer <= 0.0f) { RefreshAvailableIngredients(); refreshIngredientsTimer = RefreshIngredientsInterval; } refreshIngredientsTimer -= deltaTime; bool isClient = GameMain.NetworkMember?.IsClient ?? false; if (!isClient) { if (fabricatedItem == null || !CanBeFabricated(fabricatedItem, availableIngredients, user)) { CancelFabricating(); return; } } progressState = fabricatedItem == null ? 0.0f : (requiredTime - timeUntilReady) / requiredTime; if (isClient) { hasPower = State != SimpleStoreState.Paused; if (!hasPower) { return; } } else { hasPower = Voltage >= MinVoltage; if (!hasPower) { State = SimpleStoreState.Paused; return; } State = SimpleStoreState.Active; } float tinkeringStrength = 0f; var repairable = item.GetComponent(); if (repairable != null) { repairable.LastActiveTime = (float)Timing.TotalTime + 10.0f; if (repairable.IsTinkering) { tinkeringStrength = repairable.TinkeringStrength; } } float fabricationSpeedIncrease = 1f + tinkeringStrength * TinkeringSpeedIncrease; timeUntilReady -= deltaTime * fabricationSpeedIncrease * Math.Min(powerConsumption <= 0 ? 1 : Voltage, MaxOverVoltageFactor); UpdateRequiredTimeProjSpecific(); if (timeUntilReady <= 0.0f) { Fabricate(); #if CLIENT if (!hasSoundPlayed) { PlaySound(ActionType.OnUse, user); hasSoundPlayed = true; } #endif } } bool hasSoundPlayed = false; private Barotrauma.Networking.Client GetUsingClient() { #if SERVER return GameMain.Server.ConnectedClients.Find(c => c.Character == user); #elif CLIENT return null; #endif } private void Fabricate() { RefreshAvailableIngredients(); if (fabricatedItem == null || !CanBeFabricated(fabricatedItem, availableIngredients, user)) { CancelFabricating(); return; } if (fabricatedItem.RequiredMoney > 0) { if (user == null) { return; } if (GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign) { #if SERVER if (GetUsingClient() is { } client) { mpCampaign.TryPurchase(client, fabricatedItem.RequiredMoney); } else { user.Wallet.Deduct(fabricatedItem.RequiredMoney); } #endif } else if (GameMain.GameSession?.GameMode is CampaignMode campaign) { campaign.Bank.Deduct(fabricatedItem.RequiredMoney); } } bool ingredientsStolen = false; bool ingredientsAllowStealing = true; if (GameMain.NetworkMember is null || GameMain.NetworkMember.IsServer) { List chosenIngredients = new List(); var suitableIngredients = GetSortedSuitableIngredients(); foreach (var requiredItem in fabricatedItem.RequiredItems) { for (int i = 0; i < requiredItem.Amount; i++) { foreach (var suitableIngredient in suitableIngredients) { if (!requiredItem.MatchesItem(suitableIngredient)) { continue; } if (chosenIngredients.Contains(suitableIngredient)) { continue; } ingredientsStolen |= suitableIngredient.StolenDuringRound; if (!suitableIngredient.AllowStealing) { ingredientsAllowStealing = false; } //Leave it behind with reduced condition if it has enough to stay above 0 if (requiredItem.UseCondition && suitableIngredient.ConditionPercentage - requiredItem.MinCondition * 100 > 0.0f) { suitableIngredient.Condition -= suitableIngredient.Prefab.Health * requiredItem.MinCondition; break; } if (suitableIngredient.OwnInventory != null) { foreach (Item containedItem in suitableIngredient.OwnInventory.AllItemsMod) { if (suitableIngredient.GetComponent()?.RemoveContainedItemsOnDeconstruct ?? false) { Entity.Spawner.AddItemToRemoveQueue(containedItem); } else { containedItem.Drop(dropper: null); } } } chosenIngredients.Add(suitableIngredient); break; } } } var fabricationIngredients = new AbilityFabricationItemIngredients(chosenIngredients); user?.CheckTalents(AbilityEffectType.OnItemFabricatedIngredients, fabricationIngredients); foreach (Item availableItem in fabricationIngredients.Items) { Entity.Spawner.AddItemToRemoveQueue(availableItem); inputContainer.Inventory.RemoveItem(availableItem); } int amountFittingContainer = outputContainer.Inventory.HowManyCanBePut(fabricatedItem.TargetItem, fabricatedItem.OutCondition * fabricatedItem.TargetItem.Health); var fabricationitemAmount = new AbilityFabricationItemAmount(fabricatedItem.TargetItem, fabricatedItem.Amount); int quality = 0; if (fabricatedItem.Quality.HasValue) { quality = fabricatedItem.Quality.Value; } else if (user?.Info != null) { foreach (Character character in Character.GetFriendlyCrew(user)) { character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationitemAmount); } user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationitemAmount); quality = fabricatedItem.TargetItem.MaxStackSize > 1 ? GetFabricatedItemQuality(fabricatedItem, user).Quality : GetFabricatedItemQuality(fabricatedItem, user).RollQuality(); } int amount = (int)fabricationitemAmount.Value; if (fabricationLimits.ContainsKey(fabricatedItem.RecipeHash)) { if (amount > fabricationLimits[fabricatedItem.RecipeHash]) { amount = fabricationLimits[fabricatedItem.RecipeHash]; fabricationLimits[fabricatedItem.RecipeHash] = 0; } else { fabricationLimits[fabricatedItem.RecipeHash] -= amount; Log.Debug($"New fab limit: {fabricationLimits[fabricatedItem.RecipeHash]}"); } } var tempUser = user; for (int i = 0; i < amount; i++) { float outCondition = fabricatedItem.OutCondition; if (i < amountFittingContainer) { Entity.Spawner.AddItemToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * outCondition, quality, onSpawned: (Item spawnedItem) => { onItemSpawned(spawnedItem, tempUser); spawnedItem.Quality = quality; spawnedItem.StolenDuringRound = ingredientsStolen; spawnedItem.AllowStealing = ingredientsAllowStealing; //reset the condition in case the max condition is higher than the prefab's due to e.g. quality modifiers spawnedItem.Condition = spawnedItem.MaxCondition * outCondition; }); } else { Entity.Spawner.AddItemToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * outCondition, quality, onSpawned: (Item spawnedItem) => { onItemSpawned(spawnedItem, tempUser); spawnedItem.Quality = quality; spawnedItem.StolenDuringRound = ingredientsStolen; spawnedItem.AllowStealing = ingredientsAllowStealing; //reset the condition in case the max condition is higher than the prefab's due to e.g. quality modifiers spawnedItem.Condition = spawnedItem.MaxCondition * outCondition; }); } } void onItemSpawned(Item spawnedItem, Character user) { if (user != null && user.TeamID != CharacterTeamType.None) { foreach (WifiComponent wifiComponent in spawnedItem.GetComponents()) { wifiComponent.TeamID = user.TeamID; } } OnItemFabricated?.Invoke(spawnedItem, user); } if (user?.Info != null && !user.Removed) { foreach (Skill skill in fabricatedItem.RequiredSkills) { float addedSkill = skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill; var addedSkillValue = new AbilityFabricatorSkillGain(skill.Identifier, addedSkill); user.CheckTalents(AbilityEffectType.OnItemFabricationSkillGain, addedSkillValue); user.Info.ApplySkillGain( skill.Identifier, addedSkillValue.Value); } } var prevFabricatedItem = fabricatedItem; var prevUser = user; CancelFabricating(); amountRemaining--; if (amountRemaining > 0 && CanBeFabricated(prevFabricatedItem, availableIngredients, prevUser)) { //keep fabricating if we can fabricate more StartFabricating(prevFabricatedItem, prevUser, addToServerLog: false); } } } /// /// Power consumption of the fabricator. Only consume power when active and adjust consumption based on condition. /// public override float GetCurrentPowerConsumption(Connection connection = null) { //No consumption if not powerin or is off if (connection != this.powerIn || !IsActive) { return 0; } currPowerConsumption = PowerConsumption; item.GetComponent()?.AdjustPowerConsumption(ref currPowerConsumption); return currPowerConsumption; } public static float CalculateBonusRollPercentage(float skillLevel, float target) => Math.Clamp((skillLevel - target) / (100f - target) * 100f, min: 0, max: 100); public readonly record struct QualityResult(int Quality, bool HasRandomQuality, float PlusOnePercentage, float PlusTwoPercentage) { public static readonly QualityResult Empty = new QualityResult(0, true, 0, 0); public bool HasRandomQualityRollChance => HasRandomQuality && (PlusOnePercentage > 0f || PlusTwoPercentage > 0f); // The total real world percentage for a roll to succeed, taking into account that +1 needs to succeed for +2 to be attempted and // that the chance for only +1 goes down as +2 increases since some of the +1's will turn into +2s public float TotalPlusOnePercentage => Math.Clamp(PlusOnePercentage * (100f - PlusTwoPercentage) / 100f, min: 0, max: 100); public float TotalPlusTwoPercentage => Math.Clamp(PlusOnePercentage * PlusTwoPercentage / 100f, min: 0, max: 100); public int RollQuality() { int additionalQuality = 0; if (Roll(PlusOnePercentage)) { additionalQuality++; if (Roll(PlusTwoPercentage)) { additionalQuality++; } } return Quality + additionalQuality; static bool Roll(float percentage) => percentage >= Rand.Range(0, 100, Rand.RandSync.Unsynced); } } public const int PlusOneQualityBonusThreshold = 50, PlusTwoQualityBonusThreshold = 75; public const int PlusOneTarget = 100, PlusTwoTarget = 125; public const float PlusOneLerp = 0.2f, PlusTwoLerp = 0.4f; private static QualityResult GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user) { if (user?.Info == null) { return QualityResult.Empty; } if (fabricatedItem.TargetItem.ConfigElement.GetChildElement("Quality") == null) { return QualityResult.Empty; } int quality = 0; float floatQuality = 0.0f; floatQuality += user.GetStatValue(StatTypes.IncreaseFabricationQuality, includeSaved: false); foreach (var tag in fabricatedItem.TargetItem.Tags) { floatQuality += user.Info.GetSavedStatValue(StatTypes.IncreaseFabricationQuality, tag); } if (!fabricatedItem.TargetItem.Tags.Contains(fabricatedItem.TargetItem.Identifier)) { floatQuality += user.Info.GetSavedStatValue(StatTypes.IncreaseFabricationQuality, fabricatedItem.TargetItem.Identifier); } quality = (int)floatQuality; // Use Option here instead of 0 because we want the lowest value and a value of 0 would always be lower than any other chance Option plusOne = Option.None, plusTwo = Option.None; foreach (var skill in fabricatedItem.RequiredSkills) { float skillLevel = user.GetSkillLevel(skill.Identifier); if (skillLevel >= PlusOneQualityBonusThreshold) { //+1 quality chance if the character's skill level is >20% from the min requirement towards max skill as well as higher than 50 //e.g. if the skill requirement is 10 -> 28 (but minimum 50 threshold) //40 -> 52 //90 -> 92 var bonusChance1 = CalculateBonusRollPercentage(skillLevel, MathHelper.Lerp(skill.Level, PlusOneTarget, PlusOneLerp)); plusOne = OverrideChanceIfLess(plusOne, bonusChance1); if (skillLevel >= PlusTwoQualityBonusThreshold) { var bonusChance2 = CalculateBonusRollPercentage(skillLevel, MathHelper.Lerp(skill.Level, PlusTwoTarget, PlusTwoLerp)); plusTwo = OverrideChanceIfLess(plusTwo, bonusChance2); } else { break; } } else { break; } static Option OverrideChanceIfLess(Option original, float bonusChance) { if (original.TryUnwrap(out var originalChance)) { return originalChance > bonusChance ? Option.Some(bonusChance) : original; } return Option.Some(bonusChance); } } bool hasRandomQuality = !(fabricatedItem.TargetItem.MaxStackSize > 1); //don't randomise items with a stacksize > 1 float PlusOnePercentage = plusOne.Match(some: static f => f, none: static () => 0f); float PlusTwoPercentage = plusTwo.Match(some: static f => f, none: static () => 0f); if (!hasRandomQuality && PlusOnePercentage > 0) { quality++; if (PlusTwoPercentage > 0) { quality++; } } return new QualityResult(quality, hasRandomQuality, PlusOnePercentage, PlusTwoPercentage); } partial void UpdateRequiredTimeProjSpecific(); private static bool AnyOneHasRecipeForItem(Character user, ItemPrefab item) { return (user != null && user.HasRecipeForItem(item.Identifier)) || GameSession.GetSessionCrewCharacters(CharacterType.Bot).Any(c => c.HasRecipeForItem(item.Identifier)); } private readonly HashSet usedIngredients = new HashSet(); private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary> availableIngredients, Character character) { if (fabricableItem == null) { return false; } if (fabricableItem.RequiresRecipe) { if (character == null) { return false; } if (!AnyOneHasRecipeForItem(character, fabricableItem.TargetItem)) { return false; } } if (fabricableItem.HideForNonTraitors) { if (character is not { IsTraitor: true }) { return false; } } if (fabricableItem.RequiredMoney > 0) { switch (GameMain.GameSession?.GameMode) { case MultiPlayerCampaign mpCampaign: { if (!mpCampaign.CanAfford(fabricableItem.RequiredMoney, GetUsingClient())) { return false; } break; } case CampaignMode campaign: { if (campaign.Bank.Balance < fabricableItem.RequiredMoney) { return false; } break; } default: return false; } } if (fabricationLimits.TryGetValue(fabricableItem.RecipeHash, out int amount) && amount <= 0) { return false; } //maintain a list of used ingredients so we don't end up considering the same item a suitable for multiple required ingredients usedIngredients.Clear(); return fabricableItem.RequiredItems.All(requiredItem => { int availableItemsAmount = 0; foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs) { if (!availableIngredients.TryGetValue(requiredPrefab.Identifier, out var availableItems)) { continue; } foreach (Item availableItem in availableItems) { if (usedIngredients.Contains(availableItem)) { continue; } if (requiredItem.IsConditionSuitable(availableItem.ConditionPercentage)) { usedIngredients.Add(availableItem); availableItemsAmount++; } if (availableItemsAmount >= requiredItem.Amount) { return true; } } } return false; }); } private float GetRequiredTime(FabricationRecipe fabricableItem, Character user) { float degreeOfSuccess = FabricationDegreeOfSuccess(user, fabricableItem.RequiredSkills); float t = degreeOfSuccess < 0.5f ? degreeOfSuccess * degreeOfSuccess : degreeOfSuccess * 2; //fabricating takes 100 times longer if degree of success is close to 0 //characters with a higher skill than required can fabricate up to 100% faster float time = fabricableItem.RequiredTime / item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.FabricationSpeed, FabricationSpeed) / MathHelper.Clamp(t, 0.01f, 2.0f); if (user?.Info is { } info && fabricableItem.TargetItem is { } it) { time /= 1f + it.Tags.Sum(tag => info.GetSavedStatValue(StatTypes.FabricationSpeed, tag)); } return time; } public float FabricationDegreeOfSuccess(Character character, ImmutableArray skills) { if (skills.Length == 0) { return 1.0f; } if (character == null) { return 0.0f; } float minDegreeOfSuccess = 1.0f; foreach (var skill in skills) { float characterLevel = character.GetSkillLevel(skill.Identifier); minDegreeOfSuccess = Math.Min(minDegreeOfSuccess, (characterLevel - (skill.Level * SkillRequirementMultiplier) + 100.0f) / 2.0f / 100.0f); } return minDegreeOfSuccess; } public override float GetSkillMultiplier() { return SkillRequirementMultiplier; } private readonly HashSet linkedInventories = new HashSet(); private void RefreshAvailableIngredients() { Character user = this.user; #if CLIENT user ??= Character.Controlled; #endif linkedInventories.Clear(); List itemList = new List(); itemList.AddRange(inputContainer.Inventory.AllItems); foreach (MapEntity linkedTo in item.linkedTo) { if (linkedTo is Item linkedItem) { var itemContainer = linkedItem.GetComponent(); if (itemContainer == null) { continue; } if (user != null) { if (!itemContainer.HasRequiredItems(user, addMessage: false)) { continue; } } var deconstructor = linkedItem.GetComponent(); if (deconstructor != null) { itemContainer = deconstructor.OutputContainer; } linkedInventories.Add(itemContainer.Inventory); itemList.AddRange(itemContainer.Inventory.AllItems); } } for (int i = 0; i < itemList.Count; i++) { var container = itemList[i].GetComponent(); if (container != null) { itemList.AddRange(container.Inventory.AllItems); } } if (user?.Inventory != null && user.SelectedItem == item) { itemList.AddRange(user.Inventory.AllItems); linkedInventories.Add(user.Inventory); } foreach (Character c in Character.CharacterList) { //take materials from characters who've selected a linked container too //(e.g. cabinet that's set to display alongside the fabricator UI) if (c.SelectedItem != null && c.Inventory != null && linkedInventories.Contains(c.SelectedItem.OwnInventory) && !linkedInventories.Contains(c.Inventory)) { itemList.AddRange(c.Inventory.AllItems); linkedInventories.Add(c.Inventory); } } availableIngredients.Clear(); foreach (Item item in itemList) { var itemIdentifier = item.Prefab.Identifier; if (!availableIngredients.ContainsKey(itemIdentifier)) { availableIngredients[itemIdentifier] = new List(itemList.Count); } availableIngredients[itemIdentifier].Add(item); } foreach (var itemId in availableIngredients.Keys) { availableIngredients[itemId] = SortIngredients(availableIngredients[itemId]).ToList(); } } private IEnumerable SortIngredients(IEnumerable items) { return items .OrderByDescending(getIngredientContainerPriority) .ThenBy(it => it.Prefab.DefaultPrice?.Price ?? 0) .ThenBy(it => MathUtils.IsValid(it.Condition) ? it.Condition : 0) .ThenByDescending(it => it.ParentInventory?.FindIndex(it) ?? 0); int getIngredientContainerPriority(Item item) { if (item.ParentInventory == InputContainer.Inventory) { return 3; } else if (item.ParentInventory is CharacterInventory) { return 2; } return 1; } } private IEnumerable GetSortedSuitableIngredients() { List suitableIngredients = new List(); foreach (FabricationRecipe.RequiredItem requiredItem in fabricatedItem.RequiredItems) { foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs) { if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; } var availableItems = availableIngredients[requiredPrefab.Identifier]; suitableIngredients.AddRange( availableItems.Where(potentialItem => requiredItem.IsConditionSuitable(potentialItem.ConditionPercentage))); } } return SortIngredients(suitableIngredients); } /// /// Move the items required for fabrication into the input container. /// The method assumes that all the required ingredients are available either in the input container or linked containers. /// private void MoveIngredientsToInputContainer(FabricationRecipe targetItem) { List chosenIngredients = new List(); var suitableIngredients = GetSortedSuitableIngredients(); foreach (var requiredItem in targetItem.RequiredItems) { for (int i = 0; i < requiredItem.Amount; i++) { foreach (var suitableIngredient in suitableIngredients) { if (!requiredItem.MatchesItem(suitableIngredient)) { continue; } if (chosenIngredients.Contains(suitableIngredient)) { continue; } //in another inventory, we need to move the item if (suitableIngredient.ParentInventory != inputContainer.Inventory) { if (!inputContainer.Inventory.CanBePut(suitableIngredient)) { var unneededItem = inputContainer.Inventory.AllItems.FirstOrDefault(it => !chosenIngredients.Contains(it)); unneededItem?.Drop(null); } inputContainer.Inventory.TryPutItem(suitableIngredient, user: null); } chosenIngredients.Add(suitableIngredient); break; } } } RefreshAvailableIngredients(); } public override XElement Save(XElement parentElement) { var componentElement = base.Save(parentElement); if (fabricatedItem != null) { componentElement.Add(new XAttribute("fabricateditemidentifier", fabricatedItem.TargetItem.Identifier)); componentElement.Add(new XAttribute("savedtimeuntilready", timeUntilReady.ToString("G", CultureInfo.InvariantCulture))); componentElement.Add(new XAttribute("savedrequiredtime", requiredTime.ToString("G", CultureInfo.InvariantCulture))); } return componentElement; } public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap) { base.Load(componentElement, usePrefabValues, idRemap, isItemSwap); savedFabricatedItem = componentElement.GetAttributeString("fabricateditemidentifier", ""); savedTimeUntilReady = componentElement.GetAttributeFloat("savedtimeuntilready", 0.0f); savedRequiredTime = componentElement.GetAttributeFloat("savedrequiredtime", 0.0f); } public override void OnMapLoaded() { if (string.IsNullOrEmpty(savedFabricatedItem)) { return; } inputContainer?.OnMapLoaded(); outputContainer?.OnMapLoaded(); var recipe = fabricationRecipes.Values.FirstOrDefault(r => r.TargetItem.Identifier == savedFabricatedItem); if (recipe == null) { DebugConsole.ThrowError("Error while loading a fabricator. Can't continue fabricating \"" + savedFabricatedItem + "\" (matching recipe not found)."); } else { #if CLIENT SelectItem(null, recipe, savedRequiredTime); #endif StartFabricating(recipe, user: null); timeUntilReady = savedTimeUntilReady; requiredTime = savedRequiredTime; } savedFabricatedItem = null; } public override void RemoveComponentSpecific() { base.RemoveComponentSpecific(); OnItemFabricated = null; } class AbilityFabricatorSkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier { public AbilityFabricatorSkillGain(Identifier skillIdentifier, float skillAmount) { SkillIdentifier = skillIdentifier; Value = skillAmount; } public float Value { get; set; } public Identifier SkillIdentifier { get; set; } } class AbilityFabricationItemAmount : AbilityObject, IAbilityValue, IAbilityItemPrefab { public AbilityFabricationItemAmount(ItemPrefab itemPrefab, float itemAmount) { ItemPrefab = itemPrefab; Value = itemAmount; } public float Value { get; set; } public ItemPrefab ItemPrefab { get; set; } } internal sealed class AbilityFabricationItemIngredients : AbilityObject { public List Items { get; set; } public AbilityFabricationItemIngredients(List items) { Items = items; } } } }