using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml.Linq; using Barotrauma.Abilities; namespace Barotrauma.Items.Components { partial class Fabricator : Powered, IServerSerializable, IClientSerializable { private readonly List fabricationRecipes = new List(); private FabricationRecipe fabricatedItem; private float timeUntilReady; private float requiredTime; private string savedFabricatedItem; private float savedTimeUntilReady, savedRequiredTime; private bool hasPower; private Character user; private ItemContainer inputContainer, outputContainer; [Serialize(1.0f, true)] public float FabricationSpeed { get; set; } [Serialize(1.0f, true)] public float SkillRequirementMultiplier { get; set; } private const float TinkeringSpeedIncrease = 1.5f; private enum FabricatorState { Active = 1, Paused = 2, Stopped = 0 } private FabricatorState state; private FabricatorState 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; } } public override bool RecreateGUIOnResolutionChange => true; private float progressState; public Fabricator(Item item, XElement element) : base(item, element) { foreach (XElement subElement in element.Elements()) { if (subElement.Name.ToString().Equals("fabricableitem", StringComparison.OrdinalIgnoreCase)) { DebugConsole.ThrowError("Error in item " + item.Name + "! Fabrication recipes should be defined in the craftable item's xml, not in the fabricator."); break; } } foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) { 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); } } fabricationRecipes.Sort((r1, r2) => { int hash1 = (int)r1.TargetItem.UIntIdentifier; int hash2 = (int)r2.TargetItem.UIntIdentifier; return hash1 - hash2; }); state = FabricatorState.Stopped; InitProjSpecific(); } 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) { if (recipe.RequiredItems.Count > 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; } public void RemoveFabricationRecipes(List allowedIdentifiers) { for (int i = 0; i < fabricationRecipes.Count; i++) { if (!allowedIdentifiers.Contains(fabricationRecipes[i].TargetItem.Identifier)) { fabricationRecipes.RemoveAt(i); i--; } } CreateRecipes(); } partial void CreateRecipes(); private void StartFabricating(FabricationRecipe selectedItem, Character user, bool addToServerLog = true) { if (selectedItem == null) { return; } if (!outputContainer.Inventory.CanBePut(selectedItem.TargetItem, selectedItem.OutCondition * selectedItem.TargetItem.Health)) { return; } #if CLIENT itemList.Enabled = false; activateButton.Text = TextManager.Get("FabricatorCancel"); #endif IsActive = true; this.user = user; fabricatedItem = selectedItem; MoveIngredientsToInputContainer(selectedItem); requiredTime = GetRequiredTime(fabricatedItem, user); timeUntilReady = requiredTime; inputContainer.Inventory.Locked = true; outputContainer.Inventory.Locked = true; currPowerConsumption = powerConsumption; item.GetComponent()?.AdjustPowerConsumption(ref currPowerConsumption); if (GameMain.NetworkMember?.IsServer ?? true) { State = FabricatorState.Active; } #if SERVER if (user != null && addToServerLog) { GameServer.Log(GameServer.CharacterLogName(user) + " started fabricating " + selectedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction); } #endif } private void CancelFabricating(Character user = null) { IsActive = false; this.user = null; currPowerConsumption = 0.0f; progressState = 0.0f; timeUntilReady = 0.0f; UpdateRequiredTimeProjSpecific(); inputContainer.Inventory.Locked = false; outputContainer.Inventory.Locked = false; if (GameMain.NetworkMember?.IsServer ?? true) { State = FabricatorState.Stopped; } if (fabricatedItem == null) { return; } fabricatedItem = null; #if CLIENT itemList.Enabled = true; if (activateButton != null) { activateButton.Text = TextManager.Get("FabricatorCreate"); } #endif #if SERVER if (user != null) { GameServer.Log(GameServer.CharacterLogName(user) + " cancelled the fabrication of " + fabricatedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction); } #endif } public override void Update(float deltaTime, Camera cam) { var availableIngredients = GetAvailableIngredients(); if (fabricatedItem == null || !CanBeFabricated(fabricatedItem, availableIngredients, user)) { CancelFabricating(); return; } progressState = fabricatedItem == null ? 0.0f : (requiredTime - timeUntilReady) / requiredTime; if (GameMain.NetworkMember?.IsClient ?? false) { hasPower = State != FabricatorState.Paused; if (!hasPower) { return; } } else { hasPower = Voltage >= MinVoltage; if (!hasPower) { State = FabricatorState.Paused; return; } State = FabricatorState.Active; } var repairable = item.GetComponent(); if (repairable != null) { repairable.LastActiveTime = (float)Timing.TotalTime + 10.0f; } ApplyStatusEffects(ActionType.OnActive, deltaTime, null); if (powerConsumption <= 0) { Voltage = 1.0f; } float tinkeringStrength = 0f; if (repairable.IsTinkering) { tinkeringStrength = repairable.TinkeringStrength; } float fabricationSpeedIncrease = 1f + tinkeringStrength * TinkeringSpeedIncrease; timeUntilReady -= deltaTime * fabricationSpeedIncrease * Math.Min(Voltage, 1.0f); UpdateRequiredTimeProjSpecific(); if (timeUntilReady > 0.0f) { return; } if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { fabricatedItem.RequiredItems.ForEach(requiredItem => { for (int usedPrefabsAmount = 0; usedPrefabsAmount < requiredItem.Amount; usedPrefabsAmount++) { foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs) { if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; } var availablePrefabs = availableIngredients[requiredPrefab.Identifier]; var availablePrefab = availablePrefabs.FirstOrDefault(potentialPrefab => { return potentialPrefab.ConditionPercentage >= requiredItem.MinCondition * 100.0f && potentialPrefab.ConditionPercentage <= requiredItem.MaxCondition * 100.0f; }); if (availablePrefab == null) { continue; } if (requiredItem.UseCondition && availablePrefab.ConditionPercentage - requiredItem.MinCondition * 100 > 0.0f) //Leave it behind with reduced condition if it has enough to stay above 0 { availablePrefab.Condition -= availablePrefab.Prefab.Health * requiredItem.MinCondition; continue; } availablePrefabs.Remove(availablePrefab); Entity.Spawner.AddToRemoveQueue(availablePrefab); inputContainer.Inventory.RemoveItem(availablePrefab); } } }); int amountFittingContainer = outputContainer.Inventory.HowManyCanBePut(fabricatedItem.TargetItem, fabricatedItem.OutCondition * fabricatedItem.TargetItem.Health); var fabricationValueItem = new AbilityValueItem(fabricatedItem.Amount, fabricatedItem.TargetItem); int quality = 0; if (user?.Info != null) { foreach (Character character in Character.CharacterList.Where(c => c.TeamID == user.TeamID)) { character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationValueItem); } user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationValueItem); quality = GetFabricatedItemQuality(fabricatedItem, user); } var tempUser = user; for (int i = 0; i < (int)fabricationValueItem.Value; i++) { float outCondition = fabricatedItem.OutCondition; if (i < amountFittingContainer) { Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * outCondition, quality, onSpawned: (Item spawnedItem) => { onItemSpawned(spawnedItem, tempUser); //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.AddToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * outCondition, quality, onSpawned: (Item spawnedItem) => { onItemSpawned(spawnedItem, tempUser); //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; }); } } static void onItemSpawned(Item spawnedItem, Character user) { if (user != null && user.TeamID != CharacterTeamType.None) { foreach (WifiComponent wifiComponent in spawnedItem.GetComponents()) { wifiComponent.TeamID = user.TeamID; } } } if (user?.Info != null && !user.Removed) { foreach (Skill skill in fabricatedItem.RequiredSkills) { float userSkill = user.GetSkillLevel(skill.Identifier); float addedSkill = skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill / Math.Max(userSkill, 1.0f); var addedSkillValue = new AbilityValueString(0f, skill.Identifier); user.CheckTalents(AbilityEffectType.OnItemFabricationSkillGain, addedSkillValue); addedSkill += addedSkillValue.Value; user.Info.IncreaseSkillLevel( skill.Identifier, addedSkill); } } //disabled "continuous fabrication" for now //before we enable it, there should be some UI controls for fabricating a specific number of items /*var prevFabricatedItem = fabricatedItem; var prevUser = user; CancelFabricating(); if (CanBeFabricated(prevFabricatedItem)) { //keep fabricating if we can fabricate more StartFabricating(prevFabricatedItem, prevUser, addToServerLog: false); }*/ CancelFabricating(); } } private int GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user) { if (user == null) { return 0; } if (fabricatedItem.TargetItem.ConfigElement.GetChildElement("Quality") == null) { return 0; } int quality = 0; float floatQuality = 0.0f; foreach (string tag in fabricatedItem.TargetItem.Tags) { floatQuality += user.Info.GetSavedStatValue(StatTypes.IncreaseFabricationQuality, tag); } quality = (int)floatQuality; const int MaxCraftingSkill = 100; quality += fabricatedItem.RequiredSkills.All(s => user.GetSkillLevel(s.Identifier) >= MaxCraftingSkill) ? 1 : 0; quality += FabricationDegreeOfSuccess(user, fabricatedItem.RequiredSkills) >= 0.5f ? 1 : 0; return quality; } partial void UpdateRequiredTimeProjSpecific(); private bool CanBeFabricated(FabricationRecipe fabricableItem, Dictionary> availableIngredients, Character character) { if (fabricableItem == null) { return false; } if (fabricableItem.RequiresRecipe && (character == null || !character.HasRecipeForItem(fabricableItem.TargetItem.Identifier))) { return false; } return fabricableItem.RequiredItems.All(requiredItem => { int availablePrefabsAmount = 0; foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs) { if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; } var availablePrefabs = availableIngredients[requiredPrefab.Identifier]; foreach (Item availablePrefab in availablePrefabs) { if (availablePrefab.Condition / availablePrefab.Prefab.Health >= requiredItem.MinCondition && availablePrefab.Condition / availablePrefab.Prefab.Health <= requiredItem.MaxCondition) { availablePrefabsAmount++; } if (availablePrefabsAmount >= 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 return fabricableItem.RequiredTime / FabricationSpeed / MathHelper.Clamp(t, 0.01f, 2.0f); } public float FabricationDegreeOfSuccess(Character character, List skills) { if (skills.Count == 0) { return 1.0f; } if (character == null) { return 0.0f; } float skillSum = (from t in skills let characterLevel = character.GetSkillLevel(t.Identifier) select (characterLevel - (t.Level * SkillRequirementMultiplier))).Sum(); float average = skillSum / skills.Count; return (average + 100.0f) / 2.0f / 100.0f; } public override float GetSkillMultiplier() { return SkillRequirementMultiplier; } /// /// Get a list of all items available in the input container and linked containers /// /// private Dictionary> GetAvailableIngredients() { List availableIngredients = new List(); availableIngredients.AddRange(inputContainer.Inventory.AllItems); 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.AllItems); } } #if CLIENT if (Character.Controlled?.Inventory != null) { availableIngredients.AddRange(Character.Controlled.Inventory.AllItems); } #else if (user?.Inventory != null) { availableIngredients.AddRange(user.Inventory.AllItems); } #endif Dictionary> ingredientsDictionary = new Dictionary>(); for (int i = 0; i < availableIngredients.Count; i++) { var itemIdentifier = availableIngredients[i].prefab.Identifier; if (!ingredientsDictionary.ContainsKey(itemIdentifier)) { ingredientsDictionary[itemIdentifier] = new List(availableIngredients.Count); } ingredientsDictionary[itemIdentifier].Add(availableIngredients[i]); } return ingredientsDictionary; } /// /// 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(); bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; var availableIngredients = GetAvailableIngredients(); targetItem.RequiredItems.ForEach(requiredItem => { for (int i = 0; i < requiredItem.Amount; i++) { foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs) { if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; } var availablePrefabs = availableIngredients[requiredPrefab.Identifier]; var availablePrefab = availablePrefabs.FirstOrDefault(potentialPrefab => { return !usedItems.Contains(potentialPrefab) && potentialPrefab.ConditionPercentage >= requiredItem.MinCondition * 100.0f && potentialPrefab.ConditionPercentage <= requiredItem.MaxCondition * 100.0f; }); if (availablePrefab == null) { continue; } availablePrefabs.Remove(availablePrefab); if (availablePrefab.ParentInventory == inputContainer.Inventory) { //already in input container, all good usedItems.Add(availablePrefab); } else //in another inventory, we need to move the item { if (!inputContainer.Inventory.CanBePut(availablePrefab)) { var unneededItem = inputContainer.Inventory.AllItems.FirstOrDefault(it => !usedItems.Contains(it)); unneededItem?.Drop(null, createNetworkEvent: !isClient); } inputContainer.Inventory.TryPutItem(availablePrefab, user: null, createNetworkEvent: !isClient); } } } }); } 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(XElement componentElement, bool usePrefabValues, IdRemap idRemap) { base.Load(componentElement, usePrefabValues, idRemap); 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.Find(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; } } }