Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Collections.Generic;
6 using System.Collections.Immutable;
7 using System.Globalization;
8 using System.Linq;
9 using System.Xml.Linq;
10 using Barotrauma.Abilities;
11 
13 {
15  {
16  private ImmutableDictionary<uint, FabricationRecipe> fabricationRecipes; //this is not readonly because tutorials fuck this up!!!!
17 
18  private const int MaxAmountToFabricate = 99;
19 
20  private FabricationRecipe fabricatedItem;
21  private float timeUntilReady;
22  private float requiredTime;
23 
24  private string savedFabricatedItem;
25  private float savedTimeUntilReady, savedRequiredTime;
26 
27  private readonly Dictionary<Identifier, List<Item>> availableIngredients = new Dictionary<Identifier, List<Item>>();
28 
29  const float RefreshIngredientsInterval = 1.0f;
30  private float refreshIngredientsTimer;
31 
32  private bool hasPower;
33 
34  private Character user;
35 
36  private ItemContainer inputContainer, outputContainer;
37 
38  [Editable(MinValueFloat = 0.1f, MaxValueFloat = 1000), Serialize(1.0f, IsPropertySaveable.Yes)]
39  public float FabricationSpeed { get; set; }
40 
41  [Serialize(1.0f, IsPropertySaveable.Yes)]
42  public float SkillRequirementMultiplier { get; set; }
43 
44  private int amountToFabricate;
46  public int AmountToFabricate
47  {
48  get { return amountToFabricate; }
49  set { amountToFabricate = MathHelper.Clamp(value, 1, MaxAmountToFabricate); }
50  }
51 
52  private int amountRemaining;
53 
54  private const float TinkeringSpeedIncrease = 2.5f;
55 
56  private enum FabricatorState
57  {
58  Active = 1,
59  Paused = 2,
60  Stopped = 0
61  }
62 
63  private FabricatorState state;
64  private FabricatorState State
65  {
66  get
67  {
68  return state;
69  }
70  set
71  {
72  if (state == value) { return; }
73  state = value;
74 #if SERVER
75  serverEventId++;
76  item.CreateServerEvent(this);
77 #endif
78  }
79  }
80 
82  {
83  get { return inputContainer; }
84  }
85 
87  {
88  get { return outputContainer; }
89  }
90 
91  private float progressState;
92 
93  private readonly Dictionary<uint, int> fabricationLimits = new Dictionary<uint, int>();
94 
95  public Action<Item, Character> OnItemFabricated;
96 
98  : base(item, element)
99  {
100  foreach (var subElement in element.Elements())
101  {
102  if (subElement.Name.ToString().Equals("fabricableitem", StringComparison.OrdinalIgnoreCase))
103  {
104  DebugConsole.ThrowError("Error in item " + item.Name + "! Fabrication recipes should be defined in the craftable item's xml, not in the fabricator.",
105  contentPackage: element.ContentPackage);
106  break;
107  }
108  }
109 
110  var fabricationRecipes = new Dictionary<uint, FabricationRecipe>();
111  foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs)
112  {
113  foreach (FabricationRecipe recipe in itemPrefab.FabricationRecipes.Values)
114  {
115  if (recipe.SuitableFabricatorIdentifiers.Length > 0)
116  {
117  if (!recipe.SuitableFabricatorIdentifiers.Any(i => item.Prefab.Identifier == i || item.HasTag(i)))
118  {
119  continue;
120  }
121  }
122 
123  //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
124  var packageToLog = itemPrefab.GetParentModPackageOrThisPackage();
125 
126  bool recipeInvalid = false;
127  foreach (var requiredItem in recipe.RequiredItems)
128  {
129  if (requiredItem.ItemPrefabs.None())
130  {
131  DebugConsole.ThrowError($"Error in the fabrication recipe for \"{itemPrefab.Name}\". Could not find the ingredient \"{requiredItem}\".",
132  contentPackage: packageToLog);
133  recipeInvalid = true;
134  }
135  }
136  if (recipeInvalid) { continue; }
137 
138  if (fabricationRecipes.TryGetValue(recipe.RecipeHash, out var duplicateRecipe))
139  {
140  DebugConsole.ThrowError($"Error in the fabrication recipe for \"{itemPrefab.Name}\". Duplicate recipe in \"{duplicateRecipe.TargetItem.Identifier}\".",
141  contentPackage: packageToLog);
142  continue;
143  }
144  fabricationRecipes.Add(recipe.RecipeHash, recipe);
145  if (recipe.FabricationLimitMax >= 0)
146  {
147  fabricationLimits.Add(recipe.RecipeHash, Rand.Range(recipe.FabricationLimitMin, recipe.FabricationLimitMax + 1));
148  }
149  }
150  }
151  this.fabricationRecipes = fabricationRecipes.ToImmutableDictionary();
152 
153  state = FabricatorState.Stopped;
154  }
155 
156  public override void OnItemLoaded()
157  {
158  base.OnItemLoaded();
159  var containers = item.GetComponents<ItemContainer>().ToList();
160  if (containers.Count < 2)
161  {
162  DebugConsole.ThrowError("Error in item \"" + item.Name + "\": Fabricators must have two ItemContainer components!");
163  return;
164  }
165 
166  inputContainer = containers[0];
167  outputContainer = containers[1];
168 
169  foreach (var recipe in fabricationRecipes.Values)
170  {
171  if (recipe.RequiredItems.Length > inputContainer.Capacity)
172  {
173  DebugConsole.ThrowErrorLocalized("Error in item \"" + item.Name + "\": There's not enough room in the input inventory for the ingredients of \"" + recipe.TargetItem.Name + "\"!");
174  }
175  }
176 
177  OnItemLoadedProjSpecific();
178  }
179 
180  partial void OnItemLoadedProjSpecific();
181 
182  public override bool Select(Character character)
183  {
184  SelectProjSpecific(character);
185  return base.Select(character);
186  }
187 
188  partial void SelectProjSpecific(Character character);
189 
190  public override bool Pick(Character picker)
191  {
192  return picker != null;
193  }
194 
195  public void RemoveFabricationRecipes(IEnumerable<Identifier> allowedIdentifiers)
196  {
197  fabricationRecipes = fabricationRecipes
198  .Where(kvp => allowedIdentifiers.Contains(kvp.Value.TargetItemPrefabIdentifier))
199  .ToImmutableDictionary();
200 
201  CreateRecipes();
202  }
203 
204  partial void CreateRecipes();
205 
206  private void StartFabricating(FabricationRecipe selectedItem, Character user, bool addToServerLog = true)
207  {
208  if (selectedItem == null) { return; }
209  if (!outputContainer.Inventory.CanBePut(selectedItem.TargetItem, selectedItem.OutCondition * selectedItem.TargetItem.Health)) { return; }
210 
211  IsActive = true;
212  this.user = user;
213  fabricatedItem = selectedItem;
214  RefreshAvailableIngredients();
215 
216 #if CLIENT
217  itemList.Enabled = false;
218  if (amountInput != null)
219  {
220  amountInput.Enabled = false;
221  }
222  RefreshActivateButtonText();
223 #endif
224 
225  bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
226  if (!isClient)
227  {
228  MoveIngredientsToInputContainer(selectedItem);
229  }
230 
231  requiredTime = GetRequiredTime(fabricatedItem, user);
232  timeUntilReady = requiredTime;
233 
234  inputContainer.Inventory.Locked = true;
235  outputContainer.Inventory.Locked = true;
236 
237  if (GameMain.NetworkMember?.IsServer ?? true)
238  {
239  State = FabricatorState.Active;
240  }
241 #if SERVER
242  if (user != null && addToServerLog && selectedItem.RequiredMoney == 0)
243  {
244  if (selectedItem.RequiredMoney > 0)
245  {
246  GameServer.Log($"{GameServer.CharacterLogName(user)} bought {selectedItem.DisplayName.Value} for {selectedItem.RequiredMoney} mk from {item.Name}", ServerLog.MessageType.Money);
247  }
248  else
249  {
250  GameServer.Log($"{GameServer.CharacterLogName(user)} started fabricating {selectedItem.DisplayName.Value} in {item.Name}", ServerLog.MessageType.ItemInteraction);
251  }
252  }
253 #endif
254  }
255 
256  private void CancelFabricating(Character user = null)
257  {
258  IsActive = false;
259  this.user = null;
260  currPowerConsumption = 0.0f;
261 
262  progressState = 0.0f;
263  timeUntilReady = 0.0f;
264  UpdateRequiredTimeProjSpecific();
265  inputContainer.Inventory.Locked = false;
266  outputContainer.Inventory.Locked = false;
267 
268  if (GameMain.NetworkMember?.IsServer ?? true)
269  {
270  State = FabricatorState.Stopped;
271  }
272 
273  if (fabricatedItem == null) { return; }
274 #if SERVER
275  if (user != null)
276  {
277  GameServer.Log(GameServer.CharacterLogName(user) + " cancelled the fabrication of " + fabricatedItem.DisplayName.Value + " in " + item.Name, ServerLog.MessageType.ItemInteraction);
278  }
279 #elif CLIENT
280  itemList.Enabled = true;
281  if (amountInput != null)
282  {
283  amountInput.Enabled = amountTextMax.Enabled;
284  }
285  RefreshActivateButtonText();
286 #endif
287  fabricatedItem = null;
288  }
289 
290  public override void Update(float deltaTime, Camera cam)
291  {
292  if (refreshIngredientsTimer <= 0.0f)
293  {
294  RefreshAvailableIngredients();
295  refreshIngredientsTimer = RefreshIngredientsInterval;
296  }
297  refreshIngredientsTimer -= deltaTime;
298 
299  bool isClient = GameMain.NetworkMember?.IsClient ?? false;
300 
301  if (!isClient)
302  {
303  if (fabricatedItem == null || !CanBeFabricated(fabricatedItem, availableIngredients, user))
304  {
305  CancelFabricating();
306  return;
307  }
308  }
309 
310  progressState = fabricatedItem == null ? 0.0f : (requiredTime - timeUntilReady) / requiredTime;
311 
312  if (isClient)
313  {
314  hasPower = State != FabricatorState.Paused;
315  if (!hasPower)
316  {
317  return;
318  }
319  }
320  else
321  {
322  hasPower = Voltage >= MinVoltage;
323 
324  if (!hasPower)
325  {
326  State = FabricatorState.Paused;
327  return;
328  }
329  State = FabricatorState.Active;
330  }
331 
332  float tinkeringStrength = 0f;
333  var repairable = item.GetComponent<Repairable>();
334  if (repairable != null)
335  {
336  repairable.LastActiveTime = (float)Timing.TotalTime + 10.0f;
337  if (repairable.IsTinkering)
338  {
339  tinkeringStrength = repairable.TinkeringStrength;
340  }
341  }
342 
343  ApplyStatusEffects(ActionType.OnActive, deltaTime);
344 
345 
346  float fabricationSpeedIncrease = 1f + tinkeringStrength * TinkeringSpeedIncrease;
347 
348  timeUntilReady -= deltaTime * fabricationSpeedIncrease * Math.Min(powerConsumption <= 0 ? 1 : Voltage, MaxOverVoltageFactor);
349 
350  UpdateRequiredTimeProjSpecific();
351 
352  if (timeUntilReady <= 0.0f)
353  {
354  Fabricate();
355  }
356  }
357 
358  private Client GetUsingClient()
359  {
360 #if SERVER
361  return GameMain.Server.ConnectedClients.Find(c => c.Character == user);
362 #elif CLIENT
363  return null;
364 #endif
365  }
366 
367  private void Fabricate()
368  {
369  RefreshAvailableIngredients();
370  if (fabricatedItem == null || !CanBeFabricated(fabricatedItem, availableIngredients, user))
371  {
372  CancelFabricating();
373  return;
374  }
375 
376  if (fabricatedItem.RequiredMoney > 0)
377  {
378  if (user == null) { return; }
379  if (GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign)
380  {
381 #if SERVER
382  if (GetUsingClient() is { } client)
383  {
384  mpCampaign.TryPurchase(client, fabricatedItem.RequiredMoney);
385  }
386  else
387  {
388  user.Wallet.Deduct(fabricatedItem.RequiredMoney);
389  }
390 #endif
391  }
392  else if (GameMain.GameSession?.GameMode is CampaignMode campaign)
393  {
394  campaign.Bank.Deduct(fabricatedItem.RequiredMoney);
395  }
396  }
397 
398  bool ingredientsStolen = false;
399  bool ingredientsAllowStealing = true;
400 
401  if (GameMain.NetworkMember is null || GameMain.NetworkMember.IsServer)
402  {
403  List<Item> chosenIngredients = new List<Item>();
404  var suitableIngredients = GetSortedSuitableIngredients();
405 
406  foreach (var requiredItem in fabricatedItem.RequiredItems)
407  {
408  for (int i = 0; i < requiredItem.Amount; i++)
409  {
410  foreach (var suitableIngredient in suitableIngredients)
411  {
412  if (!requiredItem.MatchesItem(suitableIngredient)) { continue; }
413  if (chosenIngredients.Contains(suitableIngredient)) { continue; }
414 
415  ingredientsStolen |= suitableIngredient.StolenDuringRound;
416  if (!suitableIngredient.AllowStealing)
417  {
418  ingredientsAllowStealing = false;
419  }
420 
421  //Leave it behind with reduced condition if it has enough to stay above 0
422  if (requiredItem.UseCondition && suitableIngredient.ConditionPercentage - requiredItem.MinCondition * 100 > 0.0f)
423  {
424  suitableIngredient.Condition -= suitableIngredient.Prefab.Health * requiredItem.MinCondition;
425  break;
426  }
427  if (suitableIngredient.OwnInventory != null)
428  {
429  foreach (Item containedItem in suitableIngredient.OwnInventory.AllItemsMod)
430  {
431  if (suitableIngredient.GetComponent<ItemContainer>()?.RemoveContainedItemsOnDeconstruct ?? false)
432  {
433  Entity.Spawner.AddItemToRemoveQueue(containedItem);
434  }
435  else
436  {
437  containedItem.Drop(dropper: null);
438  }
439  }
440  }
441  chosenIngredients.Add(suitableIngredient);
442  break;
443  }
444  }
445  }
446 
447  var fabricationIngredients = new AbilityFabricationItemIngredients(chosenIngredients);
448  user?.CheckTalents(AbilityEffectType.OnItemFabricatedIngredients, fabricationIngredients);
449 
450  foreach (Item availableItem in fabricationIngredients.Items)
451  {
452  Entity.Spawner.AddItemToRemoveQueue(availableItem);
453  inputContainer.Inventory.RemoveItem(availableItem);
454  }
455 
456  int amountFittingContainer = outputContainer.Inventory.HowManyCanBePut(fabricatedItem.TargetItem, fabricatedItem.OutCondition * fabricatedItem.TargetItem.Health);
457 
458  var fabricationitemAmount = new AbilityFabricationItemAmount(fabricatedItem.TargetItem, fabricatedItem.Amount);
459 
460  int quality = 0;
461  if (fabricatedItem.Quality.HasValue)
462  {
463  quality = fabricatedItem.Quality.Value;
464  }
465  else if (user?.Info != null)
466  {
467  foreach (Character character in Character.GetFriendlyCrew(user))
468  {
469  character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationitemAmount);
470  }
471  user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationitemAmount);
472  quality =
473  fabricatedItem.TargetItem.MaxStackSize > 1 ?
474  GetFabricatedItemQuality(fabricatedItem, user).Quality :
475  GetFabricatedItemQuality(fabricatedItem, user).RollQuality();
476  }
477 
478  int amount = (int)fabricationitemAmount.Value;
479  if (fabricationLimits.ContainsKey(fabricatedItem.RecipeHash))
480  {
481  if (amount > fabricationLimits[fabricatedItem.RecipeHash])
482  {
483  amount = fabricationLimits[fabricatedItem.RecipeHash];
484  fabricationLimits[fabricatedItem.RecipeHash] = 0;
485  }
486  else
487  {
488  fabricationLimits[fabricatedItem.RecipeHash] -= amount;
489  }
490  }
491 
492  var tempUser = user;
493  for (int i = 0; i < amount; i++)
494  {
495  float outCondition = fabricatedItem.OutCondition;
496  if (fabricatedItem.TargetItem.ContentPackage == ContentPackageManager.VanillaCorePackage &&
497  /* we don't need info of every fabricated item, we can get a good sample size just by logging 5% */
498  Rand.Range(0.0f, 1.0f) < 0.05f)
499  {
500  GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + fabricatedItem.TargetItem.Identifier);
501  }
502  if (i < amountFittingContainer)
503  {
504  Entity.Spawner.AddItemToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * outCondition, quality,
505  onSpawned: (Item spawnedItem) =>
506  {
507  onItemSpawned(spawnedItem, tempUser);
508  spawnedItem.Quality = quality;
509  spawnedItem.StolenDuringRound = ingredientsStolen;
510  spawnedItem.AllowStealing = ingredientsAllowStealing;
511  //reset the condition in case the max condition is higher than the prefab's due to e.g. quality modifiers
512  spawnedItem.Condition = spawnedItem.MaxCondition * outCondition;
513  });
514  }
515  else
516  {
517  Entity.Spawner.AddItemToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * outCondition, quality,
518  onSpawned: (Item spawnedItem) =>
519  {
520  onItemSpawned(spawnedItem, tempUser);
521  spawnedItem.Quality = quality;
522  spawnedItem.StolenDuringRound = ingredientsStolen;
523  spawnedItem.AllowStealing = ingredientsAllowStealing;
524  //reset the condition in case the max condition is higher than the prefab's due to e.g. quality modifiers
525  spawnedItem.Condition = spawnedItem.MaxCondition * outCondition;
526  });
527  }
528  }
529 
530  void onItemSpawned(Item spawnedItem, Character user)
531  {
532  if (user != null && user.TeamID != CharacterTeamType.None)
533  {
534  foreach (WifiComponent wifiComponent in spawnedItem.GetComponents<WifiComponent>())
535  {
536  wifiComponent.TeamID = user.TeamID;
537  }
538  }
539  OnItemFabricated?.Invoke(spawnedItem, user);
540  }
541  if (user?.Info != null && !user.Removed)
542  {
543  foreach (Skill skill in fabricatedItem.RequiredSkills)
544  {
545  float addedSkill = skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill;
546  var addedSkillValue = new AbilityFabricatorSkillGain(skill.Identifier, addedSkill);
547  user.CheckTalents(AbilityEffectType.OnItemFabricationSkillGain, addedSkillValue);
548  user.Info.ApplySkillGain(
549  skill.Identifier,
550  addedSkillValue.Value);
551  }
552  }
553 
554  var prevFabricatedItem = fabricatedItem;
555  var prevUser = user;
556  CancelFabricating();
557 
558  amountRemaining--;
559  if (amountRemaining > 0 && CanBeFabricated(prevFabricatedItem, availableIngredients, prevUser))
560  {
561  //keep fabricating if we can fabricate more
562  StartFabricating(prevFabricatedItem, prevUser, addToServerLog: false);
563  }
564  }
565 
566  }
567 
571  public override float GetCurrentPowerConsumption(Connection connection = null)
572  {
573  //No consumption if not powerin or is off
574  if (connection != this.powerIn || !IsActive)
575  {
576  return 0;
577  }
578 
580  item.GetComponent<Repairable>()?.AdjustPowerConsumption(ref currPowerConsumption);
581 
582  return currPowerConsumption;
583  }
584 
585  public static float CalculateBonusRollPercentage(float skillLevel, float target)
586  => Math.Clamp((skillLevel - target) / (100f - target) * 100f, min: 0, max: 100);
587 
588  public readonly record struct QualityResult(int Quality, bool HasRandomQuality, float PlusOnePercentage, float PlusTwoPercentage)
589  {
590  public static readonly QualityResult Empty = new QualityResult(0, true, 0, 0);
591 
592  public bool HasRandomQualityRollChance => HasRandomQuality && (PlusOnePercentage > 0f || PlusTwoPercentage > 0f);
593 
594  // The total real world percentage for a roll to succeed, taking into account that +1 needs to succeed for +2 to be attempted and
595  // that the chance for only +1 goes down as +2 increases since some of the +1's will turn into +2s
596  public float TotalPlusOnePercentage => Math.Clamp(PlusOnePercentage * (100f - PlusTwoPercentage) / 100f, min: 0, max: 100);
597  public float TotalPlusTwoPercentage => Math.Clamp(PlusOnePercentage * PlusTwoPercentage / 100f, min: 0, max: 100);
598 
599  public int RollQuality()
600  {
601  int additionalQuality = 0;
602  if (Roll(PlusOnePercentage))
603  {
604  additionalQuality++;
605  if (Roll(PlusTwoPercentage))
606  {
607  additionalQuality++;
608  }
609  }
610 
611  return Quality + additionalQuality;
612 
613  static bool Roll(float percentage)
614  => percentage >= Rand.Range(0, 100, Rand.RandSync.Unsynced);
615  }
616  }
617 
618  public const int PlusOneQualityBonusThreshold = 50,
619  PlusTwoQualityBonusThreshold = 75;
620 
621  public const int PlusOneTarget = 100,
622  PlusTwoTarget = 125;
623 
624  public const float PlusOneLerp = 0.2f,
625  PlusTwoLerp = 0.4f;
626 
627  private static QualityResult GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user)
628  {
629  if (user?.Info == null) { return QualityResult.Empty; }
630  if (fabricatedItem.TargetItem.ConfigElement.GetChildElement("Quality") == null) { return QualityResult.Empty; }
631  int quality = 0;
632  float floatQuality = 0.0f;
633  floatQuality += user.GetStatValue(StatTypes.IncreaseFabricationQuality, includeSaved: false);
634  foreach (var tag in fabricatedItem.TargetItem.Tags)
635  {
636  floatQuality += user.Info.GetSavedStatValue(StatTypes.IncreaseFabricationQuality, tag);
637  }
638  if (!fabricatedItem.TargetItem.Tags.Contains(fabricatedItem.TargetItem.Identifier))
639  {
640  floatQuality += user.Info.GetSavedStatValue(StatTypes.IncreaseFabricationQuality, fabricatedItem.TargetItem.Identifier);
641  }
642  quality = (int)floatQuality;
643 
644  // 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
645  Option<float> plusOne = Option.None,
646  plusTwo = Option.None;
647 
648  foreach (var skill in fabricatedItem.RequiredSkills)
649  {
650  float skillLevel = user.GetSkillLevel(skill.Identifier);
651 
652  if (skillLevel >= PlusOneQualityBonusThreshold)
653  {
654  //+1 quality chance if the character's skill level is >20% from the min requirement towards max skill as well as higher than 50
655  //e.g. if the skill requirement is 10 -> 28 (but minimum 50 threshold)
656  //40 -> 52
657  //90 -> 92
658  var bonusChance1 = CalculateBonusRollPercentage(skillLevel, MathHelper.Lerp(skill.Level, PlusOneTarget, PlusOneLerp));
659  plusOne = OverrideChanceIfLess(plusOne, bonusChance1);
660 
661  if (skillLevel >= PlusTwoQualityBonusThreshold)
662  {
663  var bonusChance2 = CalculateBonusRollPercentage(skillLevel, MathHelper.Lerp(skill.Level, PlusTwoTarget, PlusTwoLerp));
664  plusTwo = OverrideChanceIfLess(plusTwo, bonusChance2);
665  }
666  else
667  {
668  break;
669  }
670  }
671  else
672  {
673  break;
674  }
675 
676  static Option<float> OverrideChanceIfLess(Option<float> original, float bonusChance)
677  {
678  if (original.TryUnwrap(out var originalChance))
679  {
680  return originalChance > bonusChance ? Option.Some(bonusChance) : original;
681  }
682 
683  return Option.Some(bonusChance);
684  }
685  }
686 
687  bool hasRandomQuality = !(fabricatedItem.TargetItem.MaxStackSize > 1); //don't randomise items with a stacksize > 1
688  float PlusOnePercentage = plusOne.Match(some: static f => f, none: static () => 0f);
689  float PlusTwoPercentage = plusTwo.Match(some: static f => f, none: static () => 0f);
690 
691  if (!hasRandomQuality && PlusOnePercentage > 0)
692  {
693  quality++;
694  if (PlusTwoPercentage > 0)
695  {
696  quality++;
697  }
698  }
699 
700  return new QualityResult(quality,
701  hasRandomQuality,
702  PlusOnePercentage,
703  PlusTwoPercentage);
704  }
705 
706  partial void UpdateRequiredTimeProjSpecific();
707 
708  private static bool AnyOneHasRecipeForItem(Character user, ItemPrefab item)
709  {
710  CharacterType mustHaveRecipe = GameMain.GameSession?.GameMode is { IsSinglePlayer: true } ?
711  //in single player it doesn't matter if it's a bot or a player who has the recipe
712  //(the bots can turn into a "player" when switching characters, and that could interrupt the fabrication)
713  CharacterType.Both :
714  //in MP the recipes other players have don't cound
715  CharacterType.Bot;
716  return
717  (user != null && user.HasRecipeForItem(item.Identifier)) ||
718  GameSession.GetSessionCrewCharacters(mustHaveRecipe).Any(c => c.HasRecipeForItem(item.Identifier));
719  }
720 
721  private readonly HashSet<Item> usedIngredients = new HashSet<Item>();
722 
723  private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary<Identifier, List<Item>> availableIngredients, Character character)
724  {
725  if (fabricableItem == null) { return false; }
726  if (fabricableItem.RequiresRecipe)
727  {
728  if (character == null) { return false; }
729  if (!AnyOneHasRecipeForItem(character, fabricableItem.TargetItem))
730  {
731  return false;
732  }
733  }
734 
735  if (fabricableItem.HideForNonTraitors)
736  {
737  if (character is not { IsTraitor: true }) { return false; }
738  }
739 
740  if (fabricableItem.RequiredMoney > 0)
741  {
742  switch (GameMain.GameSession?.GameMode)
743  {
744  case MultiPlayerCampaign mpCampaign:
745  {
746  if (!mpCampaign.CanAfford(fabricableItem.RequiredMoney, GetUsingClient())) { return false; }
747 
748  break;
749  }
750  case CampaignMode campaign:
751  {
752  if (campaign.Bank.Balance < fabricableItem.RequiredMoney) { return false; }
753 
754  break;
755  }
756  default:
757  return false;
758  }
759  }
760 
761  if (fabricationLimits.TryGetValue(fabricableItem.RecipeHash, out int amount) && amount <= 0)
762  {
763  return false;
764  }
765 
766  //maintain a list of used ingredients so we don't end up considering the same item a suitable for multiple required ingredients
767  usedIngredients.Clear();
768 
769  return fabricableItem.RequiredItems.All(requiredItem =>
770  {
771  int availableItemsAmount = 0;
772  foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs)
773  {
774  if (!availableIngredients.TryGetValue(requiredPrefab.Identifier, out var availableItems)) { continue; }
775 
776  foreach (Item availableItem in availableItems)
777  {
778  if (usedIngredients.Contains(availableItem)) { continue; }
779  if (requiredItem.IsConditionSuitable(availableItem.ConditionPercentage))
780  {
781  usedIngredients.Add(availableItem);
782  availableItemsAmount++;
783  }
784 
785  if (availableItemsAmount >= requiredItem.Amount)
786  {
787  return true;
788  }
789  }
790  }
791 
792  return false;
793  });
794  }
795 
796  private float GetRequiredTime(FabricationRecipe fabricableItem, Character user)
797  {
798  float degreeOfSuccess = FabricationDegreeOfSuccess(user, fabricableItem.RequiredSkills);
799 
800  float t = degreeOfSuccess < 0.5f ? degreeOfSuccess * degreeOfSuccess : degreeOfSuccess * 2;
801 
802  //fabricating takes 100 times longer if degree of success is close to 0
803  //characters with a higher skill than required can fabricate up to 100% faster
804  float time = fabricableItem.RequiredTime / item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.FabricationSpeed, FabricationSpeed) / MathHelper.Clamp(t, 0.01f, 2.0f);
805 
806  if (user?.Info is { } info && fabricableItem.TargetItem is { } it)
807  {
808  time /= 1f + it.Tags.Sum(tag => info.GetSavedStatValue(StatTypes.FabricationSpeed, tag));
809  }
810  return time;
811  }
812 
813  public float FabricationDegreeOfSuccess(Character character, ImmutableArray<Skill> skills)
814  {
815  if (skills.Length == 0) { return 1.0f; }
816  if (character == null) { return 0.0f; }
817 
818  float minDegreeOfSuccess = 1.0f;
819  foreach (var skill in skills)
820  {
821  float characterLevel = character.GetSkillLevel(skill.Identifier);
822  minDegreeOfSuccess = Math.Min(minDegreeOfSuccess, (characterLevel - (skill.Level * SkillRequirementMultiplier) + 100.0f) / 2.0f / 100.0f);
823  }
824  return minDegreeOfSuccess;
825  }
826 
827  public override float GetSkillMultiplier()
828  {
829  return SkillRequirementMultiplier;
830  }
831 
832 
833  private readonly HashSet<Inventory> linkedInventories = new HashSet<Inventory>();
834 
835  private void RefreshAvailableIngredients()
836  {
837  Character user = this.user;
838 #if CLIENT
839  user ??= Character.Controlled;
840 #endif
841  linkedInventories.Clear();
842  List<Item> itemList = new List<Item>();
843  itemList.AddRange(inputContainer.Inventory.AllItems);
844  foreach (MapEntity linkedTo in item.linkedTo)
845  {
846  if (linkedTo is Item linkedItem)
847  {
848  var itemContainer = linkedItem.GetComponent<ItemContainer>();
849  if (itemContainer == null) { continue; }
850  if (user != null)
851  {
852  if (!itemContainer.HasRequiredItems(user, addMessage: false)) { continue; }
853  }
854 
855  var deconstructor = linkedItem.GetComponent<Deconstructor>();
856  if (deconstructor != null)
857  {
858  itemContainer = deconstructor.OutputContainer;
859  }
860 
861  linkedInventories.Add(itemContainer.Inventory);
862  itemList.AddRange(itemContainer.Inventory.AllItems);
863  }
864  }
865  for (int i = 0; i < itemList.Count; i++)
866  {
867  var container = itemList[i].GetComponent<ItemContainer>();
868  if (container != null)
869  {
870  itemList.AddRange(container.Inventory.AllItems);
871  }
872  }
873  if (user?.Inventory != null && user.SelectedItem == item)
874  {
875  itemList.AddRange(user.Inventory.AllItems);
876  linkedInventories.Add(user.Inventory);
877  }
878  foreach (Character c in Character.CharacterList)
879  {
880  //take materials from characters who've selected a linked container too
881  //(e.g. cabinet that's set to display alongside the fabricator UI)
882  if (c.SelectedItem != null &&
883  c.Inventory != null &&
884  linkedInventories.Contains(c.SelectedItem.OwnInventory) &&
885  !linkedInventories.Contains(c.Inventory))
886  {
887  itemList.AddRange(c.Inventory.AllItems);
888  linkedInventories.Add(c.Inventory);
889  }
890  }
891  availableIngredients.Clear();
892  foreach (Item item in itemList)
893  {
894  var itemIdentifier = item.Prefab.Identifier;
895  if (!availableIngredients.ContainsKey(itemIdentifier))
896  {
897  availableIngredients[itemIdentifier] = new List<Item>(itemList.Count);
898  }
899  availableIngredients[itemIdentifier].Add(item);
900  }
901  foreach (var itemId in availableIngredients.Keys)
902  {
903  availableIngredients[itemId] = SortIngredients(availableIngredients[itemId]).ToList();
904  }
905  }
906 
907  private IEnumerable<Item> SortIngredients(IEnumerable<Item> items)
908  {
909  return items
910  .OrderByDescending(getIngredientContainerPriority)
911  .ThenBy(it => it.Prefab.DefaultPrice?.Price ?? 0)
912  .ThenBy(it => MathUtils.IsValid(it.Condition) ? it.Condition : 0)
913  .ThenByDescending(it => it.ParentInventory?.FindIndex(it) ?? 0);
914 
915  int getIngredientContainerPriority(Item item)
916  {
917  if (item.ParentInventory == InputContainer.Inventory)
918  {
919  return 3;
920  }
921  else if (item.ParentInventory is CharacterInventory)
922  {
923  return 2;
924  }
925  return 1;
926  }
927  }
928 
929  private IEnumerable<Item> GetSortedSuitableIngredients()
930  {
931  List<Item> suitableIngredients = new List<Item>();
932  foreach (FabricationRecipe.RequiredItem requiredItem in fabricatedItem.RequiredItems)
933  {
934  foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs)
935  {
936  if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; }
937  var availableItems = availableIngredients[requiredPrefab.Identifier];
938  suitableIngredients.AddRange(
939  availableItems.Where(potentialItem => requiredItem.IsConditionSuitable(potentialItem.ConditionPercentage)));
940  }
941  }
942 
943  return SortIngredients(suitableIngredients);
944  }
945 
950  private void MoveIngredientsToInputContainer(FabricationRecipe targetItem)
951  {
952  List<Item> chosenIngredients = new List<Item>();
953  var suitableIngredients = GetSortedSuitableIngredients();
954 
955  foreach (var requiredItem in targetItem.RequiredItems)
956  {
957  for (int i = 0; i < requiredItem.Amount; i++)
958  {
959  foreach (var suitableIngredient in suitableIngredients)
960  {
961  if (!requiredItem.MatchesItem(suitableIngredient)) { continue; }
962  if (chosenIngredients.Contains(suitableIngredient)) { continue; }
963 
964  //in another inventory, we need to move the item
965  if (suitableIngredient.ParentInventory != inputContainer.Inventory)
966  {
967  if (!inputContainer.Inventory.CanBePut(suitableIngredient))
968  {
969  var unneededItem = inputContainer.Inventory.AllItems.FirstOrDefault(it => !chosenIngredients.Contains(it));
970  unneededItem?.Drop(null);
971  }
972  inputContainer.Inventory.TryPutItem(suitableIngredient, user: null);
973  }
974  chosenIngredients.Add(suitableIngredient);
975  break;
976  }
977  }
978  }
979 
980  RefreshAvailableIngredients();
981  }
982 
983  public override XElement Save(XElement parentElement)
984  {
985  var componentElement = base.Save(parentElement);
986  if (fabricatedItem != null)
987  {
988  componentElement.Add(new XAttribute("fabricateditemidentifier", fabricatedItem.TargetItem.Identifier));
989  componentElement.Add(new XAttribute("savedtimeuntilready", timeUntilReady.ToString("G", CultureInfo.InvariantCulture)));
990  componentElement.Add(new XAttribute("savedrequiredtime", requiredTime.ToString("G", CultureInfo.InvariantCulture)));
991  }
992  return componentElement;
993  }
994 
995  public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
996  {
997  base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
998  savedFabricatedItem = componentElement.GetAttributeString("fabricateditemidentifier", "");
999  savedTimeUntilReady = componentElement.GetAttributeFloat("savedtimeuntilready", 0.0f);
1000  savedRequiredTime = componentElement.GetAttributeFloat("savedrequiredtime", 0.0f);
1001  }
1002 
1003  public override void OnMapLoaded()
1004  {
1005  if (string.IsNullOrEmpty(savedFabricatedItem)) { return; }
1006 
1007  inputContainer?.OnMapLoaded();
1008  outputContainer?.OnMapLoaded();
1009 
1010  var recipe = fabricationRecipes.Values.FirstOrDefault(r => r.TargetItem.Identifier == savedFabricatedItem);
1011  if (recipe == null)
1012  {
1013  DebugConsole.ThrowError("Error while loading a fabricator. Can't continue fabricating \"" + savedFabricatedItem + "\" (matching recipe not found).");
1014  }
1015  else
1016  {
1017 #if CLIENT
1018  SelectItem(null, recipe, savedRequiredTime);
1019 #endif
1020  StartFabricating(recipe, user: null);
1021  timeUntilReady = savedTimeUntilReady;
1022  requiredTime = savedRequiredTime;
1023  }
1024  savedFabricatedItem = null;
1025  }
1026 
1027  protected override void RemoveComponentSpecific()
1028  {
1029  base.RemoveComponentSpecific();
1030  OnItemFabricated = null;
1031  }
1032 
1033  class AbilityFabricatorSkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier
1034  {
1035  public AbilityFabricatorSkillGain(Identifier skillIdentifier, float skillAmount)
1036  {
1037  SkillIdentifier = skillIdentifier;
1038  Value = skillAmount;
1039  }
1040  public float Value { get; set; }
1041  public Identifier SkillIdentifier { get; set; }
1042  }
1043 
1044  class AbilityFabricationItemAmount : AbilityObject, IAbilityValue, IAbilityItemPrefab
1045  {
1046  public AbilityFabricationItemAmount(ItemPrefab itemPrefab, float itemAmount)
1047  {
1048  ItemPrefab = itemPrefab;
1049  Value = itemAmount;
1050  }
1051  public float Value { get; set; }
1052  public ItemPrefab ItemPrefab { get; set; }
1053  }
1054 
1055  internal sealed class AbilityFabricationItemIngredients : AbilityObject
1056  {
1057  public List<Item> Items { get; set; }
1058 
1059  public AbilityFabricationItemIngredients(List<Item> items)
1060  {
1061  Items = items;
1062  }
1063  }
1064  }
1065 }
float GetSkillLevel(string skillIdentifier)
void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
Item????????? SelectedItem
The primary selected item. It can be any device that character interacts with. This excludes items li...
float GetStatValue(StatTypes statType, bool includeSaved=true)
string? GetAttributeString(string key, string? def)
float GetAttributeFloat(string key, float def)
ContentXElement? GetChildElement(string name)
Submarine Submarine
Definition: Entity.cs:53
readonly ImmutableArray< Identifier > SuitableFabricatorIdentifiers
readonly ImmutableArray< RequiredItem > RequiredItems
readonly int FabricationLimitMin
How many of this item the fabricator can create (< 0 = unlimited)
override bool Enabled
Definition: GUIScrollBar.cs:76
static GameSession?? GameSession
Definition: GameMain.cs:88
static NetworkMember NetworkMember
Definition: GameMain.cs:190
static ImmutableHashSet< Character > GetSessionCrewCharacters(CharacterType type)
Returns a list of crew characters currently in the game with a given filter.
virtual IEnumerable< Item > AllItems
All items contained in the inventory. Stacked items are returned as individual instances....
IEnumerable< Item > AllItemsMod
All items contained in the inventory. Allows modifying the contents of the inventory while being enum...
bool CanBePut(Item item)
Can the item be put in the inventory (i.e. is there a suitable free slot or a stack the item can be p...
override string Name
Note that this is not a LocalizedString instance, just the current name of the item as a string....
override bool TryPutItem(Item item, Character user, IEnumerable< InvSlotType > allowedSlots=null, bool createNetworkEvent=true, bool ignoreCondition=false)
If there is room, puts the item in the inventory and returns true, otherwise returns false
override int HowManyCanBePut(ItemPrefab itemPrefab, int i, float? condition, bool ignoreItemsInSlot=false)
static readonly PrefabCollection< ItemPrefab > Prefabs
ImmutableDictionary< uint, FabricationRecipe > FabricationRecipes
ContentPackage GetParentModPackageOrThisPackage()
If the base prefab this one is a variant of is defined in a non-vanilla package, returns that non-van...
override ImmutableHashSet< Identifier > Tags
override void OnItemLoaded()
Called when all the components of the item have been loaded. Use to initialize connections between co...
override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
override float GetCurrentPowerConsumption(Connection connection=null)
Power consumption of the fabricator. Only consume power when active and adjust consumption based on c...
static float CalculateBonusRollPercentage(float skillLevel, float target)
override bool Pick(Character picker)
a Character has picked the item
override void OnMapLoaded()
Called when all items have been loaded. Use to initialize connections between items.
float FabricationDegreeOfSuccess(Character character, ImmutableArray< Skill > skills)
readonly record struct QualityResult(int Quality, bool HasRandomQuality, float PlusOnePercentage, float PlusTwoPercentage)
void ApplyStatusEffects(ActionType type, float deltaTime, Character character=null, Limb targetLimb=null, Entity useTarget=null, Character user=null, Vector2? worldPosition=null, float afflictionMultiplier=1.0f)
override void OnMapLoaded()
Called when all items have been loaded. Use to initialize connections between items.
const float MaxOverVoltageFactor
Maximum voltage factor when the device is being overvolted. I.e. how many times more effectively the ...
float powerConsumption
The maximum amount of power the item can draw from connected items
float currPowerConsumption
The amount of power currently consumed by the item. Negative values mean that the item is providing p...
ContentPackage? ContentPackage
Definition: Prefab.cs:37
readonly Identifier Identifier
Definition: Prefab.cs:34
Interface for entities that the clients can send events to the server
Interface for entities that the server can send events to the clients
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:19
AbilityEffectType
Definition: Enums.cs:125
CharacterType
Definition: Enums.cs:685
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
Definition: Enums.cs:180