using Barotrauma.Networking; using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; namespace Barotrauma.Items.Components { partial class Fabricator : Powered, IServerSerializable, IClientSerializable { public const float SkillIncreaseMultiplier = 0.5f; private List fabricationRecipes = new List(); private FabricationRecipe fabricatedItem; private float timeUntilReady; private float requiredTime; private Character user; private ItemContainer inputContainer, outputContainer; private float progressState; public Fabricator(Item item, XElement element) : base(item, element) { foreach (XElement subElement in element.Elements()) { if (subElement.Name.ToString().ToLowerInvariant() == "fabricableitem") { DebugConsole.ThrowError("Error in item " + item.Name + "! Fabrication recipes should be defined in the craftable item's xml, not in the fabricator."); break; } } foreach (MapEntityPrefab me in MapEntityPrefab.List) { if (!(me is ItemPrefab itemPrefab)) { continue; } foreach (FabricationRecipe recipe in itemPrefab.FabricationRecipes) { if (recipe.SuitableFabricatorIdentifiers.Length > 0) { if (!recipe.SuitableFabricatorIdentifiers.Any(i => item.prefab.Identifier == i || item.HasTag(i))) { continue; } } fabricationRecipes.Add(recipe); } } InitProjSpecific(); } public override void 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) { int ingredientCount = recipe.RequiredItems.Sum(it => it.Amount); if (ingredientCount > inputContainer.Capacity) { DebugConsole.ThrowError("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(); partial void InitProjSpecific(); 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); } private void StartFabricating(FabricationRecipe selectedItem, Character user) { if (selectedItem == null) return; if (!outputContainer.Inventory.IsEmpty()) return; #if SERVER if (user != null) { GameServer.Log(user.LogName + " started fabricating " + selectedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction); } #endif #if CLIENT itemList.Enabled = false; activateButton.Text = TextManager.Get("FabricatorCancel"); #endif MoveIngredientsToInputContainer(selectedItem); fabricatedItem = selectedItem; IsActive = true; this.user = user; requiredTime = GetRequiredTime(fabricatedItem, user); timeUntilReady = requiredTime; inputContainer.Inventory.Locked = true; outputContainer.Inventory.Locked = true; currPowerConsumption = powerConsumption; currPowerConsumption *= MathHelper.Lerp(2.0f, 1.0f, item.Condition / item.MaxCondition); } private void CancelFabricating(Character user = null) { #if SERVER if (fabricatedItem != null && user != null) { GameServer.Log(user.LogName + " cancelled the fabrication of " + fabricatedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction); } #endif IsActive = false; fabricatedItem = null; this.user = null; currPowerConsumption = 0.0f; #if CLIENT itemList.Enabled = true; if (activateButton != null) { activateButton.Text = TextManager.Get("FabricatorCreate"); } #endif progressState = 0.0f; timeUntilReady = 0.0f; inputContainer.Inventory.Locked = false; outputContainer.Inventory.Locked = false; } public override void Update(float deltaTime, Camera cam) { if (fabricatedItem == null || !CanBeFabricated(fabricatedItem)) { CancelFabricating(); return; } progressState = fabricatedItem == null ? 0.0f : (requiredTime - timeUntilReady) / requiredTime; if (voltage < minVoltage) { return; } ApplyStatusEffects(ActionType.OnActive, deltaTime, null); if (powerConsumption <= 0) { voltage = 1.0f; } timeUntilReady -= deltaTime * voltage; voltage -= deltaTime * 10.0f; if (timeUntilReady > 0.0f) { return; } var availableIngredients = GetAvailableIngredients(); foreach (FabricationRecipe.RequiredItem ingredient in fabricatedItem.RequiredItems) { for (int i = 0; i < ingredient.Amount; i++) { var requiredItem = inputContainer.Inventory.Items.FirstOrDefault(it => it != null && it.Prefab == ingredient.ItemPrefab && it.Condition >= ingredient.ItemPrefab.Health * ingredient.MinCondition); if (requiredItem == null) continue; //Item4 = use condition bool if (ingredient.UseCondition && requiredItem.Condition - ingredient.ItemPrefab.Health * ingredient.MinCondition > 0.0f) //Leave it behind with reduced condition if it has enough to stay above 0 { requiredItem.Condition -= ingredient.ItemPrefab.Health * ingredient.MinCondition; continue; } Entity.Spawner.AddToRemoveQueue(requiredItem); inputContainer.Inventory.RemoveItem(requiredItem); } } if (outputContainer.Inventory.Items.All(i => i != null)) { Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * fabricatedItem.OutCondition); } else { Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * fabricatedItem.OutCondition); } if ((GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) && user != null && !user.Removed) { foreach (Skill skill in fabricatedItem.RequiredSkills) { user.Info.IncreaseSkillLevel(skill.Identifier, skill.Level / 100.0f * SkillIncreaseMultiplier, user.WorldPosition + Vector2.UnitY * 150.0f); } } CancelFabricating(null); } private bool CanBeFabricated(FabricationRecipe fabricableItem) { if (fabricableItem == null) { return false; } List availableIngredients = GetAvailableIngredients(); return CanBeFabricated(fabricableItem, availableIngredients); } private bool CanBeFabricated(FabricationRecipe fabricableItem, IEnumerable availableIngredients) { if (fabricableItem == null) { return false; } foreach (FabricationRecipe.RequiredItem requiredItem in fabricableItem.RequiredItems) { if (availableIngredients.Count(it => IsItemValidIngredient(it, requiredItem)) < requiredItem.Amount) { return false; } } return true; } private float GetRequiredTime(FabricationRecipe fabricableItem, Character user) { float degreeOfSuccess = DegreeOfSuccess(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 return fabricableItem.RequiredTime / MathHelper.Clamp(t, 0.01f, 2.0f); } /// /// Get a list of all items available in the input container and linked containers /// /// private List GetAvailableIngredients() { List availableIngredients = new List(); availableIngredients.AddRange(inputContainer.Inventory.Items.Where(it => it != null)); foreach (MapEntity linkedTo in item.linkedTo) { if (linkedTo is Item linkedItem) { var itemContainer = linkedItem.GetComponent(); if (itemContainer == null) { continue; } var deconstructor = linkedItem.GetComponent(); if (deconstructor != null) { itemContainer = deconstructor.OutputContainer; } availableIngredients.AddRange(itemContainer.Inventory.Items.Where(it => it != null)); } } return availableIngredients; } /// /// 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) { //required ingredients that are already present in the input container List usedItems = new List(); var availableIngredients = GetAvailableIngredients(); foreach (var requiredItem in targetItem.RequiredItems) { for (int i = 0; i < requiredItem.Amount; i++) { var matchingItem = availableIngredients.Find(it => !usedItems.Contains(it) && IsItemValidIngredient(it, requiredItem)); if (matchingItem == null) { continue; } if (matchingItem.ParentInventory == inputContainer.Inventory) { //already in input container, all good usedItems.Add(matchingItem); } else //in another inventory, we need to move the item { if (inputContainer.Inventory.Items.All(it => it != null)) { var unneededItem = inputContainer.Inventory.Items.FirstOrDefault(it => !usedItems.Contains(it)); unneededItem?.Drop(null); } inputContainer.Inventory.TryPutItem(matchingItem, user: null, createNetworkEvent: true); } } } } private bool IsItemValidIngredient(Item item, FabricationRecipe.RequiredItem requiredItem) { return item != null && item.prefab == requiredItem.ItemPrefab && item.Condition / item.Prefab.Health >= requiredItem.MinCondition; } } }