using HarmonyLib;
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
/**
* SphereII_FoodSpoilage
*
* This class includes a Harmony patches to enable Food spoilage, including trigger times and delays. The main trigger spoilage code occurs
* on the XUiC_ItemStack, so all stacks of items will be affected, if the right XML is used. This needs to be enabled through the Config/blocks.xml, as well as XML changes
* to food or other items you want to degrade over time.
*
* XML Usage ( Taken from the SphereII Food Spoilage Mod )
*
*
*
* -
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
* ,perkMasterChef
*
*/
public class SphereII_FoodSpoilage {
private const string KeyNextSpoilageTick = "NextSpoilageTick";
private const string KeySpoilageAmount = "SpoilageValue";
private const string PropSpoilable = "Spoilable";
private const string AdvFeatureClass = "FoodSpoilage";
private const string Feature = "FoodSpoilage";
private static readonly bool FoodSpoilage = Configuration.CheckFeatureStatus(AdvFeatureClass, Feature);
private static readonly bool UseAlternateItemValue = Configuration.CheckFeatureStatus(AdvFeatureClass, "UseAlternateItemValue");
[HarmonyPatch(typeof(ItemValue))]
[HarmonyPatch("Clone")]
public class ItemValueClone {
private static void Postfix(ref ItemValue __result, ItemValue __instance) {
if (!FoodSpoilage)
return ;
if (!UseAlternateItemValue)
return ;
if (__instance.ItemClass == null || !__instance.ItemClass.Properties.Contains(PropSpoilable) ||
!__instance.ItemClass.Properties.GetBool(PropSpoilable))
return ;
if (__instance.Metadata == null) return ;
__result.Metadata = new Dictionary();
foreach (var text in __instance.Metadata.Keys)
{
__result.SetMetadata(text, __instance.Metadata[text].Clone());
//__result.Metadata.Add(text, __instance.Metadata[text] ?? __instance.Metadata[text].Clone());
}
}
}
// hook into the ItemStack, which should cover all types of containers. This will run in the update task.
// It is used to calculate the amount of spoilage necessary, and display the amount of freshness is left in the item.
[HarmonyPatch(typeof(XUiC_ItemStack))]
[HarmonyPatch("Update")]
public class FoodSpoilageXUiCItemStackUpdate {
private static float GetCurrentSpoilage(ItemValue itemValue) {
var spoilageAmountObj = itemValue.GetMetadata(KeySpoilageAmount);
if (spoilageAmountObj is float spoilageAmount)
{
return spoilageAmount;
}
itemValue.SetMetadata(KeySpoilageAmount, 1f, TypedMetadataValue.TypeTag.Float);
return 1f;
}
private static float UpdateCurrentSpoilage(ItemValue itemValue, float spoiled) {
var currentSpoilageAmount = GetCurrentSpoilage(itemValue);
currentSpoilageAmount += spoiled;
itemValue.SetMetadata(KeySpoilageAmount, currentSpoilageAmount, TypedMetadataValue.TypeTag.Float);
return currentSpoilageAmount;
}
// Can we skip this item stack?
private static bool IsSkippable(XUiC_ItemStack __instance) {
if (!FoodSpoilage)
return true;
// Don't process creative stacks.
if (__instance.StackLocation == XUiC_ItemStack.StackLocationTypes.Creative)
return true;
// Make sure we are dealing with legitimate stacks.
var instanceItemStack = __instance.ItemStack;
if (instanceItemStack.IsEmpty())
return true;
if (__instance.IsLocked && __instance.IsDragAndDrop)
return true;
// If the item class has a Spoilable property, that means it can spoil over time.
var itemValue = instanceItemStack.itemValue;
var itemClass = itemValue?.ItemClass;
if (itemClass == null || !itemClass.Properties.Contains(PropSpoilable) ||
!itemClass.Properties.GetBool(PropSpoilable))
{
return true;
}
return false;
}
public static bool Prefix(XUiC_ItemStack __instance) {
if (IsSkippable(__instance)) return true;
var itemStack = __instance.ItemStack;
var itemValue = itemStack.itemValue;
var itemClass = itemStack.itemValue.ItemClass;
// Make sure our starting information is correct.
var currentSpoilage = GetCurrentSpoilage(itemValue);
var strDisplay = $"XUiC_ItemStack: {itemClass.GetItemName()} :: {itemStack.count} Slot: {__instance.SlotNumber} ";
var degradationMax = 0f;
var degradationPerUse = 0f;
if (itemClass.Properties.Contains("SpoilageMax"))
degradationMax = itemClass.Properties.GetFloat("SpoilageMax");
if (itemClass.Properties.Contains("SpoilagePerTick"))
degradationPerUse = itemClass.Properties.GetFloat("SpoilagePerTick");
// Check if there's a Global Ticks Per Loss Set
var tickPerLoss = int.Parse(Configuration.GetPropertyValue("FoodSpoilage", "TickPerLoss"));
// Check if there's a item-specific TickPerLoss
if (itemClass.Properties.Contains("TickPerLoss"))
tickPerLoss = itemClass.Properties.GetInt("TickPerLoss");
strDisplay += " Ticks Per Loss: " + tickPerLoss;
var worldTime = GameManager.Instance.World.GetWorldTime();
var nextTick = GetNextSpoilageTick(itemValue);
if (nextTick <= 0)
{
nextTick = CalculateNextSpoilageTick(worldTime, tickPerLoss);
SetNextSpoilageTick(itemValue, nextTick);
}
// Throttles the amount of times it'll trigger the spoilage, based on the TickPerLoss
if (nextTick >= ToInt(worldTime))
{
return true;
}
// How much spoilage to apply
var perUse = degradationPerUse;
var basePerUse = perUse;
strDisplay += " Base Spoil: " + perUse;
float containerValue = 0;
// Additional Spoiler flags to increase or decrease the spoil rate
switch (__instance.StackLocation)
{
case XUiC_ItemStack.StackLocationTypes.ToolBelt: // Tool belt Storage check
containerValue = float.Parse(Configuration.GetPropertyValue("FoodSpoilage", "Toolbelt"));
strDisplay += " Storage Type: Tool Belt ( " + containerValue + " )";
perUse += containerValue;
break;
case XUiC_ItemStack.StackLocationTypes.Backpack: // Back pack storage check
containerValue = float.Parse(Configuration.GetPropertyValue("FoodSpoilage", "Backpack"));
strDisplay += " Storage Type: Backpack ( " + containerValue + " )";
perUse += containerValue;
break;
case XUiC_ItemStack.StackLocationTypes.LootContainer: // Loot Container Storage check
var container = __instance.xui.lootContainer;
if (container != null)
{
var blockValue = GameManager.Instance.World.GetBlock(container.ToWorldPos());
var lootContainerName = Localization.Get(Block.list[blockValue.type].GetBlockName());
strDisplay += " " + lootContainerName;
containerValue = float.Parse(Configuration.GetPropertyValue("FoodSpoilage", "Container"));
strDisplay += " Storage Type: Container ( " + containerValue + " )";
perUse += containerValue;
if (blockValue.Block.Properties.Contains("PreserveBonus"))
{
strDisplay += " Preservation Bonus ( " +
blockValue.Block.Properties.GetFloat("PreserveBonus") + " )";
var preserveBonus = blockValue.Block.Properties.GetFloat("PreserveBonus");
if (preserveBonus == -99f)
return true;
perUse -= preserveBonus;
}
}
else
{
strDisplay += " Storage Type: Container ( Undefined Configuration Block: +10 )";
perUse += 10;
}
break;
case XUiC_ItemStack.StackLocationTypes.Creative: // Ignore Creative Containers
return true;
case XUiC_ItemStack.StackLocationTypes.Equipment:
case XUiC_ItemStack.StackLocationTypes.Vehicle:
case XUiC_ItemStack.StackLocationTypes.Workstation:
case XUiC_ItemStack.StackLocationTypes.Merge:
case XUiC_ItemStack.StackLocationTypes.DewCollector:
default:
containerValue = float.Parse(Configuration.GetPropertyValue("FoodSpoilage", "Container"));
strDisplay += " Storage Type: Generic ( Default Container) ( " + containerValue + " )";
perUse += containerValue;
break;
}
strDisplay += " Spoiled This Tick: " + (perUse - basePerUse);
var minimumSpoilage = float.Parse(Configuration.GetPropertyValue("FoodSpoilage", "MinimumSpoilage"));
minimumSpoilage = Math.Max(0.1f, minimumSpoilage);
// Worse case scenario, no matter what, Spoilage will increment.
if (perUse <= minimumSpoilage)
{
strDisplay += " Minimum spoilage Detected (PerUse: " + perUse + " Minimum: " + minimumSpoilage + " )";
perUse = minimumSpoilage;
}
// Calculate how many Spoils we may have missed over time. If we left our base and came back to our storage box, this will help accurately determine how much
// spoilage should apply.
var temp = "World Time: " + worldTime + " Minus NextSpoilageTick: " + nextTick + " Tick Per Loss: " +
tickPerLoss;
AdvLogging.DisplayLog(AdvFeatureClass, temp);
var totalSpoilageMultiplier = (ToInt(worldTime) - nextTick) / tickPerLoss;
if (totalSpoilageMultiplier == 0)
totalSpoilageMultiplier = 1;
var totalSpoilage = perUse * totalSpoilageMultiplier;
strDisplay += " Spoilage Ticks Missed: " + totalSpoilageMultiplier;
strDisplay += " Total Spoilage: " + totalSpoilage;
// If we don't want any degradation, skip this step.
currentSpoilage = UpdateCurrentSpoilage(itemValue, totalSpoilage);
// Update the NextSpoilageTick value
var nextSpoilageTick = CalculateNextSpoilageTick(worldTime, tickPerLoss);
SetNextSpoilageTick(itemValue, nextSpoilageTick);
strDisplay += " Next Spoilage Tick: " + nextSpoilageTick;
strDisplay += " Recorded Spoilage: " + currentSpoilage;
AdvLogging.DisplayLog(AdvFeatureClass, strDisplay);
// If the spoil time is is greater than the degradation, loop around the stack, removing each layer of items.
while (degradationMax <= currentSpoilage)
{
// If not defined, set the foodRottingFlesh as a spoiled product. Otherwise use the global / item.
var strSpoiledItem = Configuration.GetPropertyValue("FoodSpoilage", "SpoiledItem");
if (string.IsNullOrEmpty(strSpoiledItem))
strSpoiledItem = "foodRottingFlesh";
if (itemClass.Properties.Contains("SpoiledItem"))
strSpoiledItem = itemClass.Properties.GetString("SpoiledItem");
var player = GameManager.Instance.World.GetPrimaryPlayer();
if (player && strSpoiledItem != "None")
{
var count = 1;
var fullStackSpoil = false;
if (itemClass.Properties.Contains("FullStackSpoil"))
fullStackSpoil = itemClass.Properties.GetBool("FullStackSpoil");
if (Configuration.CheckFeatureStatus(AdvFeatureClass, "FullStackSpoil") || fullStackSpoil)
{
AdvLogging.DisplayLog(AdvFeatureClass, itemClass.GetItemName() + ":Full Stack Spoil");
count = itemStack.count;
__instance.ItemStack = new ItemStack(ItemClass.GetItem(strSpoiledItem, false), count);
break;
}
var itemStackSpoiled = new ItemStack(ItemClass.GetItem(strSpoiledItem, false), count);
if (itemStackSpoiled?.itemValue?.ItemClass != null &&
itemStackSpoiled.itemValue.ItemClass.GetItemName() != itemClass.GetItemName())
{
if (!LocalPlayerUI.GetUIForPlayer(player).xui.PlayerInventory.AddItem(itemStackSpoiled, true))
{
player.world.gameManager.ItemDropServer(itemStackSpoiled, player.GetPosition(),
Vector3.zero, -1,
60f, false);
}
}
}
if (itemStack.count > 2)
{
AdvLogging.DisplayLog(AdvFeatureClass, itemClass.GetItemName() + ": Reducing Stack by 1");
itemStack.count--;
currentSpoilage = UpdateCurrentSpoilage(itemValue, -degradationMax);
}
else
{
AdvLogging.DisplayLog(AdvFeatureClass, itemClass.GetItemName() + ": Stack Depleted. Removing.");
__instance.ItemStack = new ItemStack(ItemValue.None.Clone(), 0);
break; // Nothing more to spoil
}
}
__instance.ForceRefreshItemStack();
return true;
}
public static void Postfix(XUiC_ItemStack __instance) {
if (IsSkippable(__instance)) return;
var itemStack = __instance.ItemStack;
var itemClass = itemStack.itemValue.ItemClass;
var degradationMax = 1000f;
if (itemClass.Properties.Contains("SpoilageMax"))
degradationMax = itemClass.Properties.GetFloat("SpoilageMax");
var currentSpoilage = GetCurrentSpoilage(__instance.ItemStack.itemValue);
var perCent = 1f - Mathf.Clamp01(currentSpoilage / degradationMax);
var tierColor = 7 + (int)Math.Round(8 * perCent);
if (tierColor < 0)
tierColor = 0;
if (tierColor > 7)
tierColor = 7;
// allow over-riding of the color.
if (itemClass.Properties.Contains("QualityTierColor"))
tierColor = itemClass.Properties.GetInt("QualityTierColor");
// These used to be fields of the instance, not in A20
var controller = __instance.GetChildById("durability");
if (controller?.ViewComponent is XUiV_Sprite durability)
{
durability.IsVisible = true;
durability.Color = QualityInfo.GetQualityColor(tierColor);
durability.Fill = perCent;
}
controller = __instance.GetChildById("durabilityBackground");
if (controller?.ViewComponent is XUiV_Sprite durabilityBackground)
{
durabilityBackground.IsVisible = true;
}
}
///
/// Calculates the tick for the next loss as a signed integer value.
///
///
///
///
private static int CalculateNextSpoilageTick(ulong worldTime, int ticksPerLoss) {
ulong nextTickActual = worldTime + (ulong)ticksPerLoss;
return ToInt(nextTickActual);
}
///
/// Gets the next spoilage tick from the item value. If not found, returns -1.
///
///
///
private static int GetNextSpoilageTick(ItemValue itemValue) {
var nextSpoilageTickObject = itemValue.GetMetadata(KeyNextSpoilageTick);
if (nextSpoilageTickObject is int nextSpoilageTick)
{
return nextSpoilageTick;
}
return -1;
}
///
/// Sets the next spoilage tick in the item value.
///
///
///
private static void SetNextSpoilageTick(ItemValue itemValue, int nextTick) {
itemValue.SetMetadata(KeyNextSpoilageTick, nextTick, TypedMetadataValue.TypeTag.Integer);
}
///
/// Converts an unsigned long to a signed int by discarding high-order bits.
/// This is "safer" than calling Convert.ToInt32 (which throws an OverflowException)
/// or explicit casting (which results in overflow).
///
///
///
private static int ToInt(ulong uLong) {
return (int)(uLong & int.MaxValue);
}
}
}