Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of ImprovedStations v0.1.1
ImprovedStations.dll
Decompiled 2 hours agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using ImprovedStations.Logic; using Jotunn.Utils; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyVersion("0.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace ImprovedStations { [HarmonyPatch(typeof(Plant), "GetHoverText")] internal static class PlantTimerPatch { private static Func<Plant, float> GetGrowTime; private static bool Prepare() { MethodInfo methodInfo = AccessTools.Method(typeof(Plant), "GetGrowTime", (Type[])null, (Type[])null); if (methodInfo == null) { ImprovedStationsPlugin.Log.LogError((object)"Plant.GetGrowTime not found; plant timers disabled."); return false; } GetGrowTime = AccessTools.MethodDelegate<Func<Plant, float>>(methodInfo, (object)null, true); return true; } [HarmonyPriority(200)] private static void Postfix(Plant __instance, ref string __result, ZNetView ___m_nview, Status ___m_status) { //IL_0029: Unknown result type (might be due to invalid IL or missing references) if (!ImprovedStationsConfig.Plants.Value || string.IsNullOrEmpty(__result) || (Object)(object)___m_nview == (Object)null || !___m_nview.IsValid() || (int)___m_status != 0 || (Object)(object)ZNet.instance == (Object)null) { return; } ZDO zDO = ___m_nview.GetZDO(); if (zDO != null) { long num = zDO.GetLong(ZDOVars.s_plantTime, 0L); if (num != 0L) { string line = StationForecasts.ReadyLine((ZNet.instance.GetTime() - new DateTime(num)).TotalSeconds, GetGrowTime(__instance)); __result = HoverText.InsertAfterFirstLine(__result, line); } } } } [HarmonyPatch(typeof(Fireplace), "GetHoverText")] internal static class FireplaceTimerPatch { [HarmonyPriority(200)] private static void Postfix(Fireplace __instance, ref string __result, ZNetView ___m_nview) { if (ImprovedStationsConfig.Fireplaces.Value && !string.IsNullOrEmpty(__result) && !((Object)(object)___m_nview == (Object)null) && ___m_nview.IsValid() && !__instance.m_infiniteFuel && __instance.IsBurning()) { ZDO zDO = ___m_nview.GetZDO(); if (zDO != null) { string line = StationForecasts.FuelLine(zDO.GetFloat(ZDOVars.s_fuel, 0f), __instance.m_secPerFuel); __result = HoverText.InsertAfterFirstLine(__result, line); } } } } [HarmonyPatch] internal static class ShieldGeneratorTimerPatch { private static IEnumerable<MethodBase> TargetMethods() { yield return AccessTools.Method(typeof(ShieldGenerator), "GetHoverText", (Type[])null, (Type[])null); yield return AccessTools.Method(typeof(ShieldGenerator), "OnHoverAddFuel", (Type[])null, (Type[])null); } [HarmonyPriority(200)] private static void Postfix(ShieldGenerator __instance, ref string __result, ZNetView ___m_nview) { if (!ImprovedStationsConfig.ShieldGenerators.Value || string.IsNullOrEmpty(__result) || (Object)(object)___m_nview == (Object)null || !___m_nview.IsValid() || !__instance.m_enableAttack || (Object)(object)ZNet.instance == (Object)null) { return; } ZDO zDO = ___m_nview.GetZDO(); if (zDO != null) { long num = zDO.GetLong(ZDOVars.s_startTime, 0L); if (num > 0) { string line = StationForecasts.ReadyLine((ZNet.instance.GetTime() - new DateTime(num)).TotalSeconds, __instance.m_attackChargeTime); __result = HoverText.InsertAfterFirstLine(__result, line); } } } } [HarmonyPatch(typeof(Trap), "GetHoverText")] internal static class TrapTimerPatch { [HarmonyPriority(200)] private static void Postfix(Trap __instance, ref string __result, ZNetView ___m_nview) { //IL_004e: Unknown result type (might be due to invalid IL or missing references) if (ImprovedStationsConfig.Traps.Value && !string.IsNullOrEmpty(__result) && !((Object)(object)___m_nview == (Object)null) && ___m_nview.IsValid() && !((Object)(object)ZNet.instance == (Object)null) && __instance.IsCoolingDown() && !__instance.IsArmed() && PrivateArea.CheckAccess(((Component)__instance).transform.position, 0f, false, false)) { ZDO zDO = ___m_nview.GetZDO(); if (zDO != null) { string line = StationForecasts.ReadyLine(ZNet.instance.GetTimeSeconds() - (double)zDO.GetFloat(ZDOVars.s_triggered, 0f), __instance.m_rearmCooldown); __result = HoverText.InsertAfterFirstLine(__result, line); } } } } [HarmonyPatch(typeof(InventoryGui), "UpdateRecipe")] internal static class BulkCraftPatch { private static float? _vanillaDuration; private static void Prefix(InventoryGui __instance) { if (!_vanillaDuration.HasValue) { _vanillaDuration = __instance.m_multiCraftDuration; } __instance.m_multiCraftDuration = BulkCraftRules.Duration(ImprovedStationsConfig.BulkEnabled.Value, ImprovedStationsConfig.BulkCraftSeconds.Value, _vanillaDuration.Value); } } [HarmonyPatch] internal static class CookingTimerPatch { private static Func<CookingStation, bool> IsFireLit; private static bool Prepare() { MethodInfo methodInfo = AccessTools.Method(typeof(CookingStation), "IsFireLit", (Type[])null, (Type[])null); if (methodInfo == null) { ImprovedStationsPlugin.Log.LogError((object)"CookingStation.IsFireLit not found; cooking timers disabled."); return false; } IsFireLit = AccessTools.MethodDelegate<Func<CookingStation, bool>>(methodInfo, (object)null, true); return true; } private static IEnumerable<MethodBase> TargetMethods() { yield return AccessTools.Method(typeof(CookingStation), "HoverText", (Type[])null, (Type[])null); yield return AccessTools.Method(typeof(CookingStation), "OnHoverFuelSwitch", (Type[])null, (Type[])null); } [HarmonyPriority(200)] private static void Postfix(CookingStation __instance, ref string __result, ZNetView ___m_nview) { if (!ImprovedStationsConfig.CookingStations.Value || string.IsNullOrEmpty(__result) || (Object)(object)___m_nview == (Object)null || !___m_nview.IsValid()) { return; } Transform[] slots = __instance.m_slots; if (slots == null || slots.Length == 0) { return; } ZDO zDO = ___m_nview.GetZDO(); if (zDO == null) { return; } List<CookSlot> list = null; bool flag = false; for (int i = 0; i < slots.Length; i++) { string text = zDO.GetString("slot" + i, ""); if (string.IsNullOrEmpty(text)) { continue; } int num = zDO.GetInt("slotstatus" + i, 0); if (num == 2) { continue; } if (num == 0) { flag = true; } ItemConversion val = FindConversion(__instance, text); if (val != null) { if (list == null) { list = new List<CookSlot>(); } list.Add(new CookSlot { Status = num, Cooked = zDO.GetFloat("slot" + i, 0f), CookTime = val.m_cookTime }); } } if (list != null || flag) { float num2 = zDO.GetFloat(ZDOVars.s_fuel, 0f); bool flag2 = (__instance.m_requireFire && IsFireLit(__instance)) || (__instance.m_useFuel && num2 > 0f && (__instance.m_useFueldWhileEmpty || flag)); __result = HoverText.InsertAfterFirstLine(line: (!flag2 && flag) ? StationForecasts.PausedLine((__instance.m_useFuel && num2 <= 0f) ? "no fuel" : "needs fire") : ((!flag2) ? null : ((list != null) ? StationForecasts.CookingLine(list, __instance.m_canOvercookItems) : null)), hover: __result); } } private static ItemConversion FindConversion(CookingStation station, string itemName) { List<ItemConversion> conversion = station.m_conversion; if (conversion == null) { return null; } for (int i = 0; i < conversion.Count; i++) { ItemConversion val = conversion[i]; if (val != null && (((Object)(object)val.m_from != (Object)null && ((Object)((Component)val.m_from).gameObject).name == itemName) || ((Object)(object)val.m_to != (Object)null && ((Object)((Component)val.m_to).gameObject).name == itemName))) { return val; } } return null; } } [HarmonyPatch(typeof(Fermenter), "GetHoverText")] internal static class FermenterTimerPatch { [HarmonyPriority(200)] private static void Postfix(Fermenter __instance, ref string __result, ZNetView ___m_nview, bool ___m_exposed, bool ___m_hasRoof) { //IL_0039: Unknown result type (might be due to invalid IL or missing references) if (ImprovedStationsConfig.Fermenters.Value && !((Object)(object)___m_nview == (Object)null) && ___m_nview.IsValid() && !((Object)(object)ZNet.instance == (Object)null || ___m_exposed) && ___m_hasRoof && PrivateArea.CheckAccess(((Component)__instance).transform.position, 0f, false, false)) { ZDO zDO = ___m_nview.GetZDO(); if (zDO.GetInt(ZDOVars.s_content, 0) != 0) { long num = zDO.GetLong(ZDOVars.s_startTime, 0L); string line = StationForecasts.ReadyLine((num == 0L) ? (-1.0) : (ZNet.instance.GetTime() - new DateTime(num)).TotalSeconds, __instance.m_fermentationDuration); __result = HoverText.InsertAfterFirstLine(__result, line); } } } } internal static class ImprovedStationsConfig { public static ConfigEntry<bool> BulkEnabled; public static ConfigEntry<float> BulkCraftSeconds; public static ConfigEntry<bool> Fermenters; public static ConfigEntry<bool> Smelters; public static ConfigEntry<bool> CookingStations; public static ConfigEntry<bool> Beehives; public static ConfigEntry<bool> SapCollectors; public static ConfigEntry<bool> Plants; public static ConfigEntry<bool> Fireplaces; public static ConfigEntry<bool> ShieldGenerators; public static ConfigEntry<bool> Traps; public static void Bind(ConfigFile config) { BulkEnabled = config.Bind<bool>("1 - Bulk crafting", "Enabled", true, AdminOnly("Shift+Craft makes the whole batch in one short progress bar.")); BulkCraftSeconds = config.Bind<float>("1 - Bulk crafting", "BulkCraftSeconds", 2f, AdminOnly("Seconds one Shift+Craft bulk craft takes (vanilla is 6).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 6f))); Fermenters = config.Bind<bool>("2 - Station timers", "Fermenters", true, "Show the time left on fermenters in the hover text."); Smelters = config.Bind<bool>("2 - Station timers", "Smelters", true, "Show the time left on smelters, kilns and furnaces in the hover text."); CookingStations = config.Bind<bool>("2 - Station timers", "CookingStations", true, "Show the time left on cooking stations and ovens in the hover text."); Beehives = config.Bind<bool>("2 - Station timers", "Beehives", true, "Show the time left on beehives in the hover text."); SapCollectors = config.Bind<bool>("2 - Station timers", "SapCollectors", true, "Show the time left on sap collectors in the hover text."); Plants = config.Bind<bool>("2 - Station timers", "Plants", true, "Show the time until planted crops are ripe in the hover text."); Fireplaces = config.Bind<bool>("2 - Station timers", "Fireplaces", true, "Show how long the fuel lasts on fireplaces, torches and braziers in the hover text."); ShieldGenerators = config.Bind<bool>("2 - Station timers", "ShieldGenerators", true, "Show the attack charge time on shield generators in the hover text."); Traps = config.Bind<bool>("2 - Station timers", "Traps", true, "Show the re-arm time on traps in the hover text."); } private static ConfigDescription AdminOnly(string description, AcceptableValueBase range = null) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Expected O, but got Unknown //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Expected O, but got Unknown return new ConfigDescription(description, range, new object[1] { (object)new ConfigurationManagerAttributes { IsAdminOnly = true } }); } } [BepInPlugin("com.robert.improvedstations", "ImprovedStations", "0.1.1")] [BepInDependency(/*Could not decode attribute arguments.*/)] [NetworkCompatibility(/*Could not decode attribute arguments.*/)] public class ImprovedStationsPlugin : BaseUnityPlugin { internal static ManualLogSource Log; private Harmony _harmony; private void Awake() { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; ImprovedStationsConfig.Bind(((BaseUnityPlugin)this).Config); _harmony = new Harmony("com.robert.improvedstations"); Type[] typesFromAssembly = AccessTools.GetTypesFromAssembly(typeof(ImprovedStationsPlugin).Assembly); foreach (Type type in typesFromAssembly) { try { _harmony.CreateClassProcessor(type).Patch(); } catch (Exception arg) { Log.LogError((object)$"Failed to apply patch {type.Name}: {arg}"); } } } private void OnDestroy() { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } } internal static class ProducerProgress { public static float Current(ZDO zdo) { float num = zdo.GetFloat(ZDOVars.s_product, 0f); long num2 = zdo.GetLong(ZDOVars.s_lastTime, 0L); if (num2 > 0 && (Object)(object)ZNet.instance != (Object)null) { num += (float)Math.Max(0.0, (ZNet.instance.GetTime() - new DateTime(num2)).TotalSeconds); } return num; } } [HarmonyPatch(typeof(Beehive), "GetHoverText")] internal static class BeehiveTimerPatch { private const float CacheSeconds = 2f; private static Func<Beehive, bool> CheckBiome; private static Func<Beehive, bool> HaveFreeSpace; private static int cachedId; private static float cachedAt = float.NegativeInfinity; private static bool cachedBiomeOk; private static bool cachedSpaceOk; private static bool Prepare() { MethodInfo methodInfo = AccessTools.Method(typeof(Beehive), "CheckBiome", (Type[])null, (Type[])null); MethodInfo methodInfo2 = AccessTools.Method(typeof(Beehive), "HaveFreeSpace", (Type[])null, (Type[])null); if (methodInfo == null || methodInfo2 == null) { ImprovedStationsPlugin.Log.LogError((object)"Beehive.CheckBiome or Beehive.HaveFreeSpace not found; beehive timers disabled."); return false; } CheckBiome = AccessTools.MethodDelegate<Func<Beehive, bool>>(methodInfo, (object)null, true); HaveFreeSpace = AccessTools.MethodDelegate<Func<Beehive, bool>>(methodInfo2, (object)null, true); return true; } [HarmonyPriority(200)] private static void Postfix(Beehive __instance, ref string __result, ZNetView ___m_nview) { //IL_002f: Unknown result type (might be due to invalid IL or missing references) if (!ImprovedStationsConfig.Beehives.Value || string.IsNullOrEmpty(__result) || (Object)(object)___m_nview == (Object)null || !___m_nview.IsValid() || !PrivateArea.CheckAccess(((Component)__instance).transform.position, 0f, false, false)) { return; } ZDO zDO = ___m_nview.GetZDO(); if (zDO == null) { return; } int num = zDO.GetInt(ZDOVars.s_level, 0); if (num < __instance.m_maxHoney) { int instanceID = ((Object)__instance).GetInstanceID(); if (instanceID != cachedId || Time.time - cachedAt > 2f) { cachedBiomeOk = CheckBiome(__instance); cachedSpaceOk = cachedBiomeOk && HaveFreeSpace(__instance); cachedId = instanceID; cachedAt = Time.time; } __result = HoverText.InsertAfterFirstLine(line: (!cachedBiomeOk) ? StationForecasts.PausedLine("wrong biome") : (cachedSpaceOk ? StationForecasts.ProducerLine(__instance.m_secPerUnit, ProducerProgress.Current(zDO), num, __instance.m_maxHoney) : StationForecasts.PausedLine("needs open space")), hover: __result); } } } [HarmonyPatch(typeof(SapCollector), "GetHoverText")] internal static class SapCollectorTimerPatch { [HarmonyPriority(200)] private static void Postfix(SapCollector __instance, ref string __result, ZNetView ___m_nview, ResourceRoot ___m_root) { if (!ImprovedStationsConfig.SapCollectors.Value || string.IsNullOrEmpty(__result) || (Object)(object)___m_nview == (Object)null || !___m_nview.IsValid() || (Object)(object)___m_root == (Object)null) { return; } ZDO zDO = ___m_nview.GetZDO(); if (zDO != null) { int num = zDO.GetInt(ZDOVars.s_level, 0); if (num < __instance.m_maxLevel) { string line = ((!___m_root.CanDrain(1f)) ? StationForecasts.PausedLine("no sap left") : StationForecasts.ProducerLine(__instance.m_secPerUnit, ProducerProgress.Current(zDO), num, __instance.m_maxLevel)); __result = HoverText.InsertAfterFirstLine(__result, line); } } } } [HarmonyPatch] internal static class SmelterTimerPatch { private static IEnumerable<MethodBase> TargetMethods() { yield return AccessTools.Method(typeof(Smelter), "OnHoverAddOre", (Type[])null, (Type[])null); yield return AccessTools.Method(typeof(Smelter), "OnHoverAddFuel", (Type[])null, (Type[])null); yield return AccessTools.Method(typeof(Smelter), "OnHoverEmptyOre", (Type[])null, (Type[])null); } [HarmonyPriority(200)] private static void Postfix(Smelter __instance, ref string __result, ZNetView ___m_nview, bool ___m_haveRoof, bool ___m_blockedSmoke) { if (ImprovedStationsConfig.Smelters.Value && !((Object)(object)___m_nview == (Object)null) && ___m_nview.IsValid()) { ZDO zDO = ___m_nview.GetZDO(); Windmill windmill = __instance.m_windmill; SmelterState smelterState = new SmelterState { SecPerProduct = __instance.m_secPerProduct, BakeTimer = zDO.GetFloat(ZDOVars.s_bakeTimer, 0f), Queued = zDO.GetInt(ZDOVars.s_queued, 0), NoInputNeeded = (__instance.m_maxOre == 0), UsesFuel = (__instance.m_maxFuel > 0), Fuel = zDO.GetFloat(ZDOVars.s_fuel, 0f), FuelPerProduct = __instance.m_fuelPerProduct, Power = (((Object)(object)windmill != (Object)null) ? ((double)windmill.GetPowerOutput()) : 1.0), Approximate = ((Object)(object)windmill != (Object)null) }; __result = HoverText.InsertAfterFirstLine(line: (smelterState.Queued <= 0 && !smelterState.NoInputNeeded) ? null : ((__instance.m_requiresRoof && !___m_haveRoof) ? StationForecasts.PausedLine("needs roof") : (___m_blockedSmoke ? StationForecasts.PausedLine("smoke blocked") : ((smelterState.UsesFuel && smelterState.Fuel <= 0.0) ? StationForecasts.PausedLine("no fuel") : ((!((Object)(object)windmill != (Object)null) || !(smelterState.Power <= 0.0)) ? StationForecasts.SmelterLine(smelterState) : StationForecasts.PausedLine("no wind or covered"))))), hover: __result); } } } internal static class ModInfo { public const string Guid = "com.robert.improvedstations"; public const string Name = "ImprovedStations"; public const string Version = "0.1.1"; } } namespace ImprovedStations.Logic { public static class BulkCraftRules { public static float Duration(bool enabled, float configured, float vanilla) { if (!enabled || !(configured > 0f)) { return vanilla; } return configured; } } public static class HoverText { public static string InsertAfterFirstLine(string hover, string line) { if (string.IsNullOrEmpty(hover) || string.IsNullOrEmpty(line)) { return hover; } int num = hover.IndexOf('\n'); if (num < 0) { return hover + "\n" + line; } return hover.Substring(0, num) + "\n" + line + hover.Substring(num); } } public sealed class SmelterState { public double SecPerProduct; public double BakeTimer; public double Fuel; public double Power; public int Queued; public int FuelPerProduct; public bool NoInputNeeded; public bool UsesFuel; public bool Approximate; } public sealed class CookSlot { public const int NotDone = 0; public const int Done = 1; public const int Burnt = 2; public int Status; public double Cooked; public double CookTime; } public static class StationForecasts { public static string CookingLine(IEnumerable<CookSlot> slots, bool canOvercook) { bool flag = false; bool flag2 = false; double num = 0.0; double num2 = 0.0; double num3 = 0.0; foreach (CookSlot slot in slots) { if (slot.Status == 0) { double num4 = slot.CookTime - slot.Cooked; if (!flag || num4 < num) { num = num4; } if (!flag || num4 > num2) { num2 = num4; } flag = true; } else if (slot.Status == 1 && canOvercook) { double num5 = slot.CookTime * 2.0 - slot.Cooked; if (!flag2 || num5 < num3) { num3 = num5; } flag2 = true; } } string text = null; if (flag) { text = ((TimerFormat.Duration(num) == TimerFormat.Duration(num2)) ? ("Done in " + TimerFormat.Duration(num)) : ("Next in " + TimerFormat.Duration(num) + ", all in " + TimerFormat.Duration(num2))); } string text2 = (flag2 ? ("<color=orange>Burns in " + TimerFormat.Duration(num3) + "</color>") : null); if (text != null && text2 != null) { return text + "\n" + text2; } return text ?? text2; } public static string SmelterLine(SmelterState s) { if (s.SecPerProduct <= 0.0 || s.Power <= 0.0) { return null; } if (!s.NoInputNeeded && s.Queued <= 0) { return null; } double secPerProduct = s.SecPerProduct; double num = Math.Max(0.0, secPerProduct - s.BakeTimer); int num2 = (s.NoInputNeeded ? 1 : s.Queued); double num3 = num + (double)(num2 - 1) * secPerProduct; string prefix = (s.Approximate ? "~" : ""); Func<double, string> func = (double progress) => prefix + TimerFormat.Duration(progress / s.Power); if (s.NoInputNeeded) { return "Next in " + func(num); } if (s.UsesFuel && s.FuelPerProduct > 0) { double num4 = s.Fuel * secPerProduct / (double)s.FuelPerProduct; if (num4 < num3) { int num5 = ((!(num4 < num)) ? (1 + (int)Math.Floor((num4 - num) / secPerProduct)) : 0); string text = " lasts " + func(num4) + " (" + num5 + " of " + num2 + " finish)"; if (num5 == 0) { return "Fuel" + text; } return "Next in " + func(num) + ", fuel" + text; } } if (num2 == 1) { return "Done in " + func(num); } return "Next in " + func(num) + ", all in " + func(num3); } public static string ReadyLine(double elapsed, double duration) { if (elapsed < 0.0 || elapsed > duration) { return null; } return "Ready in " + TimerFormat.Duration(duration - elapsed); } public static string FuelLine(double fuel, double secPerFuel) { if (fuel <= 0.0 || secPerFuel <= 0.0) { return null; } return "Fuel lasts " + TimerFormat.Duration(fuel * secPerFuel); } public static string ProducerLine(double secPerUnit, double product, int level, int maxLevel) { if (level >= maxLevel) { return null; } double num = Math.Max(0.0, secPerUnit - product); int num2 = maxLevel - level - 1; if (num2 <= 0) { return "Next in " + TimerFormat.Duration(num); } double seconds = num + (double)num2 * secPerUnit; return "Next in " + TimerFormat.Duration(num) + ", full in " + TimerFormat.Duration(seconds); } public static string PausedLine(string reason) { return "<color=orange>Paused: " + reason + "</color>"; } } public static class TimerFormat { public static string Duration(double seconds) { long num = (long)Math.Ceiling(Math.Max(0.0, seconds)); if (num < 60) { return num + "s"; } if (num < 3600) { return num / 60 + "m " + (num % 60).ToString("00") + "s"; } return num / 3600 + "h " + (num % 3600 / 60).ToString("00") + "m"; } } }