From 1c6d2aaae0e76b4b89e16198a4f7fdc5d3ab1704 Mon Sep 17 00:00:00 2001 From: ExSlam Date: Mon, 19 Jan 2026 19:14:37 -0500 Subject: [PATCH 01/33] fixed senbatsu infix function; it now uses the root of triangular number formula --- mods/Unofficial Patch/Unofficial Patch.cs | 54 +++++++++++------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/mods/Unofficial Patch/Unofficial Patch.cs b/mods/Unofficial Patch/Unofficial Patch.cs index 1f8791d..153297d 100755 --- a/mods/Unofficial Patch/Unofficial Patch.cs +++ b/mods/Unofficial Patch/Unofficial Patch.cs @@ -324,35 +324,35 @@ public static IEnumerable Transpiler(IEnumerable= idolCount + // + // r = ceil((sqrt(8N + 1) - 1) / 2) + float n = idolCount; + float r = (Mathf.Sqrt(8f * n + 1f) - 1f) / 2f; + + int rowsUsed = Mathf.CeilToInt(r); + + // Clamp to the real formation size: + // Anything above 15 idols still just uses all 5 rows. + rowsUsed = Mathf.Clamp(rowsUsed, 1, totalRows); + + // The game wants a "percentage per used row" kind of factor. + return 100f / rowsUsed; } + } From cdde5a57267cc19476a1094f2f8b768414399e7b Mon Sep 17 00:00:00 2001 From: ExSlam Date: Mon, 19 Jan 2026 20:33:06 -0500 Subject: [PATCH 02/33] target both opcodes call and callvirt in GetRevenue --- mods/Unofficial Patch/Unofficial Patch.cs | 84 ++++++++++++++--------- 1 file changed, 50 insertions(+), 34 deletions(-) diff --git a/mods/Unofficial Patch/Unofficial Patch.cs b/mods/Unofficial Patch/Unofficial Patch.cs index 153297d..35974f7 100755 --- a/mods/Unofficial Patch/Unofficial Patch.cs +++ b/mods/Unofficial Patch/Unofficial Patch.cs @@ -244,42 +244,58 @@ public static void Postfix(ref Relationships._relationship __instance) } } - // Fixed Concert revenue formula so that it shows accurate estimated values + // Fixed Concert revenue formula so that it shows accurate estimated values [HarmonyPatch(typeof(SEvent_Concerts._concert._projectedValues), "GetRevenue")] - public class SEvent_Concerts__concert__projectedValues_GetRevenue - { - public static IEnumerable Transpiler(IEnumerable instructions) - { - List instructionList = new List(instructions); - - for (int i = 0; i < instructionList.Count; i++) - { - if (instructionList[i].opcode == OpCodes.Call && (MethodInfo)instructionList[i].operand == AccessTools.Method(typeof(SEvent_Concerts._concert._projectedValues), "GetHype")) - { - instructionList[i].operand = AccessTools.Method(typeof(SEvent_Concerts__concert__projectedValues_GetRevenue), "Infix"); - break; - } - } + public class SEvent_Concerts__concert__projectedValues_GetRevenue + { + public static IEnumerable Transpiler(IEnumerable instructions) + { + // Copy the IL stream so we can edit it in-place. + var list = new List(instructions); + + // Locate the original GetHype call and our replacement method. + MethodInfo getHype = AccessTools.Method(typeof(SEvent_Concerts._concert._projectedValues), "GetHype"); + MethodInfo infix = AccessTools.Method(typeof(SEvent_Concerts__concert__projectedValues_GetRevenue), nameof(Infix)); + + for (int i = 0; i < list.Count; i++) + { + // Find the first call to GetHype in the IL. + if ((list[i].opcode == OpCodes.Call || list[i].opcode == OpCodes.Callvirt) && + list[i].operand is MethodInfo mi && mi == getHype) + { + // Swap to our Infix method to apply the adjusted hype curve. + list[i].opcode = OpCodes.Call; // force static call + list[i].operand = infix; + break; + } + } + + // Return the modified IL stream. + return list; + } + + public static float Infix(SEvent_Concerts._concert._projectedValues __this) + { + // Start with the game's base hype calculation. + float hype = __this.GetHype(); + + if (hype > 1f) + { + // Avoid target-typed new() for max compatibility + LinearFunction._function function = new LinearFunction._function(); + // Configure a linear mapping with points (0, 0.5) and (1, 0.25). + function.Init(0f, 0.5f, 1f, 0.25f); + + // Convert "hype above 1" into a scaled bonus, then re-add the baseline. + float num2 = hype - 1f; + hype = num2 * function.GetY(num2) + 1f; + } + + // Return the adjusted hype value. + return hype; + } + } - return instructionList.AsEnumerable(); - } - - public static float Infix(SEvent_Concerts._concert._projectedValues __this) - { - - float hype = __this.GetHype(); - if (hype > 1) - { - LinearFunction._function function = new(); - function.Init(0f, 0.5f, 1f, 0.25f); - - float num2 = hype - 1f; - hype = num2 * function.GetY(num2) + 1f; - } - - return hype; - } - } // Fixed Concert revenue formula so that it shows accurate estimated values From 437795af83bc42020341f1963761aec623b1f079 Mon Sep 17 00:00:00 2001 From: ExSlam Date: Mon, 19 Jan 2026 20:51:02 -0500 Subject: [PATCH 03/33] added fix: senbatsu stats calculation doesn't use just cute and checks the correct parameter for accurate results --- mods/Unofficial Patch/Unofficial Patch.cs | 103 ++++++++++++++++------ 1 file changed, 74 insertions(+), 29 deletions(-) diff --git a/mods/Unofficial Patch/Unofficial Patch.cs b/mods/Unofficial Patch/Unofficial Patch.cs index 35974f7..403f1f7 100755 --- a/mods/Unofficial Patch/Unofficial Patch.cs +++ b/mods/Unofficial Patch/Unofficial Patch.cs @@ -314,11 +314,11 @@ public static bool Prefix(ref float _val) // Fixed senbatsu stats calculation so it doesn't punish you if you don't have enough idols to fill all rows [HarmonyPatch(typeof(singles._single), "SenbatsuCalcParam")] - public class singles__single_SenbatsuCalcParam - { - public static IEnumerable Transpiler(IEnumerable instructions) - { - List instructionList = new List(instructions); + public class singles__single_SenbatsuCalcParam + { + public static IEnumerable Transpiler(IEnumerable instructions) + { + List instructionList = new List(instructions); int index = -1; for (int i = 0; i < instructionList.Count; i++) @@ -366,16 +366,39 @@ public static float Infix(int idolCount) rowsUsed = Mathf.Clamp(rowsUsed, 1, totalRows); // The game wants a "percentage per used row" kind of factor. - return 100f / rowsUsed; - } - - } - - - // Dating status is visible for underage members - [HarmonyPatch(typeof(data_girls.girls), "GetPartnerString")] - public class data_girls_girls_GetPartnerString - { + return 100f / rowsUsed; + } + + } + + // Fix senbatsu parameter queries to use the requested param type. + [HarmonyPatch(typeof(singles._single), "GetSenbatsuParamValue")] + public class singles__single_GetSenbatsuParamValue + { + // Cache the private calculator so we can call it with the correct param type. + private static readonly MethodInfo SenbatsuCalcParam = AccessTools.Method( + typeof(singles._single), + "SenbatsuCalcParam", + new Type[] { typeof(List), typeof(data_girls._paramType), typeof(Groups._group) }); + + public static bool Prefix(singles._single __instance, data_girls._paramType Type, ref float __result) + { + // Fall back to vanilla behavior if reflection fails. + if (SenbatsuCalcParam == null) + return true; + + // Compute the value using the requested param type (instead of always "cute"). + var param = (data_girls.girls.param)SenbatsuCalcParam.Invoke(__instance, new object[] { __instance.girls, Type, null }); + __result = param.val; + return false; + } + } + + + // Dating status is visible for underage members + [HarmonyPatch(typeof(data_girls.girls), "GetPartnerString")] + public class data_girls_girls_GetPartnerString + { public static IEnumerable Transpiler(IEnumerable instructions) { List instructionList = new List(instructions); @@ -435,11 +458,11 @@ public static void Postfix(ref List __result, data_girls.gi // Fixed event and dialogue checks for Influence to check for Influence instead of Friendship [HarmonyPatch(typeof(vn_requirements), "CheckGirl", new Type[] { typeof(data_girls.girls), typeof(string), typeof(string) })] - public class vn_requirements_CheckGirl - { - public static IEnumerable Transpiler(IEnumerable instructions) - { - List instructionList = new(instructions); + public class vn_requirements_CheckGirl + { + public static IEnumerable Transpiler(IEnumerable instructions) + { + List instructionList = new(instructions); bool breakFlag = false; for (int i = 0; i < instructionList.Count; i++) @@ -460,15 +483,37 @@ public static IEnumerable Transpiler(IEnumerable Date: Mon, 19 Jan 2026 22:32:15 -0500 Subject: [PATCH 04/33] refactored out all magic numbers --- mods/Unofficial Patch/Unofficial Patch.cs | 1089 +++++++++++++++------ 1 file changed, 809 insertions(+), 280 deletions(-) diff --git a/mods/Unofficial Patch/Unofficial Patch.cs b/mods/Unofficial Patch/Unofficial Patch.cs index 403f1f7..d207d0d 100755 --- a/mods/Unofficial Patch/Unofficial Patch.cs +++ b/mods/Unofficial Patch/Unofficial Patch.cs @@ -10,108 +10,409 @@ namespace UnofficialPatch { - [HarmonyPatch(typeof(Profile_Fans_Pies), "Render_Pies")] + // Centralized logging utilities for Unofficial Patch patches. + internal static class PatchLog + { + // Shared logging prefix for Unofficial Patch output. + private const string LogPrefix = "[UnofficialPatch] "; + // Marker used when Harmony metadata cannot be read. + private const string UnknownTarget = "UnknownTarget"; + // Empty-count sentinel for collection checks. + private const int EmptyCount = 0; + + // Protects the once-only warning cache. + private static readonly object OnceLock = new object(); + // Tracks warnings that were already emitted to prevent log spam. + private static readonly HashSet OnceKeys = new HashSet(); + + // Logs a warning for the given patch type. + public static void Warn(string message) + { + Warn(typeof(TPatch), message); + } + + // Logs a warning once per patch + message. + public static void WarnOnce(string message) + { + WarnOnce(typeof(TPatch), message); + } + + // Logs a warning once per patch type, regardless of message. + public static void WarnOncePerPatch(string message) + { + WarnOncePerPatch(typeof(TPatch), message); + } + + // Logs a warning for the given patch type, including the resolved Harmony target if available. + public static void Warn(Type patchType, string message) + { + bool usedFallback; + string target = GetTargetName(patchType, out usedFallback); + string prefix = LogPrefix + target; + if (usedFallback && patchType != null) + { + prefix += " (patch: " + patchType.Name + ")"; + } + Debug.LogWarning(prefix + ": " + message); + } + + // Logs a warning once per patch + message combination. + public static void WarnOnce(Type patchType, string message) + { + string key = GetOnceKey(patchType, message); + lock (OnceLock) + { + if (OnceKeys.Contains(key)) + return; + OnceKeys.Add(key); + } + Warn(patchType, message); + } + + // Logs a warning once per patch type to avoid repeated spam. + public static void WarnOncePerPatch(Type patchType, string message) + { + string key = GetOnceKey(patchType, string.Empty); + lock (OnceLock) + { + if (OnceKeys.Contains(key)) + return; + OnceKeys.Add(key); + } + Warn(patchType, message); + } + + // Builds a readable Harmony target name, falling back to UnknownTarget if metadata is missing. + private static string GetTargetName(Type patchType, out bool usedFallback) + { + usedFallback = true; + if (patchType == null) + return UnknownTarget; + + object[] attrs = patchType.GetCustomAttributes(typeof(HarmonyPatch), true); + if (attrs == null || attrs.Length == EmptyCount) + return UnknownTarget; + + List targets = new List(); + foreach (object attr in attrs) + { + string target = GetTargetName(attr); + if (!string.IsNullOrEmpty(target)) + { + targets.Add(target); + } + } + + if (targets.Count == EmptyCount) + return UnknownTarget; + + usedFallback = false; + return string.Join(", ", targets.Distinct()); + } + + // Extracts HarmonyPatch info without hard dependency on a specific Harmony version. + private static string GetTargetName(object patchAttribute) + { + if (patchAttribute == null) + return null; + + object info = GetMember(patchAttribute, "info"); + Type declaringType = GetMember(info, "declaringType") ?? GetMember(patchAttribute, "declaringType"); + string methodName = GetMember(info, "methodName") ?? GetMember(patchAttribute, "methodName"); + Type[] argumentTypes = GetMember(info, "argumentTypes") ?? GetMember(patchAttribute, "argumentTypes"); + object methodType = GetMember(info, "methodType") ?? GetMember(patchAttribute, "methodType"); + + string typeName = declaringType != null ? (declaringType.FullName ?? declaringType.Name) : null; + string signature = FormatArgs(argumentTypes); + + if (!string.IsNullOrEmpty(typeName) && !string.IsNullOrEmpty(methodName)) + return typeName + "." + methodName + signature; + + if (!string.IsNullOrEmpty(typeName)) + return typeName; + + if (!string.IsNullOrEmpty(methodName)) + return methodName + signature; + + if (methodType != null) + return methodType.ToString(); + + return null; + } + + // Reads a private field/property by name using reflection. + private static object GetMember(object instance, string name) + { + if (instance == null) + return null; + + Type type = instance.GetType(); + const BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + PropertyInfo property = type.GetProperty(name, flags); + if (property != null) + return property.GetValue(instance, null); + + FieldInfo field = type.GetField(name, flags); + if (field != null) + return field.GetValue(instance); + + return null; + } + + // Strongly-typed wrapper around GetMember. + private static T GetMember(object instance, string name) where T : class + { + return GetMember(instance, name) as T; + } + + // Formats argument type lists to help log target signatures. + private static string FormatArgs(Type[] argumentTypes) + { + if (argumentTypes == null) + return string.Empty; + if (argumentTypes.Length == EmptyCount) + return "()"; + return "(" + string.Join(", ", argumentTypes.Select(t => t != null ? t.Name : "null")) + ")"; + } + + // Builds a stable cache key for once-only warnings. + private static string GetOnceKey(Type patchType, string message) + { + string typeName = patchType != null ? patchType.FullName : "null"; + return typeName + "|" + message; + } + } + + // Shared IL helper predicates to keep opcode checks consistent and reusable. + internal static class IlHelpers + { + // Identifies call/callvirt instructions targeting a specific method. + public static bool IsCallTo(CodeInstruction instruction, MethodInfo target) + { + if (instruction == null || target == null) + return false; + + if (instruction.opcode != OpCodes.Call && instruction.opcode != OpCodes.Callvirt) + return false; + + return instruction.operand is MethodInfo method && method == target; + } + + // Checks whether an instruction loads an int constant with any ldc.i4 opcode form. + public static bool IsLdcI4(CodeInstruction instruction, int value) + { + if (instruction == null) + return false; + + // Match the specific ldc.i4 opcode variants that encode constant -1..8 directly. + if (instruction.opcode == OpCodes.Ldc_I4_M1) + return value == -1; + if (instruction.opcode == OpCodes.Ldc_I4_0) + return value == 0; + if (instruction.opcode == OpCodes.Ldc_I4_1) + return value == 1; + if (instruction.opcode == OpCodes.Ldc_I4_2) + return value == 2; + if (instruction.opcode == OpCodes.Ldc_I4_3) + return value == 3; + if (instruction.opcode == OpCodes.Ldc_I4_4) + return value == 4; + if (instruction.opcode == OpCodes.Ldc_I4_5) + return value == 5; + if (instruction.opcode == OpCodes.Ldc_I4_6) + return value == 6; + if (instruction.opcode == OpCodes.Ldc_I4_7) + return value == 7; + if (instruction.opcode == OpCodes.Ldc_I4_8) + return value == 8; + if (instruction.opcode == OpCodes.Ldc_I4_S) + return instruction.operand is sbyte sbyteValue && sbyteValue == value; + if (instruction.opcode == OpCodes.Ldc_I4) + return instruction.operand is int intValue && intValue == value; + + return false; + } + + // Checks whether an instruction loads a local variable. + public static bool IsLdloc(CodeInstruction instruction) + { + if (instruction == null) + return false; + + return instruction.opcode == OpCodes.Ldloc || + instruction.opcode == OpCodes.Ldloc_S || + instruction.opcode == OpCodes.Ldloc_0 || + instruction.opcode == OpCodes.Ldloc_1 || + instruction.opcode == OpCodes.Ldloc_2 || + instruction.opcode == OpCodes.Ldloc_3; + } + + // Checks whether an instruction stores a local variable. + public static bool IsStloc(CodeInstruction instruction) + { + if (instruction == null) + return false; + + return instruction.opcode == OpCodes.Stloc || + instruction.opcode == OpCodes.Stloc_S || + instruction.opcode == OpCodes.Stloc_0 || + instruction.opcode == OpCodes.Stloc_1 || + instruction.opcode == OpCodes.Stloc_2 || + instruction.opcode == OpCodes.Stloc_3; + } + + // Checks for conditional branches that jump when the stack value is true. + public static bool IsBranchTrue(CodeInstruction instruction) + { + if (instruction == null) + return false; + + return instruction.opcode == OpCodes.Brtrue || instruction.opcode == OpCodes.Brtrue_S; + } + } + + // Fixes fan pie rendering so the adult slice accounts for YA/Teen stacking. + [HarmonyPatch(typeof(Profile_Fans_Pies), "Render_Pies")] public class Profile_Fans_Pies_Render_Pies { + // No-fan sentinel for early exit. + private const long NoFans = 0L; - // Fixed fan pie charts so that it is correct + // Fixes age-pie rendering so adult slice reflects the residual after YA/Teen allocation. public static void Postfix(Profile_Fans_Pies __instance, data_girls.girls ___Girl) { - if (___Girl.GetFans_Total() == 0L) + // Skip when there are no fans to render, avoiding divide-by-zero ratios in vanilla code. + if (___Girl.GetFans_Total() == NoFans) return; - __instance.Fans_Pie_Adult.GetComponent().fillAmount += __instance.Fans_Pie_YA.GetComponent().fillAmount - __instance.Fans_Pie_Teen.GetComponent().fillAmount; + // Adjust the adult slice to correct the stacked slice math. + __instance.Fans_Pie_Adult.GetComponent().fillAmount += + __instance.Fans_Pie_YA.GetComponent().fillAmount - + __instance.Fans_Pie_Teen.GetComponent().fillAmount; } - } - [HarmonyPatch(typeof(Tour_New_Popup), "Render")] + // Keeps tour expected revenue text color aligned with profitability. + [HarmonyPatch(typeof(Tour_New_Popup), "Render")] public class Tour_New_Popup_Render { - // Fixed tour revenue text color to consider savings + // Keeps the expected revenue color consistent by always evaluating profitability after savings. public static void Postfix(ref Tour_New_Popup __instance) { - if (__instance.Tour.ExpectedRevenue <= __instance.Tour.ProductionCost - __instance.Tour.Saving) - return; - - ExtensionMethods.SetColor(__instance.ExpectedRevenue, mainScript.green32); + // Apply savings before comparing against expected revenue. + long effectiveCost = __instance.Tour.ProductionCost - __instance.Tour.Saving; + // Positive profit should be green; losses should be red. + bool profitable = __instance.Tour.ExpectedRevenue > effectiveCost; + ExtensionMethods.SetColor(__instance.ExpectedRevenue, profitable ? mainScript.green32 : mainScript.red32); } } - [HarmonyPatch(typeof(Theaters), "GetStaminaCost")] + // Restores stamina costs for theater schedules. + [HarmonyPatch(typeof(Theaters), "GetStaminaCost")] public class Theaters_GetStaminaCost { - // Fixed Theater so that it uses stamina. + // Stamina costs for theater schedules. + private const float NoStaminaCost = 0f; + private const float PerformanceStaminaCost = 5f; + private const float ManzaiStaminaCost = 2f; + private const float HardModeMultiplier = 2f; + + // Fixes the vanilla method which always returned 0. public static void Postfix(Theaters._theater._schedule._type Type, ref float __result) { - float num = 0f; + // Start with no stamina cost by default. + float staminaCost = NoStaminaCost; if (Type == Theaters._theater._schedule._type.performance) { - num = 5f; + staminaCost = PerformanceStaminaCost; } else if (Type == Theaters._theater._schedule._type.manzai) { - num = 2f; + staminaCost = ManzaiStaminaCost; } + // Hard mode doubles the cost. if (staticVars.IsHard()) { - num *= 2f; + staminaCost *= HardModeMultiplier; } - __result = num; + // Return the corrected stamina cost. + __result = staminaCost; } } - [HarmonyPatch(typeof(Theaters), "CompleteDay")] + // Aligns theater revenue timing and payout distribution with the schedule. + [HarmonyPatch(typeof(Theaters), "CompleteDay")] public class Theaters_CompleteDay { - // Fixed Theater so that revenue stats are not offset by one day + // Day-of-month used by the base game for subscription revenue. + private const int FirstDayOfMonth = 1; + // Sentinel values for revenue and counts. + private const long NoRevenue = 0L; + private const int NoGirls = 0; + // Offset for accessing the latest stats entry. + private const int LastIndexOffset = 1; + // Parameters for the floating money UI icon. + private const float MoneyFloatStartScale = 0f; + private const float MoneyFloatEndScale = 1f; + private const float MoneyFloatDelay = 0f; + + // Ensures Doing_Now reflects today's schedule before the base method runs. public static bool Prefix() { foreach (Theaters._theater theater in Theaters.Theaters_) { - // Fix so that auto schedules contribute revenue on the day of + // Force the schedule selection so auto schedules pay out today. theater.Doing_Now = theater.GetSchedule().Type; } return true; } - // Fixed Theater so that average stats ignore days off, and so that girls earnings are increased by revenue + // Fixes revenue accounting and income distribution after the base method completes. public static void Postfix() { foreach (Theaters._theater theater in Theaters.Theaters_) { - // Fix so that auto schedules contribute revenue on the day of + // Ensure auto schedules pay out on the same day when they convert to performance/manzai. if (theater.GetSchedule().Type == Theaters._theater._schedule._type.auto && (theater.Doing_Now == Theaters._theater._schedule._type.manzai || theater.Doing_Now == Theaters._theater._schedule._type.performance)) { long rev = theater.GetTicketSales(); - if (rev > 0) resources.Add(resources.type.money, rev); - if (staticVars.dateTime.Day != 1) + if (rev > NoRevenue) + { + // Add the earned revenue to player resources. + resources.Add(resources.type.money, rev); + } + if (staticVars.dateTime.Day != FirstDayOfMonth) { - theater.GetRoom().addFloat(Floats.type.icon_money, "", true, null, 0f, 1f, 0f, null); + // Show a money float icon on non-subscription days. + theater.GetRoom().addFloat(Floats.type.icon_money, "", true, null, MoneyFloatStartScale, MoneyFloatEndScale, MoneyFloatDelay, null); } } - // Fix so that days off have no revenue + // Days off should contribute zero revenue to stats. if (theater.Doing_Now == Theaters._theater._schedule._type.day_off) { - theater.Stats[theater.Stats.Count - 1].Revenue = 0; + theater.Stats[theater.Stats.Count - LastIndexOffset].Revenue = NoRevenue; } + // Split ticket/subscription revenue among participating girls. if (theater.Doing_Now == Theaters._theater._schedule._type.performance || theater.Doing_Now == Theaters._theater._schedule._type.manzai) { - int num2 = theater.GetGroup().GetGirls(true, false, null).Count; - long num = theater.GetTicketSales(); - if (theater.AreSubsUnlocked() && staticVars.dateTime.Day == 1) - { - num += theater.GetSubRevenue(); - } - foreach (data_girls.girls girls2 in theater.GetGroup().GetGirls(true, false, null)) + List girls = theater.GetGroup().GetGirls(true, false, null); + int girlCount = girls.Count; + long num = theater.GetTicketSales(); + if (theater.AreSubsUnlocked() && staticVars.dateTime.Day == FirstDayOfMonth) + { + // Include subscription revenue on the monthly payout day. + num += theater.GetSubRevenue(); + } + foreach (data_girls.girls girls2 in girls) { - if (num > 0L && num2 > 0) + if (num > NoRevenue && girlCount > NoGirls) { - girls2.Earn(num / (long)num2); + // Divide revenue evenly across current participants. + girls2.Earn(num / (long)girlCount); } } } @@ -123,33 +424,41 @@ public static void Postfix() [HarmonyPatch(typeof(Theaters._theater), "GetAvgAttendance")] public class Theaters__theater_GetAvgAttendance { + // Rolling window size for averages. + private const int DaysInWeek = 7; + private const int NoStats = 0; + private const int NoDaysCounted = 0; + private const int LastIndexOffset = 1; + private const float ZeroAverage = 0f; + public static void Postfix(ref int __result, Theaters._theater __instance) { - float num = 0f; - float num2 = 7f; - if (__instance.Stats.Count == 0) + // Skip if there are no stats to average. + if (__instance.Stats.Count == NoStats) return; - if (__instance.Stats.Count < 7) - { - num2 = __instance.Stats.Count; - } - int num3 = __instance.Stats.Count - 1; - int num4 = 0; - while (num3 >= __instance.Stats.Count - num2) - { - if (__instance.Stats[num3].Schedule.Type != Theaters._theater._schedule._type.day_off) - { - num += __instance.Stats[num3].Attendance; - num4++; - } - num3--; - } - if (num4 != 0) - { - num /= num4; - } - __result = Mathf.RoundToInt(num); + // Only inspect the most recent week (or fewer days if not enough data). + int daysToCheck = Mathf.Min(__instance.Stats.Count, DaysInWeek); + float totalAttendance = ZeroAverage; + int countedDays = NoDaysCounted; + int index = __instance.Stats.Count - LastIndexOffset; + while (index >= __instance.Stats.Count - daysToCheck) + { + // Ignore day-off entries so the average reflects performance days. + if (__instance.Stats[index].Schedule.Type != Theaters._theater._schedule._type.day_off) + { + totalAttendance += __instance.Stats[index].Attendance; + countedDays++; + } + index--; + } + // Only divide if at least one valid day was counted. + if (countedDays != NoDaysCounted) + { + totalAttendance /= countedDays; + } + // Return a rounded attendance average. + __result = Mathf.RoundToInt(totalAttendance); } } @@ -158,33 +467,41 @@ public static void Postfix(ref int __result, Theaters._theater __instance) [HarmonyPatch(typeof(Theaters._theater), "GetAvgRevenue")] public class Theaters__theater_GetAvgRevenue { + // Rolling window size for averages. + private const int DaysInWeek = 7; + private const int NoStats = 0; + private const int NoDaysCounted = 0; + private const int LastIndexOffset = 1; + private const float ZeroAverage = 0f; + public static void Postfix(ref int __result, Theaters._theater __instance) { - float num = 0f; - float num2 = 7f; - if (__instance.Stats.Count == 0) + // Skip if there are no stats to average. + if (__instance.Stats.Count == NoStats) return; - if (__instance.Stats.Count < 7) - { - num2 = __instance.Stats.Count; - } - int num3 = __instance.Stats.Count - 1; - int num4 = 0; - while (num3 >= __instance.Stats.Count - num2) - { - if (__instance.Stats[num3].Schedule.Type != Theaters._theater._schedule._type.day_off) - { - num += __instance.Stats[num3].Revenue; - num4++; - } - num3--; - } - if (num4 != 0) - { - num /= num4; - } - __result = Mathf.RoundToInt(num); + // Only inspect the most recent week (or fewer days if not enough data). + int daysToCheck = Mathf.Min(__instance.Stats.Count, DaysInWeek); + float totalRevenue = ZeroAverage; + int countedDays = NoDaysCounted; + int index = __instance.Stats.Count - LastIndexOffset; + while (index >= __instance.Stats.Count - daysToCheck) + { + // Ignore day-off entries so the average reflects earning days. + if (__instance.Stats[index].Schedule.Type != Theaters._theater._schedule._type.day_off) + { + totalRevenue += __instance.Stats[index].Revenue; + countedDays++; + } + index--; + } + // Only divide if at least one valid day was counted. + if (countedDays != NoDaysCounted) + { + totalRevenue /= countedDays; + } + // Return a rounded revenue average. + __result = Mathf.RoundToInt(totalRevenue); } } @@ -193,20 +510,29 @@ public static void Postfix(ref int __result, Theaters._theater __instance) [HarmonyPatch(typeof(Theaters), "GetLastWeekEarning")] public class Theaters_GetLastWeekEarning { + // Tooltip is intended to show a full week. + private const int DaysInWeek = 7; + // Approximate weeks per month used by the base game for sub revenue. + private const float SubRevenueWeeksPerMonth = 4.35f; + public static void Postfix(ref long __result) { + // Start with the base value from the game. long output = __result; foreach (Theaters._theater theater in Theaters.Theaters_) { - if (theater.Stats.Count >= 7) + // Add the missing 7th day for each theater. + if (theater.Stats.Count >= DaysInWeek) { - output += theater.Stats[theater.Stats.Count - 7].Revenue; + output += theater.Stats[theater.Stats.Count - DaysInWeek].Revenue; } if (theater.AreSubsUnlocked()) { - output += (long)Mathf.Round(theater.GetSubRevenue() / 4.35f); + // Include subscription revenue spread across an average month. + output += (long)Mathf.Round(theater.GetSubRevenue() / SubRevenueWeeksPerMonth); } } + // Return the corrected tooltip total. __result = output; } } @@ -215,16 +541,22 @@ public static void Postfix(ref long __result) [HarmonyPatch(typeof(Cafes), "GetLastWeekEarning")] public class Cafes_GetLastWeekEarning { + // Tooltip is intended to show a full week. + private const int DaysInWeek = 7; + public static void Postfix(ref int __result) { + // Start with the base value from the game. int output = __result; foreach (Cafes._cafe cafe in Cafes.Cafes_) { - if (cafe.Stats.Count >= 7) + // Add the missing 7th day for each cafe. + if (cafe.Stats.Count >= DaysInWeek) { - output += cafe.Stats[cafe.Stats.Count - 7].Profit; + output += cafe.Stats[cafe.Stats.Count - DaysInWeek].Profit; } } + // Return the corrected tooltip total. __result = output; } } @@ -234,67 +566,82 @@ public static void Postfix(ref int __result) [HarmonyPatch(typeof(Relationships._relationship), "BreakUp")] public class Relationships__relationship_BreakUp { + // Indices for the two relationship participants. + private const int FirstPartnerIndex = 0; + private const int SecondPartnerIndex = 1; + public static void Postfix(ref Relationships._relationship __instance) { + // Only clear known status when the pair was actually dating. if (!__instance.Dating) return; - __instance.Girls[0].DatingData.Is_Partner_Status_Known = false; - __instance.Girls[1].DatingData.Is_Partner_Status_Known = false; + // Hide partner status for both sides after breakup. + __instance.Girls[FirstPartnerIndex].DatingData.Is_Partner_Status_Known = false; + __instance.Girls[SecondPartnerIndex].DatingData.Is_Partner_Status_Known = false; } } // Fixed Concert revenue formula so that it shows accurate estimated values [HarmonyPatch(typeof(SEvent_Concerts._concert._projectedValues), "GetRevenue")] - public class SEvent_Concerts__concert__projectedValues_GetRevenue - { - public static IEnumerable Transpiler(IEnumerable instructions) - { - // Copy the IL stream so we can edit it in-place. - var list = new List(instructions); - - // Locate the original GetHype call and our replacement method. - MethodInfo getHype = AccessTools.Method(typeof(SEvent_Concerts._concert._projectedValues), "GetHype"); - MethodInfo infix = AccessTools.Method(typeof(SEvent_Concerts__concert__projectedValues_GetRevenue), nameof(Infix)); - - for (int i = 0; i < list.Count; i++) - { - // Find the first call to GetHype in the IL. - if ((list[i].opcode == OpCodes.Call || list[i].opcode == OpCodes.Callvirt) && - list[i].operand is MethodInfo mi && mi == getHype) - { - // Swap to our Infix method to apply the adjusted hype curve. - list[i].opcode = OpCodes.Call; // force static call - list[i].operand = infix; - break; - } - } - - // Return the modified IL stream. - return list; - } - - public static float Infix(SEvent_Concerts._concert._projectedValues __this) - { - // Start with the game's base hype calculation. - float hype = __this.GetHype(); - - if (hype > 1f) - { - // Avoid target-typed new() for max compatibility - LinearFunction._function function = new LinearFunction._function(); - // Configure a linear mapping with points (0, 0.5) and (1, 0.25). - function.Init(0f, 0.5f, 1f, 0.25f); - - // Convert "hype above 1" into a scaled bonus, then re-add the baseline. - float num2 = hype - 1f; - hype = num2 * function.GetY(num2) + 1f; - } - - // Return the adjusted hype value. - return hype; - } - } + public class SEvent_Concerts__concert__projectedValues_GetRevenue + { + // Hype curve configuration used by the adjusted revenue formula. + private const float HypeBaseline = 1f; + private const float LinearPointX0 = 0f; + private const float LinearPointY0 = 0.5f; + private const float LinearPointX1 = 1f; + private const float LinearPointY1 = 0.25f; + + public static IEnumerable Transpiler(IEnumerable instructions) + { + // Locate the original GetHype call and our replacement method. + MethodInfo getHype = AccessTools.Method(typeof(SEvent_Concerts._concert._projectedValues), "GetHype"); + MethodInfo infix = AccessTools.Method(typeof(SEvent_Concerts__concert__projectedValues_GetRevenue), nameof(Infix)); + + // Abort if Harmony lookup fails so we don't corrupt IL. + if (getHype == null || infix == null) + { + PatchLog.WarnOncePerPatch("GetHype/Infix method lookup failed."); + return instructions; + } + + var matcher = new CodeMatcher(instructions); + matcher.MatchForward(false, new CodeMatch(ci => IlHelpers.IsCallTo(ci, getHype))); + + if (matcher.IsInvalid) + { + PatchLog.WarnOncePerPatch("GetHype call not found."); + return instructions; + } + + // Swap to our Infix method to apply the adjusted hype curve. + matcher.SetOpcodeAndOperand(OpCodes.Call, infix); + return matcher.InstructionEnumeration(); + } + + public static float Infix(SEvent_Concerts._concert._projectedValues __this) + { + // Start with the game's base hype calculation. + float hype = __this.GetHype(); + + // Only reshape hype above the baseline (1.0). + if (hype > HypeBaseline) + { + // Avoid target-typed new() for max compatibility + LinearFunction._function function = new LinearFunction._function(); + // Configure a linear mapping with points (0, 0.5) and (1, 0.25). + function.Init(LinearPointX0, LinearPointY0, LinearPointX1, LinearPointY1); + + // Convert "hype above 1" into a scaled bonus, then re-add the baseline. + float excessHype = hype - HypeBaseline; + hype = excessHype * function.GetY(excessHype) + HypeBaseline; + } + + // Return the adjusted hype value. + return hype; + } + } @@ -302,11 +649,15 @@ public static float Infix(SEvent_Concerts._concert._projectedValues __this) [HarmonyPatch(typeof(SEvent_Concerts._concert._projectedValues), "GetString")] public class SEvent_Concerts__concert__projectedValues_GetString { + // Ratio cap corresponding to 100%. + private const float MaxRatio = 1f; + public static bool Prefix(ref float _val) { - if (_val >= 99.5) + // Clamp the ratio so the display never exceeds 100%. + if (_val > MaxRatio) { - _val = 99; + _val = MaxRatio; } return true; } @@ -314,42 +665,88 @@ public static bool Prefix(ref float _val) // Fixed senbatsu stats calculation so it doesn't punish you if you don't have enough idols to fill all rows [HarmonyPatch(typeof(singles._single), "SenbatsuCalcParam")] - public class singles__single_SenbatsuCalcParam - { - public static IEnumerable Transpiler(IEnumerable instructions) - { - List instructionList = new List(instructions); + public class singles__single_SenbatsuCalcParam + { + // Percent scaling constant used by the base formula. + private const float PercentScale = 100f; + // IL pattern length for the 100f / rows divisor. + private const int DivPatternLength = 4; + // Senbatsu row limits and guard values. + private const int NoIdols = 0; + private const int MinRows = 1; + private const int MaxRows = 5; + private const int FirstIndex = 0; + // Constants for triangular number inversion. + private const float TriangularScale = 8f; + private const float TriangularOffset = 1f; + private const float TriangularDivisor = 2f; + private const float ZeroPercent = 0f; - int index = -1; - for (int i = 0; i < instructionList.Count; i++) + public static IEnumerable Transpiler(IEnumerable instructions) + { + // Resolve the infix method that computes percent based on actual filled rows. + MethodInfo infix = AccessTools.Method(typeof(singles__single_SenbatsuCalcParam), nameof(Infix)); + if (infix == null) { - if (instructionList[i].opcode == OpCodes.Stloc_2) - { - index = i; - break; - } + PatchLog.WarnOncePerPatch("Infix method lookup failed."); + return instructions; } - if (index != -1) + var matcher = new CodeMatcher(instructions); + // Find the sequence that divides 100f by the row count (num2). + matcher.MatchForward(false, + new CodeMatch(ci => ci.opcode == OpCodes.Ldc_R4 && ci.operand is float value && value == PercentScale), + new CodeMatch(ci => IlHelpers.IsLdloc(ci)), + new CodeMatch(OpCodes.Conv_R4), + new CodeMatch(OpCodes.Div)); + + if (matcher.IsInvalid) { - instructionList.Insert(index + 1, new CodeInstruction(OpCodes.Ldloc_0)); - instructionList.Insert(index + 2, new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(singles__single_SenbatsuCalcParam), "Infix"))); - instructionList.Insert(index + 3, new CodeInstruction(OpCodes.Stloc_2)); + PatchLog.WarnOncePerPatch("100/rows divisor not found."); + return instructions; } - return instructionList.AsEnumerable(); + // Ensure the divisor result is immediately stored in a local. + CodeInstruction storeInstruction = matcher.InstructionAt(DivPatternLength); + if (storeInstruction == null || !IlHelpers.IsStloc(storeInstruction)) + { + PatchLog.WarnOncePerPatch("100/rows divisor found, but store opcode not found."); + return instructions; + } + + // Replace 100f / num2 with Infix(_girls) to derive rows from actual filled slots. + var labels = matcher.Instruction.labels.ToList(); + var blocks = matcher.Instruction.blocks.ToList(); + matcher.RemoveInstructions(DivPatternLength); + // Ldarg_1 loads the _girls list from the original SenbatsuCalcParam signature. + var loadGirls = new CodeInstruction(OpCodes.Ldarg_1); + loadGirls.labels.AddRange(labels); + loadGirls.blocks.AddRange(blocks); + matcher.Insert(loadGirls, new CodeInstruction(OpCodes.Call, infix)); + return matcher.InstructionEnumeration(); } - public static float Infix(int idolCount) + public static float Infix(List girls) { + // Count only filled slots to determine how many rows are actually used. + int idolCount = NoIdols; + if (girls != null) + { + for (int i = FirstIndex; i < girls.Count; i++) + { + if (girls[i] != null) + { + idolCount++; + } + } + } + // Total rows in the senbatsu formation: // 1, 2, 3, 4, 5 (total capacity = 15) - const int totalRows = 5; - // Safety: if no idols, don't divide by zero. // (The game probably never passes 0, but this prevents Infinity/NaN.) - if (idolCount <= 0) - return 0f; + if (idolCount <= NoIdols) + return ZeroPercent; // Triangular number inversion: // Assume that r represents the minimum required number of rows to fit all our idols represented by n @@ -357,64 +754,99 @@ public static float Infix(int idolCount) // // r = ceil((sqrt(8N + 1) - 1) / 2) float n = idolCount; - float r = (Mathf.Sqrt(8f * n + 1f) - 1f) / 2f; + float r = (Mathf.Sqrt(TriangularScale * n + TriangularOffset) - TriangularOffset) / TriangularDivisor; - int rowsUsed = Mathf.CeilToInt(r); + // Round up to the next whole row to ensure all idols fit. + int rowsUsed = Mathf.CeilToInt(r); // Clamp to the real formation size: // Anything above 15 idols still just uses all 5 rows. - rowsUsed = Mathf.Clamp(rowsUsed, 1, totalRows); + rowsUsed = Mathf.Clamp(rowsUsed, MinRows, MaxRows); // The game wants a "percentage per used row" kind of factor. - return 100f / rowsUsed; - } - - } - - // Fix senbatsu parameter queries to use the requested param type. - [HarmonyPatch(typeof(singles._single), "GetSenbatsuParamValue")] - public class singles__single_GetSenbatsuParamValue - { - // Cache the private calculator so we can call it with the correct param type. - private static readonly MethodInfo SenbatsuCalcParam = AccessTools.Method( - typeof(singles._single), - "SenbatsuCalcParam", - new Type[] { typeof(List), typeof(data_girls._paramType), typeof(Groups._group) }); - - public static bool Prefix(singles._single __instance, data_girls._paramType Type, ref float __result) - { - // Fall back to vanilla behavior if reflection fails. - if (SenbatsuCalcParam == null) - return true; - - // Compute the value using the requested param type (instead of always "cute"). - var param = (data_girls.girls.param)SenbatsuCalcParam.Invoke(__instance, new object[] { __instance.girls, Type, null }); - __result = param.val; - return false; - } - } - - - // Dating status is visible for underage members - [HarmonyPatch(typeof(data_girls.girls), "GetPartnerString")] - public class data_girls_girls_GetPartnerString - { + return PercentScale / rowsUsed; + } + + } + + // Fix senbatsu parameter queries to use the requested param type. + [HarmonyPatch(typeof(singles._single), "GetSenbatsuParamValue")] + public class singles__single_GetSenbatsuParamValue + { + // Cache the private calculator so we can call it with the correct param type. + private static readonly MethodInfo SenbatsuCalcParam = AccessTools.Method( + typeof(singles._single), + "SenbatsuCalcParam", + new Type[] { typeof(List), typeof(data_girls._paramType), typeof(Groups._group) }); + + public static bool Prefix(singles._single __instance, data_girls._paramType Type, ref float __result) + { + // Fall back to vanilla behavior if reflection fails. + if (SenbatsuCalcParam == null) + return true; + + try + { + // Compute the value using the requested param type (instead of always "cute"). + var param = (data_girls.girls.param)SenbatsuCalcParam.Invoke(__instance, new object[] { __instance.girls, Type, null }); + __result = param.val; + return false; + } + catch (Exception ex) + { + // Log once so repeated failures do not spam the log. + PatchLog.WarnOncePerPatch("failed: " + ex); + return true; + } + } + } + + + // Dating status is visible for underage members + [HarmonyPatch(typeof(data_girls.girls), "GetPartnerString")] + public class data_girls_girls_GetPartnerString + { + // Offset to the instruction following the method call. + private const int NextInstructionOffset = 1; + public static IEnumerable Transpiler(IEnumerable instructions) { - List instructionList = new List(instructions); + // Resolve the AOC check used by the early return in the base method. + MethodInfo isAoc = AccessTools.Method(typeof(data_girls.girls), "Is_AOC"); + if (isAoc == null) + { + PatchLog.WarnOncePerPatch("Is_AOC method lookup failed."); + return instructions; + } + + var matcher = new CodeMatcher(instructions); + // Find the Is_AOC call that guards the early return. + matcher.MatchForward(false, new CodeMatch(ci => IlHelpers.IsCallTo(ci, isAoc))); - for (int i = 0; i < instructionList.Count; i++) + if (matcher.IsInvalid) { - if (instructionList[i].opcode == OpCodes.Ret) - { - instructionList[i - 1].opcode = OpCodes.Nop; - instructionList[i].opcode = OpCodes.Nop; - break; - } + PatchLog.WarnOncePerPatch("Is_AOC call not found."); + return instructions; } - return instructionList.AsEnumerable(); - } + matcher.Advance(NextInstructionOffset); + if (matcher.IsInvalid || !IlHelpers.IsBranchTrue(matcher.Instruction)) + { + PatchLog.WarnOncePerPatch("branch after Is_AOC not found."); + return instructions; + } + if (!(matcher.Operand is Label)) + { + PatchLog.WarnOncePerPatch("branch target label not found."); + return instructions; + } + + // Always continue past the early return so status is shown for underage members. + // Preserve short/long branch size to avoid IL size issues. + OpCode newBranch = matcher.Opcode == OpCodes.Brtrue_S ? OpCodes.Br_S : OpCodes.Br; + matcher.SetOpcodeAndOperand(newBranch, matcher.Operand); + return matcher.InstructionEnumeration(); + } } @@ -424,10 +856,13 @@ public class resources__fanOpinion_Add { public static void Postfix(resources._fanOpinion __instance, float val) { + // Propagate global fan opinion changes to each active girl. foreach (data_girls.girls girl in data_girls.girl) { + // Skip null entries, sick girls, and graduates who should not gain appeal. if (girl != null && !girl.IsSick() && girl.status != data_girls._status.graduated) { + // Apply the appeal delta for the matching fan type. girl.AddAppeal(__instance.type, val); } } @@ -439,14 +874,21 @@ public static void Postfix(resources._fanOpinion __instance, float val) [HarmonyPatch(typeof(Date_Gossip), "GetAvailableGossips")] public class Date_Gossip_GetAvailableGossips { + // Sentinel values for list bounds. + private const int NoGossips = 0; + private const int LastIndexOffset = 1; + public static void Postfix(ref List __result, data_girls.girls Snitch) { - if (__result.Count == 0) + // Nothing to filter if the list is empty. + if (__result.Count == NoGossips) { return; } - for (int i = __result.Count - 1; i >= 0; i--) + // Walk backwards so removals do not affect remaining indices. + for (int i = __result.Count - LastIndexOffset; i >= NoGossips; i--) { + // Remove any gossip targeting the snitch herself. if (__result[i].BullyingTarget == Snitch) { __result.RemoveAt(i); @@ -460,71 +902,148 @@ public static void Postfix(ref List __result, data_girls.gi [HarmonyPatch(typeof(vn_requirements), "CheckGirl", new Type[] { typeof(data_girls.girls), typeof(string), typeof(string) })] public class vn_requirements_CheckGirl { - public static IEnumerable Transpiler(IEnumerable instructions) - { - List instructionList = new(instructions); + // Requirement key used by the game for influence checks. + private const string InfluenceParameter = "influence"; + // Enum values from Relationships_Player._type used in the original IL. + private const int RelationshipFriendshipValue = 1; + private const int RelationshipInfluenceValue = 2; + // Sentinel for FindIndex failures. + private const int NotFoundIndex = -1; + // Offset to move from a marker instruction to the next instruction. + private const int NextInstructionOffset = 1; - bool breakFlag = false; - for (int i = 0; i < instructionList.Count; i++) + public static IEnumerable Transpiler(IEnumerable instructions) + { + // Resolve the relationship checker to ensure we update the correct call site. + MethodInfo checkRelationship = AccessTools.Method( + typeof(vn_requirements), + "CheckRelationship", + new Type[] { typeof(data_girls.girls), typeof(string), typeof(Relationships_Player._type) }); + if (checkRelationship == null) { - if (instructionList[i].opcode == OpCodes.Ldstr && (string)instructionList[i].operand == "influence") - { - breakFlag = true; - } - if (breakFlag && instructionList[i].opcode == OpCodes.Ldc_I4_1) - { - instructionList[i].opcode = OpCodes.Ldc_I4_2; - break; - } - if (breakFlag && instructionList[i].opcode == OpCodes.Callvirt) - { - // abort if it reaches the Callvirt operation without finding Ldc_I4_1 + PatchLog.WarnOncePerPatch("CheckRelationship method lookup failed."); + return instructions; + } + + List instructionList = new List(instructions); + // Find the "influence" branch marker in the IL. + int influenceIndex = instructionList.FindIndex(ci => + ci.opcode == OpCodes.Ldstr && ci.operand is string text && text == InfluenceParameter); + + if (influenceIndex == NotFoundIndex) + { + PatchLog.WarnOncePerPatch("\"influence\" marker not found."); + return instructionList.AsEnumerable(); + } + + // Locate the call to CheckRelationship that follows the influence branch. + int callIndex = NotFoundIndex; + for (int i = influenceIndex + NextInstructionOffset; i < instructionList.Count; i++) + { + if (IlHelpers.IsCallTo(instructionList[i], checkRelationship)) + { + callIndex = i; break; } } - return instructionList.AsEnumerable(); - } - } - - // Fix "variable" requirements to respect leading negation. - [HarmonyPatch(typeof(vn_requirements), "CheckGirl", new Type[] { typeof(data_girls.girls), typeof(string), typeof(string) })] - public class vn_requirements_CheckGirl_Variable - { - public static bool Prefix(data_girls.girls girl, string parameter, string formula, ref bool __result) - { - if (parameter != "variable") - return true; - - bool negate = false; - if (!string.IsNullOrEmpty(formula) && formula[0] == '!') + if (callIndex == NotFoundIndex) { - negate = true; - formula = formula.Substring(1); + PatchLog.WarnOncePerPatch("CheckRelationship call not found after \"influence\" marker."); + return instructionList.AsEnumerable(); } - - bool hasVariable = girl.IsVariable(formula); - __result = negate ? !hasVariable : hasVariable; - return false; - } - } - - - // Fix stamina cost of performance thumbnail if Energetic policy - [HarmonyPatch(typeof(Activities._activity), "GetDescription")] - public class Activities__activity_GetDescription + + // The relationship enum should be the last integer pushed before the call. + int enumIndex = callIndex - NextInstructionOffset; + if (enumIndex <= influenceIndex) + { + PatchLog.WarnOncePerPatch("enum constant not found before CheckRelationship call."); + return instructionList.AsEnumerable(); + } + + CodeInstruction enumInstruction = instructionList[enumIndex]; + if (IlHelpers.IsLdcI4(enumInstruction, RelationshipFriendshipValue)) + { + // Replace Friendship with Influence. + enumInstruction.opcode = OpCodes.Ldc_I4_2; + // Clear the operand to match the ldc.i4.2 opcode form. + enumInstruction.operand = null; + instructionList[enumIndex] = enumInstruction; + return instructionList.AsEnumerable(); + } + + if (IlHelpers.IsLdcI4(enumInstruction, RelationshipInfluenceValue)) + { + // Already patched or game fixed it upstream. + return instructionList.AsEnumerable(); + } + + PatchLog.WarnOncePerPatch("unexpected enum opcode before CheckRelationship call."); + return instructionList.AsEnumerable(); + } + } + + // Fix "variable" requirements to respect leading negation. + [HarmonyPatch(typeof(vn_requirements), "CheckGirl", new Type[] { typeof(data_girls.girls), typeof(string), typeof(string) })] + public class vn_requirements_CheckGirl_Variable { + // Prefix used by dialogue scripts to negate variable requirements. + private const char NegationPrefix = '!'; + private const int PrefixIndex = 0; + private const int NegationPrefixLength = 1; + + public static bool Prefix(data_girls.girls girl, string parameter, string formula, ref bool __result) + { + // Only override the "variable" branch; let the rest of CheckGirl run normally. + if (parameter != "variable") + return true; + + // Preserve base behavior for graduated girls. + if (girl.status == data_girls._status.graduated) + { + __result = false; + return false; + } + + bool negate = false; + if (!string.IsNullOrEmpty(formula) && formula[PrefixIndex] == NegationPrefix) + { + negate = true; + formula = formula.Substring(NegationPrefixLength); + } + + // Evaluate the variable and apply negation if requested. + bool hasVariable = girl.IsVariable(formula); + __result = negate ? !hasVariable : hasVariable; + return false; + } + } + + + // Fix stamina cost of performance thumbnail if Energetic policy + [HarmonyPatch(typeof(Activities._activity), "GetDescription")] + public class Activities__activity_GetDescription + { + // Energetic policy overrides the displayed stamina cost. + private const int EnergeticStaminaCost = 4; + private const string PointsKey = "PT"; + private const string StaminaKey = "STAMINA"; + private const string CostPrefix = "-"; + private const string CostSeparator = " "; + public static void Postfix(ref Activities._activity __instance, ref string __result) { + // Only adjust performance activities when Energetic policy is active. if (__instance.type == Activity._type.performance && policies.GetSelectedPolicyValue(policies._type.performances).Value == policies._value.performances_energy) { + // Build a localized "-4 PT stamina" string for the thumbnail. __result = string.Concat(new object[] { - "-", - 4, - Language.Data["PT"], - " ", - Language.Data["STAMINA"].ToLower() + CostPrefix, + EnergeticStaminaCost, + Language.Data[PointsKey], + CostSeparator, + Language.Data[StaminaKey].ToLower() }); } } @@ -533,29 +1052,39 @@ public static void Postfix(ref Activities._activity __instance, ref string __res // Fix custom event check for "hired a staffer of type" [HarmonyPatch(typeof(vn_requirements), "CheckMeta")] - public class vn_requirements_CheckMeta - { + public class vn_requirements_CheckMeta + { + // Metadata parameter and formula keys. + private const string StaffParameter = "staff"; + private const string VocalFormula = "vocal"; + private const string DanceFormula = "dance"; + private const string OfficeFormula = "office"; + private const string StyleFormula = "style"; + private const int MinimumStaffCount = 0; + public static void Postfix(string parameter, string formula, ref bool __result) { - if (parameter != "staff") + // Only handle staff-type checks; let other meta parameters remain unchanged. + if (parameter != StaffParameter) return; - if (formula == "vocal" && staff.CountStaffersOfType(agency._type.recordingStudio) > 0) - { - __result = true; - } - else if(formula == "dance" && staff.CountStaffersOfType(agency._type.danceStudio) > 0) - { - __result = true; - } - else if(formula == "office" && staff.CountStaffersOfType(agency._type.office) > 0) - { - __result = true; - } - else if(formula == "style" && staff.CountStaffersOfType(agency._type.dressingRoom) > 0) - { - __result = true; - } + // Map formula keys to their matching staff room types. + if (formula == VocalFormula && staff.CountStaffersOfType(agency._type.recordingStudio) > MinimumStaffCount) + { + __result = true; + } + else if(formula == DanceFormula && staff.CountStaffersOfType(agency._type.danceStudio) > MinimumStaffCount) + { + __result = true; + } + else if(formula == OfficeFormula && staff.CountStaffersOfType(agency._type.office) > MinimumStaffCount) + { + __result = true; + } + else if(formula == StyleFormula && staff.CountStaffersOfType(agency._type.dressingRoom) > MinimumStaffCount) + { + __result = true; + } } From 4a7c8364ff008b16783d0b4d507b633aa06d5b72 Mon Sep 17 00:00:00 2001 From: ExSlam Date: Tue, 20 Jan 2026 00:20:30 -0500 Subject: [PATCH 05/33] fix pie chart, hype clamps to 200% max correctly, breakups correctly set idol's status to unknown --- mods/Unofficial Patch/Unofficial Patch.cs | 115 ++++++++++++---------- 1 file changed, 65 insertions(+), 50 deletions(-) diff --git a/mods/Unofficial Patch/Unofficial Patch.cs b/mods/Unofficial Patch/Unofficial Patch.cs index d207d0d..44115e0 100755 --- a/mods/Unofficial Patch/Unofficial Patch.cs +++ b/mods/Unofficial Patch/Unofficial Patch.cs @@ -279,18 +279,26 @@ public class Profile_Fans_Pies_Render_Pies private const long NoFans = 0L; // Fixes age-pie rendering so adult slice reflects the residual after YA/Teen allocation. - public static void Postfix(Profile_Fans_Pies __instance, data_girls.girls ___Girl) - { - // Skip when there are no fans to render, avoiding divide-by-zero ratios in vanilla code. - if (___Girl.GetFans_Total() == NoFans) - return; - - // Adjust the adult slice to correct the stacked slice math. - __instance.Fans_Pie_Adult.GetComponent().fillAmount += - __instance.Fans_Pie_YA.GetComponent().fillAmount - - __instance.Fans_Pie_Teen.GetComponent().fillAmount; - } - } + public static void Postfix(Profile_Fans_Pies __instance, data_girls.girls ___Girl) + { + // Skip when there are no fans to render, avoiding divide-by-zero ratios in vanilla code. + if (___Girl.GetFans_Total() == NoFans) + return; + + Image teenImage = __instance.Fans_Pie_Teen.GetComponent(); + Image yaImage = __instance.Fans_Pie_YA.GetComponent(); + Image adultImage = __instance.Fans_Pie_Adult.GetComponent(); + + float teenFill = teenImage.fillAmount; + float yaFill = yaImage.fillAmount; + float adultFill = adultImage.fillAmount; + + // The prefab renders Teen as the base image with Adult and YA as child images on top. + // Use cumulative fills so the visible slices match the ratios (YA, then Adult, then Teen). + adultImage.fillAmount = adultFill + yaFill - teenFill; + teenImage.fillAmount = adultFill + yaFill; + } + } // Keeps tour expected revenue text color aligned with profitability. [HarmonyPatch(typeof(Tour_New_Popup), "Render")] @@ -563,22 +571,28 @@ public static void Postfix(ref int __result) // Fixed so that when girls dating within the group break up, their relationship status is no longer known - [HarmonyPatch(typeof(Relationships._relationship), "BreakUp")] - public class Relationships__relationship_BreakUp - { - // Indices for the two relationship participants. - private const int FirstPartnerIndex = 0; - private const int SecondPartnerIndex = 1; - - public static void Postfix(ref Relationships._relationship __instance) - { - // Only clear known status when the pair was actually dating. - if (!__instance.Dating) - return; - - // Hide partner status for both sides after breakup. - __instance.Girls[FirstPartnerIndex].DatingData.Is_Partner_Status_Known = false; - __instance.Girls[SecondPartnerIndex].DatingData.Is_Partner_Status_Known = false; + [HarmonyPatch(typeof(Relationships._relationship), "BreakUp")] + public class Relationships__relationship_BreakUp + { + // Indices for the two relationship participants. + private const int FirstPartnerIndex = 0; + private const int SecondPartnerIndex = 1; + + public static void Prefix(Relationships._relationship __instance, ref bool __state) + { + // Capture whether the pair was dating before BreakUp clears the flag. + __state = __instance.Dating; + } + + public static void Postfix(Relationships._relationship __instance, bool __state) + { + // Only clear known status when the pair was actually dating. + if (!__state) + return; + + // Hide partner status for both sides after breakup. + __instance.Girls[FirstPartnerIndex].DatingData.Is_Partner_Status_Known = false; + __instance.Girls[SecondPartnerIndex].DatingData.Is_Partner_Status_Known = false; } } @@ -620,16 +634,17 @@ public static IEnumerable Transpiler(IEnumerable HypeBaseline) - { - // Avoid target-typed new() for max compatibility - LinearFunction._function function = new LinearFunction._function(); + public static float Infix(SEvent_Concerts._concert._projectedValues __this) + { + // Start with the game's base hype calculation. + float hype = __this.GetHype(); + + // Only reshape hype above the baseline (1.0). + // NOTE: This patch does not yet apply the club-venue exemption or FUJI_3_TICKETS multiplier. + if (hype > HypeBaseline) + { + // Avoid target-typed new() for max compatibility + LinearFunction._function function = new LinearFunction._function(); // Configure a linear mapping with points (0, 0.5) and (1, 0.25). function.Init(LinearPointX0, LinearPointY0, LinearPointX1, LinearPointY1); @@ -647,18 +662,18 @@ public static float Infix(SEvent_Concerts._concert._projectedValues __this) // Fixed Concert revenue formula so that it shows accurate estimated values [HarmonyPatch(typeof(SEvent_Concerts._concert._projectedValues), "GetString")] - public class SEvent_Concerts__concert__projectedValues_GetString - { - // Ratio cap corresponding to 100%. - private const float MaxRatio = 1f; - - public static bool Prefix(ref float _val) - { - // Clamp the ratio so the display never exceeds 100%. - if (_val > MaxRatio) - { - _val = MaxRatio; - } + public class SEvent_Concerts__concert__projectedValues_GetString + { + // Hype is capped at 200% (2.0) by the base game. + private const float MaxRatio = 2f; + + public static bool Prefix(ref float _val) + { + // Clamp ratios so hype does not exceed its intended 200% cap. + if (_val > MaxRatio) + { + _val = MaxRatio; + } return true; } } From 51eddd28f04bd1ae3129d0fa27b4ada27008d23a Mon Sep 17 00:00:00 2001 From: ExSlam Date: Tue, 20 Jan 2026 00:34:30 -0500 Subject: [PATCH 06/33] fix: improve fan pie rendering to accurately reflect teen/YA/adult ratios and prevent overflow --- mods/Unofficial Patch/Unofficial Patch.cs | 39 ++++++++++++++++------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/mods/Unofficial Patch/Unofficial Patch.cs b/mods/Unofficial Patch/Unofficial Patch.cs index 44115e0..fd483f5 100755 --- a/mods/Unofficial Patch/Unofficial Patch.cs +++ b/mods/Unofficial Patch/Unofficial Patch.cs @@ -273,12 +273,14 @@ public static bool IsBranchTrue(CodeInstruction instruction) // Fixes fan pie rendering so the adult slice accounts for YA/Teen stacking. [HarmonyPatch(typeof(Profile_Fans_Pies), "Render_Pies")] - public class Profile_Fans_Pies_Render_Pies - { - // No-fan sentinel for early exit. - private const long NoFans = 0L; - - // Fixes age-pie rendering so adult slice reflects the residual after YA/Teen allocation. + public class Profile_Fans_Pies_Render_Pies + { + // No-fan sentinel for early exit. + private const long NoFans = 0L; + // Cap for cumulative pie fills. + private const float MaxPieFill = 1f; + + // Fixes age-pie rendering so the stacked slices match the teen/YA/adult ratios. public static void Postfix(Profile_Fans_Pies __instance, data_girls.girls ___Girl) { // Skip when there are no fans to render, avoiding divide-by-zero ratios in vanilla code. @@ -289,14 +291,29 @@ public static void Postfix(Profile_Fans_Pies __instance, data_girls.girls ___Gir Image yaImage = __instance.Fans_Pie_YA.GetComponent(); Image adultImage = __instance.Fans_Pie_Adult.GetComponent(); - float teenFill = teenImage.fillAmount; - float yaFill = yaImage.fillAmount; - float adultFill = adultImage.fillAmount; + float teenRatio = teenImage.fillAmount; + float yaRatio = yaImage.fillAmount; + // Base game sets Adult fill to adult + teen, so subtract teen to recover the adult slice. + float adultRatio = adultImage.fillAmount - teenRatio; + if (adultRatio < 0f) + { + adultRatio = 0f; + } + + float totalRatio = teenRatio + yaRatio + adultRatio; + // Guard against rounding overshoot by scaling to the expected 0..1 range. + if (totalRatio > MaxPieFill && totalRatio > 0f) + { + float scale = MaxPieFill / totalRatio; + teenRatio *= scale; + yaRatio *= scale; + adultRatio *= scale; + } // The prefab renders Teen as the base image with Adult and YA as child images on top. // Use cumulative fills so the visible slices match the ratios (YA, then Adult, then Teen). - adultImage.fillAmount = adultFill + yaFill - teenFill; - teenImage.fillAmount = adultFill + yaFill; + adultImage.fillAmount = adultRatio + yaRatio; + teenImage.fillAmount = adultRatio + yaRatio + teenRatio; } } From 2ef9d105fcda894d06d21e1c4f3fc8543b11a004 Mon Sep 17 00:00:00 2001 From: ExSlam Date: Tue, 20 Jan 2026 00:35:33 -0500 Subject: [PATCH 07/33] fix: update concert revenue calculation to include FUJI ticket bonus and improve hype handling for club venues --- mods/Unofficial Patch/Unofficial Patch.cs | 58 ++++++++++++++--------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/mods/Unofficial Patch/Unofficial Patch.cs b/mods/Unofficial Patch/Unofficial Patch.cs index fd483f5..1c8f090 100755 --- a/mods/Unofficial Patch/Unofficial Patch.cs +++ b/mods/Unofficial Patch/Unofficial Patch.cs @@ -615,14 +615,18 @@ public static void Postfix(Relationships._relationship __instance, bool __state) // Fixed Concert revenue formula so that it shows accurate estimated values [HarmonyPatch(typeof(SEvent_Concerts._concert._projectedValues), "GetRevenue")] - public class SEvent_Concerts__concert__projectedValues_GetRevenue - { - // Hype curve configuration used by the adjusted revenue formula. - private const float HypeBaseline = 1f; - private const float LinearPointX0 = 0f; - private const float LinearPointY0 = 0.5f; - private const float LinearPointX1 = 1f; - private const float LinearPointY1 = 0.25f; + public class SEvent_Concerts__concert__projectedValues_GetRevenue + { + // Hype curve configuration used by the adjusted revenue formula. + private const float HypeBaseline = 1f; + private const float LinearPointX0 = 0f; + private const float LinearPointY0 = 0.5f; + private const float LinearPointX1 = 1f; + private const float LinearPointY1 = 0.25f; + // Variable flag and multiplier for FUJI ticket bonus. + private const string FujiTicketsVariable = "FUJI_3_TICKETS"; + private const string TrueValue = "true"; + private const float FujiTicketsMultiplier = 1.05f; public static IEnumerable Transpiler(IEnumerable instructions) { @@ -656,24 +660,32 @@ public static float Infix(SEvent_Concerts._concert._projectedValues __this) // Start with the game's base hype calculation. float hype = __this.GetHype(); - // Only reshape hype above the baseline (1.0). - // NOTE: This patch does not yet apply the club-venue exemption or FUJI_3_TICKETS multiplier. - if (hype > HypeBaseline) + // Club venues never use the post-100% hype curve in the base game. + bool isClubVenue = __this.Parent != null && __this.Parent.Venue == SEvent_Concerts._venue.club; + + // Only reshape hype above the baseline (1.0) for non-club venues. + if (!isClubVenue && hype > HypeBaseline) { // Avoid target-typed new() for max compatibility LinearFunction._function function = new LinearFunction._function(); - // Configure a linear mapping with points (0, 0.5) and (1, 0.25). - function.Init(LinearPointX0, LinearPointY0, LinearPointX1, LinearPointY1); - - // Convert "hype above 1" into a scaled bonus, then re-add the baseline. - float excessHype = hype - HypeBaseline; - hype = excessHype * function.GetY(excessHype) + HypeBaseline; - } - - // Return the adjusted hype value. - return hype; - } - } + // Configure a linear mapping with points (0, 0.5) and (1, 0.25). + function.Init(LinearPointX0, LinearPointY0, LinearPointX1, LinearPointY1); + + // Convert "hype above 1" into a scaled bonus, then re-add the baseline. + float excessHype = hype - HypeBaseline; + hype = excessHype * function.GetY(excessHype) + HypeBaseline; + } + + // Apply the FUJI ticket bonus if enabled. + if (variables.Get(FujiTicketsVariable) == TrueValue) + { + hype *= FujiTicketsMultiplier; + } + + // Return the adjusted hype value. + return hype; + } + } From a5a867e788363d9c09fd0866250ee7674c399e18 Mon Sep 17 00:00:00 2001 From: ExSlam Date: Tue, 20 Jan 2026 14:34:06 -0500 Subject: [PATCH 08/33] use proper match.Set rather than SetOpcodeAndOperand --- Directory.Build.props | 3 ++- NuGet.Config | 4 ++-- mods/Unofficial Patch/Unofficial Patch.cs | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index fef8962..2500099 100755 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,8 @@ $(APPDATA)\..\LocalLow\Glitch Pitch\Idol Manager\Mods $(APPDATA)\..\LocalLow\Glitch Pitch\Idol Manager\Mods - $(SolutionDir)\..\dll + $(SolutionDir)\..\dll + $(MSBuildThisFileDirectory)..\dll Idol Manager Mod diff --git a/NuGet.Config b/NuGet.Config index 80334c0..bc14187 100755 --- a/NuGet.Config +++ b/NuGet.Config @@ -2,6 +2,6 @@ - + - \ No newline at end of file + diff --git a/mods/Unofficial Patch/Unofficial Patch.cs b/mods/Unofficial Patch/Unofficial Patch.cs index 1c8f090..4ea2c5d 100755 --- a/mods/Unofficial Patch/Unofficial Patch.cs +++ b/mods/Unofficial Patch/Unofficial Patch.cs @@ -651,7 +651,7 @@ public static IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable Date: Mon, 23 Feb 2026 22:12:19 -0500 Subject: [PATCH 09/33] =?UTF-8?q?=EF=BB=BFfix:=20harden=20fast-forward=20a?= =?UTF-8?q?nd=20audition=20flows=20across=20mods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FastForward - centralize speed calculation/apply logic and clamp/validate multiplier parsing - add popup-aware suspend/restore hooks so custom super-fast speed does not break audition popup init - clear pending restore when leaving fast state - bump version to 1.2.1 Targeted Auditions - harden ScrollRect setup with null checks and component reuse - add portrait-load watchdog (Set/Reset/Close tracking + timeout fallback) to avoid recruitment popup deadlocks - keep popup interactive after timeout and fill missing portraits from fallback sprite when available - bump version to 2.0.2 Harmony Checker - guard Mods button lookup against missing UI nodes to avoid menu-time exceptions Unofficial Patch - rework theater payout fix to use vanilla-written daily stat rows instead of pre-mutating Doing_Now - keep day-off revenue at zero and split actual show-day revenue (plus first-day subs) safely among active girls - add breakup null-safety around relationship/girl access before clearing partner-known flags - replace fragile GetPartnerString transpiler with postfix string builder for underage partner visibility --- mods/FastForward/FastForward.cs | 304 +++++++++++++---- mods/FastForward/FastForward.csproj | 2 +- mods/Harmony Checker/Harmony Checker.cs | 42 ++- mods/Targeted Auditions/Targeted Auditions.cs | 253 ++++++++++++-- .../Targeted Auditions.csproj | 2 +- mods/Unofficial Patch/Unofficial Patch.cs | 314 +++++++++++------- 6 files changed, 681 insertions(+), 236 deletions(-) diff --git a/mods/FastForward/FastForward.cs b/mods/FastForward/FastForward.cs index 2aff2f6..f88f86b 100755 --- a/mods/FastForward/FastForward.cs +++ b/mods/FastForward/FastForward.cs @@ -1,66 +1,238 @@ -using HarmonyLib; -using UnityEngine; -using TMPro; -using static FastForward.FastForward; - -namespace FastForward -{ - public class FastForward - { - public const string VARID = "FastForward_Multiplier"; - public const string DEFAULT_VAR = "5"; - } - - /// - /// Patch class for the TimeControlButton's OnClick method to implement faster time acceleration. - /// - [HarmonyPatch(typeof(TimeControlButton), "OnClick")] - public class TimeControlButton_OnClick - { - /// - /// Prefix method to enhance the fast forward functionality when clicking the fast forward button twice. - /// - /// The instance of the TimeControlButton being clicked. - /// Boolean indicating whether the original method should be executed. - public static bool Prefix(TimeControlButton __instance) - { - double mult = double.Parse(variables.Get(VARID) ?? DEFAULT_VAR); - double speed = 200 * mult; - - if (__instance.Type != mainScript._time_state.fast || staticVars.timeState != mainScript._time_state.fast || staticVars.dateTimeAddMinutesPerSecond == speed) - return true; - - staticVars.dateTimeAddMinutesPerSecond = speed; - Camera.main.GetComponent().TimeControls_Fast.GetComponent().color = mainScript.gold32; - - return false; - } - - } - - /// - /// Patch class for the Controls' Update method to implement hotkey-based time acceleration. - /// - [HarmonyPatch(typeof(Controls), "Update")] - public class Controls_Update - { - /// - /// Postfix method to add hotkey functionality for speeding up time when pressing the '4' key. - /// - public static void Postfix() - { - if (mainScript.IsBlockingHotkeys()) - return; - - if (Input.GetKeyDown(KeyCode.Alpha4)) - { - double mult = double.Parse(variables.Get(VARID) ?? DEFAULT_VAR); - double speed = 200 * mult; - Camera.main.GetComponent().Time_SetState(mainScript._time_state.fast); - staticVars.dateTimeAddMinutesPerSecond = speed; - - Camera.main.GetComponent().TimeControls_Fast.GetComponent().color = mainScript.gold32; - } - } - } -} +using HarmonyLib; +using UnityEngine; +using TMPro; +using System; +using System.Globalization; +using static FastForward.FastForward; + +namespace FastForward +{ + public class FastForward + { + public const string VARID = "FastForward_Multiplier"; + public const string DEFAULT_VAR = "5"; + public const double BASE_FAST_SPEED = 200d; + public const double MAX_MULTIPLIER = 50d; + public const double EPSILON = 0.001d; + + // Stores the user's pre-popup super-fast speed so it can be restored after forced popup pause. + private static double pendingAuditionRestoreSpeed = 0d; + + internal static double GetConfiguredMultiplier() + { + string raw = variables.Get(VARID); + if (string.IsNullOrWhiteSpace(raw)) + { + raw = DEFAULT_VAR; + } + + if (!double.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out double multiplier)) + { + multiplier = 5d; + } + + if (double.IsNaN(multiplier) || double.IsInfinity(multiplier)) + { + multiplier = 5d; + } + + if (multiplier < 1d) + { + multiplier = 1d; + } + else if (multiplier > MAX_MULTIPLIER) + { + multiplier = MAX_MULTIPLIER; + } + + return multiplier; + } + + internal static double GetConfiguredSpeed() + { + return BASE_FAST_SPEED * GetConfiguredMultiplier(); + } + + internal static void ApplySuperFast(mainScript main) + { + if (main == null) + { + return; + } + + // Use vanilla state transition first so all game-side effects still run as expected. + main.Time_SetState(mainScript._time_state.fast); + staticVars.dateTimeAddMinutesPerSecond = GetConfiguredSpeed(); + SetFastButtonColor(mainScript.gold32); + } + + internal static void SetFastButtonColor(Color32 color) + { + if (Camera.main == null) + { + return; + } + + mainScript main = Camera.main.GetComponent(); + if (main == null || main.TimeControls_Fast == null) + { + return; + } + + TextMeshProUGUI fastLabel = main.TimeControls_Fast.GetComponent(); + if (fastLabel != null) + { + fastLabel.color = color; + } + } + + internal static void SuspendSuperFastForAuditionPopup() + { + // Only suspend custom speeds; vanilla fast (200) does not need this safety fallback. + if (staticVars.timeState != mainScript._time_state.fast || staticVars.dateTimeAddMinutesPerSecond <= BASE_FAST_SPEED + EPSILON) + { + return; + } + + if (pendingAuditionRestoreSpeed <= BASE_FAST_SPEED + EPSILON) + { + pendingAuditionRestoreSpeed = staticVars.dateTimeAddMinutesPerSecond; + } + + // Drop to vanilla fast while popup initialization runs to avoid race conditions in recruitment UI. + staticVars.dateTimeAddMinutesPerSecond = BASE_FAST_SPEED; + SetFastButtonColor(mainScript.green32); + } + + internal static void TryRestoreSuperFastAfterResume(mainScript main) + { + if (pendingAuditionRestoreSpeed <= BASE_FAST_SPEED + EPSILON) + { + return; + } + + // Respect user state changes made while popup flow was active. + if (staticVars.timeState != mainScript._time_state.fast) + { + pendingAuditionRestoreSpeed = 0d; + return; + } + + // Only restore when popup system resumes vanilla fast speed. + if (staticVars.dateTimeAddMinutesPerSecond > BASE_FAST_SPEED + EPSILON) + { + pendingAuditionRestoreSpeed = 0d; + return; + } + + staticVars.dateTimeAddMinutesPerSecond = pendingAuditionRestoreSpeed; + pendingAuditionRestoreSpeed = 0d; + SetFastButtonColor(mainScript.gold32); + } + + internal static void ClearPendingRestoreIfNotFast(mainScript._time_state state) + { + if (state != mainScript._time_state.fast) + { + pendingAuditionRestoreSpeed = 0d; + } + } + } + + /// + /// Patch class for the TimeControlButton's OnClick method to implement faster time acceleration. + /// + [HarmonyPatch(typeof(TimeControlButton), "OnClick")] + public class TimeControlButton_OnClick + { + /// + /// Prefix method to enhance the fast forward functionality when clicking the fast forward button twice. + /// + /// The instance of the TimeControlButton being clicked. + /// Boolean indicating whether the original method should be executed. + public static bool Prefix(TimeControlButton __instance) + { + double speed = GetConfiguredSpeed(); + + if (__instance.Type != mainScript._time_state.fast || staticVars.timeState != mainScript._time_state.fast || Math.Abs(staticVars.dateTimeAddMinutesPerSecond - speed) <= EPSILON) + return true; + + ApplySuperFast(Camera.main != null ? Camera.main.GetComponent() : null); + return false; + } + } + + /// + /// Patch class for the Controls' Update method to implement hotkey-based time acceleration. + /// + [HarmonyPatch(typeof(Controls), "Update")] + public class Controls_Update + { + /// + /// Postfix method to add hotkey functionality for speeding up time when pressing the '4' key. + /// + public static void Postfix() + { + if (mainScript.IsBlockingHotkeys()) + return; + + if (Input.GetKeyDown(KeyCode.Alpha4)) + { + ApplySuperFast(Camera.main != null ? Camera.main.GetComponent() : null); + } + } + } + + /// + /// Ensures custom super-fast speed does not interfere with audition popup initialization. + /// + [HarmonyPatch(typeof(PopupManager), "Open")] + public class PopupManager_Open + { + /// + /// Prefix method to temporarily downgrade custom super-fast speed when opening the audition popup. + /// + /// Popup type being opened. + public static void Prefix(PopupManager._type type) + { + if (type != PopupManager._type.audition) + { + return; + } + + SuspendSuperFastForAuditionPopup(); + } + } + + /// + /// Restores previously suspended super-fast speed after popup-driven forced pause is released. + /// + [HarmonyPatch(typeof(mainScript), "Time_Resume")] + public class mainScript_Time_Resume + { + /// + /// Postfix method to restore the user's pre-popup custom speed. + /// + /// mainScript instance. + public static void Postfix(mainScript __instance) + { + TryRestoreSuperFastAfterResume(__instance); + } + } + + /// + /// Clears pending speed restoration when user manually switches away from fast mode. + /// + [HarmonyPatch(typeof(mainScript), "Time_SetState")] + public class mainScript_Time_SetState + { + /// + /// Postfix method to cancel restore state if current mode is no longer fast. + /// + /// Requested time state. + public static void Postfix(mainScript._time_state state) + { + ClearPendingRestoreIfNotFast(state); + } + } +} diff --git a/mods/FastForward/FastForward.csproj b/mods/FastForward/FastForward.csproj index 28be217..5448677 100755 --- a/mods/FastForward/FastForward.csproj +++ b/mods/FastForward/FastForward.csproj @@ -5,7 +5,7 @@ com.tel.fastforward Press '4' or click the fast-forward button twice to go faster! Tel - 1.2.0 + 1.2.1 ["gameplay"] diff --git a/mods/Harmony Checker/Harmony Checker.cs b/mods/Harmony Checker/Harmony Checker.cs index bbbeff9..b1c13e5 100755 --- a/mods/Harmony Checker/Harmony Checker.cs +++ b/mods/Harmony Checker/Harmony Checker.cs @@ -1,18 +1,36 @@ using HarmonyLib; using UnityEngine; -namespace HarmonyChecker -{ - // change button text - [HarmonyPatch(typeof(MainMenu_Buttons_Controller), "Start")] - public class MainMenu_Buttons_Controller_Start +namespace HarmonyChecker +{ + // change button text + [HarmonyPatch(typeof(MainMenu_Buttons_Controller), "Start")] + public class MainMenu_Buttons_Controller_Start { public const string BUTTON_LABEL = "IMHI_INSTALLED"; - public static void Postfix(ref MainMenu_Buttons_Controller __instance) - { - Lang_Button modButton = __instance.Main_Container.transform.Find("Mods").GetComponentInChildren(); - modButton.Constant = BUTTON_LABEL; - } - } -} + public static void Postfix(ref MainMenu_Buttons_Controller __instance) + { + // Safety: some UI variants rename/remove the Mods button. + // If that happens we should not throw here, because menu-time exceptions can prevent other mod hooks from running. + if (__instance == null || __instance.Main_Container == null) + { + return; + } + + Transform modsTransform = __instance.Main_Container.transform.Find("Mods"); + if (modsTransform == null) + { + return; + } + + Lang_Button modButton = modsTransform.GetComponentInChildren(); + if (modButton == null) + { + return; + } + + modButton.Constant = BUTTON_LABEL; + } + } +} diff --git a/mods/Targeted Auditions/Targeted Auditions.cs b/mods/Targeted Auditions/Targeted Auditions.cs index 649c2b5..d11b97a 100755 --- a/mods/Targeted Auditions/Targeted Auditions.cs +++ b/mods/Targeted Auditions/Targeted Auditions.cs @@ -14,9 +14,9 @@ namespace CustomAuditions /// Patches the Popup_Audition class to allow scrolling cards in the audition popup. /// // Set up audition popup to allow scrolling cards - [HarmonyPatch(typeof(Popup_Audition), "Start")] - public class Popup_Audition_Start - { + [HarmonyPatch(typeof(Popup_Audition), "Start")] + public class Popup_Audition_Start + { /// /// Sets the properties of a RectTransform. @@ -37,34 +37,219 @@ private static void SetRectTransform(RectTransform rt, Vector2 anchorMin, Vector /// /// Postfix method to set up the scrollable audition popup. /// - /// The instance of Popup_Audition being patched. - public static void Postfix(Popup_Audition __instance) - { - if (__instance.Cards_Container.transform.parent.GetComponent() != null) - return; - - // Create ScrollRect container and attach to panel - GameObject scrollContainer = new(AUD_SCROLLRECT_NAME, typeof(RectTransform), typeof(ScrollRect)); - RectTransform scrollRectTransform = scrollContainer.GetComponent(); + /// The instance of Popup_Audition being patched. + public static void Postfix(Popup_Audition __instance) + { + if (__instance == null || __instance.Cards_Container == null) + return; + + Transform currentParent = __instance.Cards_Container.transform.parent; + if (currentParent == null) + return; + + if (currentParent.GetComponent() != null) + return; + + // Create ScrollRect container and attach to panel + GameObject scrollContainer = new(AUD_SCROLLRECT_NAME, typeof(RectTransform), typeof(ScrollRect)); + RectTransform scrollRectTransform = scrollContainer.GetComponent(); SetRectTransform(scrollRectTransform, Vector2.zero, Vector2.one, Vector2.zero, Vector2.zero); // Configure the ScrollRect - ScrollRect scrollRect = scrollContainer.GetComponent(); - scrollRect.content = __instance.Cards_Container.GetComponent(); // attach content - scrollRect.vertical = false; - scrollRect.horizontal = true; - scrollRect.movementType = ScrollRect.MovementType.Elastic; - scrollRect.elasticity = 0.1f; - scrollRect.inertia = false; - scrollRect.scrollSensitivity = 20; - - // Configure hierarchy - scrollContainer.transform.SetParent(__instance.Cards_Container.transform.parent, false); - __instance.Cards_Container.transform.SetParent(scrollContainer.transform, false); - - __instance.Cards_Container.AddComponent().horizontalFit = ContentSizeFitter.FitMode.PreferredSize; - } - } + ScrollRect scrollRect = scrollContainer.GetComponent(); + scrollRect.content = __instance.Cards_Container.GetComponent(); // attach content + scrollRect.viewport = scrollRectTransform; + scrollRect.vertical = false; + scrollRect.horizontal = true; + scrollRect.movementType = ScrollRect.MovementType.Elastic; + scrollRect.elasticity = 0.1f; + scrollRect.inertia = false; + scrollRect.scrollSensitivity = 20; + + // Configure hierarchy + scrollContainer.transform.SetParent(currentParent, false); + __instance.Cards_Container.transform.SetParent(scrollContainer.transform, false); + + // Reuse existing fitter if one exists to avoid duplicate component warnings. + ContentSizeFitter fitter = __instance.Cards_Container.GetComponent(); + if (fitter == null) + { + fitter = __instance.Cards_Container.AddComponent(); + } + fitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize; + } + } + + /// + /// Tracks audition popup load start times so portrait loading can fail-safe instead of hanging forever. + /// + [HarmonyPatch(typeof(Popup_Audition), "Set")] + public class Popup_Audition_Set + { + /// + /// Postfix method that records when a new audition batch starts loading. + /// + /// Popup instance. + public static void Postfix(Popup_Audition __instance) + { + if (__instance == null) + { + return; + } + + auditionLoadStartedAt[__instance.GetInstanceID()] = Time.unscaledTime; + } + } + + /// + /// Clears load watchdog state when audition popup is reset. + /// + [HarmonyPatch(typeof(Popup_Audition), "Reset")] + public class Popup_Audition_Reset + { + /// + /// Postfix method that removes stale watchdog entries. + /// + /// Popup instance. + public static void Postfix(Popup_Audition __instance) + { + if (__instance == null) + { + return; + } + + auditionLoadStartedAt.Remove(__instance.GetInstanceID()); + } + } + + /// + /// Clears load watchdog state when audition popup is closed. + /// + [HarmonyPatch(typeof(Popup_Audition), "Close")] + public class Popup_Audition_Close + { + /// + /// Prefix method that removes stale watchdog entries before close logic runs. + /// + /// Popup instance. + public static void Prefix(Popup_Audition __instance) + { + if (__instance == null) + { + return; + } + + auditionLoadStartedAt.Remove(__instance.GetInstanceID()); + } + } + + /// + /// Prevents recruitment popup deadlocks when one portrait never resolves. + /// + [HarmonyPatch(typeof(Popup_Audition), "PortraitsLoaded")] + public class Popup_Audition_PortraitsLoaded + { + /// + /// Postfix method that applies a timeout fallback for stuck portrait loading. + /// + /// Popup instance. + /// Original readiness result. + public static void Postfix(Popup_Audition __instance, ref bool __result) + { + if (__result || __instance == null || __instance.Cards_Container == null) + { + return; + } + + int popupId = __instance.GetInstanceID(); + if (!auditionLoadStartedAt.TryGetValue(popupId, out float startedAt)) + { + return; + } + + float elapsed = Time.unscaledTime - startedAt; + if (elapsed < PORTRAIT_LOAD_TIMEOUT_SECONDS) + { + return; + } + + // The vanilla coroutine waits indefinitely for all portraits. With large candidate counts this can + // deadlock the popup (blur shown, cards never become interactive). After timeout, continue anyway. + EnsurePopupIsVisible(__instance); + Sprite fallbackSprite = FindFallbackPortraitSprite(__instance); + bool missingPortraits = FillMissingPortraits(__instance, fallbackSprite); + if (missingPortraits) + { + Debug.Log("[Targeted Auditions] Portrait load timed out. Continuing with fallback portraits."); + } + + __result = true; + } + + private static void EnsurePopupIsVisible(Popup_Audition popup) + { + CanvasGroup cg = popup.GetComponent(); + if (cg != null) + { + cg.alpha = 1f; + cg.blocksRaycasts = true; + cg.interactable = true; + } + + RectTransform rt = popup.GetComponent(); + if (rt != null) + { + rt.localScale = Vector3.one; + } + } + + private static Sprite FindFallbackPortraitSprite(Popup_Audition popup) + { + foreach (Transform child in popup.Cards_Container.transform) + { + Audition_Closed_Card closedCard = child.GetComponent(); + if (closedCard == null || closedCard.Portrait == null) + { + continue; + } + + Image image = closedCard.Portrait.GetComponent(); + if (image != null && image.sprite != null) + { + return image.sprite; + } + } + + return null; + } + + private static bool FillMissingPortraits(Popup_Audition popup, Sprite fallback) + { + bool hadMissing = false; + foreach (Transform child in popup.Cards_Container.transform) + { + Audition_Closed_Card closedCard = child.GetComponent(); + if (closedCard == null || closedCard.Portrait == null) + { + continue; + } + + Image image = closedCard.Portrait.GetComponent(); + if (image == null || image.sprite != null) + { + continue; + } + + hadMissing = true; + if (fallback != null) + { + image.sprite = fallback; + } + } + + return hadMissing; + } + } /// /// Patches the Auditions class to set variables at the start of an audition. @@ -287,8 +472,8 @@ public static void Postfix() /// /// Contains utility methods and variables for custom auditions. /// - class CustomAuditions - { + class CustomAuditions + { public const string DEF_MINAGE_STR = "12"; public const string DEF_MAXAGE_STR = "23"; public const string DEF_CHANCE_LES_STR = "7"; @@ -304,7 +489,8 @@ class CustomAuditions public const string VARID_COUNT = "CustomAudition_Count"; - public const string AUD_SCROLLRECT_NAME = "ScrollContainer"; + public const string AUD_SCROLLRECT_NAME = "ScrollContainer"; + public const float PORTRAIT_LOAD_TIMEOUT_SECONDS = 6f; public const string VARID_AGELIMIT_POPUP_TOGGLE = "AuditionAgeLimit_TogglePopup"; public const string DEF_AGELIMIT_POPUP_TOGGLE = "0"; @@ -317,8 +503,9 @@ class CustomAuditions public static int chanceLesbian = 7; public static int chanceBi = 14; - public static bool agePopup = false; - public static bool inputValid = false; + public static bool agePopup = false; + public static bool inputValid = false; + public static Dictionary auditionLoadStartedAt = new(); /// /// Parses the age range string and sets the minAge and maxAge values. diff --git a/mods/Targeted Auditions/Targeted Auditions.csproj b/mods/Targeted Auditions/Targeted Auditions.csproj index d1ed09c..6130bdc 100755 --- a/mods/Targeted Auditions/Targeted Auditions.csproj +++ b/mods/Targeted Auditions/Targeted Auditions.csproj @@ -5,7 +5,7 @@ com.tel.customauditions (formerly Audition Age Limits) Customise you auditions to target girls by age, skill and sexuality. Tel - 2.0.1 + 2.0.2 ["gameplay"] diff --git a/mods/Unofficial Patch/Unofficial Patch.cs b/mods/Unofficial Patch/Unofficial Patch.cs index 4ea2c5d..9d7ea07 100755 --- a/mods/Unofficial Patch/Unofficial Patch.cs +++ b/mods/Unofficial Patch/Unofficial Patch.cs @@ -367,83 +367,93 @@ public static void Postfix(Theaters._theater._schedule._type Type, ref float __r // Aligns theater revenue timing and payout distribution with the schedule. [HarmonyPatch(typeof(Theaters), "CompleteDay")] - public class Theaters_CompleteDay - { - // Day-of-month used by the base game for subscription revenue. - private const int FirstDayOfMonth = 1; - // Sentinel values for revenue and counts. - private const long NoRevenue = 0L; - private const int NoGirls = 0; - // Offset for accessing the latest stats entry. - private const int LastIndexOffset = 1; - // Parameters for the floating money UI icon. - private const float MoneyFloatStartScale = 0f; - private const float MoneyFloatEndScale = 1f; - private const float MoneyFloatDelay = 0f; - - // Ensures Doing_Now reflects today's schedule before the base method runs. - public static bool Prefix() - { - foreach (Theaters._theater theater in Theaters.Theaters_) - { - // Force the schedule selection so auto schedules pay out today. - theater.Doing_Now = theater.GetSchedule().Type; - } - return true; - } - - - // Fixes revenue accounting and income distribution after the base method completes. - public static void Postfix() - { - foreach (Theaters._theater theater in Theaters.Theaters_) - { - // Ensure auto schedules pay out on the same day when they convert to performance/manzai. - if (theater.GetSchedule().Type == Theaters._theater._schedule._type.auto && - (theater.Doing_Now == Theaters._theater._schedule._type.manzai || theater.Doing_Now == Theaters._theater._schedule._type.performance)) - { - long rev = theater.GetTicketSales(); - if (rev > NoRevenue) - { - // Add the earned revenue to player resources. - resources.Add(resources.type.money, rev); - } - if (staticVars.dateTime.Day != FirstDayOfMonth) - { - // Show a money float icon on non-subscription days. - theater.GetRoom().addFloat(Floats.type.icon_money, "", true, null, MoneyFloatStartScale, MoneyFloatEndScale, MoneyFloatDelay, null); - } - } - - // Days off should contribute zero revenue to stats. - if (theater.Doing_Now == Theaters._theater._schedule._type.day_off) - { - theater.Stats[theater.Stats.Count - LastIndexOffset].Revenue = NoRevenue; - } - - // Split ticket/subscription revenue among participating girls. - if (theater.Doing_Now == Theaters._theater._schedule._type.performance || theater.Doing_Now == Theaters._theater._schedule._type.manzai) - { - List girls = theater.GetGroup().GetGirls(true, false, null); - int girlCount = girls.Count; - long num = theater.GetTicketSales(); - if (theater.AreSubsUnlocked() && staticVars.dateTime.Day == FirstDayOfMonth) + public class Theaters_CompleteDay + { + // Day-of-month used by the base game for subscription revenue. + private const int FirstDayOfMonth = 1; + // Sentinel values for revenue and counts. + private const long NoRevenue = 0L; + private const int NoGirls = 0; + // Offset for accessing the latest stats entry. + private const int LastIndexOffset = 1; + + // Fixes revenue accounting and income distribution after the base method completes. + public static void Postfix() + { + foreach (Theaters._theater theater in Theaters.Theaters_) + { + // Important: do not rewrite Doing_Now before vanilla runs. + // Vanilla uses yesterday's Doing_Now at the beginning of CompleteDay to settle previous-day revenue. + // We only read the stat row vanilla just wrote for "today" and correct payouts from that. + if (theater == null || theater.Stats == null || theater.Stats.Count == 0) + { + continue; + } + + Theaters._theater._stat latestStat = theater.Stats[theater.Stats.Count - LastIndexOffset]; + if (latestStat == null || latestStat.Schedule == null) + { + continue; + } + + // Days off should contribute zero revenue to stats. + if (latestStat.Schedule.Type == Theaters._theater._schedule._type.day_off) + { + latestStat.Revenue = NoRevenue; + continue; + } + + // Split ticket/subscription revenue among participants for actual show days. + bool isShowDay = + latestStat.Schedule.Type == Theaters._theater._schedule._type.performance || + latestStat.Schedule.Type == Theaters._theater._schedule._type.manzai; + if (!isShowDay) + { + continue; + } + + Groups._group group = theater.GetGroup(); + if (group == null) + { + continue; + } + + List girls = group.GetGirls(true, false, null); + int girlCount = girls != null ? girls.Count : NoGirls; + if (girlCount <= NoGirls) + { + continue; + } + + // Use the revenue value from the stat row vanilla just recorded for this day. + long payout = latestStat.Revenue; + if (theater.AreSubsUnlocked() && staticVars.dateTime.Day == FirstDayOfMonth) + { + // Include monthly subscription revenue only on the first day, matching vanilla timing. + payout += theater.GetSubRevenue(); + } + + if (payout <= NoRevenue) + { + continue; + } + + long split = payout / (long)girlCount; + if (split <= NoRevenue) + { + continue; + } + + foreach (data_girls.girls girl in girls) + { + if (girl != null) { - // Include subscription revenue on the monthly payout day. - num += theater.GetSubRevenue(); + girl.Earn(split); } - foreach (data_girls.girls girls2 in girls) - { - if (num > NoRevenue && girlCount > NoGirls) - { - // Divide revenue evenly across current participants. - girls2.Earn(num / (long)girlCount); - } - } - } - } - } - } + } + } + } + } // Fixed Theater so that average stats ignore days off [HarmonyPatch(typeof(Theaters._theater), "GetAvgAttendance")] @@ -598,20 +608,26 @@ public class Relationships__relationship_BreakUp public static void Prefix(Relationships._relationship __instance, ref bool __state) { // Capture whether the pair was dating before BreakUp clears the flag. - __state = __instance.Dating; + __state = __instance != null && __instance.Dating; } public static void Postfix(Relationships._relationship __instance, bool __state) { // Only clear known status when the pair was actually dating. - if (!__state) + if (!__state || __instance == null || __instance.Girls == null || __instance.Girls.Count < 2) return; // Hide partner status for both sides after breakup. - __instance.Girls[FirstPartnerIndex].DatingData.Is_Partner_Status_Known = false; - __instance.Girls[SecondPartnerIndex].DatingData.Is_Partner_Status_Known = false; - } - } + if (__instance.Girls[FirstPartnerIndex] != null) + { + __instance.Girls[FirstPartnerIndex].DatingData.Is_Partner_Status_Known = false; + } + if (__instance.Girls[SecondPartnerIndex] != null) + { + __instance.Girls[SecondPartnerIndex].DatingData.Is_Partner_Status_Known = false; + } + } + } // Fixed Concert revenue formula so that it shows accurate estimated values [HarmonyPatch(typeof(SEvent_Concerts._concert._projectedValues), "GetRevenue")] @@ -846,52 +862,104 @@ public static bool Prefix(singles._single __instance, data_girls._paramType Type } - // Dating status is visible for underage members - [HarmonyPatch(typeof(data_girls.girls), "GetPartnerString")] - public class data_girls_girls_GetPartnerString - { - // Offset to the instruction following the method call. - private const int NextInstructionOffset = 1; - - public static IEnumerable Transpiler(IEnumerable instructions) - { - // Resolve the AOC check used by the early return in the base method. - MethodInfo isAoc = AccessTools.Method(typeof(data_girls.girls), "Is_AOC"); - if (isAoc == null) - { - PatchLog.WarnOncePerPatch("Is_AOC method lookup failed."); - return instructions; - } - - var matcher = new CodeMatcher(instructions); - // Find the Is_AOC call that guards the early return. - matcher.MatchForward(false, new CodeMatch(ci => IlHelpers.IsCallTo(ci, isAoc))); - - if (matcher.IsInvalid) - { - PatchLog.WarnOncePerPatch("Is_AOC call not found."); - return instructions; - } - - matcher.Advance(NextInstructionOffset); - if (matcher.IsInvalid || !IlHelpers.IsBranchTrue(matcher.Instruction)) - { - PatchLog.WarnOncePerPatch("branch after Is_AOC not found."); - return instructions; - } - if (!(matcher.Operand is Label)) + // Dating status is visible for underage members. + // A postfix is safer than a transpiler here and avoids invalid IL after upstream changes. + [HarmonyPatch(typeof(data_girls.girls), "GetPartnerString")] + public class data_girls_girls_GetPartnerString + { + public static void Postfix(data_girls.girls __instance, ref string __result) + { + if (__instance == null) { - PatchLog.WarnOncePerPatch("branch target label not found."); - return instructions; + return; } - // Always continue past the early return so status is shown for underage members. - // Preserve short/long branch size to avoid IL size issues. - OpCode newBranch = matcher.Opcode == OpCodes.Brtrue_S ? OpCodes.Br_S : OpCodes.Br; - matcher.Set(newBranch, matcher.Operand); - return matcher.InstructionEnumeration(); + // Keep vanilla behavior for AOC members. + if (__instance.Is_AOC()) + { + return; + } + + __result = BuildPartnerString(__instance); } - } + + private static string BuildPartnerString(data_girls.girls girl) + { + string text = ""; + if (!girl.DatingData.Is_Partner_Status_Known) + { + text += Language.Data["PROFILE__DATING_UNKNOWN"]; + } + else if (girl.DatingData.Partner_Status_Known_To_Player == data_girls.girls._dating_data._partner_status.free) + { + text += Language.Data["PROFILE__DATING_NOT_DATING"]; + } + else if (girl.DatingData.Partner_Status_Known_To_Player == data_girls.girls._dating_data._partner_status.taken_idol) + { + data_girls.girls girlfriend = girl.GetGirlfriend(); + if (girlfriend != null) + { + text += Language.Insert("PROFILE__DATING_IDOL", new string[] + { + girlfriend.GetName(true) + }); + } + else + { + text += Language.Data["PROFILE__DATING_IDOL_UNKNOWN"]; + } + } + else if (girl.DatingData.Partner_Status_Known_To_Player == data_girls.girls._dating_data._partner_status.taken_outside_bf) + { + text += Language.Data["PROFILE__DATING_HAS_BF"]; + } + else if (girl.DatingData.Partner_Status_Known_To_Player == data_girls.girls._dating_data._partner_status.taken_outside_gf) + { + text += Language.Data["PROFILE__DATING_HAS_GF"]; + } + else if (girl.DatingData.Partner_Status_Known_To_Player == data_girls.girls._dating_data._partner_status.taken_player) + { + text += Language.Data["PROFILE__DATING_YOU"]; + } + + text += "\n"; + if (girl.DatingData.Is_Sexuality_Known) + { + if (girl.sexuality == data_girls.girls._sexuality.straight) + { + text += Language.Data["PROFILE__DATING_STRAIGHT"]; + } + else if (girl.sexuality == data_girls.girls._sexuality.lesbian) + { + text += Language.Data["PROFILE__DATING_LESBIAN"]; + } + else + { + text += Language.Data["PROFILE__DATING_BI"]; + } + } + else + { + text += Language.Data["PROFILE__DATING_PREF_UNKNOWN"]; + } + + if (girl.DatingData.Previous_Attempt != Date_Flirt._flirt._category.NONE + && girl.DatingData.Partner_Status_Known_To_Player != data_girls.girls._dating_data._partner_status.taken_player) + { + text += "\n"; + if (girl.DatingData.Is_Uninterested || (girl.DatingData.Is_Sexuality_Known && !Date_Flirt.IsCompatibleSexuality(girl))) + { + text += Language.Data["PROFILE__DATING_NOT_INTERESTED"]; + } + else + { + text += Language.Data["PROFILE__DATING_INTERESTED"]; + } + } + + return text; + } + } // Fixed fan opinion to be impacted by concerts, SSK/show cancellation and random events From 98b815384b2402e777f62d4fe094af7637064964 Mon Sep 17 00:00:00 2001 From: ExSlam Date: Thu, 5 Mar 2026 16:33:56 -0500 Subject: [PATCH 10/33] fix: ensure mod settings button installation and handle null data in SaveManager --- mods/ModMenus/ModMenus.cs | 383 ++++++++++++++++++++++++++++++++++---- 1 file changed, 347 insertions(+), 36 deletions(-) diff --git a/mods/ModMenus/ModMenus.cs b/mods/ModMenus/ModMenus.cs index deb17c9..3e62eea 100755 --- a/mods/ModMenus/ModMenus.cs +++ b/mods/ModMenus/ModMenus.cs @@ -16,38 +16,125 @@ namespace ModMenus /// Ensures the mod menu popup is available when the game starts. /// [HarmonyPatch(typeof(PopupManager), "Start")] - public class PopupManager_Start - { - /// - /// Postfix method that generates the mod menu popup. - /// - public static void Postfix() - { - GenerateMenuPopup(); - } - } + public class PopupManager_Start + { + /// + /// Postfix method that generates the mod menu popup. + /// + public static void Postfix() + { + GenerateMenuPopup(); + ModMenusBootstrap.EnsureButtonInstalled(); + } + } /// /// Integrates the mod menu access point into the game's existing UI. /// - [HarmonyPatch(typeof(Tabs_Manager), "Awake")] - public class Tabs_Manager_Awake - { - /// - /// Postfix method that adds a mod menu button to the settings panel. - /// - public static void Postfix() - { - Transform settingsPanelTransform = Camera.main.GetComponent().Data.GetComponent().GetTab(Tabs_Manager._tab._type.settings).Tab.transform.Find("ScrollRect").Find("Container"); - GameObject mainMenuButton = settingsPanelTransform.Find("Main Menu").gameObject; - GameObject modMenuButton = CloneButton(mainMenuButton, settingsPanelTransform, BUTTON_OBJ_NAME, BUTTON_LABEL, false, false); - modMenuButton.transform.SetSiblingIndex(settingsPanelTransform.childCount - 2); - modMenuButton.GetComponent // Set up audition popup to allow scrolling cards - [HarmonyPatch(typeof(Popup_Audition), "Start")] - public class Popup_Audition_Start - { + [HarmonyPatch(typeof(Popup_Audition), "Start")] + public class Popup_Audition_Start + { /// /// Sets the properties of a RectTransform. @@ -37,219 +37,237 @@ private static void SetRectTransform(RectTransform rt, Vector2 anchorMin, Vector /// /// Postfix method to set up the scrollable audition popup. /// - /// The instance of Popup_Audition being patched. - public static void Postfix(Popup_Audition __instance) - { - if (__instance == null || __instance.Cards_Container == null) - return; - - Transform currentParent = __instance.Cards_Container.transform.parent; - if (currentParent == null) - return; - - if (currentParent.GetComponent() != null) - return; - - // Create ScrollRect container and attach to panel - GameObject scrollContainer = new(AUD_SCROLLRECT_NAME, typeof(RectTransform), typeof(ScrollRect)); - RectTransform scrollRectTransform = scrollContainer.GetComponent(); + /// The instance of Popup_Audition being patched. + public static void Postfix(Popup_Audition __instance) + { + if (__instance == null || __instance.Cards_Container == null) + return; + + Transform currentParent = __instance.Cards_Container.transform.parent; + if (currentParent == null) + return; + + if (currentParent.GetComponent() != null) + return; + + // Create ScrollRect container and attach to panel + GameObject scrollContainer = new(AUD_SCROLLRECT_NAME, typeof(RectTransform), typeof(ScrollRect)); + RectTransform scrollRectTransform = scrollContainer.GetComponent(); SetRectTransform(scrollRectTransform, Vector2.zero, Vector2.one, Vector2.zero, Vector2.zero); // Configure the ScrollRect - ScrollRect scrollRect = scrollContainer.GetComponent(); - scrollRect.content = __instance.Cards_Container.GetComponent(); // attach content - scrollRect.viewport = scrollRectTransform; - scrollRect.vertical = false; - scrollRect.horizontal = true; - scrollRect.movementType = ScrollRect.MovementType.Elastic; - scrollRect.elasticity = 0.1f; - scrollRect.inertia = false; - scrollRect.scrollSensitivity = 20; - - // Configure hierarchy - scrollContainer.transform.SetParent(currentParent, false); - __instance.Cards_Container.transform.SetParent(scrollContainer.transform, false); - - // Reuse existing fitter if one exists to avoid duplicate component warnings. - ContentSizeFitter fitter = __instance.Cards_Container.GetComponent(); - if (fitter == null) - { - fitter = __instance.Cards_Container.AddComponent(); - } - fitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize; - } - } - - /// - /// Tracks audition popup load start times so portrait loading can fail-safe instead of hanging forever. - /// - [HarmonyPatch(typeof(Popup_Audition), "Set")] - public class Popup_Audition_Set - { - /// - /// Postfix method that records when a new audition batch starts loading. - /// - /// Popup instance. - public static void Postfix(Popup_Audition __instance) - { - if (__instance == null) - { - return; - } - - auditionLoadStartedAt[__instance.GetInstanceID()] = Time.unscaledTime; - } - } - - /// - /// Clears load watchdog state when audition popup is reset. - /// - [HarmonyPatch(typeof(Popup_Audition), "Reset")] - public class Popup_Audition_Reset - { - /// - /// Postfix method that removes stale watchdog entries. - /// - /// Popup instance. - public static void Postfix(Popup_Audition __instance) - { - if (__instance == null) - { - return; - } - - auditionLoadStartedAt.Remove(__instance.GetInstanceID()); - } - } - - /// - /// Clears load watchdog state when audition popup is closed. - /// - [HarmonyPatch(typeof(Popup_Audition), "Close")] - public class Popup_Audition_Close - { - /// - /// Prefix method that removes stale watchdog entries before close logic runs. - /// - /// Popup instance. - public static void Prefix(Popup_Audition __instance) - { - if (__instance == null) - { - return; - } - - auditionLoadStartedAt.Remove(__instance.GetInstanceID()); - } - } - - /// - /// Prevents recruitment popup deadlocks when one portrait never resolves. - /// - [HarmonyPatch(typeof(Popup_Audition), "PortraitsLoaded")] - public class Popup_Audition_PortraitsLoaded - { - /// - /// Postfix method that applies a timeout fallback for stuck portrait loading. - /// - /// Popup instance. - /// Original readiness result. - public static void Postfix(Popup_Audition __instance, ref bool __result) - { - if (__result || __instance == null || __instance.Cards_Container == null) - { - return; - } - - int popupId = __instance.GetInstanceID(); - if (!auditionLoadStartedAt.TryGetValue(popupId, out float startedAt)) - { - return; - } - - float elapsed = Time.unscaledTime - startedAt; - if (elapsed < PORTRAIT_LOAD_TIMEOUT_SECONDS) - { - return; - } - - // The vanilla coroutine waits indefinitely for all portraits. With large candidate counts this can - // deadlock the popup (blur shown, cards never become interactive). After timeout, continue anyway. - EnsurePopupIsVisible(__instance); - Sprite fallbackSprite = FindFallbackPortraitSprite(__instance); - bool missingPortraits = FillMissingPortraits(__instance, fallbackSprite); - if (missingPortraits) - { - Debug.Log("[Targeted Auditions] Portrait load timed out. Continuing with fallback portraits."); - } - - __result = true; - } - - private static void EnsurePopupIsVisible(Popup_Audition popup) - { - CanvasGroup cg = popup.GetComponent(); - if (cg != null) - { - cg.alpha = 1f; - cg.blocksRaycasts = true; - cg.interactable = true; - } - - RectTransform rt = popup.GetComponent(); - if (rt != null) - { - rt.localScale = Vector3.one; - } - } - - private static Sprite FindFallbackPortraitSprite(Popup_Audition popup) - { - foreach (Transform child in popup.Cards_Container.transform) - { - Audition_Closed_Card closedCard = child.GetComponent(); - if (closedCard == null || closedCard.Portrait == null) - { - continue; - } - - Image image = closedCard.Portrait.GetComponent(); - if (image != null && image.sprite != null) - { - return image.sprite; - } - } - - return null; - } - - private static bool FillMissingPortraits(Popup_Audition popup, Sprite fallback) - { - bool hadMissing = false; - foreach (Transform child in popup.Cards_Container.transform) - { - Audition_Closed_Card closedCard = child.GetComponent(); - if (closedCard == null || closedCard.Portrait == null) - { - continue; - } - - Image image = closedCard.Portrait.GetComponent(); - if (image == null || image.sprite != null) - { - continue; - } - - hadMissing = true; - if (fallback != null) - { - image.sprite = fallback; - } - } - - return hadMissing; - } - } + ScrollRect scrollRect = scrollContainer.GetComponent(); + scrollRect.content = __instance.Cards_Container.GetComponent(); // attach content + scrollRect.viewport = scrollRectTransform; + scrollRect.vertical = false; + scrollRect.horizontal = true; + scrollRect.movementType = ScrollRect.MovementType.Elastic; + scrollRect.elasticity = 0.1f; + scrollRect.inertia = false; + scrollRect.scrollSensitivity = 20; + + // Configure hierarchy + scrollContainer.transform.SetParent(currentParent, false); + __instance.Cards_Container.transform.SetParent(scrollContainer.transform, false); + + // Reuse existing fitter if one exists to avoid duplicate component warnings. + ContentSizeFitter fitter = __instance.Cards_Container.GetComponent(); + if (fitter == null) + { + fitter = __instance.Cards_Container.AddComponent(); + } + fitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize; + } + } + + /// + /// Tracks audition popup loading from the moment Set begins. + /// Starting before the original Set keeps the watchdog state aligned with + /// Assistant Manager's delayed per-manager audition handoff. + /// + [HarmonyPatch(typeof(Popup_Audition), "Set", new Type[] { typeof(Auditions.data), typeof(bool) })] + public class Popup_Audition_Set + { + public static void Prefix(Popup_Audition __instance) + { + if (__instance == null) + { + return; + } + + auditionLoadStartedAt[__instance.GetInstanceID()] = Time.unscaledTime; + } + + public static Exception Finalizer(Popup_Audition __instance, Exception __exception) + { + if (__exception == null) + { + return null; + } + + if (__instance != null) + { + auditionLoadStartedAt.Remove(__instance.GetInstanceID()); + } + + Debug.LogError( + "[Targeted Auditions] Popup_Audition.Set failed:\n" + + __exception); + + // Preserve the original exception. + return __exception; + } + } + + /// + /// Clears load watchdog state when audition popup is reset. + /// + [HarmonyPatch(typeof(Popup_Audition), "Reset")] + public class Popup_Audition_Reset + { + /// + /// Postfix method that removes stale watchdog entries. + /// + /// Popup instance. + public static void Postfix(Popup_Audition __instance) + { + if (__instance == null) + { + return; + } + + auditionLoadStartedAt.Remove(__instance.GetInstanceID()); + } + } + + /// + /// Clears load watchdog state when audition popup is closed. + /// + [HarmonyPatch(typeof(Popup_Audition), "Close")] + public class Popup_Audition_Close + { + /// + /// Prefix method that removes stale watchdog entries before close logic runs. + /// + /// Popup instance. + public static void Prefix(Popup_Audition __instance) + { + if (__instance == null) + { + return; + } + + auditionLoadStartedAt.Remove(__instance.GetInstanceID()); + } + } + + /// + /// Prevents recruitment popup deadlocks when one portrait never resolves. + /// + [HarmonyPatch(typeof(Popup_Audition), "PortraitsLoaded")] + public class Popup_Audition_PortraitsLoaded + { + /// + /// Postfix method that applies a timeout fallback for stuck portrait loading. + /// + /// Popup instance. + /// Original readiness result. + public static void Postfix(Popup_Audition __instance, ref bool __result) + { + if (__result || __instance == null || __instance.Cards_Container == null) + { + return; + } + + int popupId = __instance.GetInstanceID(); + if (!auditionLoadStartedAt.TryGetValue(popupId, out float startedAt)) + { + return; + } + + float elapsed = Time.unscaledTime - startedAt; + if (elapsed < PORTRAIT_LOAD_TIMEOUT_SECONDS) + { + return; + } + + // The vanilla coroutine waits indefinitely for all portraits. With large candidate counts this can + // deadlock the popup (blur shown, cards never become interactive). After timeout, continue anyway. + EnsurePopupIsVisible(__instance); + Sprite fallbackSprite = FindFallbackPortraitSprite(__instance); + bool missingPortraits = FillMissingPortraits(__instance, fallbackSprite); + if (missingPortraits) + { + Debug.Log("[Targeted Auditions] Portrait load timed out. Continuing with fallback portraits."); + } + + __result = true; + } + + private static void EnsurePopupIsVisible(Popup_Audition popup) + { + CanvasGroup cg = popup.GetComponent(); + if (cg != null) + { + cg.alpha = 1f; + cg.blocksRaycasts = true; + cg.interactable = true; + } + + RectTransform rt = popup.GetComponent(); + if (rt != null) + { + rt.localScale = Vector3.one; + } + } + + private static Sprite FindFallbackPortraitSprite(Popup_Audition popup) + { + foreach (Transform child in popup.Cards_Container.transform) + { + Audition_Closed_Card closedCard = child.GetComponent(); + if (closedCard == null || closedCard.Portrait == null) + { + continue; + } + + Image image = closedCard.Portrait.GetComponent(); + if (image != null && image.sprite != null) + { + return image.sprite; + } + } + + return null; + } + + private static bool FillMissingPortraits(Popup_Audition popup, Sprite fallback) + { + bool hadMissing = false; + foreach (Transform child in popup.Cards_Container.transform) + { + Audition_Closed_Card closedCard = child.GetComponent(); + if (closedCard == null || closedCard.Portrait == null) + { + continue; + } + + Image image = closedCard.Portrait.GetComponent(); + if (image == null || image.sprite != null) + { + continue; + } + + hadMissing = true; + if (fallback != null) + { + image.sprite = fallback; + } + } + + return hadMissing; + } + } /// /// Patches the Auditions class to set variables at the start of an audition. @@ -263,25 +281,7 @@ public class Auditions_GenerateGirls /// The instance of Auditions being patched. public static void Prefix(Auditions __instance) { - // Set audition age limits (only if popup is not used) - bool toggle = int.Parse(variables.Get(VARID_AGELIMIT_POPUP_TOGGLE) ?? DEF_AGELIMIT_POPUP_TOGGLE) == 1; - if (!toggle) - { - minAge = int.Parse(variables.Get(VARID_MINAGE) ?? DEF_MINAGE_STR); - maxAge = int.Parse(variables.Get(VARID_MAXAGE) ?? DEF_MAXAGE_STR); - if (maxAge < minAge) - { - // swap values - maxAge = int.Parse(variables.Get(VARID_MINAGE) ?? DEF_MAXAGE_STR); - minAge = int.Parse(variables.Get(VARID_MAXAGE) ?? DEF_MINAGE_STR); - - // correct default variables - defaultMaxAge = maxAge; - defaultMinAge = minAge; - variables.Set(VARID_MAXAGE, maxAge.ToString()); - variables.Set(VARID_MINAGE, minAge.ToString()); - } - } + LoadConfiguredAgeRange(); // Set sexual orientation float varLesbian = float.Parse(variables.Get(VARID_LESCHANCE) ?? DEF_CHANCE_LES_STR); @@ -310,6 +310,19 @@ public static void Prefix(Auditions __instance) } + + public static Exception Finalizer(Exception __exception) + { + if (__exception != null) + { + Debug.LogError( + "[Targeted Auditions] Auditions.GenerateGirls failed:\n" + + __exception); + } + + // Preserve the original exception. + return __exception; + } } /// @@ -428,52 +441,27 @@ public class data_girls_girls_GenerateBirthday { public static void Postfix(ref data_girls.girls __instance) { - DateTime dateTime = staticVars.dateTime - .AddYears(-maxAge - 1) - .AddYears(UnityEngine.Random.Range(0, maxAge - minAge + 1)) - .AddMonths(UnityEngine.Random.Range(0, 12)) - .AddDays(UnityEngine.Random.Range(0, 31)); - __instance.SetBirthday(dateTime); + ApplyRandomBirthdayInConfiguredRange(__instance); } } /// - /// Patches the CM_Player_Audition_Button class to handle age input popup. (obsolete) + /// Loads Targeted Auditions settings before scripted unique-idol auditions generate candidates. /// - [HarmonyPatch(typeof(CM_Player_Audition_Button), "OnClick")] - public class Auditions_GenerateAudition + [HarmonyPatch(typeof(Auditions), "CustomAudition", new Type[] { typeof(string) })] + public class Auditions_CustomAudition_String { - /// - /// Postfix method to show the age input popup if enabled. (disabled) - /// - public static void Postfix() + public static void Prefix() { - bool toggle = int.Parse(variables.Get(VARID_AGELIMIT_POPUP_TOGGLE) ?? DEF_AGELIMIT_POPUP_TOGGLE) == 1; - if (toggle) - { - defaultMinAge = int.Parse(variables.Get(VARID_MINAGE) ?? DEF_MINAGE_STR); - defaultMaxAge = int.Parse(variables.Get(VARID_MAXAGE) ?? DEF_MAXAGE_STR); - if (defaultMaxAge < defaultMinAge) - { - // swap values - defaultMaxAge = int.Parse(variables.Get(VARID_MINAGE) ?? DEF_MAXAGE_STR); - defaultMinAge = int.Parse(variables.Get(VARID_MAXAGE) ?? DEF_MINAGE_STR); - - // correct variables - variables.Set(VARID_MAXAGE, maxAge.ToString()); - variables.Set(VARID_MINAGE, minAge.ToString()); - } - agePopup = true; - Camera.main.GetComponent().Data.GetComponent().Open(PopupManager._type.staff_nickname, true); - } + LoadConfiguredAgeRange(); } } /// /// Contains utility methods and variables for custom auditions. /// - class CustomAuditions - { + class CustomAuditions + { public const string DEF_MINAGE_STR = "12"; public const string DEF_MAXAGE_STR = "23"; public const string DEF_CHANCE_LES_STR = "7"; @@ -489,8 +477,8 @@ class CustomAuditions public const string VARID_COUNT = "CustomAudition_Count"; - public const string AUD_SCROLLRECT_NAME = "ScrollContainer"; - public const float PORTRAIT_LOAD_TIMEOUT_SECONDS = 6f; + public const string AUD_SCROLLRECT_NAME = "ScrollContainer"; + public const float PORTRAIT_LOAD_TIMEOUT_SECONDS = 6f; public const string VARID_AGELIMIT_POPUP_TOGGLE = "AuditionAgeLimit_TogglePopup"; public const string DEF_AGELIMIT_POPUP_TOGGLE = "0"; @@ -503,9 +491,9 @@ class CustomAuditions public static int chanceLesbian = 7; public static int chanceBi = 14; - public static bool agePopup = false; - public static bool inputValid = false; - public static Dictionary auditionLoadStartedAt = new(); + public static bool agePopup = false; + public static bool inputValid = false; + public static Dictionary auditionLoadStartedAt = new(); /// /// Parses the age range string and sets the minAge and maxAge values. @@ -578,5 +566,40 @@ public static bool IsInputValid(string ageRange) public static Dictionary priorityDict = new(); + public static void LoadConfiguredAgeRange() + { + // The former per-audition age popup is retired. Always use the Mod Menu range. + variables.Set(VARID_AGELIMIT_POPUP_TOGGLE, DEF_AGELIMIT_POPUP_TOGGLE); + + minAge = int.Parse(variables.Get(VARID_MINAGE) ?? DEF_MINAGE_STR); + maxAge = int.Parse(variables.Get(VARID_MAXAGE) ?? DEF_MAXAGE_STR); + if (maxAge < minAge) + { + int originalMinAge = minAge; + minAge = maxAge; + maxAge = originalMinAge; + + defaultMaxAge = maxAge; + defaultMinAge = minAge; + variables.Set(VARID_MAXAGE, maxAge.ToString()); + variables.Set(VARID_MINAGE, minAge.ToString()); + } + } + + public static void ApplyRandomBirthdayInConfiguredRange(data_girls.girls girl) + { + if (girl == null) + { + return; + } + + DateTime dateTime = staticVars.dateTime + .AddYears(-maxAge - 1) + .AddYears(UnityEngine.Random.Range(0, maxAge - minAge + 1)) + .AddMonths(UnityEngine.Random.Range(0, 12)) + .AddDays(UnityEngine.Random.Range(0, 31)); + girl.SetBirthday(dateTime); + } + } } diff --git a/mods/Targeted Auditions/Targeted Auditions.csproj b/mods/Targeted Auditions/Targeted Auditions.csproj index 6130bdc..6e56b1e 100755 --- a/mods/Targeted Auditions/Targeted Auditions.csproj +++ b/mods/Targeted Auditions/Targeted Auditions.csproj @@ -1,11 +1,11 @@ - + Targeted Auditions com.tel.customauditions (formerly Audition Age Limits) Customise you auditions to target girls by age, skill and sexuality. Tel - 2.0.2 + 2.0.3 ["gameplay"] diff --git a/mods/Targeted Auditions/assets/JSON/Mod Menu/modmenu.json b/mods/Targeted Auditions/assets/JSON/Mod Menu/modmenu.json index d3985b8..098e24c 100755 --- a/mods/Targeted Auditions/assets/JSON/Mod Menu/modmenu.json +++ b/mods/Targeted Auditions/assets/JSON/Mod Menu/modmenu.json @@ -15,21 +15,6 @@ "maxValue": 100, "defaultValue": 23 }, - { - "type": "dropdown", - "varID": "AuditionAgeLimit_TogglePopup", - "labelID": "AUDITIONAGELIMIT__MODMENU__TOGGLE", - "defaultValue": 1, - "itemIDList": ["No", "Yes"], - "ignore": true - }, - { - "type": "checkbox", - "varID": "AuditionAgeLimit_TogglePopup", - "labelID": "AUDITIONAGELIMIT__MODMENU__TOGGLE", - "defaultValue": false, - "ignore": true - }, { "type": "slider", "varID": "CustomAudition_Prio_cute", From 7cecd5b450dba8e16167615dac03b78ccde3413a Mon Sep 17 00:00:00 2001 From: ExSlam Date: Sat, 15 Aug 2026 13:52:58 -0400 Subject: [PATCH 13/33] Made graduation blocking more robust, and compatible with other Tel Mods --- mods/Never Graduate/Never Graduate.cs | 79 +++++++++++++++++------ mods/Never Graduate/Never Graduate.csproj | 50 +++++++------- 2 files changed, 86 insertions(+), 43 deletions(-) diff --git a/mods/Never Graduate/Never Graduate.cs b/mods/Never Graduate/Never Graduate.cs index a0563a7..0aad7be 100755 --- a/mods/Never Graduate/Never Graduate.cs +++ b/mods/Never Graduate/Never Graduate.cs @@ -1,18 +1,61 @@ -using HarmonyLib; -using System; -using UnityEngine; -using System.Reflection; - -namespace NeverGraduate -{ - // Never check for graduations unless the girl is fired - [HarmonyPatch(typeof(data_girls), "UpdateGraduationDates")] - public class data_girls_UpdateGraduationDates - { - public static bool Prefix() - { - return false; - } - } - -} +using HarmonyLib; + +namespace NeverGraduate +{ + // Hard stop for the normal weekly graduation pipeline. + // Returning false here means the game never reaches Graduation_Set_Default_Date() + // or Graduation_Date_Update() from UpdateGraduationDates(). That makes this mod + // authoritative over graduation timing: Job Hopper and Worker Rights cannot move + // an idol toward graduation while Never Graduate is loaded. + [HarmonyPatch(typeof(data_girls), "UpdateGraduationDates")] + public class data_girls_UpdateGraduationDates + { + public static bool Prefix() + { + return false; + } + } + + // Second lock for callers that bypass UpdateGraduationDates and invoke the per-idol + // date check directly. If this mod is enabled on a save where an idol had already + // announced graduation, restore her previous status before suppressing the check. + [HarmonyPatch(typeof(data_girls.girls), "Graduation_Date_Update")] + public static class data_girls_girls_Graduation_Date_Update + { + public static bool Prefix(data_girls.girls __instance) + { + if (__instance != null && __instance.status == data_girls._status.announced_graduation) + { + data_girls._status previousStatus = __instance.previous_status; + + // SetStatus normally refuses to leave announced_graduation, so unlock the + // stored status first, then call SetStatus to fire the normal UI/update hook. + __instance.status = previousStatus; + __instance.SetStatus(previousStatus); + } + + return false; + } + } + + // No automatic or mod-triggered announcement may put an idol onto a graduation countdown. + [HarmonyPatch(typeof(data_girls.girls), "Graduation_Announce")] + public static class data_girls_girls_Graduation_Announce + { + public static bool Prefix() + { + return false; + } + } + + // Some story/mod code calls the confirmation method directly, bypassing Graduation_Announce. + // Block that route too so the no-graduation invariant cannot be sidestepped accidentally. + [HarmonyPatch(typeof(data_girls.girls), "Graduation_Announce_Confirm")] + public static class data_girls_girls_Graduation_Announce_Confirm + { + public static bool Prefix() + { + return false; + } + } +} diff --git a/mods/Never Graduate/Never Graduate.csproj b/mods/Never Graduate/Never Graduate.csproj index 798e627..e754b7d 100755 --- a/mods/Never Graduate/Never Graduate.csproj +++ b/mods/Never Graduate/Never Graduate.csproj @@ -1,25 +1,25 @@ - - - - Never Graduate - com.tel.nevergraduate - This makes the game never check for graduations, unless the girl is fired. - Tel - 1.0.0 - ["gameplay"] - - - 0 - - $(HarmonyID) - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - + + + + Never Graduate + com.tel.nevergraduate + Prevents graduation-date checks and graduation announcements while enabled. Graduation-date changes from other mods are ignored; firing remains available. + Tel + 1.2.0 + ["gameplay"] + + + 0 + + $(HarmonyID) + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + From 2c8fa52ea6233ce930a14a65486faecc53747c25 Mon Sep 17 00:00:00 2001 From: ExSlam Date: Sat, 15 Aug 2026 13:54:04 -0400 Subject: [PATCH 14/33] Fixed math issues and bugs with Going Viral --- mods/Going Viral/ApplyTrending.cs | 416 ++++++----- mods/Going Viral/BonusTooltip.cs | 279 ++++--- mods/Going Viral/FanMechanics.cs | 494 ++++++------- mods/Going Viral/FanTooltip.cs | 536 +++++++------- mods/Going Viral/Going Viral.csproj | 50 +- mods/Going Viral/Resource.cs | 172 +++-- mods/Going Viral/TrendingManager.cs | 686 ++++++++---------- mods/Going Viral/TriggerTrending.cs | 307 ++++---- .../assets/JSON/Constants/constants.json | 174 +++-- 9 files changed, 1529 insertions(+), 1585 deletions(-) diff --git a/mods/Going Viral/ApplyTrending.cs b/mods/Going Viral/ApplyTrending.cs index 39c39ee..cd88f21 100755 --- a/mods/Going Viral/ApplyTrending.cs +++ b/mods/Going Viral/ApplyTrending.cs @@ -1,173 +1,243 @@ -using HarmonyLib; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection.Emit; -using System.Reflection; -using static GoingViral.TrendingManager; -using UnityEngine; - -namespace GoingViral -{ - - - // Apply fans increase to singles - [HarmonyPatch(typeof(singles), "GenerateSales")] - public class singles_GenerateSales - { - public static void Postfix(singles._single single) - { - if (IsTrending() == TrendingStatus.trending) - { - long totalNewFans = 0; - long casualNewFans = 0; - foreach (singles._single._sales sale in single.sales) - { - totalNewFans += sale.new_fans; - if (sale.fan.IsType(resources.fanType.casual)) - { - casualNewFans += sale.new_fans; - } - } - foreach (singles._single._sales sale in single.sales) - { - if (sale.fan.IsType(resources.fanType.casual)) - { - sale.new_fans = (long)Math.Round(sale.new_fans * totalNewFans / casualNewFans * GetTrendingCoeff()); - } - } - } - } - } - - // Apply fans increase to shows - [HarmonyPatch(typeof(Shows._show), "SetSales")] - public class Shows__show_SetSales - { - public static IEnumerable Transpiler(IEnumerable instructions) - { - List instructionList = new(instructions); - - int index = -1; - object newFansOperand = null; - object fanOperand = null; - for (int i = 0; i < instructionList.Count; i++) - { - if (instructionList[i].opcode == OpCodes.Ldloc_S && instructionList[i].operand is LocalVariableInfo localVariable && localVariable.LocalIndex == 12) - { - index = i; - newFansOperand = instructionList[i].operand; - } - if (instructionList[i].opcode == OpCodes.Ldloc_S && instructionList[i].operand is LocalVariableInfo localVariable2 && localVariable2.LocalIndex == 7) - { - fanOperand = instructionList[i].operand; - } - if (instructionList[i].opcode == OpCodes.Call && (MethodInfo)instructionList[i].operand == AccessTools.Method(typeof(data_girls), "AddFans_Equally", new Type[] { typeof(long), typeof(resources._fan), typeof(List) })) - { - break; - } - } - - if (index != -1) - { - instructionList.Insert(index + 1, new CodeInstruction(OpCodes.Ldloc_S, fanOperand)); - instructionList.Insert(index + 2, new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(Shows__show_SetSales), "Infix"))); - instructionList.Insert(index + 3, new CodeInstruction(OpCodes.Stloc_S, newFansOperand)); - instructionList.Insert(index + 4, new CodeInstruction(OpCodes.Ldloc_S, newFansOperand)); - } - - return instructionList.AsEnumerable(); - } - - public static int Infix(int fanCount, resources._fan fan) - { - if (IsTrending() == TrendingStatus.trending) - { - if (fan.IsType(resources.fanType.casual)) - { - fanCount = Mathf.RoundToInt(fanCount * GetTrendingCoeff()); - } - } - return fanCount; - } - } - - // Apply fans increase to businesses - [HarmonyPatch(typeof(business._proposal), "set_newFans")] - public class business__proposal_set_newFans - { - public static void Postfix(ref business._proposal __instance) - { - if (IsTrending() == TrendingStatus.trending) - { - __instance._newFans = Mathf.RoundToInt(__instance._newFans * GetTrendingCoeff()); - } - } - } - - - // Reduce fans_per_week to orig value for contracts - [HarmonyPatch(typeof(business), "AddActiveProposal")] - public class business_AddActiveProposal - { - public static void Postfix(ref business __instance) - { - if (IsTrending() == TrendingStatus.trending) - { - __instance.ActiveProposals[__instance.ActiveProposals.Count - 1].Fans_per_week = Mathf.RoundToInt(__instance.ActiveProposals[__instance.ActiveProposals.Count - 1].Fans_per_week / GetTrendingCoeff()); - } - } - } - - // Apply fans increase to business contracts - [HarmonyPatch(typeof(business), "DoWeeklyFans")] - public class business_DoWeeklyFans - { - public static void Postfix(ref business __instance) - { - if (IsTrending() == TrendingStatus.trending) - { - foreach (business.active_proposal active_proposal in __instance.ActiveProposals) - { - if (active_proposal.Fans_per_week > 0) - { - active_proposal.Girl.AddFans(Mathf.RoundToInt(active_proposal.Fans_per_week * (GetTrendingCoeff() - 1)), null); - } - } - } - } - } - - // Apply fans increase to business contracts - [HarmonyPatch(typeof(Contracts_Line), "Set")] - public class Contracts_Line_Set - { - public static void Postfix(ref Contracts_Line __instance, business.active_proposal ___ActiveProposal) - { - if (IsTrending() == TrendingStatus.trending) - { - ExtensionMethods.SetText( - __instance.NewFans, - ExtensionMethods.formatNumber( - Mathf.RoundToInt(___ActiveProposal.Fans_per_week * GetTrendingCoeff()), - false, - false - ) - ); - } - } - } - - // Apply fans increase to tour - [HarmonyPatch(typeof(SEvent_Tour.tour), "GetNewFansByAttendance")] - public class SEvent_Tour_tour_GetNewFansByAttendance - { - public static void Postfix(ref int __result) - { - if (IsTrending() == TrendingStatus.trending) - { - __result = Mathf.RoundToInt(__result * GetTrendingCoeff()); - } - } - } -} +using HarmonyLib; +using System; +using System.Collections.Generic; +using UnityEngine; +using static GoingViral.TrendingManager; + +namespace GoingViral +{ + [HarmonyPatch(typeof(singles), "GenerateSales")] + public class singles_GenerateSales + { + public static void Postfix(singles._single single) + { + if (IsTrending() != TrendingStatus.trending || single == null || single.sales == null) + return; + + long totalNewFans = 0; + long casualNewFans = 0; + List casualSales = new List(); + foreach (singles._single._sales sale in single.sales) + { + if (sale == null) continue; + totalNewFans = SafeAdd(totalNewFans, sale.new_fans); + if (sale.fan != null && sale.fan.IsType(resources.fanType.casual)) + { + casualSales.Add(sale); + casualNewFans = SafeAdd(casualNewFans, sale.new_fans); + } + } + + if (totalNewFans <= 0 || casualSales.Count == 0) + return; + + long targetTotal = ScaleLong(totalNewFans, GetTrendingCoeff()); + long bonus = Math.Max(0, targetTotal - totalNewFans); + if (bonus == 0) + return; + + long assigned = 0; + for (int i = 0; i < casualSales.Count; i++) + { + long share; + if (i == casualSales.Count - 1) + { + share = bonus - assigned; + } + else if (casualNewFans > 0) + { + share = (long)Math.Round(bonus * Math.Max(0d, (double)casualSales[i].new_fans) / casualNewFans, MidpointRounding.AwayFromZero); + share = Math.Min(share, bonus - assigned); + } + else + { + share = (bonus - assigned) / (casualSales.Count - i); + } + + casualSales[i].new_fans = SafeAdd(casualSales[i].new_fans, share); + assigned = SafeAdd(assigned, share); + } + } + } + + // Scope show fan additions so the viral bonus can be added without relying on compiler local numbers. + [HarmonyPatch(typeof(Shows._show), "SetSales")] + public class Shows__show_SetSales + { + internal sealed class FanBucket + { + public resources._fan Fan; + public long BaseFans; + } + + internal sealed class Context + { + public Shows._show Show; + public long BaseNewFans; + public bool AddingTrendingBonus; + public readonly List CasualBuckets = new List(); + } + + private static readonly Stack contexts = new Stack(); + internal static Context Current { get { return contexts.Count > 0 ? contexts.Peek() : null; } } + + [HarmonyPriority(Priority.First)] + public static void Prefix(Shows._show __instance) + { + contexts.Push(new Context { Show = __instance }); + } + + public static Exception Finalizer(Exception __exception) + { + if (contexts.Count > 0) contexts.Pop(); + return __exception; + } + } + + [HarmonyPatch(typeof(data_girls), "AddFans_Equally", new Type[] { typeof(long), typeof(resources._fan), typeof(List) })] + public class data_girls_AddFans_Equally_TrendingRecorder + { + [HarmonyPriority(Priority.Last)] + public static void Prefix(long total_fans, resources._fan _Fan) + { + Shows__show_SetSales.Context context = Shows__show_SetSales.Current; + if (context == null || context.AddingTrendingBonus || total_fans <= 0) + return; + + context.BaseNewFans = SafeAdd(context.BaseNewFans, total_fans); + if (_Fan != null && _Fan.IsType(resources.fanType.casual)) + context.CasualBuckets.Add(new Shows__show_SetSales.FanBucket { Fan = _Fan, BaseFans = total_fans }); + } + } + + [HarmonyPatch(typeof(Shows._show), "SetNewFans")] + public class Shows__show_SetNewFans_Trending + { + [HarmonyPriority(Priority.Last)] + public static void Prefix(ref int val) + { + Shows__show_SetSales.Context context = Shows__show_SetSales.Current; + if (context == null || IsTrending() != TrendingStatus.trending || context.BaseNewFans <= 0) + return; + + long target = ScaleLong(context.BaseNewFans, GetTrendingCoeff()); + long bonus = Math.Max(0, target - context.BaseNewFans); + if (bonus == 0 || context.CasualBuckets.Count == 0) + { + val = context.BaseNewFans > int.MaxValue ? int.MaxValue : (int)context.BaseNewFans; + return; + } + + List cast = context.Show != null ? context.Show.GetCast() : null; + if (cast == null || cast.Count == 0) + { + val = context.BaseNewFans > int.MaxValue ? int.MaxValue : (int)context.BaseNewFans; + return; + } + + long casualBase = 0; + foreach (Shows__show_SetSales.FanBucket bucket in context.CasualBuckets) + casualBase = SafeAdd(casualBase, Math.Max(0, bucket.BaseFans)); + + long assigned = 0; + context.AddingTrendingBonus = true; + try + { + for (int i = 0; i < context.CasualBuckets.Count; i++) + { + Shows__show_SetSales.FanBucket bucket = context.CasualBuckets[i]; + long share; + if (i == context.CasualBuckets.Count - 1) + share = bonus - assigned; + else if (casualBase > 0) + { + share = (long)Math.Round(bonus * (double)Math.Max(0, bucket.BaseFans) / casualBase, MidpointRounding.AwayFromZero); + share = Math.Min(share, bonus - assigned); + } + else + share = (bonus - assigned) / (context.CasualBuckets.Count - i); + + if (share > 0 && bucket.Fan != null) + { + data_girls.AddFans_Equally(share, bucket.Fan, cast); + assigned = SafeAdd(assigned, share); + } + } + } + finally + { + context.AddingTrendingBonus = false; + } + + long displayed = SafeAdd(context.BaseNewFans, assigned); + if (displayed > int.MaxValue) val = int.MaxValue; + else if (displayed < int.MinValue) val = int.MinValue; + else val = (int)displayed; + } + } + + [HarmonyPatch(typeof(business._proposal), "set_newFans")] + public class business__proposal_set_newFans + { + public static void Postfix(business._proposal __instance) + { + if (IsTrending() == TrendingStatus.trending && __instance != null && __instance._newFans > 0) + __instance._newFans = ScaleInt(__instance._newFans, GetTrendingCoeff()); + } + } + + [HarmonyPatch(typeof(business), "AddActiveProposal")] + public class business_AddActiveProposal + { + public static void Postfix(business __instance) + { + if (IsTrending() != TrendingStatus.trending || __instance == null || __instance.ActiveProposals == null || __instance.ActiveProposals.Count == 0) + return; + + business.active_proposal proposal = __instance.ActiveProposals[__instance.ActiveProposals.Count - 1]; + float coeff = GetTrendingCoeff(); + if (proposal != null && proposal.Fans_per_week > 0 && coeff > 0f) + proposal.Fans_per_week = Mathf.RoundToInt(proposal.Fans_per_week / coeff); + } + } + + [HarmonyPatch(typeof(business), "DoWeeklyFans")] + public class business_DoWeeklyFans + { + public static void Postfix(business __instance) + { + if (IsTrending() != TrendingStatus.trending || __instance == null || __instance.ActiveProposals == null) + return; + + float extraCoeff = GetTrendingCoeff() - 1f; + foreach (business.active_proposal proposal in __instance.ActiveProposals) + { + if (proposal != null && proposal.Girl != null && proposal.Fans_per_week > 0) + proposal.Girl.AddFans(ScaleLong(proposal.Fans_per_week, extraCoeff), null); + } + } + } + + [HarmonyPatch(typeof(Contracts_Line), "Set")] + public class Contracts_Line_Set + { + public static void Postfix(Contracts_Line __instance, business.active_proposal ___ActiveProposal) + { + if (IsTrending() != TrendingStatus.trending || __instance == null || __instance.NewFans == null || ___ActiveProposal == null) + return; + + ExtensionMethods.SetText(__instance.NewFans, + ExtensionMethods.formatNumber(ScaleLong(___ActiveProposal.Fans_per_week, GetTrendingCoeff()), false, false)); + } + } + + [HarmonyPatch(typeof(SEvent_Tour.tour), "GetNewFansByAttendance")] + public class SEvent_Tour_tour_GetNewFansByAttendance + { + public static void Postfix(ref int __result) + { + if (IsTrending() == TrendingStatus.trending && __result > 0) + __result = ScaleInt(__result, GetTrendingCoeff()); + } + } +} diff --git a/mods/Going Viral/BonusTooltip.cs b/mods/Going Viral/BonusTooltip.cs index 655fd15..129a72c 100755 --- a/mods/Going Viral/BonusTooltip.cs +++ b/mods/Going Viral/BonusTooltip.cs @@ -1,141 +1,138 @@ -using HarmonyLib; -using System; -using System.Reflection; -using UnityEngine; - -namespace GoingViral -{ - // tooltip for shows - [HarmonyPatch(typeof(Show_Popup), "SetParam")] - public class Show_Popup_SetParam - { - public static void Postfix(Show_Popup __instance, Show_Popup_Param_Button._type type, Shows._param ___medium, Shows._param ___genre) - { - if (type != Show_Popup_Param_Button._type.medium) - return; - - if (___medium.media_type == Shows._param._media_type.tv) - { - Show_Popup_Param_Button[] buttons = __instance.Grid_Genre.GetComponentsInChildren(); - foreach (Show_Popup_Param_Button button in buttons) - { - // Get last show - DateTime? lastShowDate = null; - foreach (Shows._show show in Shows.shows) - { - if (show.LaunchDate != null && show.medium.media_type == Shows._param._media_type.tv && show.genre.id == ___genre.id) - { - if (lastShowDate == null || show.LaunchDate > (lastShowDate ?? staticVars.dateTime)) - { - lastShowDate = show.LaunchDate; - } - } - } - - string tooltipText = Traverse.Create(button.GetComponent()).Field("tooltipText").GetValue() as string; - - - if (lastShowDate != null) - { - int days = (staticVars.dateTime - (lastShowDate ?? staticVars.dateTime)).Days; - - string clr = mainScript.green; - if (days < 365) - { - clr = mainScript.red; - } - tooltipText += mainScript.separator + Language.Data["TREND__GENRE"]; - - if (days != 1) - { - tooltipText += ExtensionMethods.color(Language.Insert("DAYS_AGO", new[] { days.ToString() }), clr); - } - else - { - tooltipText += ExtensionMethods.color(Language.Data["ONE_DAY_AGO"], clr); - } - } - else - { - tooltipText += mainScript.separator + Language.Data["TREND__GENRE_NEVER"]; - } - - button.GetComponent().SetTooltip(button.param.DescriptionReplaceVariables(tooltipText)); - } - } - else - { - Show_Popup_Param_Button[] buttons = __instance.Grid_Genre.GetComponentsInChildren(); - foreach (Show_Popup_Param_Button button in buttons) - { - button.GetComponent().SetTooltip(button.param.GetTooltip()); - } - } - } - - } - - - // tooltip for single medium - [HarmonyPatch(typeof(Shows._param), "GetTooltip")] - public class Shows__param_GetTooltip - { - public static void Postfix(ref Shows._param __instance, ref string __result) - { - if (__instance.ParamType == Shows._param._paramType.medium) - { - __result += mainScript.separator + Language.Data["TREND__MEDIUM"]; - } - } - } - - // tooltip for single genres - [HarmonyPatch(typeof(SinglePopup_GenreButton), "RenderTooltip")] - public class SinglePopup_GenreButton_RenderTooltip - { - public static void Postfix(ref SinglePopup_GenreButton __instance) - { - singles._param param = __instance.param; - if (param.derived != "business" && !param.IsRiskyMarketing()) - { - return; - } - - singles._param._special_type marketing = param.Special_Type; - if(marketing != singles._param._special_type.ad_campaign && marketing != singles._param._special_type.viral_campaign && marketing != singles._param._special_type.fake_scandal) - { - return; - } - - ButtonDefault buttonDefault = Traverse.Create(__instance).Field("buttonDefault").GetValue() as ButtonDefault; - string tooltipText = Traverse.Create(buttonDefault).Field("tooltipText").GetValue() as string; - - - tooltipText += mainScript.separator; - int chanceSuccess = Mathf.RoundToInt(TrendingManager.GetTrendingChance(param, Single_Marketing_Roll._result.success_crit) * param.GetSuccessChance(Single_Marketing_Roll._result.success_crit) / 100); - tooltipText += Language.Insert("TREND__SINGLE_SUCCESS", chanceSuccess.ToString()); - - int chanceFailure = 0; - switch (marketing) - { - case singles._param._special_type.ad_campaign: - case singles._param._special_type.viral_campaign: - chanceFailure = Mathf.RoundToInt(TrendingManager.GetTrendingChance(param, Single_Marketing_Roll._result.fail) * param.GetSuccessChance(Single_Marketing_Roll._result.fail) / 100 + - TrendingManager.GetTrendingChance(param, Single_Marketing_Roll._result.fail_crit) * param.GetSuccessChance(Single_Marketing_Roll._result.fail_crit) / 100); - break; - case singles._param._special_type.fake_scandal: - chanceFailure = Mathf.RoundToInt(TrendingManager.GetTrendingChance(param.GetSuccessModifier(Single_Marketing_Roll._result.fail, false)) * param.GetSuccessChance(Single_Marketing_Roll._result.fail) / 100 + - TrendingManager.GetTrendingChance(param.GetSuccessModifier(Single_Marketing_Roll._result.fail_crit, false)) * param.GetSuccessChance(Single_Marketing_Roll._result.fail_crit) / 100); - break; - default: - break; - } - - tooltipText += "\n" + Language.Insert("TREND__SINGLE_FAIL", chanceFailure.ToString()); - tooltipText += "\n" + Language.Data["TREND__SINGLE_DESC"]; - - buttonDefault.SetTooltip(param.DescriptionReplaceVariables(tooltipText)); - } - - } -} +using HarmonyLib; +using System; +using UnityEngine; + +namespace GoingViral +{ + [HarmonyPatch(typeof(Show_Popup), "SetParam")] + public class Show_Popup_SetParam + { + [HarmonyAfter("com.tel.traitsfix")] + public static void Postfix(Show_Popup __instance, Show_Popup_Param_Button._type type, Shows._param ___medium) + { + if (type != Show_Popup_Param_Button._type.medium || __instance == null || __instance.Grid_Genre == null) + return; + + Show_Popup_Param_Button[] buttons = __instance.Grid_Genre.GetComponentsInChildren(); + if (buttons == null) + return; + + bool isTv = ___medium != null && ___medium.media_type == Shows._param._media_type.tv; + foreach (Show_Popup_Param_Button button in buttons) + { + if (button == null || button.param == null) + continue; + ButtonDefault buttonDefault = button.GetComponent(); + if (buttonDefault == null) + continue; + + if (!isTv) + { + buttonDefault.SetTooltip(button.param.GetTooltip()); + continue; + } + + // Rebuild from the parameter tooltip each time. Other mods patching GetTooltip are + // preserved, while our own TV suffix cannot accumulate across repeated SetParam calls. + string tooltip = button.param.GetTooltip() ?? ""; + + DateTime? lastShowDate = GetLastTvShowDate(button.param.id); + if (lastShowDate.HasValue) + { + int days = Math.Max(0, (staticVars.dateTime - lastShowDate.Value).Days); + string color = days >= 365 ? mainScript.green : mainScript.red; + tooltip += mainScript.separator + Language.Data["TREND__GENRE"]; + tooltip += days == 1 + ? ExtensionMethods.color(Language.Data["ONE_DAY_AGO"], color) + : ExtensionMethods.color(Language.Insert("DAYS_AGO", new string[] { days.ToString() }), color); + } + else + { + tooltip += mainScript.separator + Language.Data["TREND__GENRE_NEVER"]; + } + + buttonDefault.SetTooltip(button.param.DescriptionReplaceVariables(tooltip)); + } + } + + private static DateTime? GetLastTvShowDate(int genreId) + { + DateTime? result = null; + if (Shows.shows == null) + return null; + + foreach (Shows._show show in Shows.shows) + { + if (show == null || show.medium == null || show.genre == null || show.LaunchDate == default(DateTime)) + continue; + if (show.medium.media_type != Shows._param._media_type.tv || show.genre.id != genreId) + continue; + if (!result.HasValue || show.LaunchDate > result.Value) + result = show.LaunchDate; + } + return result; + } + } + + [HarmonyPatch(typeof(Shows._param), "GetTooltip")] + public class Shows__param_GetTooltip + { + public static void Postfix(Shows._param __instance, ref string __result) + { + if (__instance != null && __instance.ParamType == Shows._param._paramType.medium) + __result = (__result ?? "") + mainScript.separator + Language.Data["TREND__MEDIUM"]; + } + } + + [HarmonyPatch(typeof(SinglePopup_GenreButton), "RenderTooltip")] + public class SinglePopup_GenreButton_RenderTooltip + { + public static void Postfix(SinglePopup_GenreButton __instance) + { + if (__instance == null || __instance.param == null) + return; + + singles._param param = __instance.param; + if (param.derived != "business" && !param.IsRiskyMarketing()) + return; + + singles._param._special_type marketing = param.Special_Type; + if (marketing != singles._param._special_type.ad_campaign && + marketing != singles._param._special_type.viral_campaign && + marketing != singles._param._special_type.fake_scandal) + return; + + ButtonDefault buttonDefault = Traverse.Create(__instance).Field("buttonDefault").GetValue() as ButtonDefault; + if (buttonDefault == null) + return; + + string tooltip = Traverse.Create(buttonDefault).Field("tooltipText").GetValue() as string; + if (tooltip == null) tooltip = ""; + tooltip += mainScript.separator; + + float success = TrendingManager.GetTrendingChance(param, Single_Marketing_Roll._result.success_crit) * + param.GetSuccessChance(Single_Marketing_Roll._result.success_crit) / 100f; + int chanceSuccess = Mathf.Clamp(Mathf.RoundToInt(success), 0, 100); + tooltip += Language.Insert("TREND__SINGLE_SUCCESS", new string[] { chanceSuccess.ToString() }); + + float failure = 0f; + if (marketing == singles._param._special_type.ad_campaign || marketing == singles._param._special_type.viral_campaign) + { + failure += TrendingManager.GetTrendingChance(param, Single_Marketing_Roll._result.fail_crit) * + param.GetSuccessChance(Single_Marketing_Roll._result.fail_crit) / 100f; + } + else if (marketing == singles._param._special_type.fake_scandal) + { + failure += TrendingManager.GetTrendingChance(param.GetSuccessModifier(Single_Marketing_Roll._result.fail, false)) * + param.GetSuccessChance(Single_Marketing_Roll._result.fail) / 100f; + failure += TrendingManager.GetTrendingChance(param.GetSuccessModifier(Single_Marketing_Roll._result.fail_crit, false)) * + param.GetSuccessChance(Single_Marketing_Roll._result.fail_crit) / 100f; + } + + int chanceFailure = Mathf.Clamp(Mathf.RoundToInt(failure), 0, 100); + tooltip += "\n" + Language.Insert("TREND__SINGLE_FAIL", new string[] { chanceFailure.ToString() }); + tooltip += "\n" + Language.Data["TREND__SINGLE_DESC"]; + buttonDefault.SetTooltip(param.DescriptionReplaceVariables(tooltip)); + } + } +} diff --git a/mods/Going Viral/FanMechanics.cs b/mods/Going Viral/FanMechanics.cs index 7fe5065..10a399d 100755 --- a/mods/Going Viral/FanMechanics.cs +++ b/mods/Going Viral/FanMechanics.cs @@ -1,276 +1,218 @@ -using HarmonyLib; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection.Emit; -using System.Reflection; -using static GoingViral.TrendingManager; -using UnityEngine; -using static Achievements; -using static singles; - -namespace GoingViral -{ - - // Apply 3x casual bias to fan appeal for adding fans - // Separate opinion from fan decrease - [HarmonyPatch(typeof(data_girls.girls), "AddFans", new Type[] { typeof(long), typeof(resources.fanType?) })] - public class data_girls_girls_AddFans - { - public static IEnumerable Transpiler(IEnumerable instructions) - { - List instructionList = new(instructions); - - int index = -1; - bool breakFlag = false; - object operandi = null; - for (int i = 0; i < instructionList.Count; i++) - { - if (breakFlag && instructionList[i].opcode == OpCodes.Stloc_S) - { - index = i; - operandi = instructionList[i].operand; - break; - } - if (instructionList[i].opcode == OpCodes.Callvirt && instructionList[i].operand is MethodBase method && method.Name == "GetTotalAppeal" && method.DeclaringType == typeof(resources._fan)) - { - breakFlag = true; - } - } - - if (index != -1) - { - instructionList.Insert(index + 1, new CodeInstruction(OpCodes.Ldarg_0)); - instructionList.Insert(index + 2, new CodeInstruction(OpCodes.Ldloc_3)); - instructionList.Insert(index + 3, new CodeInstruction(OpCodes.Ldarg_1)); - instructionList.Insert(index + 4, new CodeInstruction(OpCodes.Ldloc_S, operandi)); - instructionList.Insert(index + 5, new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(data_girls_girls_AddFans), "Infix"))); - instructionList.Insert(index + 6, new CodeInstruction(OpCodes.Stloc_S, operandi)); - } - - return instructionList.AsEnumerable(); - } - - public static float Infix(data_girls.girls __this, resources._fan fan, long val, float fanProportion) - { - - // Apply default calc with opinions for decrease in fans - if (val < 0) - { - float churnTotal = 0; - - foreach (resources._fan _fan in __this.Fans) - { - churnTotal += GetFanChurn(_fan.appeal, _fan.Ratio, _fan.hardcoreness); - } - - fanProportion = GetFanChurn(fan.appeal, fan.Ratio, fan.hardcoreness) / churnTotal; - } - // Apply new calc excluding opinions for increase in fans - else if (val > 0) - { - float acquisitionTotal = 0; - - foreach (resources._fan _fan in __this.Fans) - { - acquisitionTotal += GetFanAcquisition(__this, _fan.appeal, _fan.hardcoreness); - } - - fanProportion = GetFanAcquisition(__this, fan.appeal, fan.hardcoreness) / acquisitionTotal; - } - - - return fanProportion; - } - - - } - - // When losing fans, use appeal and opinion instead of fame to distribute across girls - [HarmonyPatch(typeof(data_girls), "AddFans")] - public class data_girls_AddFans - { - public static IEnumerable Transpiler(IEnumerable instructions) - { - List instructionList = new(instructions); - - int index = -1; - object coeffOperand = null; - object girlOperand = null; - for (int i = 0; i < instructionList.Count; i++) - { - if (instructionList[i].opcode == OpCodes.Stloc_S && instructionList[i].operand is LocalVariableInfo girl && girl.LocalIndex == 6) - { - girlOperand = instructionList[i].operand; - } - if (instructionList[i].opcode == OpCodes.Stloc_S && instructionList[i].operand is LocalVariableInfo coeff && coeff.LocalIndex == 7) - { - index = i; - coeffOperand = instructionList[i].operand; - break; - } - } - - if (index != -1) - { - instructionList.Insert(index + 1, new CodeInstruction(OpCodes.Ldloc_S, girlOperand)); - instructionList.Insert(index + 2, new CodeInstruction(OpCodes.Ldloc_S, coeffOperand)); - instructionList.Insert(index + 3, new CodeInstruction(OpCodes.Ldarg_0)); - instructionList.Insert(index + 4, new CodeInstruction(OpCodes.Ldarg_1)); - instructionList.Insert(index + 5, new CodeInstruction(OpCodes.Ldarg_2)); - instructionList.Insert(index + 6, new CodeInstruction(OpCodes.Ldarg_3)); - instructionList.Insert(index + 7, new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(data_girls_AddFans), "Infix"))); - instructionList.Insert(index + 8, new CodeInstruction(OpCodes.Stloc_S, coeffOperand)); - } - - return instructionList.AsEnumerable(); - } - - public static float Infix(data_girls.girls girl, float coeff, long total_fans, resources.fanType? fanType, List Girls, data_girls.girls ExceptionGirl) - { - if (total_fans >= 0) - { - return coeff; - } - - float churnTotal = 0; - - foreach (data_girls.girls _girl in Girls) - { - if (_girl.status != data_girls._status.graduated && _girl != ExceptionGirl) - { - foreach (resources._fan _fan in _girl.Fans) - { - if (_fan.IsType(fanType)) - { - churnTotal += GetFanChurn(_fan.appeal, _fan.Ratio, _fan.hardcoreness); - } - } - } - } - - float girlChurn = 0; - foreach (resources._fan _fan in girl.Fans) - { - if (_fan.IsType(fanType)) - { - girlChurn += GetFanChurn(_fan.appeal, _fan.Ratio, _fan.hardcoreness); - } - } - - float fanProportion = girlChurn / churnTotal; - - - return fanProportion; - } - } - - // Set opinion for theater - [HarmonyPatch(typeof(resources), "OnNewWeek")] - public class resources_OnNewWeek - { - public static void Postfix() - { - foreach (Theaters._theater theater in Theaters.Theaters_) - { - if(theater == null || theater.Stats.Count < 7) - { - continue; - } - - - foreach (object fanTypeEnum in Enum.GetValues(typeof(resources.fanType))) - { - resources.fanType type = (resources.fanType)fanTypeEnum; - - float thisWeek = 0; - for (int i = 1; i <= 7; i++) - { - if(type == theater.Stats[theater.Stats.Count - i].Schedule.FanType) - { - thisWeek += 1; - } - } - - if (theater.Stats.Count < 14) - { - if(thisWeek > 0) - { - foreach (data_girls.girls girl in theater.GetGroup().GetGirls()) - { - if (girl != null && girl.status != data_girls._status.graduated) - { - girl.AddAppeal(type, 1); - NotificationManager.AddNotification(Language.Insert("THEATER__FANS_LIKE", new string[] - { - ExtensionMethods.color(resources.GetFanTitle(type), mainScript.green), - theater.GetGroup().Title - }), mainScript.green32, NotificationManager._notification._type.fans_opinion_change); - } - } - } - continue; - } - - float pastWeek = 0; - for (int i = 8; i <= 14; i++) - { - if (type == theater.Stats[theater.Stats.Count - i].Schedule.FanType) - { - pastWeek += 1; - } - } - - foreach (data_girls.girls girl in theater.GetGroup().GetGirls()) - { - if (girl != null && girl.status != data_girls._status.graduated) - { - if(thisWeek > pastWeek) - { - girl.AddAppeal(type, 1); - NotificationManager.AddNotification(Language.Insert("THEATER__FANS_LIKE", new string[] - { - ExtensionMethods.color(resources.GetFanTitle(type), mainScript.green), - theater.GetGroup().Title - }), mainScript.green32, NotificationManager._notification._type.fans_opinion_change); - } - else if (thisWeek < pastWeek) - { - girl.AddAppeal(type, -1); - NotificationManager.AddNotification(Language.Insert("THEATER__FANS_DISLIKE", new string[] - { - ExtensionMethods.color(resources.GetFanTitle(type), mainScript.red), - theater.GetGroup().Title - }), mainScript.red32, NotificationManager._notification._type.fans_opinion_change); - } - } - } - } - - } - } - } - - - // Fixed fan opinion to be impacted by concerts, SSK/show cancellation and random events - [HarmonyPatch(typeof(resources._fanOpinion), "Add")] - public class resources__fanOpinion_Add - { - public static void Postfix(resources._fanOpinion __instance, float val) - { - if (Harmony.HasAnyPatches("com.tel.unofficialpatch")) - { - return; - } - foreach (data_girls.girls girl in data_girls.girl) - { - if (girl != null && girl.status != data_girls._status.graduated) - { - girl.AddAppeal(__instance.type, val); - } - } - } - } - - -} +using HarmonyLib; +using System; +using System.Collections.Generic; +using UnityEngine; +using static GoingViral.TrendingManager; + +namespace GoingViral +{ + // Replace appeal weights only while an idol is actually distributing a fan change. + [HarmonyPatch(typeof(data_girls.girls), "AddFans", new Type[] { typeof(long), typeof(resources.fanType?) })] + public class data_girls_girls_AddFans + { + internal sealed class Context + { + public data_girls.girls Girl; + public long Value; + } + + private static readonly Stack contexts = new Stack(); + internal static Context Current { get { return contexts.Count > 0 ? contexts.Peek() : null; } } + + [HarmonyPriority(Priority.First)] + public static void Prefix(data_girls.girls __instance, long val) + { + contexts.Push(new Context { Girl = __instance, Value = val }); + } + + public static Exception Finalizer(Exception __exception) + { + if (contexts.Count > 0) contexts.Pop(); + return __exception; + } + } + + [HarmonyPatch(typeof(resources._fan), "GetTotalAppeal")] + public class resources__fan_GetTotalAppeal_TrendingWeights + { + [HarmonyPriority(Priority.Last)] + public static void Postfix(resources._fan __instance, ref float __result) + { + data_girls_girls_AddFans.Context context = data_girls_girls_AddFans.Current; + if (context == null || context.Girl == null || __instance == null) + return; + + float weight; + if (context.Value < 0) + weight = GetFanChurn(__instance.appeal, __instance.Ratio, __instance.hardcoreness); + else if (context.Value > 0) + weight = GetFanAcquisition(context.Girl, __instance.appeal, __instance.hardcoreness); + else + return; + + if (!float.IsNaN(weight) && !float.IsInfinity(weight)) + __result = Math.Max(0f, weight); + } + } + + // While the global AddFans routine allocates losses among idols, substitute churn weights + // for fame points. The original allocator and its rounding remain intact for compatibility. + [HarmonyPatch(typeof(data_girls), "AddFans", new Type[] { typeof(long), typeof(resources.fanType?), typeof(List), typeof(data_girls.girls) })] + public class data_girls_AddFans + { + internal sealed class Context + { + public long Value; + public resources.fanType? FanType; + public List Girls; + public data_girls.girls ExceptionGirl; + } + + private static readonly Stack contexts = new Stack(); + internal static Context Current { get { return contexts.Count > 0 ? contexts.Peek() : null; } } + + [HarmonyPriority(Priority.First)] + public static void Prefix(long total_fans, resources.fanType? fanType, List Girls, data_girls.girls ExceptionGirl) + { + contexts.Push(new Context + { + Value = total_fans, + FanType = fanType, + Girls = Girls, + ExceptionGirl = ExceptionGirl + }); + } + + public static Exception Finalizer(Exception __exception) + { + if (contexts.Count > 0) contexts.Pop(); + return __exception; + } + + internal static bool IsEligible(Context context, data_girls.girls girl) + { + if (context == null || girl == null || girl == context.ExceptionGirl || girl.status == data_girls._status.graduated) + return false; + List pool = context.Girls ?? data_girls.girl; + return pool != null && pool.Contains(girl); + } + + internal static float GetGirlChurnWeight(Context context, data_girls.girls girl) + { + if (!IsEligible(context, girl) || girl.Fans == null) + return 0f; + + float total = 0f; + foreach (resources._fan fan in girl.Fans) + { + if (fan != null && fan.IsType(context.FanType)) + total += GetFanChurn(fan.appeal, fan.Ratio, fan.hardcoreness); + } + return total; + } + } + + [HarmonyPatch(typeof(data_girls.girls), "GetFamePoints")] + public class data_girls_girls_GetFamePoints_TrendingChurn + { + [HarmonyPriority(Priority.Last)] + public static void Postfix(data_girls.girls __instance, ref float __result) + { + data_girls_AddFans.Context context = data_girls_AddFans.Current; + if (context == null || context.Value >= 0 || !data_girls_AddFans.IsEligible(context, __instance)) + return; + + float weight = data_girls_AddFans.GetGirlChurnWeight(context, __instance); + if (!float.IsNaN(weight) && !float.IsInfinity(weight) && weight >= 0f) + __result = weight * 1000f; // avoids vanilla's <1 fame fallback while preserving proportions + } + } + + [HarmonyPatch(typeof(resources), "OnNewWeek")] + public class resources_OnNewWeek + { + public static void Postfix() + { + if (Theaters.Theaters_ == null) + return; + + foreach (Theaters._theater theater in Theaters.Theaters_) + { + if (theater == null || theater.Stats == null || theater.Stats.Count < 7 || theater.GetGroup() == null) + continue; + + foreach (resources.fanType type in Enum.GetValues(typeof(resources.fanType))) + { + int thisWeek = CountScheduledDays(theater, type, 1, 7); + if (theater.Stats.Count < 14) + { + if (thisWeek > 0) + ApplyTheaterOpinion(theater, type, 1f); + continue; + } + + int pastWeek = CountScheduledDays(theater, type, 8, 14); + if (thisWeek > pastWeek) + ApplyTheaterOpinion(theater, type, 1f); + else if (thisWeek < pastWeek) + ApplyTheaterOpinion(theater, type, -1f); + } + } + } + + private static int CountScheduledDays(Theaters._theater theater, resources.fanType type, int fromLatest, int toLatest) + { + int count = 0; + if (theater == null || theater.Stats == null) + return 0; + + for (int offset = fromLatest; offset <= toLatest; offset++) + { + int index = theater.Stats.Count - offset; + if (index < 0 || index >= theater.Stats.Count) + continue; + Theaters._theater._stat stat = theater.Stats[index]; + if (stat != null && stat.Schedule != null && stat.Schedule.FanType == type) + count++; + } + return count; + } + + private static void ApplyTheaterOpinion(Theaters._theater theater, resources.fanType type, float value) + { + Groups._group group = theater != null ? theater.GetGroup() : null; + if (group == null || group.GetGirls() == null) + return; + + foreach (data_girls.girls girl in group.GetGirls()) + { + if (girl != null && girl.status != data_girls._status.graduated) + girl.AddAppeal(type, value); + } + + string key = value > 0 ? "THEATER__FANS_LIKE" : "THEATER__FANS_DISLIKE"; + string color = value > 0 ? mainScript.green : mainScript.red; + Color32 color32 = value > 0 ? mainScript.green32 : mainScript.red32; + NotificationManager.AddNotification( + Language.Insert(key, new string[] { ExtensionMethods.color(resources.GetFanTitle(type), color), group.Title }), + color32, + NotificationManager._notification._type.fans_opinion_change); + } + } + + [HarmonyPatch(typeof(resources._fanOpinion), "Add")] + public class resources__fanOpinion_Add + { + public static void Postfix(resources._fanOpinion __instance, float val) + { + if (Harmony.HasAnyPatches("com.tel.unofficialpatch") || __instance == null || data_girls.girl == null) + return; + + foreach (data_girls.girls girl in data_girls.girl) + { + if (girl != null && girl.status != data_girls._status.graduated) + girl.AddAppeal(__instance.type, val); + } + } + } +} diff --git a/mods/Going Viral/FanTooltip.cs b/mods/Going Viral/FanTooltip.cs index 99aaf6b..52d2d9b 100755 --- a/mods/Going Viral/FanTooltip.cs +++ b/mods/Going Viral/FanTooltip.cs @@ -1,289 +1,247 @@ -using HarmonyLib; -using System; -using System.Reflection; -using UnityEngine.UI; -using UnityEngine; -using static GoingViral.TrendingManager; - -namespace GoingViral -{ - // Render tooltip - [HarmonyPatch(typeof(tooltip_fans), "Render", new Type[] { })] - public class tooltip_fans_Render - { - [HarmonyAfter("com.tel.fanattrition")] - public static bool Prefix(tooltip_fans __instance) - { - if (__instance != null && __instance.gameObject.transform != null) - { - if (!Harmony.HasAnyPatches("com.tel.fanattrition")) - { - resources.RecalcFans(); - - __instance.gameObject.GetComponentsInChildren()[0].text = GetFanLine(resources.fanType.hardcore); - __instance.gameObject.GetComponentsInChildren()[1].text = GetFanLine(resources.fanType.casual); - __instance.gameObject.GetComponentsInChildren()[2].text = GetFanLine(resources.fanType.male); - __instance.gameObject.GetComponentsInChildren()[3].text = GetFanLine(resources.fanType.female); - __instance.gameObject.GetComponentsInChildren()[4].text = GetFanLine(resources.fanType.teen); - __instance.gameObject.GetComponentsInChildren()[5].text = GetFanLine(resources.fanType.youngAdult); - __instance.gameObject.GetComponentsInChildren()[6].text = GetFanLine(resources.fanType.adult); - - __instance.gameObject.GetComponentsInChildren()[7].text = mainScript.separator_no_linebreaks; - - __instance.gameObject.GetComponentsInChildren()[10].text = GetShowLine(Shows._param._media_type.internet); - __instance.gameObject.GetComponentsInChildren()[11].text = GetShowLine(Shows._param._media_type.tv); - __instance.gameObject.GetComponentsInChildren()[12].text = GetShowLine(Shows._param._media_type.radio); - - __instance.gameObject.GetComponentsInChildren()[13].text = GetCafeLine(); - - } - __instance.gameObject.GetComponentsInChildren()[8].text = GetContractsLine(business._type.ad); - __instance.gameObject.GetComponentsInChildren()[9].text = GetContractsLine(business._type.tv_drama); - - __instance.gameObject.GetComponentsInChildren()[14].text = GetChurnLine(); - __instance.gameObject.GetComponentsInChildren()[15].text = mainScript.separator_no_linebreaks; - __instance.gameObject.GetComponentsInChildren()[16].text = GetTrendingLine(); - - var RenderFanChange = __instance.GetType().GetMethod("RenderFanChange", BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null); - RenderFanChange.Invoke(__instance, null); - LayoutRebuilder.ForceRebuildLayoutImmediate(__instance.gameObject.GetComponent()); - return false; - } - return true; - } - - static string GetChurnLine() - { - if (Harmony.HasAnyPatches("com.tel.fanattrition")) - { - if (resources.FansChange < 0) - { - return Language.Data["CHURN"] + ": " + ExtensionMethods.color(ExtensionMethods.formatNumber(resources.FansChange * 7) + " " + Language.Data["PER_WEEK"], mainScript.red); - } - else - { - return Language.Data["CHURN"] + ": " + 0 + " " + Language.Data["PER_WEEK"]; - } - } - return ""; - } - - static string GetTrendingLine() - { - if (trending > 1) - { - return Language.Data["TIP__TRENDING"] + ": " + ExtensionMethods.color(GetTrendingCoeff() + "x (" + trending + " " + Language.Data["TIP__DAYS_LEFT"] + ")", mainScript.green); - } - else if (trending > 0) - { - return Language.Data["TIP__TRENDING"] + ": " + ExtensionMethods.color(GetTrendingCoeff() + "x (" + trending + " " + Language.Data["TIP__DAY_LEFT"] + ")", mainScript.green); - } - else if (trending < -1) - { - return Language.Data["TIP__TRENDING"] + ": " + ExtensionMethods.color(GetTrendingCoeff() + "x (" + -trending + " " + Language.Data["TIP__DAY_LEFT"] + ")", mainScript.red); - } - else if (trending < 0) - { - return Language.Data["TIP__TRENDING"] + ": " + ExtensionMethods.color(GetTrendingCoeff() + "x (" + -trending + " " + Language.Data["TIP__DAYS_LEFT"] + ")", mainScript.red); - } - else - { - return Language.Data["TIP__TRENDING"] + ": " + Language.Data["TIP__NONE"]; - } - } - - static string GetCafeLine() - { - if (cafeFans > 0) - { - return Language.Data["TIP__CAFE"] + ": " + ExtensionMethods.color("+" + ExtensionMethods.formatNumber(cafeFans) + " " + Language.Data["PER_WEEK"], mainScript.green); - } - else - { - return Language.Data["TIP__CAFE"] + ": " + cafeFans + " " + Language.Data["PER_WEEK"]; - } - } - - static string GetFanLine(resources.fanType FanType) - { - int num = 0; - float appeal = 0f; - foreach (data_girls.girls girls in data_girls.GetActiveGirls()) - { - if (girls != null) - { - if (girls.FanAppeal.Count == 0) - { - girls.RecalcFanAppeal(); - } - appeal += girls.GetFanAppeal(FanType).ratio; - num++; - } - } - - string appealStr = "0%"; - string ratioStr = "0%"; - if (num > 0) - { - float avgAppeal = appeal / num; - if (avgAppeal >= 0.4) - { - appealStr = ExtensionMethods.color(ExtensionMethods.toPercent(avgAppeal) + "%", mainScript.green); - } - else if (avgAppeal <= 0.3) - { - appealStr = ExtensionMethods.color(ExtensionMethods.toPercent(avgAppeal) + "%", mainScript.red); - } - else - { - appealStr = ExtensionMethods.toPercent(avgAppeal) + "%"; - } - - float fanRatio = (float)resources.GetFansTotal(FanType) / resources.GetFansTotal(); - float upper = 0.6f; - float lower = 0.4f; - if (FanType == resources.fanType.teen || FanType == resources.fanType.youngAdult || FanType == resources.fanType.adult) - { - upper = 0.4f; - lower = 0.25f; - } - if (fanRatio >= upper) - { - ratioStr = ExtensionMethods.color(ExtensionMethods.toPercent(fanRatio) + "%", mainScript.green); - } - else if (fanRatio <= lower) - { - ratioStr = ExtensionMethods.color(ExtensionMethods.toPercent(fanRatio) + "%", mainScript.red); - } - else - { - ratioStr = ExtensionMethods.toPercent(fanRatio) + "%"; - } - } - - return resources.GetFanTitle(FanType) + ": " + ratioStr + " " + Language.Data["TIP__OF_TOTAL"] + " (" + appealStr + " " + Language.Data["TIP__APPEAL"] + ")"; - } - - static string GetContractsLine(business._type Type) - { - - string fanStr = "0 " + Language.Data["PER_WEEK"]; - if (Type == business._type.ad) - { - if (adFans > 0) - { - fanStr = ExtensionMethods.color("+" + ExtensionMethods.formatNumber(adFans) + " " + Language.Data["PER_WEEK"], mainScript.green); - } - return Language.Data["TIP__AD"] + ": " + fanStr; - } - else if (Type == business._type.tv_drama) - { - if (dramaFans > 0) - { - fanStr = ExtensionMethods.color("+" + ExtensionMethods.formatNumber(dramaFans) + " " + Language.Data["PER_WEEK"], mainScript.green); - } - return Language.Data["TIP__DRAMA"] + ": " + fanStr; - } - return ""; - } - - static string GetShowLine(Shows._param._media_type Type) - { - - string fanStr = "0 " + Language.Data["PER_WEEK"]; - if (Type == Shows._param._media_type.tv) - { - if (tvFans > 0) - { - fanStr = ExtensionMethods.color("+" + ExtensionMethods.formatNumber(tvFans) + " " + Language.Data["PER_WEEK"], mainScript.green); - } - return Language.Data["TIP__TV"] + ": " + fanStr; - } - else if (Type == Shows._param._media_type.internet) - { - if (netFans > 0) - { - fanStr = ExtensionMethods.color("+" + ExtensionMethods.formatNumber(netFans) + " " + Language.Data["PER_WEEK"], mainScript.green); - } - return Language.Data["TIP__INTERNET"] + ": " + fanStr; - } - else if (Type == Shows._param._media_type.radio) - { - if (radioFans > 0) - { - fanStr = ExtensionMethods.color("+" + ExtensionMethods.formatNumber(radioFans) + " " + Language.Data["PER_WEEK"], mainScript.green); - } - return Language.Data["TIP__RADIO"] + ": " + fanStr; - } - return ""; - } - } - - // Set up the fan tooltip structure - [HarmonyPatch(typeof(tooltip_fans), "Start", new Type[] { })] - public class tooltip_fans_Start - { - [HarmonyAfter("com.tel.fanattrition")] - public static bool Prefix(tooltip_fans __instance) - { - var AddPadding = __instance.GetType().GetMethod("AddPadding", BindingFlags.NonPublic | BindingFlags.Instance); - - if (!Harmony.HasAnyPatches("com.tel.fanattrition")) - { - AddLine(__instance, ""); - AddLine(__instance, ""); - AddLine(__instance, ""); - AddLine(__instance, ""); - AddLine(__instance, ""); - AddLine(__instance, ""); - AddLine(__instance, ""); - AddLine(__instance, ""); - AddLine(__instance, ""); - AddLine(__instance, ""); - AddLine(__instance, ""); - AddLine(__instance, ""); - AddLine(__instance, ""); - AddLine(__instance, ""); - AddLine(__instance, ""); - } - AddLine(__instance, ""); - AddLine(__instance, ""); - - return true; - } - - static void AddLine(tooltip_fans instance, string txt) - { - GameObject gameObject = UnityEngine.Object.Instantiate(instance.prefab_line); - gameObject.transform.SetParent(instance.gameObject.transform, false); - gameObject.GetComponent().Set(txt); - } - } - - - - // Render the bottom line of fan tooltip - [HarmonyPatch(typeof(tooltip_fans), "RenderFanChange", new Type[] { })] - public class tooltip_fans_RenderFanChange - { - [HarmonyAfter("com.tel.fanattrition")] - public static void Postfix(tooltip_fans __instance) - { - long netChange = adFans + dramaFans + netFans + tvFans + radioFans + resources.FansChange * 7; - string changeStr = ExtensionMethods.formatNumber(netChange, false, false); - if (netChange > 0) - { - changeStr = ExtensionMethods.color("+" + changeStr + " " + Language.Data["PER_WEEK"], mainScript.green); - } - else if (netChange < 0) - { - changeStr = ExtensionMethods.color(changeStr + " " + Language.Data["PER_WEEK"], mainScript.red); - } - ExtensionMethods.SetText(__instance.fan_change, string.Concat(new string[] - { - Language.Data["TOTAL"] + ": ", - changeStr - })); - } - } -} +using HarmonyLib; +using System; +using UnityEngine; +using UnityEngine.UI; +using static GoingViral.TrendingManager; + +namespace GoingViral +{ + internal static class ViralFanTooltip + { + private const string Hardcore = "GV_Hardcore"; + private const string Casual = "GV_Casual"; + private const string Male = "GV_Male"; + private const string Female = "GV_Female"; + private const string Teen = "GV_Teen"; + private const string YoungAdult = "GV_YoungAdult"; + private const string Adult = "GV_Adult"; + private const string Separator1 = "GV_Separator1"; + private const string Ad = "GV_Ad"; + private const string Drama = "GV_Drama"; + private const string Internet = "GV_Internet"; + private const string TV = "GV_TV"; + private const string Radio = "GV_Radio"; + private const string Cafe = "GV_Cafe"; + private const string Churn = "GV_Churn"; + private const string Separator2 = "GV_Separator2"; + private const string Trending = "GV_Trending"; + + internal static readonly string[] BaseLines = + { + Hardcore, Casual, Male, Female, Teen, YoungAdult, Adult, Separator1, + Ad, Drama, Internet, TV, Radio, Cafe, Churn + }; + + internal static void EnsureLine(tooltip_fans instance, string name) + { + if (instance == null || instance.prefab_line == null || instance.transform == null || instance.transform.Find(name) != null) + return; + GameObject line = UnityEngine.Object.Instantiate(instance.prefab_line); + line.name = name; + line.transform.SetParent(instance.transform, false); + tooltip_fans_line component = line.GetComponent(); + if (component != null) component.Set(""); + } + + internal static void EnsureExtraLines(tooltip_fans instance) + { + EnsureLine(instance, Separator2); + EnsureLine(instance, Trending); + } + + internal static void SetLine(tooltip_fans instance, string name, string text) + { + if (instance == null || instance.transform == null) + return; + Transform transform = instance.transform.Find(name); + if (transform == null) + return; + tooltip_fans_line line = transform.GetComponent(); + if (line != null) line.Set(text ?? ""); + } + + internal static void RenderBaseLines(tooltip_fans instance) + { + resources.RecalcFans(); + SetLine(instance, Hardcore, GetFanLine(resources.fanType.hardcore)); + SetLine(instance, Casual, GetFanLine(resources.fanType.casual)); + SetLine(instance, Male, GetFanLine(resources.fanType.male)); + SetLine(instance, Female, GetFanLine(resources.fanType.female)); + SetLine(instance, Teen, GetFanLine(resources.fanType.teen)); + SetLine(instance, YoungAdult, GetFanLine(resources.fanType.youngAdult)); + SetLine(instance, Adult, GetFanLine(resources.fanType.adult)); + SetLine(instance, Separator1, mainScript.separator_no_linebreaks); + SetLine(instance, Ad, GetContractsLine(business._type.ad)); + SetLine(instance, Drama, GetContractsLine(business._type.tv_drama)); + SetLine(instance, Internet, GetShowLine(Shows._param._media_type.internet)); + SetLine(instance, TV, GetShowLine(Shows._param._media_type.tv)); + SetLine(instance, Radio, GetShowLine(Shows._param._media_type.radio)); + SetLine(instance, Cafe, GetCafeLine()); + SetLine(instance, Churn, GetChurnLine()); + } + + internal static void RenderExtras(tooltip_fans instance) + { + EnsureExtraLines(instance); + SetLine(instance, Separator2, mainScript.separator_no_linebreaks); + SetLine(instance, Trending, GetTrendingLine()); + RenderTotalChange(instance); + if (instance != null) + { + RectTransform rect = instance.GetComponent(); + if (rect != null) LayoutRebuilder.ForceRebuildLayoutImmediate(rect); + } + } + + internal static void RenderTotalChange(tooltip_fans instance) + { + if (instance == null || instance.fan_change == null) + return; + long netChange = 0; + netChange = SafeAdd(netChange, adFans); + netChange = SafeAdd(netChange, dramaFans); + netChange = SafeAdd(netChange, netFans); + netChange = SafeAdd(netChange, tvFans); + netChange = SafeAdd(netChange, radioFans); + netChange = SafeAdd(netChange, cafeFans); + netChange = SafeAdd(netChange, ScaleLong(resources.FansChange, 7f)); + + string text = ExtensionMethods.formatNumber(netChange, false, false) + " " + Language.Data["PER_WEEK"]; + if (netChange > 0) text = ExtensionMethods.color("+" + text, mainScript.green); + else if (netChange < 0) text = ExtensionMethods.color(text, mainScript.red); + ExtensionMethods.SetText(instance.fan_change, Language.Data["TOTAL"] + ": " + text); + } + + private static string GetChurnLine() + { + if (!Harmony.HasAnyPatches("com.tel.fanattrition")) + return ""; + long churn = Math.Min(ScaleLong(resources.FansChange, 7f), 0L); + string text = ExtensionMethods.formatNumber(churn) + " " + Language.Data["PER_WEEK"]; + if (churn < 0) text = ExtensionMethods.color(text, mainScript.red); + return Language.Data["CHURN"] + ": " + text; + } + + private static string GetTrendingLine() + { + if (trending == 0) + return Language.Data["TIP__TRENDING"] + ": " + Language.Data["TIP__NONE"]; + + long days = Math.Abs(trending); + string daysKey = days == 1 ? "TIP__DAY_LEFT" : "TIP__DAYS_LEFT"; + string value = GetTrendingCoeff() + "x (" + days + " " + Language.Data[daysKey] + ")"; + return Language.Data["TIP__TRENDING"] + ": " + + ExtensionMethods.color(value, trending > 0 ? mainScript.green : mainScript.red); + } + + private static string GetCafeLine() + { + string value = ExtensionMethods.formatNumber(Math.Max(0, cafeFans)) + " " + Language.Data["PER_WEEK"]; + if (cafeFans > 0) value = ExtensionMethods.color("+" + value, mainScript.green); + return Language.Data["TIP__CAFE"] + ": " + value; + } + + private static string GetFanLine(resources.fanType type) + { + int count = 0; + float appeal = 0f; + var girls = data_girls.GetActiveGirls(); + if (girls != null) + { + foreach (data_girls.girls girl in girls) + { + if (girl == null) continue; + if (girl.FanAppeal == null || girl.FanAppeal.Count == 0) girl.RecalcFanAppeal(); + singles._fanAppeal fanAppeal = girl.GetFanAppeal(type); + if (fanAppeal == null) continue; + appeal += fanAppeal.ratio; + count++; + } + } + + float avgAppeal = count > 0 ? appeal / count : 0f; + string appealText = ExtensionMethods.toPercent(avgAppeal) + "%"; + if (avgAppeal >= 0.4f) appealText = ExtensionMethods.color(appealText, mainScript.green); + else if (avgAppeal <= 0.3f) appealText = ExtensionMethods.color(appealText, mainScript.red); + + long totalFans = resources.GetFansTotal(); + float fanRatio = totalFans > 0 ? (float)resources.GetFansTotal(type) / totalFans : 0f; + string ratioText = ExtensionMethods.toPercent(fanRatio) + "%"; + float upper = (type == resources.fanType.teen || type == resources.fanType.youngAdult || type == resources.fanType.adult) ? 0.4f : 0.6f; + float lower = (type == resources.fanType.teen || type == resources.fanType.youngAdult || type == resources.fanType.adult) ? 0.25f : 0.4f; + if (fanRatio >= upper) ratioText = ExtensionMethods.color(ratioText, mainScript.green); + else if (fanRatio <= lower) ratioText = ExtensionMethods.color(ratioText, mainScript.red); + + return resources.GetFanTitle(type) + ": " + ratioText + " " + Language.Data["TIP__OF_TOTAL"] + + " (" + appealText + " " + Language.Data["TIP__APPEAL"] + ")"; + } + + private static string GetContractsLine(business._type type) + { + long value = type == business._type.ad ? adFans : type == business._type.tv_drama ? dramaFans : 0; + string label = type == business._type.ad ? Language.Data["TIP__AD"] : Language.Data["TIP__DRAMA"]; + string text = ExtensionMethods.formatNumber(Math.Max(0, value)) + " " + Language.Data["PER_WEEK"]; + if (value > 0) text = ExtensionMethods.color("+" + text, mainScript.green); + return label + ": " + text; + } + + private static string GetShowLine(Shows._param._media_type type) + { + long value = 0; + string label = ""; + if (type == Shows._param._media_type.tv) { value = tvFans; label = Language.Data["TIP__TV"]; } + else if (type == Shows._param._media_type.internet) { value = netFans; label = Language.Data["TIP__INTERNET"]; } + else if (type == Shows._param._media_type.radio) { value = radioFans; label = Language.Data["TIP__RADIO"]; } + string text = ExtensionMethods.formatNumber(Math.Max(0, value)) + " " + Language.Data["PER_WEEK"]; + if (value > 0) text = ExtensionMethods.color("+" + text, mainScript.green); + return label + ": " + text; + } + } + + [HarmonyPatch(typeof(tooltip_fans), "Start", new Type[] { })] + public class tooltip_fans_Start + { + [HarmonyAfter("com.tel.fanattrition")] + public static void Prefix(tooltip_fans __instance) + { + if (!Harmony.HasAnyPatches("com.tel.fanattrition")) + { + foreach (string name in ViralFanTooltip.BaseLines) + ViralFanTooltip.EnsureLine(__instance, name); + } + ViralFanTooltip.EnsureExtraLines(__instance); + } + } + + [HarmonyPatch(typeof(tooltip_fans), "Render", new Type[] { })] + public class tooltip_fans_Render + { + [HarmonyAfter("com.tel.fanattrition")] + public static bool Prefix(tooltip_fans __instance) + { + if (Harmony.HasAnyPatches("com.tel.fanattrition")) + return true; + if (__instance == null || __instance.transform == null) + return true; + + ViralFanTooltip.RenderBaseLines(__instance); + ViralFanTooltip.RenderExtras(__instance); + return false; + } + + public static void Postfix(tooltip_fans __instance) + { + ViralFanTooltip.RenderExtras(__instance); + } + } + + [HarmonyPatch(typeof(tooltip_fans), "RenderFanChange", new Type[] { })] + public class tooltip_fans_RenderFanChange + { + [HarmonyAfter("com.tel.fanattrition")] + public static void Postfix(tooltip_fans __instance) + { + ViralFanTooltip.RenderTotalChange(__instance); + } + } +} diff --git a/mods/Going Viral/Going Viral.csproj b/mods/Going Viral/Going Viral.csproj index 6fde472..85e24c5 100755 --- a/mods/Going Viral/Going Viral.csproj +++ b/mods/Going Viral/Going Viral.csproj @@ -1,25 +1,25 @@ - - - - Going Viral - com.tel.goingviral - Introduces a trending mechanism. You have a small chance to go viral and become trending, which doubles your fan growth. - Tel - 1.0.0 - ["gameplay"] - - - 0 - - $(HarmonyID) - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - + + + + Going Viral + com.tel.goingviral + Introduces a trending mechanism. You have a small chance to go viral and become trending, which doubles your fan growth. + Tel + 1.0.1 + ["gameplay"] + + + 0 + + $(HarmonyID) + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + diff --git a/mods/Going Viral/Resource.cs b/mods/Going Viral/Resource.cs index 483218e..eac35e2 100755 --- a/mods/Going Viral/Resource.cs +++ b/mods/Going Viral/Resource.cs @@ -1,62 +1,110 @@ -using HarmonyLib; -using UnityEngine; -using System; -using System.Collections.Generic; -using System.Reflection.Emit; -using System.Reflection; -using System.Linq; - -namespace GoingViral -{ - - // Trending stat and fan counts - // Apply trending to churn - [HarmonyPatch(typeof(resources), "OnNewDay")] - public class resources_OnNewDay - { - [HarmonyAfter("com.tel.fanattrition")] - public static void Postfix() - { - if(TrendingManager.trending > 0) - { - TrendingManager.trending = Math.Min(90, Math.Max(0, TrendingManager.trending - 1)); - } - else if(TrendingManager.trending < 0) - { - TrendingManager.trending = Math.Max(-90, Math.Min(0, TrendingManager.trending + 1)); - resources.FansChange = (long)Mathf.Round(resources.FansChange * TrendingManager.GetTrendingCoeff()); - } - Debug.Log("Trending: " + TrendingManager.trending); - - TrendingManager.UpdateFanCount(); - } - } - - // Save trending resource - [HarmonyPatch(typeof(resources), "SaveFunction")] - public class resources_SaveFunction - { - public static void Postfix() - { - Camera.main.GetComponent().GetSavedData().resources__Resources.Add(new resources.ResourceData{Type = resources.type.buzz, Val = TrendingManager.trending + 10000 }); - } - } - - // Set trending resource - [HarmonyPatch(typeof(resources), "Set")] - public class resources_Set - { - public static bool Prefix(resources.type _type, long val) - { - if(_type == resources.type.buzz) - { - if(val >= 10000) - { - TrendingManager.trending = val - 10000; - } - } - return true; - } - } - -} +using HarmonyLib; +using System; +using UnityEngine; + +namespace GoingViral +{ + [HarmonyPatch(typeof(resources), "OnNewDay")] + public class resources_OnNewDay + { + [HarmonyAfter("com.tel.fanattrition")] + public static void Postfix() + { + if (TrendingManager.trending < 0) + { + // Apply today's crisis before consuming a day, including the final day. + resources.FansChange = TrendingManager.ScaleLong(resources.FansChange, TrendingManager.GetTrendingCoeff()); + TrendingManager.trending = Math.Min(0, TrendingManager.trending + 1); + } + else if (TrendingManager.trending > 0) + { + TrendingManager.trending = Math.Max(0, TrendingManager.trending - 1); + } + + TrendingManager.UpdateFanCount(); + } + } + + [HarmonyPatch(typeof(resources), "SaveFunction")] + public class resources_SaveFunction + { + private const long SAVE_MARKER = 1000000L; + private const long SAVE_OFFSET = 100L; + + public static void Postfix() + { + if (Camera.main == null) + return; + mainScript main = Camera.main.GetComponent(); + if (main == null || main.GetSavedData() == null || main.GetSavedData().resources__Resources == null) + return; + + main.GetSavedData().resources__Resources.Add(new resources.ResourceData + { + Type = resources.type.buzz, + Val = SAVE_MARKER + SAVE_OFFSET + TrendingManager.trending + }); + } + } + + [HarmonyPatch(typeof(resources), "Set")] + public class resources_Set + { + private const long SAVE_MARKER = 1000000L; + private const long SAVE_OFFSET = 100L; + + public static bool Prefix(resources.type _type, long val) + { + // Tagged Buzz values are private save records. Never reinterpret a runtime Buzz Set. + if (!resources_LoadFunction.Loading || _type != resources.type.buzz) + return true; + + // New marker. Consume it instead of allowing vanilla Set() to overwrite real Buzz. + if (val >= SAVE_MARKER && val <= SAVE_MARKER + 200L) + { + long loaded = Math.Max(TrendingManager.MIN_TREND_DAYS, + Math.Min(TrendingManager.MAX_TREND_DAYS, val - SAVE_MARKER - SAVE_OFFSET)); + TrendingManager.trending = loaded < 0 && !Harmony.HasAnyPatches("com.tel.fanattrition") ? 0 : loaded; + return false; + } + + // Backward compatibility with the old 10000 + trending marker, including negative trends. + // Legitimate game Buzz is capped far below this range. + if (val >= 9900L && val <= 10100L) + { + long loaded = Math.Max(TrendingManager.MIN_TREND_DAYS, + Math.Min(TrendingManager.MAX_TREND_DAYS, val - 10000L)); + TrendingManager.trending = loaded < 0 && !Harmony.HasAnyPatches("com.tel.fanattrition") ? 0 : loaded; + return false; + } + + return true; + } + } + + [HarmonyPatch(typeof(resources), "LoadFunction")] + public class resources_LoadFunction + { + private static int loadDepth; + internal static bool Loading { get { return loadDepth > 0; } } + + [HarmonyPriority(Priority.First)] + public static void Prefix() + { + loadDepth++; + // Prevent a save without a marker from inheriting static trend state from a prior loaded game. + TrendingManager.trending = 0; + } + + public static void Postfix() + { + TrendingManager.UpdateFanCount(); + } + + public static Exception Finalizer(Exception __exception) + { + if (loadDepth > 0) loadDepth--; + return __exception; + } + } +} diff --git a/mods/Going Viral/TrendingManager.cs b/mods/Going Viral/TrendingManager.cs index 2c30ed9..b942094 100755 --- a/mods/Going Viral/TrendingManager.cs +++ b/mods/Going Viral/TrendingManager.cs @@ -1,378 +1,308 @@ -using HarmonyLib; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using UnityEngine; - -namespace GoingViral -{ - public class TrendingManager - { - - public static long trending = 0; - - public static long adFans = 0; - public static long dramaFans = 0; - public static long tvFans = 0; - public static long radioFans = 0; - public static long netFans = 0; - public static long cafeFans = 0; - - - public static TrendingStatus IsTrending() - { - if (trending > 0) - { - return TrendingStatus.trending; - } - else if (trending < 0) - { - return TrendingStatus.crisis; - } - return TrendingStatus.none; - } - - public static void SetTrending(long val) - { - if (IsTrending() != TrendingStatus.none || val == 0) - { - return; - } - - int maxTrending = 91; - int minTrending = -91; - if (!Harmony.HasAnyPatches("com.tel.fanattrition")) - { - minTrending = 0; - } - trending = Math.Min(Math.Max(val, minTrending), maxTrending); - if (trending > 0) - { - NotificationManager.AddNotification( - Language.Insert("NOTIF__TRENDING", new string[] { Groups.GetMainGroup().Title }) + "\n" + Language.Insert("NOTIF__TRENDING_DAYS", new string[] { val.ToString() }), - mainScript.green32, - NotificationManager._notification._type.other); - } - else if (trending < 0) - { - if (Harmony.HasAnyPatches("com.tel.fanattrition")) - { - NotificationManager.AddNotification( - Language.Insert("NOTIF__CRISIS", new string[] { Groups.GetMainGroup().Title }) + "\n" + Language.Insert("NOTIF__CRISIS_DAYS", new string[] { (-val).ToString() }), - mainScript.red32, - NotificationManager._notification._type.other); - } - } - } - - public static float GetTrendingCoeff() - { - float coeff = 1; - - if (trending != 0) - { - coeff += (float)Math.Round((double)trending / 10 + 1, 1); - } - if (trending < 0) - { - coeff *= -1; - } - - return coeff; - } - - public static void UpdateFanCount() - { - - adFans = 0; - dramaFans = 0; - tvFans = 0; - radioFans = 0; - netFans = 0; - cafeFans = 0; - foreach (business.active_proposal active_proposal in Camera.main.GetComponent().Data.GetComponent().ActiveProposals) - { - if (active_proposal.Fans_per_week > 0) - { - if (active_proposal.Type == business._type.ad) - { - adFans += active_proposal.Fans_per_week; - } - else if (active_proposal.Type == business._type.tv_drama) - { - dramaFans += active_proposal.Fans_per_week; - } - } - } - - foreach (Shows._show show in Shows.shows) - { - if (show.status != Shows._show._status.normal && show.status != Shows._show._status.working && show.status != Shows._show._status.canceled) - { - if (show.medium.media_type == Shows._param._media_type.tv) - { - tvFans += show.fans[show.fans.Count - 1]; - } - else if (show.medium.media_type == Shows._param._media_type.radio) - { - radioFans += show.fans[show.fans.Count - 1]; - } - else if (show.medium.media_type == Shows._param._media_type.internet) - { - netFans += show.fans[show.fans.Count - 1]; - } - } - } - - foreach (Cafes._cafe cafe in Cafes.Cafes_) - { - int dayCount = 0; - for (int i = cafe.Stats.Count - 1; i >= 0; i--) - { - cafeFans += cafe.Stats[i].New_Fans; - dayCount++; - if (dayCount >= 7) - { - break; - } - } - } - } - - public enum TrendingStatus - { - none, - trending, - crisis - } - - public static float GetTrendingChance(float scandalPoints) - { - float p = 5; - if (scandalPoints >= 10) - { - p *= 10; - } - return p; - } - public static long GetTrendingMagnitude(float scandalPoints) - { - long magnitude = -UnityEngine.Random.Range(2, 8) * 7; - if (scandalPoints > 1) - { - magnitude = Mathf.RoundToInt(magnitude * 1.5f); - } - return magnitude; - } - - - public static float GetTrendingChance(Shows._show show) - { - float fameCoeff = 0; - float fameSum = 0f; - int count = 0; - if (show.fame.Count > 0) - { - fameCoeff = show.fame[0] / 20; - } - if (show.castType == Shows._show._castType.entireGroup) - { - fameSum += (float)resources.GetFameLevel(); - count++; - } - else - { - foreach (data_girls.girls girls in show.girls) - { - if (girls != null) - { - fameSum += girls.GetFameLevel(); - count++; - } - } - } - if (count != 0) - { - fameCoeff += fameSum / count / 20; - } - if (show.mc != null) - { - fameCoeff += show.mc.fame / 20; - } - Shows._param genre = show.genre; - - float levelCoeff = genre.GetLevel() / 5 + 0.5f; - - DateTime? lastShowDate = null; - - // Get last show - foreach (Shows._show _show in Shows.shows) - { - if (_show != show && _show.LaunchDate != null && _show.medium.media_type == Shows._param._media_type.tv && _show.genre.id == genre.id) - { - if (lastShowDate == null || _show.LaunchDate > (lastShowDate ?? staticVars.dateTime)) - { - lastShowDate = _show.LaunchDate; - } - } - } - - float daysSinceCoeff = 1; - if (lastShowDate != null) - { - daysSinceCoeff = (staticVars.dateTime - (lastShowDate ?? staticVars.dateTime)).Days; - daysSinceCoeff = Math.Min(365, daysSinceCoeff) / 365; - } - return 15f * daysSinceCoeff * fameCoeff * levelCoeff; - } - public static long GetTrendingMagnitude(Shows._show show) - { - return UnityEngine.Random.Range(31, 61); - } - - public static float GetTrendingChance(singles._param marketing, Single_Marketing_Roll._result marketingResult, Groups._group group = null, float trendCoeff = 0) - { - float p = 0; - if (marketing == null) - { - return p; - } - - float saturationCoeff = 1; - if(group != null) - { - saturationCoeff = singles.GetSaturationCoeff(group); - } - float modifiedTrendCoeff = (trendCoeff / 0.18f + 1) / 2; - if (marketingResult == Single_Marketing_Roll._result.success_crit) - { - switch (marketing.Special_Type) - { - case singles._param._special_type.ad_campaign: - p = 100 * modifiedTrendCoeff; - break; - case singles._param._special_type.viral_campaign: - p = 70 * modifiedTrendCoeff; - break; - case singles._param._special_type.fake_scandal: - p = 40 * modifiedTrendCoeff + 60; - break; - case singles._param._special_type.lewd_pv: - case singles._param._special_type.edgy_pv: - case singles._param._special_type.artsy_pv: - //p = 50 * modifiedTrendCoeff; - break; - default: - break; - } - p *= saturationCoeff; - } - else if (marketingResult == Single_Marketing_Roll._result.fail_crit) - { - switch (marketing.Special_Type) - { - case singles._param._special_type.ad_campaign: - p = 50; - break; - case singles._param._special_type.viral_campaign: - p = 33; - break; - case singles._param._special_type.fake_scandal: - case singles._param._special_type.lewd_pv: - case singles._param._special_type.edgy_pv: - case singles._param._special_type.artsy_pv: - //p = 50; - break; - default: - break; - } - } - return p; - } - public static long GetTrendingMagnitude(singles._param marketing, Single_Marketing_Roll._result marketingResult) - { - long magnitude = 0; - if (marketing == null) - { - return magnitude; - } - - if (marketingResult == Single_Marketing_Roll._result.success_crit) - { - switch (marketing.Special_Type) - { - case singles._param._special_type.ad_campaign: - magnitude = UnityEngine.Random.Range(31, 61); - break; - case singles._param._special_type.viral_campaign: - magnitude = UnityEngine.Random.Range(31, 91); - break; - case singles._param._special_type.lewd_pv: - case singles._param._special_type.edgy_pv: - case singles._param._special_type.artsy_pv: - magnitude = UnityEngine.Random.Range(31, 61); - break; - case singles._param._special_type.fake_scandal: - magnitude = UnityEngine.Random.Range(31, 91); - break; - default: - break; - } - } - else if (marketingResult == Single_Marketing_Roll._result.fail_crit) - { - switch (marketing.Special_Type) - { - case singles._param._special_type.ad_campaign: - magnitude = -UnityEngine.Random.Range(15, 46); - break; - case singles._param._special_type.viral_campaign: - magnitude = -UnityEngine.Random.Range(31, 61); - break; - case singles._param._special_type.lewd_pv: - case singles._param._special_type.edgy_pv: - case singles._param._special_type.artsy_pv: - magnitude = -UnityEngine.Random.Range(15, 46); - break; - case singles._param._special_type.fake_scandal: - default: - break; - } - } - return magnitude; - } - - - static int appealMultiplier = 3; - - public static float GetFanChurn(float appeal, float opinion, resources.fanType type) - { - float aversion = 1 / (appeal + opinion * appeal + 0.001f); - float x = 1; - if (type == resources.fanType.casual) - { - x = appealMultiplier * -Math.Min(-1, GetTrendingCoeff() * 2); - } - return aversion * x; - } - public static float GetFanAcquisition(data_girls.girls girl, float appeal, resources.fanType type) - { - float x = 1; - float girlFame = girl.GetFameLevel(); - float groupFame = resources.GetFameLevel(); - float avgFame = (girlFame * 2 + groupFame) / 3 / 10; - LinearFunction._function getCoeffFromFame = new(); - getCoeffFromFame.Init(3f, -appealMultiplier, 7f, appealMultiplier); - float fameCoeff = Math.Min(appealMultiplier, Math.Max(-appealMultiplier, getCoeffFromFame.GetY(avgFame))); - - if (type == resources.fanType.casual) - { - x = fameCoeff * Math.Max(1, GetTrendingCoeff() * 2); - } - return appeal * x; - } - } - -} +using HarmonyLib; +using System; +using UnityEngine; + +namespace GoingViral +{ + public class TrendingManager + { + public const long MAX_TREND_DAYS = 90; + public const long MIN_TREND_DAYS = -90; + + public static long trending = 0; + public static long adFans = 0; + public static long dramaFans = 0; + public static long tvFans = 0; + public static long radioFans = 0; + public static long netFans = 0; + public static long cafeFans = 0; + + public enum TrendingStatus + { + none, + trending, + crisis + } + + public static TrendingStatus IsTrending() + { + if (trending > 0) return TrendingStatus.trending; + if (trending < 0) return TrendingStatus.crisis; + return TrendingStatus.none; + } + + public static void SetTrending(long val) + { + if (IsTrending() != TrendingStatus.none || val == 0) + return; + + long min = Harmony.HasAnyPatches("com.tel.fanattrition") ? MIN_TREND_DAYS : 0; + trending = Math.Max(min, Math.Min(MAX_TREND_DAYS, val)); + if (trending == 0) + return; + + Groups._group group = Groups.GetMainGroup(); + string groupTitle = group != null ? group.Title : ""; + if (trending > 0) + { + NotificationManager.AddNotification( + Language.Insert("NOTIF__TRENDING", new string[] { groupTitle }) + "\n" + + Language.Insert("NOTIF__TRENDING_DAYS", new string[] { trending.ToString() }), + mainScript.green32, + NotificationManager._notification._type.other); + } + else + { + NotificationManager.AddNotification( + Language.Insert("NOTIF__CRISIS", new string[] { groupTitle }) + "\n" + + Language.Insert("NOTIF__CRISIS_DAYS", new string[] { Math.Abs(trending).ToString() }), + mainScript.red32, + NotificationManager._notification._type.other); + } + } + + // Positive trends multiply gains; negative trends multiply churn by the same magnitude. + // Keep the original positive curve (day 1 ~= 2.1x, day 90 = 11x) without the old sign flip. + public static float GetTrendingCoeff() + { + if (trending == 0) + return 1f; + return (float)Math.Round(2d + Math.Abs((double)trending) / 10d, 1); + } + + public static void UpdateFanCount() + { + adFans = dramaFans = tvFans = radioFans = netFans = cafeFans = 0; + + try + { + mainScript main = Camera.main != null ? Camera.main.GetComponent() : null; + business businessData = main != null && main.Data != null ? main.Data.GetComponent() : null; + if (businessData != null && businessData.ActiveProposals != null) + { + foreach (business.active_proposal proposal in businessData.ActiveProposals) + { + if (proposal == null || proposal.Fans_per_week <= 0) + continue; + if (proposal.Type == business._type.ad) + adFans = SafeAdd(adFans, proposal.Fans_per_week); + else if (proposal.Type == business._type.tv_drama) + dramaFans = SafeAdd(dramaFans, proposal.Fans_per_week); + } + } + } + catch { } + + if (Shows.shows != null) + { + foreach (Shows._show show in Shows.shows) + { + if (show == null || show.medium == null || show.fans == null || show.fans.Count == 0) + continue; + if (show.status == Shows._show._status.normal || show.status == Shows._show._status.working || show.status == Shows._show._status.canceled) + continue; + + long latest = show.fans[show.fans.Count - 1]; + if (show.medium.media_type == Shows._param._media_type.tv) + tvFans = SafeAdd(tvFans, latest); + else if (show.medium.media_type == Shows._param._media_type.radio) + radioFans = SafeAdd(radioFans, latest); + else if (show.medium.media_type == Shows._param._media_type.internet) + netFans = SafeAdd(netFans, latest); + } + } + + if (Cafes.Cafes_ != null) + { + foreach (Cafes._cafe cafe in Cafes.Cafes_) + { + if (cafe == null || cafe.Stats == null) + continue; + int start = Math.Max(0, cafe.Stats.Count - 7); + for (int i = start; i < cafe.Stats.Count; i++) + { + if (cafe.Stats[i] != null) + cafeFans = SafeAdd(cafeFans, cafe.Stats[i].New_Fans); + } + } + } + } + + public static float GetTrendingChance(float scandalPoints) + { + float p = scandalPoints >= 10f ? 50f : 5f; + return Mathf.Clamp(p, 0f, 100f); + } + + public static long GetTrendingMagnitude(float scandalPoints) + { + long magnitude = -UnityEngine.Random.Range(2, 8) * 7L; + if (scandalPoints > 1f) + magnitude = Mathf.RoundToInt(magnitude * 1.5f); + return Math.Max(MIN_TREND_DAYS, magnitude); + } + + public static float GetTrendingChance(Shows._show show) + { + if (show == null || show.genre == null || show.medium == null || show.medium.media_type != Shows._param._media_type.tv) + return 0f; + + float fameCoeff = 0f; + if (show.fame != null && show.fame.Count > 0) + fameCoeff += show.fame[0] / 20f; + + float fameSum = 0f; + int count = 0; + if (show.castType == Shows._show._castType.entireGroup) + { + fameSum = resources.GetFameLevel(); + count = 1; + } + else if (show.girls != null) + { + foreach (data_girls.girls girl in show.girls) + { + if (girl == null) continue; + fameSum += girl.GetFameLevel(); + count++; + } + } + if (count > 0) + fameCoeff += (fameSum / count) / 20f; + if (show.mc != null) + fameCoeff += show.mc.fame / 20f; + + float levelCoeff = show.genre.GetLevel() / 5f + 0.5f; + DateTime? lastShowDate = null; + if (Shows.shows != null) + { + foreach (Shows._show other in Shows.shows) + { + if (other == null || other == show || other.medium == null || other.genre == null) + continue; + if (other.LaunchDate == default(DateTime) || other.medium.media_type != Shows._param._media_type.tv || other.genre.id != show.genre.id) + continue; + if (!lastShowDate.HasValue || other.LaunchDate > lastShowDate.Value) + lastShowDate = other.LaunchDate; + } + } + + float daysSinceCoeff = 1f; + if (lastShowDate.HasValue) + { + int days = Math.Max(0, (staticVars.dateTime - lastShowDate.Value).Days); + daysSinceCoeff = Math.Min(365, days) / 365f; + } + return Mathf.Clamp(15f * daysSinceCoeff * fameCoeff * levelCoeff, 0f, 100f); + } + + public static long GetTrendingMagnitude(Shows._show show) + { + return UnityEngine.Random.Range(31, 61); + } + + public static float GetTrendingChance(singles._param marketing, Single_Marketing_Roll._result marketingResult, Groups._group group = null, float trendCoeff = 0f) + { + if (marketing == null) + return 0f; + + float p = 0f; + float saturationCoeff = group != null ? singles.GetSaturationCoeff(group) : 1f; + float modifiedTrendCoeff = (trendCoeff / 0.18f + 1f) / 2f; + + if (marketingResult == Single_Marketing_Roll._result.success_crit) + { + switch (marketing.Special_Type) + { + case singles._param._special_type.ad_campaign: p = 100f * modifiedTrendCoeff; break; + case singles._param._special_type.viral_campaign: p = 70f * modifiedTrendCoeff; break; + case singles._param._special_type.fake_scandal: p = 40f * modifiedTrendCoeff + 60f; break; + } + p *= saturationCoeff; + } + else if (marketingResult == Single_Marketing_Roll._result.fail_crit) + { + switch (marketing.Special_Type) + { + case singles._param._special_type.ad_campaign: p = 50f; break; + case singles._param._special_type.viral_campaign: p = 33f; break; + } + } + return Mathf.Clamp(p, 0f, 100f); + } + + public static long GetTrendingMagnitude(singles._param marketing, Single_Marketing_Roll._result marketingResult) + { + if (marketing == null) + return 0; + + if (marketingResult == Single_Marketing_Roll._result.success_crit) + { + switch (marketing.Special_Type) + { + case singles._param._special_type.ad_campaign: return UnityEngine.Random.Range(31, 61); + case singles._param._special_type.viral_campaign: return UnityEngine.Random.Range(31, 91); + case singles._param._special_type.fake_scandal: return UnityEngine.Random.Range(31, 91); + case singles._param._special_type.lewd_pv: + case singles._param._special_type.edgy_pv: + case singles._param._special_type.artsy_pv: return UnityEngine.Random.Range(31, 61); + } + } + else if (marketingResult == Single_Marketing_Roll._result.fail_crit) + { + switch (marketing.Special_Type) + { + case singles._param._special_type.ad_campaign: return -UnityEngine.Random.Range(15, 46); + case singles._param._special_type.viral_campaign: return -UnityEngine.Random.Range(31, 61); + case singles._param._special_type.lewd_pv: + case singles._param._special_type.edgy_pv: + case singles._param._special_type.artsy_pv: return -UnityEngine.Random.Range(15, 46); + } + } + return 0; + } + + private const int appealMultiplier = 3; + + public static float GetFanChurn(float appeal, float opinion, resources.fanType type) + { + float safeAppeal = Math.Max(0f, appeal); + float safeOpinion = Mathf.Clamp01(opinion); + float aversion = 1f / (safeAppeal * (1f + safeOpinion) + 0.001f); + return aversion * (type == resources.fanType.casual ? appealMultiplier : 1f); + } + + public static float GetFanAcquisition(data_girls.girls girl, float appeal, resources.fanType type) + { + float weight = Math.Max(0f, appeal); + if (type == resources.fanType.casual) + weight *= appealMultiplier; + return weight; + } + + public static int ScaleInt(int value, float coeff) + { + long scaled = ScaleLong(value, coeff); + if (scaled > int.MaxValue) return int.MaxValue; + if (scaled < int.MinValue) return int.MinValue; + return (int)scaled; + } + + public static long ScaleLong(long value, float coeff) + { + if (float.IsNaN(coeff) || float.IsInfinity(coeff)) + return value; + double scaled = Math.Round(value * (double)coeff, MidpointRounding.AwayFromZero); + if (scaled >= long.MaxValue) return long.MaxValue; + if (scaled <= long.MinValue) return long.MinValue; + return (long)scaled; + } + + public static long SafeAdd(long a, long b) + { + if (b > 0 && a > long.MaxValue - b) return long.MaxValue; + if (b < 0 && a < long.MinValue - b) return long.MinValue; + return a + b; + } + } +} diff --git a/mods/Going Viral/TriggerTrending.cs b/mods/Going Viral/TriggerTrending.cs index fd37897..30911a1 100755 --- a/mods/Going Viral/TriggerTrending.cs +++ b/mods/Going Viral/TriggerTrending.cs @@ -1,164 +1,143 @@ -using HarmonyLib; -using System; -using TMPro; -using UnityEngine; -using static GoingViral.TrendingManager; - -namespace GoingViral -{ - // display single triggering neg trending - [HarmonyPatch(typeof(Single_Marketing_Roll), "OnFail")] - public class Single_Marketing_Roll_OnFail - { - public static bool Prefix(ref TrendingManager.TrendingStatus __state) - { - __state = TrendingManager.IsTrending(); - return true; - } - public static void Postfix(ref Single_Marketing_Roll __instance, TrendingManager.TrendingStatus __state) - { - if (__state != TrendingStatus.none) - { - return; - } - - singles._single single = Traverse.Create(__instance).Field("Single").GetValue() as singles._single; - if (TrendingManager.IsTrending() == TrendingManager.TrendingStatus.crisis) - { - __instance.Description.GetComponent().text += "\n" + Language.Insert("NOTIF__CRISIS", single.GetGroup().Title); - return; - } - } - } - // display single triggering neg trending - [HarmonyPatch(typeof(Single_Marketing_Roll), "OnFailCrit")] - public class Single_Marketing_Roll_OnFailCrit - { - public static bool Prefix(ref TrendingManager.TrendingStatus __state) - { - __state = TrendingManager.IsTrending(); - return true; - } - public static void Postfix(ref Single_Marketing_Roll __instance, TrendingManager.TrendingStatus __state) - { - if (__state != TrendingStatus.none) - { - return; - } - - singles._single single = Traverse.Create(__instance).Field("Single").GetValue() as singles._single; - if (TrendingManager.IsTrending() == TrendingManager.TrendingStatus.crisis) - { - __instance.Description.GetComponent().text += "\n" + Language.Insert("NOTIF__CRISIS", single.GetGroup().Title); - return; - } - - singles._param marketing = single.GetRiskyMarketing(); - - float coeff = Rivals.GetSinglesCoeff(single); - - float p = TrendingManager.GetTrendingChance(marketing, single.Marketing_Result_Status, single.GetGroup(), coeff); - if (mainScript.chance(p)) - { - SetTrending(TrendingManager.GetTrendingMagnitude(marketing, single.Marketing_Result_Status)); - __instance.Description.GetComponent().text += "\n" + Language.Insert("NOTIF__CRISIS", single.GetGroup().Title); - } - } - } - // display single triggering trending - [HarmonyPatch(typeof(Single_Marketing_Roll), "OnSuccessCrit")] - public class Single_Marketing_Roll_OnSuccessCrit - { - public static bool Prefix(ref TrendingManager.TrendingStatus __state) - { - __state = TrendingManager.IsTrending(); - return true; - } - public static void Postfix(ref Single_Marketing_Roll __instance, TrendingManager.TrendingStatus __state) - { - if (__state != TrendingStatus.none) - { - return; - } - - singles._single single = Traverse.Create(__instance).Field("Single").GetValue() as singles._single; - singles._param marketing = single.GetRiskyMarketing(); - - float coeff = Rivals.GetSinglesCoeff(single); - - float p = TrendingManager.GetTrendingChance(marketing, single.Marketing_Result_Status, single.GetGroup(), coeff); - if (mainScript.chance(p)) - { - SetTrending(TrendingManager.GetTrendingMagnitude(marketing, single.Marketing_Result_Status)); - __instance.Description.GetComponent().text += "\n" + Language.Insert("NOTIF__TRENDING", single.GetGroup().Title); - } - } - } - - - - // chance of trending on crit success after 1 week - [HarmonyPatch(typeof(Shows._show), "NewEpisode")] - public class Shows__show_NewEpisode - { - public static void Postfix(Shows._show __instance) - { - if (IsTrending() != TrendingStatus.none || __instance.episodeCount != 2 || __instance.medium.media_type != Shows._param._media_type.tv) - { - return; - } - - float p = TrendingManager.GetTrendingChance(__instance); - if (mainScript.chance(p)) - { - SetTrending(TrendingManager.GetTrendingMagnitude(__instance)); - } - } - } - - - // chance of crisis upon scandal points - [HarmonyPatch(typeof(data_girls.girls), "addParam")] - public class data_girls_girls_addParam - { - public static void Postfix(data_girls._paramType type, float val) - { - if (IsTrending() != TrendingStatus.none) - { - return; - } - if (type != data_girls._paramType.scandalPoints || val <= 0) - { - return; - } - float p = TrendingManager.GetTrendingChance(val); - if (mainScript.chance(p)) - { - SetTrending(TrendingManager.GetTrendingMagnitude(val)); - } - } - } - - // chance of crisis upon scandal points - [HarmonyPatch(typeof(resources), "_Add")] - public class resources__Add - { - public static void Postfix(resources.type _type, long val) - { - if (IsTrending() != TrendingStatus.none) - { - return; - } - if (_type != resources.type.scandalPoints || val <= 0) - { - return; - } - float p = TrendingManager.GetTrendingChance(val); - if (mainScript.chance(p)) - { - SetTrending(TrendingManager.GetTrendingMagnitude(val)); - } - } - } - -} +using HarmonyLib; +using TMPro; +using UnityEngine; +using static GoingViral.TrendingManager; + +namespace GoingViral +{ + internal static class TrendingRollUI + { + internal static singles._single GetSingle(Single_Marketing_Roll roll) + { + return roll == null ? null : Traverse.Create(roll).Field("Single").GetValue() as singles._single; + } + + internal static void Append(Single_Marketing_Roll roll, string key, singles._single single) + { + if (roll == null || roll.Description == null || single == null) + return; + TextMeshProUGUI text = roll.Description.GetComponent(); + Groups._group group = single.GetGroup(); + if (text == null || group == null) + return; + text.text += "\n" + Language.Insert(key, new string[] { group.Title }); + } + } + + [HarmonyPatch(typeof(Single_Marketing_Roll), "OnFail")] + public class Single_Marketing_Roll_OnFail + { + public static void Prefix(ref TrendingStatus __state) + { + __state = IsTrending(); + } + + public static void Postfix(Single_Marketing_Roll __instance, TrendingStatus __state) + { + if (__state == TrendingStatus.none && IsTrending() == TrendingStatus.crisis) + TrendingRollUI.Append(__instance, "NOTIF__CRISIS", TrendingRollUI.GetSingle(__instance)); + } + } + + [HarmonyPatch(typeof(Single_Marketing_Roll), "OnFailCrit")] + public class Single_Marketing_Roll_OnFailCrit + { + public static void Prefix(ref TrendingStatus __state) + { + __state = IsTrending(); + } + + public static void Postfix(Single_Marketing_Roll __instance, TrendingStatus __state) + { + if (__state != TrendingStatus.none) + return; + + singles._single single = TrendingRollUI.GetSingle(__instance); + if (single == null) + return; + + // A fake-scandal resource change may already have started the crisis. + if (IsTrending() == TrendingStatus.crisis) + { + TrendingRollUI.Append(__instance, "NOTIF__CRISIS", single); + return; + } + + singles._param marketing = single.GetRiskyMarketing(); + Groups._group group = single.GetGroup(); + float coeff = Rivals.GetSinglesCoeff(single); + float chance = GetTrendingChance(marketing, single.Marketing_Result_Status, group, coeff); + if (!mainScript.chance(chance)) + return; + + SetTrending(GetTrendingMagnitude(marketing, single.Marketing_Result_Status)); + if (IsTrending() == TrendingStatus.crisis) + TrendingRollUI.Append(__instance, "NOTIF__CRISIS", single); + } + } + + [HarmonyPatch(typeof(Single_Marketing_Roll), "OnSuccessCrit")] + public class Single_Marketing_Roll_OnSuccessCrit + { + public static void Prefix(ref TrendingStatus __state) + { + __state = IsTrending(); + } + + public static void Postfix(Single_Marketing_Roll __instance, TrendingStatus __state) + { + if (__state != TrendingStatus.none) + return; + + singles._single single = TrendingRollUI.GetSingle(__instance); + if (single == null) + return; + singles._param marketing = single.GetRiskyMarketing(); + Groups._group group = single.GetGroup(); + float coeff = Rivals.GetSinglesCoeff(single); + float chance = GetTrendingChance(marketing, single.Marketing_Result_Status, group, coeff); + if (!mainScript.chance(chance)) + return; + + SetTrending(GetTrendingMagnitude(marketing, single.Marketing_Result_Status)); + if (IsTrending() == TrendingStatus.trending) + TrendingRollUI.Append(__instance, "NOTIF__TRENDING", single); + } + } + + [HarmonyPatch(typeof(Shows._show), "NewEpisode")] + public class Shows__show_NewEpisode + { + public static void Postfix(Shows._show __instance) + { + if (__instance == null || __instance.medium == null || IsTrending() != TrendingStatus.none || + __instance.episodeCount != 2 || __instance.medium.media_type != Shows._param._media_type.tv) + return; + + if (mainScript.chance(GetTrendingChance(__instance))) + SetTrending(GetTrendingMagnitude(__instance)); + } + } + + [HarmonyPatch(typeof(data_girls.girls), "addParam")] + public class data_girls_girls_addParam + { + public static void Postfix(data_girls._paramType type, float val) + { + if (IsTrending() == TrendingStatus.none && type == data_girls._paramType.scandalPoints && val > 0f && + mainScript.chance(GetTrendingChance(val))) + SetTrending(GetTrendingMagnitude(val)); + } + } + + [HarmonyPatch(typeof(resources), "_Add")] + public class resources__Add + { + public static void Postfix(resources.type _type, long val) + { + if (IsTrending() == TrendingStatus.none && _type == resources.type.scandalPoints && val > 0 && + mainScript.chance(GetTrendingChance(val))) + SetTrending(GetTrendingMagnitude(val)); + } + } +} diff --git a/mods/Going Viral/assets/JSON/Constants/constants.json b/mods/Going Viral/assets/JSON/Constants/constants.json index 18d6235..33f03d6 100755 --- a/mods/Going Viral/assets/JSON/Constants/constants.json +++ b/mods/Going Viral/assets/JSON/Constants/constants.json @@ -1,78 +1,98 @@ -[ - { - id: "CHURN", - text: "Churn Rate" - },{ - id: "TIP__OF_TOTAL", - text: "of Total" - },{ - id: "TIP__AD", - text: "Ad Contracts" - },{ - id: "TIP__DRAMA", - text: "Drama Contracts" - },{ - id: "TIP__TV", - text: "TV Shows" - },{ - id: "TIP__INTERNET", - text: "Internet Shows" - },{ - id: "TIP__RADIO", - text: "Radio Shows" - },{ - id: "TIP__RADIO", - text: "Radio Shows" - },{ - id: "TIP__TRENDING", - text: "Trending Multiplier" - },{ - id: "TIP__NONE", - text: "None" - },{ - id: "TIP__DAYS_LEFT", - text: "days left" - },{ - id: "TIP__DAY_LEFT", - text: "day left" - },{ - id: "NOTIF__TRENDING", - text: "@ is going viral on social media!" - },{ - id: "NOTIF__TRENDING_DAYS", - text: "@ days of bonus fans from singles, shows, businesses and tours" - },{ - id: "NOTIF__CRISIS", - text: "@ is getting negative press!" - },{ - id: "NOTIF__CRISIS_DAYS", - text: "@ days of penalty to churn rate" - },{ - id: "TREND__GENRE", - text: "Last TV show: " - },{ - id: "TREND__GENRE_NEVER", - text: "Last TV show: {green}N/A{/color}" - },{ - id: "TREND__MEDIUM", - text: "Up to 15% chance of trending based on days since your last TV show, cast fame, mc fame and genre level" - },{ - id: "TREND__SINGLE_SUCCESS", - text: "Chance to go viral: {green}@%{/color}" - },{ - id: "TREND__SINGLE_FAIL", - text: "Chance for bad press: {red}@%{/color}" - },{ - id: "TREND__SINGLE_DESC", - text: "Can change depending on the single's genre, lyrics and choreography trend rankings." - },{ - id: "ONE_DAY_AGO", - text: "1 day ago" - },{ - id: "THEATER__FANS_LIKE", - text: "@1\nfans like @2's theater schedule" - },{ - id: "THEATER__FANS_DISLIKE", - text: "@1\nfans dislike @2's theater schedule" - } +[ + { + "id": "CHURN", + "text": "Churn Rate" + }, + { + "id": "TIP__OF_TOTAL", + "text": "of Total" + }, + { + "id": "TIP__AD", + "text": "Ad Contracts" + }, + { + "id": "TIP__DRAMA", + "text": "Drama Contracts" + }, + { + "id": "TIP__TV", + "text": "TV Shows" + }, + { + "id": "TIP__INTERNET", + "text": "Internet Shows" + }, + { + "id": "TIP__RADIO", + "text": "Radio Shows" + }, + { + "id": "TIP__TRENDING", + "text": "Trending Multiplier" + }, + { + "id": "TIP__NONE", + "text": "None" + }, + { + "id": "TIP__DAYS_LEFT", + "text": "days left" + }, + { + "id": "TIP__DAY_LEFT", + "text": "day left" + }, + { + "id": "NOTIF__TRENDING", + "text": "@ is going viral on social media!" + }, + { + "id": "NOTIF__TRENDING_DAYS", + "text": "@ days of bonus fans from singles, shows, businesses and tours" + }, + { + "id": "NOTIF__CRISIS", + "text": "@ is getting negative press!" + }, + { + "id": "NOTIF__CRISIS_DAYS", + "text": "@ days of penalty to churn rate" + }, + { + "id": "TREND__GENRE", + "text": "Last TV show: " + }, + { + "id": "TREND__GENRE_NEVER", + "text": "Last TV show: {green}N/A{/color}" + }, + { + "id": "TREND__MEDIUM", + "text": "Up to 15% chance of trending based on days since your last TV show, cast fame, mc fame and genre level" + }, + { + "id": "TREND__SINGLE_SUCCESS", + "text": "Chance to go viral: {green}@%{/color}" + }, + { + "id": "TREND__SINGLE_FAIL", + "text": "Chance for bad press: {red}@%{/color}" + }, + { + "id": "TREND__SINGLE_DESC", + "text": "Can change depending on the single's genre, lyrics and choreography trend rankings." + }, + { + "id": "ONE_DAY_AGO", + "text": "1 day ago" + }, + { + "id": "THEATER__FANS_LIKE", + "text": "@1\nfans like @2's theater schedule" + }, + { + "id": "THEATER__FANS_DISLIKE", + "text": "@1\nfans dislike @2's theater schedule" + } ] \ No newline at end of file From 517e466cfa605a5b849ebaa5f0b417c7894b243f Mon Sep 17 00:00:00 2001 From: ExSlam Date: Sat, 15 Aug 2026 13:54:39 -0400 Subject: [PATCH 15/33] Adjusted some stat limits --- shared/StatLimits/StatLimits.cs | 220 ++++++++++++++++---------------- 1 file changed, 109 insertions(+), 111 deletions(-) diff --git a/shared/StatLimits/StatLimits.cs b/shared/StatLimits/StatLimits.cs index b13a542..9a000f9 100755 --- a/shared/StatLimits/StatLimits.cs +++ b/shared/StatLimits/StatLimits.cs @@ -1,111 +1,109 @@ -using HarmonyLib; -using System; -using System.Collections.Generic; -using System.Linq; -using UnityEngine; - -namespace StatLimits -{ - - // Limits for business proposal stats - [HarmonyPatch(typeof(business._proposal), "GetGirlCoeff")] - public class Business__proposal_GetGirlCoeff_Limits - { - [HarmonyPriority(Priority.Last)] - public static void Postfix(ref float __result) - { - __result = Mathf.Max(0, Mathf.Min(20, __result)); - } - } - - - // Limits for show stats - [HarmonyPatch(typeof(data_girls), "GetAverageParam")] - public class Data_girls_GetAverageParam_Limits - { - [HarmonyPriority(Priority.Last)] - public static void Postfix(ref float __result) - { - __result = Mathf.Max(0, Mathf.Min(100, __result)); - } - } - - - // Limits for single senbatsu stats - [HarmonyPatch(typeof(singles._single), "SenbatsuCalcParam")] - public class Singles__single_SenbatsuCalcParam_Limits - { - [HarmonyPriority(Priority.Last)] - public static void Postfix(ref data_girls.girls.param __result) - { - __result.val = Mathf.Max(0, Mathf.Min(100, __result.val)); - } - } - - // Limits for show senbatsu stats - [HarmonyPatch(typeof(Shows._show), "SenbatsuCalcParam")] - public class Shows__show_SenbatsuCalcParam_Limits - { - [HarmonyPriority(Priority.Last)] - public static void Postfix(ref data_girls.girls.param __result) - { - __result.val = Mathf.Max(0, Mathf.Min(100, __result.val)); - } - } - - // Limits for stats of concert songs - [HarmonyPatch(typeof(SEvent_Concerts._concert._song), "GetSkillValue")] - public class SEvent_Concerts__concert__song_GetSkillValue_Limits - { - [HarmonyPriority(Priority.Last)] - public static void Postfix(ref int __result) - { - __result = Math.Max(0, Math.Min(100, __result)); - } - } - - - // Limits for stats for concert MCs - [HarmonyPatch(typeof(SEvent_Concerts._concert._mc), "GetSkillValue")] - public class SEvent_Concerts__concert__mc_GetSkillValue_Limits - { - [HarmonyPriority(Priority.Last)] - public static void Postfix(ref int __result) - { - __result = Math.Max(0, Math.Min(100, __result)); - } - } - - // Limit the show cast params - [HarmonyPatch(typeof(Show_Popup), "AddCastParam")] - public class Show_Popup_AddCastParam_Limits - { - [HarmonyPriority(Priority.Last)] - public static void Postfix(ref List ___girlParams) - { - ___girlParams.Last().val = Mathf.Max(0, Mathf.Min(100, ___girlParams.Last().val)); - } - } - - // Limit the show cast params - [HarmonyPatch(typeof(Shows._show), "AddCastParam")] - public class Shows__show_AddCastParam_Limits - { - [HarmonyPriority(Priority.Last)] - public static void Postfix(ref Shows._show __instance) - { - __instance.girlParams.Last().val = Mathf.Max(0, Mathf.Min(100, __instance.girlParams.Last().val)); - } - } - - // Limit team chemistry - [HarmonyPatch(typeof(data_girls), "GetTeamChemistry")] - public class data_girls_GetTeamChemistry_Patch - { - [HarmonyPriority(Priority.Last)] - public static void Postfix(ref float __result) - { - __result = Mathf.Max(0, Mathf.Min(100, __result)); - } - } -} +using HarmonyLib; +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace StatLimits +{ + [HarmonyPatch(typeof(business._proposal), "GetGirlCoeff")] + public class Business__proposal_GetGirlCoeff_Limits + { + [HarmonyPriority(Priority.Last)] + public static void Postfix(ref float __result) + { + __result = Mathf.Clamp(__result, 0f, 20f); + } + } + + [HarmonyPatch(typeof(data_girls), "GetAverageParam")] + public class Data_girls_GetAverageParam_Limits + { + [HarmonyPriority(Priority.Last)] + public static void Postfix(ref float __result) + { + __result = Mathf.Clamp(__result, 0f, 100f); + } + } + + [HarmonyPatch(typeof(singles._single), "SenbatsuCalcParam")] + public class Singles__single_SenbatsuCalcParam_Limits + { + [HarmonyPriority(Priority.Last)] + public static void Postfix(ref data_girls.girls.param __result) + { + if (__result != null) + __result.val = Mathf.Clamp(__result.val, 0f, 100f); + } + } + + [HarmonyPatch(typeof(Shows._show), "SenbatsuCalcParam")] + public class Shows__show_SenbatsuCalcParam_Limits + { + [HarmonyPriority(Priority.Last)] + public static void Postfix(ref data_girls.girls.param __result) + { + if (__result != null) + __result.val = Mathf.Clamp(__result.val, 0f, 100f); + } + } + + [HarmonyPatch(typeof(SEvent_Concerts._concert._song), "GetSkillValue")] + public class SEvent_Concerts__concert__song_GetSkillValue_Limits + { + [HarmonyPriority(Priority.Last)] + public static void Postfix(ref int __result) + { + __result = Math.Max(0, Math.Min(100, __result)); + } + } + + [HarmonyPatch(typeof(SEvent_Concerts._concert._mc), "GetSkillValue")] + public class SEvent_Concerts__concert__mc_GetSkillValue_Limits + { + [HarmonyPriority(Priority.Last)] + public static void Postfix(ref int __result) + { + __result = Math.Max(0, Math.Min(100, __result)); + } + } + + [HarmonyPatch(typeof(Show_Popup), "AddCastParam")] + public class Show_Popup_AddCastParam_Limits + { + [HarmonyPriority(Priority.Last)] + public static void Postfix(List ___girlParams) + { + if (___girlParams == null || ___girlParams.Count == 0) + return; + + data_girls.girls.param param = ___girlParams[___girlParams.Count - 1]; + if (param != null) + param.val = Mathf.Clamp(param.val, 0f, 100f); + } + } + + [HarmonyPatch(typeof(Shows._show), "AddCastParam")] + public class Shows__show_AddCastParam_Limits + { + [HarmonyPriority(Priority.Last)] + public static void Postfix(Shows._show __instance) + { + if (__instance == null || __instance.girlParams == null || __instance.girlParams.Count == 0) + return; + + data_girls.girls.param param = __instance.girlParams[__instance.girlParams.Count - 1]; + if (param != null) + param.val = Mathf.Clamp(param.val, 0f, 100f); + } + } + + [HarmonyPatch(typeof(data_girls), "GetTeamChemistry")] + public class data_girls_GetTeamChemistry_Patch + { + [HarmonyPriority(Priority.Last)] + public static void Postfix(ref float __result) + { + __result = Mathf.Clamp(__result, 0f, 100f); + } + } +} From 055537e1bd55b03756dd4d8dcd66d9e1e55ee9db Mon Sep 17 00:00:00 2001 From: ExSlam Date: Sat, 15 Aug 2026 13:55:25 -0400 Subject: [PATCH 16/33] Fixed many issues and some possible crash bugs --- mods/Traits Fix/Traits Fix.cs | 1577 +++++++++++++++-------------- mods/Traits Fix/Traits Fix.csproj | 54 +- 2 files changed, 840 insertions(+), 791 deletions(-) diff --git a/mods/Traits Fix/Traits Fix.cs b/mods/Traits Fix/Traits Fix.cs index ca70a31..fb49cc3 100755 --- a/mods/Traits Fix/Traits Fix.cs +++ b/mods/Traits Fix/Traits Fix.cs @@ -1,764 +1,813 @@ -using HarmonyLib; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection.Emit; -using UnityEngine; -using static TraitFix.TraitsFix; - -namespace TraitFix -{ - - [HarmonyPatch(typeof(data_girls), "AgeDeterioration")] - public class Data_girls_AgeDeterioration - { - // Girls with Live Fast trait have double the rate of stat decreases after their peak age - public static void Postfix() - { - foreach (data_girls.girls girls in data_girls.girl) - { - if (girls != null - && girls.status != data_girls._status.graduated - && girls.trait == traits._trait._type.Live_fast) - { - for(int i = 0; i < LIVEFAST_DETERIORATION - 1; i++) - { - girls.AgeDeterioration(); - } - } - } - } - } - - [HarmonyPatch(typeof(Birthday_Popup), "DoParam")] - public class Birthday_Popup_DoParam - { - public static IEnumerable Transpiler(IEnumerable instructions) - { - List instructionList = new(instructions); - - int index = -1; - bool breakFlag = false; - for (int i = 0; i < instructionList.Count; i++) - { - if (breakFlag && instructionList[i].opcode == OpCodes.Stloc_1) - { - index = i; - break; - } - if (instructionList[i].opcode == OpCodes.Ldc_R4 && (float)instructionList[i].operand == 0.975f) - { - breakFlag = true; - } - } - - if (index != -1) - { - instructionList.Insert(index + 1, new CodeInstruction(OpCodes.Ldarg_0)); - instructionList.Insert(index + 2, new CodeInstruction(OpCodes.Ldloc_1)); - instructionList.Insert(index + 3, new CodeInstruction(OpCodes.Ldloc_0)); - instructionList.Insert(index + 4, new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(Birthday_Popup_DoParam), "Infix"))); - instructionList.Insert(index + 5, new CodeInstruction(OpCodes.Stloc_1)); - } - - return instructionList.AsEnumerable(); - } - - public static float Infix(Birthday_Popup __this, float newVal, float orig) - { - if (__this.Girl.trait != traits._trait._type.Live_fast) - return orig; - - return LIVEFAST_MODIFIER * (newVal - orig) + orig; - } - } - - - [HarmonyPatch(typeof(data_girls.girls), "GetAppealOfStat")] - public class Data_girls_girls_GetAppealOfStat - { - // Girls with Trendy trait have 1.5x the appeal to non-adults and 0.5x the appeal to adults. - public static void Postfix(ref float __result, resources.fanType _FanType, data_girls.girls __instance) - { - if (__instance.trait != traits._trait._type.Trendy) - return; - - switch(_FanType) - { - case resources.fanType.adult: - __result *= TRENDY_ADULT_MODIFIER; - break; - case resources.fanType.youngAdult: - __result *= TRENDY_YA_MODIFIER; - break; - case resources.fanType.teen: - __result *= TRENDY_TEEN_MODIFIER; - break; - } - } - } - - [HarmonyPatch(typeof(data_girls.girls), "UpdateDatingStatus")] - public class Data_girls_girls_UpdateDatingStatus - { - - // If there is an Indiscreet member, girls in dating relationships unknown to the player have a 2% chance - // of having the relationship revealed each week - public static void Postfix(ref data_girls.girls __instance) - { - if (mainScript.chance(INDISCREET_CHANCE) && __instance.DatingData.Is_Taken() && !__instance.DatingData.Is_Partner_Status_Known) - { - bool leaker = false; - foreach (data_girls.girls girls in data_girls.GetActiveGirls()) - { - if (girls != __instance && girls.trait == traits._trait._type.Indiscreet) - { - leaker = true; - } - } - if (!leaker) - return; - - string labelID = INDISCREET_LABEL_OUTSIDE; - if (policies.GetSelectedPolicyValue(policies._type.dating).Value == policies._value.dating_forbidden) - { - labelID = INDISCREET_LABEL_OUTSIDE_SCANDAL; - __instance.addParam(data_girls._paramType.scandalPoints, 1f, false); - } - - NotificationManager.AddNotification( - Language.Insert(labelID, new string[] { __instance.GetName() }), - mainScript.red32, - NotificationManager._notification._type.idol_relationship_change - ); - - __instance.getParam(data_girls._paramType.mentalStamina).add(-30f, false); - __instance.DatingData.Is_Partner_Status_Known = true; - __instance.DatingData.Partner_Status_Known_To_Player = __instance.DatingData.Partner_Status; - } - } - } - - [HarmonyPatch(typeof(Relationships._relationship), "CheckDating")] - public class Relationships__relationship_CheckDating - { - // If there is an Indiscreet member, girls in dating relationships unknown to the player have a 2% chance - // of having the relationship revealed each week - public static void Postfix(ref Relationships._relationship __instance) - { - if (mainScript.chance(INDISCREET_CHANCE) && __instance.Dating && !__instance.IsRelationshipKnown()) - { - bool leaker = false; - foreach (data_girls.girls girls in data_girls.GetActiveGirls(null)) - { - if (girls != __instance.Girls[0] && girls != __instance.Girls[1] && girls.trait == traits._trait._type.Indiscreet) - { - leaker = true; - } - } - if (!leaker) - return; - - string labelID = INDISCREET_LABEL_OUTSIDE; - if (policies.GetSelectedPolicyValue(policies._type.dating).Value == policies._value.dating_forbidden) - { - labelID = INDISCREET_LABEL_INSIDE; - __instance.Girls[0].addParam(data_girls._paramType.scandalPoints, 1f, false); - __instance.Girls[1].addParam(data_girls._paramType.scandalPoints, 1f, false); - } - - NotificationManager.AddNotification(Language.Insert(labelID, new string[] - { - __instance.Girls[0].GetName(), - __instance.Girls[1].GetName() - }), mainScript.red32, NotificationManager._notification._type.idol_relationship_change); - - __instance.Girls[0].getParam(data_girls._paramType.mentalStamina).add(-30f, false); - __instance.Girls[1].getParam(data_girls._paramType.mentalStamina).add(-30f, false); - __instance.Girls[0].DatingData.Is_Partner_Status_Known = true; - __instance.Girls[1].DatingData.Is_Partner_Status_Known = true; - __instance.Girls[0].DatingData.Partner_Status_Known_To_Player = data_girls.girls._dating_data._partner_status.taken_idol; - __instance.Girls[1].DatingData.Partner_Status_Known_To_Player = data_girls.girls._dating_data._partner_status.taken_idol; - } - } - } - - // Maternal and Precocious default relationship is positive based on age criteria - [HarmonyPatch(typeof(Relationships._relationship), "Initialize")] - public class Relationships__relationship_Initialize - { - public static void Postfix(ref Relationships._relationship __instance) - { - int age0 = __instance.Girls[0].GetAge(); - int age1 = __instance.Girls[1].GetAge(); - if (__instance.Girls[0].trait == traits._trait._type.Maternal && age0 > age1) - { - __instance.Dynamic = Relationships._relationship._dynamic.positive; - } - else if (__instance.Girls[1].trait == traits._trait._type.Maternal && age1 > age0) - { - __instance.Dynamic = Relationships._relationship._dynamic.positive; - } - else if (__instance.Girls[0].trait == traits._trait._type.Precocious && age0 < age1) - { - __instance.Dynamic = Relationships._relationship._dynamic.positive; - } - else if (__instance.Girls[1].trait == traits._trait._type.Precocious && age1 < age0) - { - __instance.Dynamic = Relationships._relationship._dynamic.positive; - } - } - } - - // Maternal and Precocious default relationship is positive based on age criteria - [HarmonyPatch(typeof(Relationships), "Do_Dynamic")] - public class Relationships_Do_Dynamic - { - public static void Postfix() - { - foreach (Relationships._relationship relationship in Relationships.RelationshipsData) - { - if (relationship.Dynamic == Relationships._relationship._dynamic.positive) - { - AdjustForAgeTraits(relationship); - } - - // Get either last center of main group or last center of girl's group - if (IsCenter(relationship.Girls[0]) - && relationship.Girls[0].trait == traits._trait._type.Arrogant) - { - relationship.Add(ARROGANT_PENALTY / 2); - } - else if(IsCenter(relationship.Girls[1]) - && relationship.Girls[1].trait == traits._trait._type.Arrogant) - { - relationship.Add(ARROGANT_PENALTY / 2); - } - } - } - - private static void AdjustForAgeTraits(Relationships._relationship relationship) - { - // Maternal - if (relationship.Girls[0].trait == traits._trait._type.Maternal && relationship.Girls[0].GetAge() > relationship.Girls[1].GetAge()) - { - relationship.Add(MATERNAL_BONUS / 2); - } - else if (relationship.Girls[1].trait == traits._trait._type.Maternal && relationship.Girls[1].GetAge() > relationship.Girls[0].GetAge()) - { - relationship.Add(MATERNAL_BONUS / 2); - } - - // Precocious - if (relationship.Girls[0].trait == traits._trait._type.Precocious && relationship.Girls[0].GetAge() < relationship.Girls[1].GetAge()) - { - relationship.Add(PRECOCIOUS_BONUS / 2); - } - else if (relationship.Girls[1].trait == traits._trait._type.Precocious && relationship.Girls[1].GetAge() < relationship.Girls[0].GetAge()) - { - relationship.Add(PRECOCIOUS_BONUS / 2); - } - } - } - - // Girls with Forgiving trait will never dislike or hate any other girls - [HarmonyPatch(typeof(Relationships._relationship), "Recalc")] - public class Relationships__relationship_Recalc - { - public static void Postfix(ref Relationships._relationship __instance) - { - if (__instance.Ratio >= FORGIVING_THR) - return; - - if (__instance.Girls[0].trait == traits._trait._type.Forgiving || __instance.Girls[1].trait == traits._trait._type.Forgiving) - { - __instance.Ratio = FORGIVING_THR; - } - - } - } - - // Girls with Meme Queen trait get +10 to all stats for internet shows - [HarmonyPatch(typeof(Shows._show), "AddCastParam")] - public class Shows__show_AddCastParam - { - public static void Postfix(data_girls._paramType type, List girlList, ref Shows._show __instance) - { - if (type == data_girls._paramType.teamChemistry) - return; - - foreach (data_girls.girls girls in girlList) - { - if (girls != null - && !girls.IsSick() - && girls.trait == traits._trait._type.Meme_queen - && __instance.medium.media_type == Shows._param._media_type.internet) - { - __instance.girlParams.Last().val += MEME_INT_SHOW; - break; - } - } - } - } - - // Girls with Meme Queen trait get +10 to all stats for internet shows - [HarmonyPatch(typeof(Show_Popup), "AddCastParam")] - public class Show_Popup_AddCastParam - { - public static void Postfix(data_girls._paramType type, List girlList, ref List ___girlParams, Shows._param ___medium) - { - if (type == data_girls._paramType.teamChemistry) - return; - - foreach (data_girls.girls girls in girlList) - { - if (girls != null - && !girls.IsSick() - && girls.trait == traits._trait._type.Meme_queen - && ___medium.media_type == Shows._param._media_type.internet) - { - ___girlParams.Last().val += MEME_INT_SHOW; - break; - } - } - } - } - - // Update shows on medium selection just in case of meme queen - [HarmonyPatch(typeof(Show_Popup), "SetParam")] - public class Show_Popup_SetParam - { - public static void Postfix(ref Show_Popup __instance, Shows._show._castType? ___castType) - { - if (___castType == null) - return; - - __instance.SetCastType(___castType.Value); - } - } - - // Girls with Meme Queen trait get +10% success rate and +5% crit success rate when participating in viral marketing campaigns - [HarmonyPatch(typeof(singles._param), "GetSuccessChance", new Type[] { typeof(Single_Marketing_Roll._result), typeof(int), typeof(singles._single) })] - public class Singles__param_GetSuccessChance - { - public static void Postfix(ref float __result, singles._param __instance, Single_Marketing_Roll._result Result, singles._single Single) - { - if (Single == null || __instance.Special_Type != singles._param._special_type.viral_campaign) - return; - - bool flag = false; - foreach (data_girls.girls girls in Single.girls) - { - if (girls != null - && !girls.IsSick() - && girls.trait == traits._trait._type.Meme_queen) - { - flag = true; - break; - } - } - if (flag) - { - switch(Result) - { - case Single_Marketing_Roll._result.success: - __result += MEME_VIRAL_SUCCESS; - break; - case Single_Marketing_Roll._result.success_crit: - __result += MEME_VIRAL_SUCCESS_CRIT; - break; - case Single_Marketing_Roll._result.fail: - __result += MEME_VIRAL_FAIL; - break; - } - } - } - } - - // Girls with Annoying trait cause other members to spend 1.2x physical stamina in shows - [HarmonyPatch(typeof(Shows._show), "SetStamina")] - public class Shows__show_SetStamina - { - public static void Postfix(Shows._show __instance) - { - List cast = __instance.GetCast(); - float staminaCost = __instance.GetStaminaCost(); - int annoyCount = 0; - foreach (data_girls.girls girls in cast) - { - if (girls != null - && girls.trait == traits._trait._type.Annoying - && girls.IsActive()) - { - annoyCount++; - } - } - if (annoyCount == 0) - return; - - foreach (data_girls.girls girls2 in cast) - { - if (girls2.IsActive()) - { - if (annoyCount > 1 || (annoyCount == 1 && girls2.trait != traits._trait._type.Annoying)) - { - girls2.addParam(data_girls._paramType.physicalStamina, -staminaCost * ANNOYING_MODIFIER, false); - } - } - } - } - } - - // Girls with Misandry trait have a 20% chance of receiving bad opinions from Male fans when participating in a single with handshakes - [HarmonyPatch(typeof(singles), "ReleaseSingle")] - public class Singles_ReleaseSingle - { - public static void Postfix(singles._single single) - { - foreach (data_girls.girls girls in single.girls) - { - if (girls != null - && !girls.IsSick() - && girls.trait == traits._trait._type.Misandry - && (single.IsIndividualHS() || single.IsGroupHS()) - && mainScript.chance(20)) - { - girls.AddAppeal(resources.fanType.male, MISANDRY_MODIFIER); - } - } - } - } - - // Girls with Perfectionist trait get -20 to mental stamina when world tours end with less than 80% average attendance - [HarmonyPatch(typeof(SEvent_Tour), "FinishTour")] - public class SEvent_Tour_FinishTour - { - public static void Postfix(SEvent_Tour __instance) - { - List activeGirls = data_girls.GetActiveGirls(); - foreach (data_girls.girls girls in activeGirls) - { - if (girls.trait == traits._trait._type.Perfectionist - && __instance.Tour.GetAverageAttendance() < PERFECTIONIST_TOUR_ATT) - { - girls.getParam(data_girls._paramType.mentalStamina).add(PERFECTIONIST_MENTAL, false); - } - } - } - } - - // Girls with Perfectionist trait get -20 to mental stamina when they participate in concerts with less than 100% hype. - [HarmonyPatch(typeof(SEvent_Concerts._concert), "Finish")] - public class SEvent_Concerts__concert_Finish - { - public static void Postfix(SEvent_Concerts._concert __instance) - { - foreach (data_girls.girls girls in __instance.GetGirls(true)) - { - if (girls.trait == traits._trait._type.Perfectionist - && __instance.Hype < PERFECTIONIST_HYPE) - { - girls.getParam(data_girls._paramType.mentalStamina).add(PERFECTIONIST_MENTAL, false); - } - } - } - } - - - // Apply traits to businesses - [HarmonyPatch(typeof(business._proposal), "GetGirlCoeff")] - public class Business__proposal_GetGirlCoeff - { - [HarmonyPriority(Priority.First)] - public static void Prefix() - { - patchGetVal = true; - } - - [HarmonyPriority(Priority.VeryLow)] - public static void Postfix(data_girls.girls _girl, ref float __result, business._proposal __instance) - { - // Girls with Photogenic trait have +100% to photoshoots - if (__instance.type == business._type.photoshoot - && _girl.trait == traits._trait._type.Photogenic) - { - __result += PHOTOGENIC_MODIFIER; - } - - patchGetVal = false; - } - } - - - // Stat changes for shows - [HarmonyPatch(typeof(data_girls), "GetAverageParam")] - public class Data_girls_GetAverageParam - { - [HarmonyPriority(Priority.First)] - public static void Prefix(List Girls) - { - patchGetVal = true; - showCast = Girls; - } - - [HarmonyPriority(Priority.VeryLow)] - public static void Postfix() - { - patchGetVal = false; - showCast = null; - } - } - - // Stat changes for shows (for team chemistray calc) - [HarmonyPatch(typeof(Shows._show), "SenbatsuCalcParam")] - public class Shows__show_SenbatsuCalcParam - { - [HarmonyPriority(Priority.First)] - public static void Prefix(List _girls) - { - patchGetVal = true; - showCast = _girls; - } - - [HarmonyPriority(Priority.VeryLow)] - public static void Postfix() - { - patchGetVal = false; - showCast = null; - } - } - - // Stat changes for singles - [HarmonyPatch(typeof(singles._single), "SenbatsuCalcParam")] - public class Singles__single_SenbatsuCalcParam - { - [HarmonyPriority(Priority.First)] - public static void Prefix() - { - patchGetVal = true; - } - - [HarmonyPriority(Priority.VeryLow)] - public static void Postfix() - { - patchGetVal = false; - } - } - - // Stat changes for concert songs - [HarmonyPatch(typeof(SEvent_Concerts._concert._song), "GetSkillValue")] - public class SEvent_Concerts__concert__song_GetSkillValue - { - [HarmonyPriority(Priority.First)] - public static void Prefix() - { - patchGetVal = true; - } - - [HarmonyPriority(Priority.VeryLow)] - public static void Postfix() - { - patchGetVal = false; - } - } - - - // Stat changes for concert MCs - [HarmonyPatch(typeof(SEvent_Concerts._concert._mc), "GetSkillValue")] - public class SEvent_Concerts__concert__mc_GetSkillValue - { - [HarmonyPriority(Priority.First)] - public static void Prefix() - { - patchGetVal = true; - } - - - [HarmonyPriority(Priority.VeryLow)] - public static void Postfix() - { - patchGetVal = false; - } - } - - // Apply traits to parameters and skills - [HarmonyPatch(typeof(data_girls.girls.param), "GetVal")] - public class data_girls_girls_param_GetVal - { - public static void Postfix(ref float __result, data_girls.girls.param __instance) - { - if (!patchGetVal) - return; - - __result += GetTraitModifier(__instance.Parent, __instance.type, showCast); - } - } - - - public class TraitsFix - { - public const int ANXIETY_MODIFIER = -10; - public const int CLUMSY_DANCE_MODIFIER = -30; - public const int CLUMSY_FUNNY_MODIFIER = 30; - public const int WORRIER_MODIFIER = -20; - public const int COMPLACENT_MODIFIER = -20; - public const int LONEWOLF_MODIFIER = 40; - public const int DEFEATIST_MODIFIER = -20; - public const int UNDERDOG_MODIFIER = 20; - public const float PHOTOGENIC_MODIFIER = 1f; - public const float PERFECTIONIST_MENTAL = -20; - public const float PERFECTIONIST_HYPE = 100; - public const float PERFECTIONIST_TOUR_ATT = 80; - public const float MISANDRY_MODIFIER = -1; - public const float ANNOYING_MODIFIER = 0.2f; - public const float MEME_VIRAL_SUCCESS = 10; - public const float MEME_VIRAL_SUCCESS_CRIT = 5; - public const float MEME_VIRAL_FAIL = -15; - public const float MEME_INT_SHOW = 10; - public const float FORGIVING_THR = 0.5f; - public const float MATERNAL_BONUS = 0.3f; - public const float PRECOCIOUS_BONUS = 0.3f; - public const float ARROGANT_PENALTY = -0.5f; - public const int INDISCREET_CHANCE = 2; - public const float TRENDY_ADULT_MODIFIER = 0.5f; - public const float TRENDY_YA_MODIFIER = 1.5f; - public const float TRENDY_TEEN_MODIFIER = 1.5f; - public const float LIVEFAST_MODIFIER = 2; - public const int LIVEFAST_DETERIORATION = 2; - - public const string INDISCREET_LABEL_OUTSIDE = "IDOL__OUTSIDE_LEAK"; - public const string INDISCREET_LABEL_INSIDE = "IDOL__INSIDE_LEAK_SCANDAL"; - public const string INDISCREET_LABEL_OUTSIDE_SCANDAL = "IDOL__OUTSIDE_LEAK_SCANDAL"; - - public static bool patchGetVal = false; - public static bool patchSetVal = false; - public static bool patchSet = false; - - public static List showCast = null; - - // This method calculates the modifier to girl parameters based on their trait. - public static int GetTraitModifier(data_girls.girls girls, data_girls._paramType type, List cast = null) - { - if (girls != null && data_girls.IsStatParam(type)) - { - switch(girls.trait) - { - case traits._trait._type.Anxiety: - if (IsEventUpcoming()) - return ANXIETY_MODIFIER; - break; - case traits._trait._type.Clumsy: - if (type == data_girls._paramType.dance) - return CLUMSY_DANCE_MODIFIER; - - if (type == data_girls._paramType.funny) - return CLUMSY_FUNNY_MODIFIER; - break; - case traits._trait._type.Worrier: - if (resources.GetScandalPointsTotal() > 0L) - return WORRIER_MODIFIER; - break; - case traits._trait._type.Complacent: - if (IsCenter(girls) - && (type == data_girls._paramType.vocal || type == data_girls._paramType.dance)) - return COMPLACENT_MODIFIER; - break; - case traits._trait._type.Lone_Wolf: - if (cast != null) - { - int count = 0; - foreach (data_girls.girls g in cast) - { - if (g != null && !g.IsSick()) - count++; - - if (count > 1) break; - } - if (count == 1) - return LONEWOLF_MODIFIER; - } - break; - } - - singles._single mainSingle = singles.GetLatestReleasedSingle(false, Groups.GetMainGroup()); - singles._single groupSingle = singles.GetLatestReleasedSingle(false, girls.GetGroup()); - - singles._single recentSingle = GetRecentSingle(groupSingle, mainSingle); - - if (recentSingle != null - && (staticVars.dateTime - recentSingle.ReleaseData.ReleaseDate).Days >= staticVars.dateTime.Day - && recentSingle.ReleaseData.Chart_Position != 1) - { - if (girls.trait == traits._trait._type.Defeatist) - { - return DEFEATIST_MODIFIER; - } - else if (girls.trait == traits._trait._type.Underdog) - { - return UNDERDOG_MODIFIER; - } - } - } - return 0; - } - - public static bool IsCenter(data_girls.girls girl) - { - singles._single mainSingle = singles.GetLatestReleasedSingle(false, Groups.GetMainGroup()); - singles._single groupSingle = singles.GetLatestReleasedSingle(false, girl.GetGroup()); - - return (groupSingle?.GetCenter() == girl) || (mainSingle?.GetCenter() == girl); - } - - private static bool IsEventUpcoming() - { - foreach (var tour in SEvent_Tour.Tours) - { - if (tour.Status != SEvent_Tour.tour._status.finished) - { - return true; - } - } - foreach (var sSK in SEvent_SSK.Elections) - { - if (sSK.Status != SEvent_Tour.tour._status.finished) - { - return true; - } - } - foreach (var concert in SEvent_Concerts.Concerts) - { - if (concert.Status != SEvent_Tour.tour._status.finished) - { - return true; - } - } - return false; - } - - // Helper method to get the more recent single based on release date and sales - private static singles._single GetRecentSingle(singles._single groupSingle, singles._single mainSingle) - { - if (groupSingle == null) return mainSingle; - if (mainSingle == null) return groupSingle; - - if (groupSingle.ReleaseData.ReleaseDate > mainSingle.ReleaseData.ReleaseDate) - { - return groupSingle; - } - else if (groupSingle.ReleaseData.ReleaseDate < mainSingle.ReleaseData.ReleaseDate) - { - return mainSingle; - } - else if (groupSingle.ReleaseData.Sales > mainSingle.ReleaseData.Sales) - { - return groupSingle; - } - else - { - return mainSingle; - } - } - } - -} +using HarmonyLib; +using System; +using System.Collections.Generic; +using UnityEngine; +using static TraitFix.TraitsFix; + +namespace TraitFix +{ + [HarmonyPatch(typeof(data_girls), "AgeDeterioration")] + public class Data_girls_AgeDeterioration + { + // Live Fast: double random post-peak deterioration. + public static void Postfix() + { + if (data_girls.girl == null) + return; + + foreach (data_girls.girls girl in data_girls.girl) + { + if (girl == null || girl.status == data_girls._status.graduated || girl.trait != traits._trait._type.Live_fast) + continue; + + for (int i = 0; i < LIVEFAST_DETERIORATION - 1; i++) + girl.AgeDeterioration(); + } + } + } + + // Live Fast birthday deterioration without a compiler-local-dependent transpiler. + [HarmonyPatch(typeof(Birthday_Popup), "DoParam")] + public class Birthday_Popup_DoParam + { + [HarmonyPriority(Priority.First)] + public static void Prefix(Birthday_Popup __instance, data_girls._paramType Prm, ref bool __state) + { + __state = BeginBirthdayDeterioration(__instance?.Girl, Prm); + } + + public static Exception Finalizer(Exception __exception, bool __state) + { + if (__state) + EndBirthdayDeterioration(); + return __exception; + } + } + + [HarmonyPatch(typeof(data_girls.girls.param), "setVal")] + public class data_girls_girls_param_setVal_Birthday + { + [HarmonyPriority(Priority.VeryLow)] + public static void Prefix(data_girls.girls.param __instance, ref float newVal) + { + if (__instance == null) + return; + newVal = AdjustBirthdayDeterioration(__instance.Parent, __instance.type, newVal); + } + } + + [HarmonyPatch(typeof(Birthday_Stat), "Set")] + public class Birthday_Stat_Set + { + [HarmonyPriority(Priority.VeryLow)] + public static void Prefix(data_girls._paramType Type, float OldVal, ref float NewVal) + { + NewVal = AdjustBirthdayDeteriorationDisplay(Type, OldVal, NewVal); + } + } + + [HarmonyPatch(typeof(data_girls.girls), "GetAppealOfStat")] + public class Data_girls_girls_GetAppealOfStat + { + public static void Postfix(ref float __result, resources.fanType _FanType, data_girls.girls __instance) + { + if (__instance == null || __instance.trait != traits._trait._type.Trendy) + return; + + switch (_FanType) + { + case resources.fanType.adult: + __result *= TRENDY_ADULT_MODIFIER; + break; + case resources.fanType.youngAdult: + __result *= TRENDY_YA_MODIFIER; + break; + case resources.fanType.teen: + __result *= TRENDY_TEEN_MODIFIER; + break; + } + } + } + + [HarmonyPatch(typeof(data_girls.girls), "UpdateDatingStatus")] + public class Data_girls_girls_UpdateDatingStatus + { + // Outside relationships get one reveal roll; idol-idol dating is handled by CheckDating below. + public static void Postfix(data_girls.girls __instance) + { + if (__instance?.DatingData == null + || !__instance.DatingData.Is_Taken_Outside() + || __instance.DatingData.Is_Partner_Status_Known + || !mainScript.chance(INDISCREET_CHANCE) + || !HasIndiscreetLeaker(__instance)) + { + return; + } + + string labelID = INDISCREET_LABEL_OUTSIDE; + if (IsDatingForbidden()) + { + labelID = INDISCREET_LABEL_OUTSIDE_SCANDAL; + __instance.addParam(data_girls._paramType.scandalPoints, 1f, false); + } + + NotificationManager.AddNotification( + Language.Insert(labelID, new string[] { __instance.GetName() }), + mainScript.red32, + NotificationManager._notification._type.idol_relationship_change); + + __instance.getParam(data_girls._paramType.mentalStamina)?.add(-30f, false); + __instance.DatingData.Is_Partner_Status_Known = true; + __instance.DatingData.Partner_Status_Known_To_Player = __instance.DatingData.Partner_Status; + } + } + + [HarmonyPatch(typeof(Relationships._relationship), "CheckDating")] + public class Relationships__relationship_CheckDating + { + public static void Postfix(Relationships._relationship __instance) + { + if (!TryGetRelationshipGirls(__instance, out data_girls.girls girl0, out data_girls.girls girl1) || !__instance.Dating) + return; + + // Repair old one-sided knowledge and prevent the same relationship leaking every week. + if (girl0.DatingData.Is_Partner_Status_Known || girl1.DatingData.Is_Partner_Status_Known) + { + MarkIdolRelationshipKnown(girl0, girl1); + return; + } + if (__instance.IsRelationshipKnown()) + return; + + if (!mainScript.chance(INDISCREET_CHANCE) || !HasIndiscreetLeaker(girl0, girl1)) + return; + + string labelID = INDISCREET_LABEL_OUTSIDE; + if (IsDatingForbidden()) + { + labelID = INDISCREET_LABEL_INSIDE; + girl0.addParam(data_girls._paramType.scandalPoints, 1f, false); + girl1.addParam(data_girls._paramType.scandalPoints, 1f, false); + } + + NotificationManager.AddNotification( + Language.Insert(labelID, new string[] { girl0.GetName(), girl1.GetName() }), + mainScript.red32, + NotificationManager._notification._type.idol_relationship_change); + + girl0.getParam(data_girls._paramType.mentalStamina)?.add(-30f, false); + girl1.getParam(data_girls._paramType.mentalStamina)?.add(-30f, false); + MarkIdolRelationshipKnown(girl0, girl1); + } + } + + [HarmonyPatch(typeof(Relationships._relationship), "Initialize")] + public class Relationships__relationship_Initialize + { + public static void Postfix(Relationships._relationship __instance) + { + if (!TryGetRelationshipGirls(__instance, out data_girls.girls girl0, out data_girls.girls girl1)) + return; + + int age0 = girl0.GetAge(); + int age1 = girl1.GetAge(); + if (girl0.trait == traits._trait._type.Maternal && age0 > age1) + __instance.Dynamic = Relationships._relationship._dynamic.positive; + else if (girl1.trait == traits._trait._type.Maternal && age1 > age0) + __instance.Dynamic = Relationships._relationship._dynamic.positive; + else if (girl0.trait == traits._trait._type.Precocious && age0 < age1) + __instance.Dynamic = Relationships._relationship._dynamic.positive; + else if (girl1.trait == traits._trait._type.Precocious && age1 < age0) + __instance.Dynamic = Relationships._relationship._dynamic.positive; + } + } + + [HarmonyPatch(typeof(Relationships), "Do_Dynamic")] + public class Relationships_Do_Dynamic + { + public static void Postfix() + { + if (Relationships.RelationshipsData == null) + return; + + foreach (Relationships._relationship relationship in Relationships.RelationshipsData) + { + if (!TryGetRelationshipGirls(relationship, out data_girls.girls girl0, out data_girls.girls girl1)) + continue; + + if (relationship.Dynamic == Relationships._relationship._dynamic.positive) + AdjustForAgeTraits(relationship, girl0, girl1); + + if (IsCenter(girl0) && girl0.trait == traits._trait._type.Arrogant) + relationship.Add(ARROGANT_PENALTY / 2f); + else if (IsCenter(girl1) && girl1.trait == traits._trait._type.Arrogant) + relationship.Add(ARROGANT_PENALTY / 2f); + } + } + + private static void AdjustForAgeTraits(Relationships._relationship relationship, data_girls.girls girl0, data_girls.girls girl1) + { + // Relationship.Add halves positive values. Vanilla already contributes +0.05; + // Add(0.3) contributes +0.15 more, producing the advertised 4x (+0.20 total). + if (girl0.trait == traits._trait._type.Maternal && girl0.GetAge() > girl1.GetAge()) + relationship.Add(MATERNAL_BONUS); + else if (girl1.trait == traits._trait._type.Maternal && girl1.GetAge() > girl0.GetAge()) + relationship.Add(MATERNAL_BONUS); + + if (girl0.trait == traits._trait._type.Precocious && girl0.GetAge() < girl1.GetAge()) + relationship.Add(PRECOCIOUS_BONUS); + else if (girl1.trait == traits._trait._type.Precocious && girl1.GetAge() < girl0.GetAge()) + relationship.Add(PRECOCIOUS_BONUS); + } + } + + [HarmonyPatch(typeof(Relationships._relationship), "Recalc")] + public class Relationships__relationship_Recalc + { + public static void Postfix(Relationships._relationship __instance) + { + if (!TryGetRelationshipGirls(__instance, out data_girls.girls girl0, out data_girls.girls girl1) + || __instance.Ratio >= FORGIVING_THR) + return; + + if (girl0.trait == traits._trait._type.Forgiving || girl1.trait == traits._trait._type.Forgiving) + __instance.Ratio = FORGIVING_THR; + } + } + + [HarmonyPatch(typeof(Shows._show), "AddCastParam")] + public class Shows__show_AddCastParam + { + public static void Postfix(data_girls._paramType type, List girlList, Shows._show __instance) + { + if (type == data_girls._paramType.teamChemistry + || __instance?.medium == null + || __instance.medium.media_type != Shows._param._media_type.internet + || !HasActiveMemeQueen(girlList) + || __instance.girlParams == null + || __instance.girlParams.Count == 0) + return; + + data_girls.girls.param param = __instance.girlParams[__instance.girlParams.Count - 1]; + if (param != null && param.type == type) + param.val += MEME_INT_SHOW; + } + } + + [HarmonyPatch(typeof(Show_Popup), "AddCastParam")] + public class Show_Popup_AddCastParam + { + public static void Postfix(data_girls._paramType type, List girlList, List ___girlParams, Shows._param ___medium) + { + if (type == data_girls._paramType.teamChemistry + || ___medium == null + || ___medium.media_type != Shows._param._media_type.internet + || !HasActiveMemeQueen(girlList) + || ___girlParams == null + || ___girlParams.Count == 0) + return; + + data_girls.girls.param param = ___girlParams[___girlParams.Count - 1]; + if (param != null && param.type == type) + param.val += MEME_INT_SHOW; + } + } + + // Recalculate only when the medium changes, avoiding a recalculation on every SetParam. + [HarmonyPatch(typeof(Show_Popup), "SetParam")] + public class Show_Popup_SetParam + { + public static void Postfix(Show_Popup __instance, Show_Popup_Param_Button._type type, Shows._show._castType? ___castType) + { + if (__instance == null || type != Show_Popup_Param_Button._type.medium || ___castType == null) + return; + __instance.SetCastType(___castType.Value); + } + } + + [HarmonyPatch(typeof(singles._param), "GetSuccessChance", new Type[] { typeof(Single_Marketing_Roll._result), typeof(int), typeof(singles._single) })] + public class Singles__param_GetSuccessChance + { + public static void Postfix(ref float __result, singles._param __instance, Single_Marketing_Roll._result Result, singles._single Single) + { + if (__instance == null || Single?.girls == null || __instance.Special_Type != singles._param._special_type.viral_campaign) + return; + + bool hasMemeQueen = false; + foreach (data_girls.girls girl in Single.girls) + { + if (girl != null && !girl.IsSick() && girl.trait == traits._trait._type.Meme_queen) + { + hasMemeQueen = true; + break; + } + } + if (!hasMemeQueen) + return; + + // Do not patch fail directly: vanilla derives regular fail chance from the + // success/crit chances, so subtracting 15 from fail double-counted this bonus. + if (Result == Single_Marketing_Roll._result.success) + __result += MEME_VIRAL_SUCCESS; + else if (Result == Single_Marketing_Roll._result.success_crit) + __result += MEME_VIRAL_SUCCESS_CRIT; + } + } + + [HarmonyPatch(typeof(Shows._show), "SetStamina")] + public class Shows__show_SetStamina + { + public static void Postfix(Shows._show __instance) + { + List cast = __instance?.GetCast(); + if (cast == null || cast.Count == 0) + return; + + float staminaCost = __instance.GetStaminaCost(); + int annoyingCount = 0; + foreach (data_girls.girls girl in cast) + { + if (girl != null && girl.trait == traits._trait._type.Annoying && girl.IsActive()) + annoyingCount++; + } + if (annoyingCount == 0) + return; + + foreach (data_girls.girls girl in cast) + { + if (girl == null || !girl.IsActive()) + continue; + if (annoyingCount > 1 || girl.trait != traits._trait._type.Annoying) + girl.addParam(data_girls._paramType.physicalStamina, -staminaCost * ANNOYING_MODIFIER, false); + } + } + } + + [HarmonyPatch(typeof(singles), "ReleaseSingle")] + public class Singles_ReleaseSingle + { + public static void Postfix(singles._single single) + { + if (single?.girls == null) + return; + + bool hasHandshake = single.IsIndividualHS() || single.IsGroupHS(); + if (!hasHandshake) + return; + + foreach (data_girls.girls girl in single.girls) + { + if (girl != null && !girl.IsSick() && girl.trait == traits._trait._type.Misandry && mainScript.chance(20)) + girl.AddAppeal(resources.fanType.male, MISANDRY_MODIFIER); + } + } + } + + // FinishTour clears Tour before returning, so capture the attendance result first. + [HarmonyPatch(typeof(SEvent_Tour), "FinishTour")] + public class SEvent_Tour_FinishTour + { + public static void Prefix(SEvent_Tour __instance, ref bool __state) + { + __state = __instance?.Tour != null && __instance.Tour.GetAverageAttendance() < PERFECTIONIST_TOUR_ATT; + } + + public static void Postfix(bool __state) + { + if (!__state) + return; + List activeGirls = data_girls.GetActiveGirls(); + if (activeGirls == null) + return; + + foreach (data_girls.girls girl in activeGirls) + { + if (girl != null && girl.trait == traits._trait._type.Perfectionist) + girl.getParam(data_girls._paramType.mentalStamina)?.add(PERFECTIONIST_MENTAL, false); + } + } + } + + [HarmonyPatch(typeof(SEvent_Concerts._concert), "Finish")] + public class SEvent_Concerts__concert_Finish + { + public static void Postfix(SEvent_Concerts._concert __instance) + { + if (__instance == null || __instance.Hype >= PERFECTIONIST_HYPE) + return; + List girls = __instance.GetGirls(true); + if (girls == null) + return; + + foreach (data_girls.girls girl in girls) + { + if (girl != null && girl.trait == traits._trait._type.Perfectionist) + girl.getParam(data_girls._paramType.mentalStamina)?.add(PERFECTIONIST_MENTAL, false); + } + } + } + + // Trait stat contexts use a stack plus Finalizers so nested calls and exceptions cannot poison later GetVal calls. + [HarmonyPatch(typeof(business._proposal), "GetGirlCoeff")] + public class Business__proposal_GetGirlCoeff + { + [HarmonyPriority(Priority.First)] + public static void Prefix() => BeginTraitCalculation(); + + public static void Postfix(data_girls.girls _girl, ref float __result, business._proposal __instance) + { + if (__instance != null && _girl != null && __instance.type == business._type.photoshoot && _girl.trait == traits._trait._type.Photogenic) + __result += PHOTOGENIC_MODIFIER; + } + + public static Exception Finalizer(Exception __exception) + { + EndTraitCalculation(); + return __exception; + } + } + + [HarmonyPatch(typeof(data_girls), "GetAverageParam")] + public class Data_girls_GetAverageParam + { + [HarmonyPriority(Priority.First)] + public static void Prefix(List Girls) => BeginTraitCalculation(Girls); + + public static Exception Finalizer(Exception __exception) + { + EndTraitCalculation(); + return __exception; + } + } + + [HarmonyPatch(typeof(Shows._show), "SenbatsuCalcParam")] + public class Shows__show_SenbatsuCalcParam + { + [HarmonyPriority(Priority.First)] + public static void Prefix(List _girls) => BeginTraitCalculation(_girls); + + public static Exception Finalizer(Exception __exception) + { + EndTraitCalculation(); + return __exception; + } + } + + [HarmonyPatch(typeof(singles._single), "SenbatsuCalcParam")] + public class Singles__single_SenbatsuCalcParam + { + [HarmonyPriority(Priority.First)] + public static void Prefix() => BeginTraitCalculation(); + + public static Exception Finalizer(Exception __exception) + { + EndTraitCalculation(); + return __exception; + } + } + + [HarmonyPatch(typeof(SEvent_Concerts._concert._song), "GetSkillValue")] + public class SEvent_Concerts__concert__song_GetSkillValue + { + [HarmonyPriority(Priority.First)] + public static void Prefix() => BeginTraitCalculation(); + + public static Exception Finalizer(Exception __exception) + { + EndTraitCalculation(); + return __exception; + } + } + + [HarmonyPatch(typeof(SEvent_Concerts._concert._mc), "GetSkillValue")] + public class SEvent_Concerts__concert__mc_GetSkillValue + { + [HarmonyPriority(Priority.First)] + public static void Prefix() => BeginTraitCalculation(); + + public static Exception Finalizer(Exception __exception) + { + EndTraitCalculation(); + return __exception; + } + } + + [HarmonyPatch(typeof(data_girls.girls.param), "GetVal")] + public class data_girls_girls_param_GetVal + { + public static void Postfix(ref float __result, data_girls.girls.param __instance) + { + if (!IsTraitCalculationActive || __instance == null) + return; + __result += GetTraitModifier(__instance.Parent, __instance.type, CurrentTraitCast); + } + } + + public class TraitsFix + { + public const int ANXIETY_MODIFIER = -10; + public const int CLUMSY_DANCE_MODIFIER = -30; + public const int CLUMSY_FUNNY_MODIFIER = 30; + public const int WORRIER_MODIFIER = -20; + public const int COMPLACENT_MODIFIER = -20; + public const int LONEWOLF_MODIFIER = 40; + public const int DEFEATIST_MODIFIER = -20; + public const int UNDERDOG_MODIFIER = 20; + public const float PHOTOGENIC_MODIFIER = 1f; + public const float PERFECTIONIST_MENTAL = -20; + public const float PERFECTIONIST_HYPE = 100; + public const float PERFECTIONIST_TOUR_ATT = 80; + public const float MISANDRY_MODIFIER = -1; + public const float ANNOYING_MODIFIER = 0.2f; + public const float MEME_VIRAL_SUCCESS = 10; + public const float MEME_VIRAL_SUCCESS_CRIT = 5; + public const float MEME_INT_SHOW = 10; + public const float FORGIVING_THR = 0.5f; + public const float MATERNAL_BONUS = 0.3f; + public const float PRECOCIOUS_BONUS = 0.3f; + public const float ARROGANT_PENALTY = -0.5f; + public const int INDISCREET_CHANCE = 2; + public const float TRENDY_ADULT_MODIFIER = 0.5f; + public const float TRENDY_YA_MODIFIER = 1.5f; + public const float TRENDY_TEEN_MODIFIER = 1.5f; + public const float LIVEFAST_MODIFIER = 2f; + public const int LIVEFAST_DETERIORATION = 2; + + public const string INDISCREET_LABEL_OUTSIDE = "IDOL__OUTSIDE_LEAK"; + public const string INDISCREET_LABEL_INSIDE = "IDOL__INSIDE_LEAK_SCANDAL"; + public const string INDISCREET_LABEL_OUTSIDE_SCANDAL = "IDOL__OUTSIDE_LEAK_SCANDAL"; + + private sealed class TraitCalculationContext + { + public List Cast; + } + + private sealed class BirthdayDeteriorationContext + { + public data_girls.girls Girl; + public data_girls._paramType Type; + public float OldValue; + } + + private static readonly Stack traitCalculationContexts = new Stack(); + private static readonly Stack birthdayDeteriorationContexts = new Stack(); + + public static bool IsTraitCalculationActive => traitCalculationContexts.Count > 0; + public static List CurrentTraitCast => IsTraitCalculationActive ? traitCalculationContexts.Peek().Cast : null; + + public static int GetTraitModifier(data_girls.girls girl, data_girls._paramType type, List cast = null) + { + if (girl == null || !data_girls.IsStatParam(type)) + return 0; + + switch (girl.trait) + { + case traits._trait._type.Anxiety: + if (IsEventUpcoming()) return ANXIETY_MODIFIER; + break; + case traits._trait._type.Clumsy: + if (type == data_girls._paramType.dance) return CLUMSY_DANCE_MODIFIER; + if (type == data_girls._paramType.funny) return CLUMSY_FUNNY_MODIFIER; + break; + case traits._trait._type.Worrier: + if (resources.GetScandalPointsTotal() > 0L) return WORRIER_MODIFIER; + break; + case traits._trait._type.Complacent: + if (IsCenter(girl) && (type == data_girls._paramType.vocal || type == data_girls._paramType.dance)) + return COMPLACENT_MODIFIER; + break; + case traits._trait._type.Lone_Wolf: + if (cast != null) + { + int count = 0; + foreach (data_girls.girls castGirl in cast) + { + if (castGirl != null && castGirl.IsActive() && !castGirl.IsSick()) + count++; + if (count > 1) break; + } + if (count == 1) return LONEWOLF_MODIFIER; + } + break; + } + + if (girl.trait == traits._trait._type.Defeatist || girl.trait == traits._trait._type.Underdog) + { + singles._single mainSingle = singles.GetLatestReleasedSingle(false, Groups.GetMainGroup()); + Groups._group girlGroup = girl.GetGroup(); + singles._single groupSingle = girlGroup != null ? singles.GetLatestReleasedSingle(false, girlGroup) : null; + singles._single recentSingle = GetRecentSingle(groupSingle, mainSingle); + if (DidSingleMissNumberOne(recentSingle)) + return girl.trait == traits._trait._type.Defeatist ? DEFEATIST_MODIFIER : UNDERDOG_MODIFIER; + } + + return 0; + } + + public static bool IsCenter(data_girls.girls girl) + { + if (girl == null) + return false; + singles._single mainSingle = singles.GetLatestReleasedSingle(false, Groups.GetMainGroup()); + Groups._group girlGroup = girl.GetGroup(); + singles._single groupSingle = girlGroup != null ? singles.GetLatestReleasedSingle(false, girlGroup) : null; + return groupSingle?.GetCenter() == girl || mainSingle?.GetCenter() == girl; + } + + public static void BeginTraitCalculation(List cast = null) + { + traitCalculationContexts.Push(new TraitCalculationContext { Cast = cast }); + } + + public static void EndTraitCalculation() + { + if (traitCalculationContexts.Count > 0) + traitCalculationContexts.Pop(); + } + + public static bool BeginBirthdayDeterioration(data_girls.girls girl, data_girls._paramType type) + { + if (girl == null + || girl.trait != traits._trait._type.Live_fast + || girl.GetAge() <= girl.peakAge + || type == data_girls._paramType.funny + || type == data_girls._paramType.smart) + return false; + + data_girls.girls.param param = girl.getParam(type); + if (param == null) + return false; + + birthdayDeteriorationContexts.Push(new BirthdayDeteriorationContext + { + Girl = girl, + Type = type, + OldValue = param.val + }); + return true; + } + + public static void EndBirthdayDeterioration() + { + if (birthdayDeteriorationContexts.Count > 0) + birthdayDeteriorationContexts.Pop(); + } + + public static float AdjustBirthdayDeterioration(data_girls.girls girl, data_girls._paramType type, float newValue) + { + if (birthdayDeteriorationContexts.Count == 0) + return newValue; + BirthdayDeteriorationContext context = birthdayDeteriorationContexts.Peek(); + if (context.Girl != girl || context.Type != type || newValue >= context.OldValue) + return newValue; + return Mathf.Clamp(context.OldValue + LIVEFAST_MODIFIER * (newValue - context.OldValue), 1f, 100f); + } + + public static float AdjustBirthdayDeteriorationDisplay(data_girls._paramType type, float oldValue, float newValue) + { + if (birthdayDeteriorationContexts.Count == 0) + return newValue; + BirthdayDeteriorationContext context = birthdayDeteriorationContexts.Peek(); + if (context.Type != type || newValue >= oldValue) + return newValue; + return Mathf.Clamp(oldValue + LIVEFAST_MODIFIER * (newValue - oldValue), 1f, 100f); + } + + public static bool IsDatingForbidden() + { + policies.value datingPolicy = policies.GetSelectedPolicyValue(policies._type.dating); + return datingPolicy != null && datingPolicy.Value == policies._value.dating_forbidden; + } + + public static bool HasActiveMemeQueen(List girls) + { + if (girls == null) + return false; + foreach (data_girls.girls girl in girls) + { + if (girl != null && girl.IsActive() && !girl.IsSick() && girl.trait == traits._trait._type.Meme_queen) + return true; + } + return false; + } + + public static bool HasIndiscreetLeaker(params data_girls.girls[] excludedGirls) + { + List activeGirls = data_girls.GetActiveGirls(null); + if (activeGirls == null) + return false; + + foreach (data_girls.girls girl in activeGirls) + { + if (girl == null || girl.trait != traits._trait._type.Indiscreet) + continue; + bool excluded = false; + if (excludedGirls != null) + { + foreach (data_girls.girls excludedGirl in excludedGirls) + { + if (girl == excludedGirl) + { + excluded = true; + break; + } + } + } + if (!excluded) + return true; + } + return false; + } + + public static bool TryGetRelationshipGirls(Relationships._relationship relationship, out data_girls.girls girl0, out data_girls.girls girl1) + { + girl0 = null; + girl1 = null; + if (relationship?.Girls == null || relationship.Girls.Count < 2) + return false; + girl0 = relationship.Girls[0]; + girl1 = relationship.Girls[1]; + return girl0 != null && girl1 != null && girl0.DatingData != null && girl1.DatingData != null; + } + + public static void MarkIdolRelationshipKnown(data_girls.girls girl0, data_girls.girls girl1) + { + if (girl0?.DatingData == null || girl1?.DatingData == null) + return; + girl0.DatingData.Is_Partner_Status_Known = true; + girl1.DatingData.Is_Partner_Status_Known = true; + girl0.DatingData.Partner_Status_Known_To_Player = data_girls.girls._dating_data._partner_status.taken_idol; + girl1.DatingData.Partner_Status_Known_To_Player = data_girls.girls._dating_data._partner_status.taken_idol; + } + + private static bool DidSingleMissNumberOne(singles._single single) + { + if (single?.ReleaseData == null) + return false; + + DateTime releaseDate = single.ReleaseData.ReleaseDate; + if (releaseDate.Year == staticVars.dateTime.Year && releaseDate.Month == staticVars.dateTime.Month) + return false; + + int chartPosition = single.ReleaseData.Chart_Position; + if (chartPosition <= 0) + chartPosition = ResolveChartPosition(single); + return chartPosition > 1; + } + + private static int ResolveChartPosition(singles._single single) + { + if (single?.ReleaseData == null || Rivals.Date_To_Month == null) + return 0; + + // Player singles released in a month are part of that month's chart data. + DateTime releaseMonth = single.ReleaseData.ReleaseDate; + foreach (Rivals._date_to_month_id month in Rivals.Date_To_Month) + { + if (month == null || month.Date.Year != releaseMonth.Year || month.Date.Month != releaseMonth.Month) + continue; + + List chartSingles = Rivals.GetSingles(month.ID); + if (chartSingles == null) + return 0; + for (int i = 0; i < chartSingles.Count; i++) + { + Rivals._group._single chartSingle = chartSingles[i]; + if (chartSingle != null && chartSingle.Player && chartSingle.SingleID == single.id) + return i + 1; + } + return 0; + } + return 0; + } + + private static bool IsEventUpcoming() + { + if (SEvent_Tour.Tours != null) + { + foreach (SEvent_Tour.tour tour in SEvent_Tour.Tours) + if (tour != null && tour.Status != SEvent_Tour.tour._status.finished) return true; + } + if (SEvent_SSK.Elections != null) + { + foreach (SEvent_SSK._SSK election in SEvent_SSK.Elections) + if (election != null && election.Status != SEvent_Tour.tour._status.finished) return true; + } + if (SEvent_Concerts.Concerts != null) + { + foreach (SEvent_Concerts._concert concert in SEvent_Concerts.Concerts) + if (concert != null && concert.Status != SEvent_Tour.tour._status.finished) return true; + } + return false; + } + + private static singles._single GetRecentSingle(singles._single groupSingle, singles._single mainSingle) + { + if (groupSingle?.ReleaseData == null) return mainSingle?.ReleaseData != null ? mainSingle : null; + if (mainSingle?.ReleaseData == null) return groupSingle; + if (groupSingle.ReleaseData.ReleaseDate > mainSingle.ReleaseData.ReleaseDate) return groupSingle; + if (groupSingle.ReleaseData.ReleaseDate < mainSingle.ReleaseData.ReleaseDate) return mainSingle; + return groupSingle.ReleaseData.Sales > mainSingle.ReleaseData.Sales ? groupSingle : mainSingle; + } + } +} diff --git a/mods/Traits Fix/Traits Fix.csproj b/mods/Traits Fix/Traits Fix.csproj index 408b182..a46c82d 100755 --- a/mods/Traits Fix/Traits Fix.csproj +++ b/mods/Traits Fix/Traits Fix.csproj @@ -1,27 +1,27 @@ - - - - Traits Fix - com.tel.traitsfix - This mod adds implementations for all traits that don't work in the original game. - Tel - 1.1.0 - ["gameplay"] - - - 0 - - $(HarmonyID) - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - + + + + Traits Fix + com.tel.traitsfix + This mod adds implementations for all traits that don't work in the original game. + Tel + 1.1.1 + ["gameplay"] + + + 0 + + $(HarmonyID) + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + From 3c045ed4d41430b411e86f968ed2644b5fa736fe Mon Sep 17 00:00:00 2001 From: ExSlam Date: Sat, 15 Aug 2026 13:56:51 -0400 Subject: [PATCH 17/33] Fixed maths, added compatibility with Never Graduate mod so that Idols do not graduate if that mod is enabled --- mods/Worker Rights/Worker Rights.cs | 189 ++++++++++++------------ mods/Worker Rights/Worker Rights.csproj | 50 +++---- 2 files changed, 119 insertions(+), 120 deletions(-) diff --git a/mods/Worker Rights/Worker Rights.cs b/mods/Worker Rights/Worker Rights.cs index ce35f6d..32f5884 100755 --- a/mods/Worker Rights/Worker Rights.cs +++ b/mods/Worker Rights/Worker Rights.cs @@ -1,95 +1,94 @@ -using HarmonyLib; -using System; -using UnityEngine; -using System.Reflection; -using static WorkerRights.WorkerRights; - -namespace WorkerRights -{ - public class WorkerRights - { - public const int FIRE_MIN_DAYS = 30; - public const int DEF_SALARY = 20000; - public const float MAXFAME_SALARY_EARN_COEFF = 0.1f; - public const int GRAD_DAYS_PENALTY = 9; - public const int GRAD_DAYS_PENALTY_SEVERE = 27; - } - - // Staff cannot be fired using scandal points within the first month - [HarmonyPatch(typeof(staff._staff), "CanFire")] - public class staff__staff_CanFire - { - public static void Postfix(staff._staff __instance, ref bool __result) - { - if ((staticVars.dateTime - __instance.HireDate).Days < FIRE_MIN_DAYS) - { - __result = false; - } - } - } - // 20000 yen/wk is the expected starting salary for 100% satisfaction - [HarmonyPatch(typeof(data_girls.girls), "GetExpectedSalary")] - public class data_girls_girls_GetExpectedSalary - { - public static void Postfix(ref int __result, data_girls.girls __instance) - { - if (__instance.GetFameLevel() < 1f) - { - __result = DEF_SALARY * 2; - } - } - } - - //In hard and normal mode, penalty for low salary satisfaction increased 10x - [HarmonyPatch(typeof(data_girls.girls), "Graduation_Date_Update")] - public class data_girls_girls_Graduation_Date_Update - { - public static void Postfix(ref data_girls.girls __instance) - { - int salarySatisfaction_Percentage = __instance.GetSalarySatisfaction_Percentage(); - if (staticVars.IsEasy() || __instance.status == data_girls._status.announced_graduation || policies.GetSelectedPolicyValue(policies._type.salary).Value != policies._value.salary_manual) - return; - - int daysModifier = 0; - if (salarySatisfaction_Percentage < 20) - { - daysModifier -= GRAD_DAYS_PENALTY_SEVERE; - } - else if (salarySatisfaction_Percentage < 50) - { - daysModifier -= GRAD_DAYS_PENALTY; - } - __instance.Graduation_Date.AddDays(daysModifier); - } - } - - // In hard mode, idols at 10 fame will expect at least 10% of their earnings as salary - [HarmonyPatch(typeof(data_girls.girls), "GetExpectedSalary_Total")] - public class data_girls_girls_GetExpectedSalary_Total - { - public static void Postfix(ref long __result, data_girls.girls __instance) - { - if (!staticVars.IsHard()) - return; - - int expected = __instance.GetExpectedSalary(); - float earning = __instance.GetAverageEarnings(); - if (__instance.GetFameLevel() == 10 && (earning * MAXFAME_SALARY_EARN_COEFF) > expected) - { - __result = (long)Mathf.Round(earning * MAXFAME_SALARY_EARN_COEFF); - } - } - } - - - // Default salary set to 20000 - [HarmonyPatch(typeof(data_girls), "GenerateGirl")] - public class data_girls_GenerateGirl - { - public static void Postfix(ref data_girls.girls __result) - { - __result.salary = DEF_SALARY; - } - } - -} +using HarmonyLib; +using System; +using UnityEngine; +using static WorkerRights.WorkerRights; + +namespace WorkerRights +{ + public class WorkerRights + { + public const int FIRE_MIN_DAYS = 30; + public const int DEF_SALARY = 20000; + public const float MAXFAME_SALARY_EARN_COEFF = 0.1f; + public const int GRAD_DAYS_PENALTY = 10; + public const int GRAD_DAYS_PENALTY_SEVERE = 30; + } + + [HarmonyPatch(typeof(staff._staff), "CanFire")] + public class staff__staff_CanFire + { + public static void Postfix(staff._staff __instance, ref bool __result) + { + if (__instance != null && (staticVars.dateTime - __instance.HireDate).Days < FIRE_MIN_DAYS) + __result = false; + } + } + + [HarmonyPatch(typeof(data_girls.girls), "GetExpectedSalary")] + public class data_girls_girls_GetExpectedSalary + { + public static void Postfix(data_girls.girls __instance, ref int __result) + { + if (__instance != null && __instance.GetFameLevel() < 1f) + __result = Math.Max(__result, DEF_SALARY * 2); + } + } + + // Vanilla's own Graduation_Date_Update calls DateTime.AddDays without assigning the returned DateTime, + // so the salary date adjustment is a no-op. Apply the complete advertised 10x low-salary penalty here. + // This stays on the game's weekly Graduation_Date_Update cadence rather than silently becoming a daily 7x rebalance. + [HarmonyPatch(typeof(data_girls.girls), "Graduation_Date_Update")] + public class data_girls_girls_Graduation_Date_Update + { + [HarmonyBefore("com.tel.nevergraduate")] + public static void Postfix(data_girls.girls __instance) + { + if (__instance == null || staticVars.IsEasy() || __instance.status == data_girls._status.announced_graduation) + return; + + policies.value salaryPolicy = policies.GetSelectedPolicyValue(policies._type.salary); + if (salaryPolicy == null || salaryPolicy.Value != policies._value.salary_manual) + return; + + int satisfaction = __instance.GetSalarySatisfaction_Percentage(); + int daysModifier = 0; + if (satisfaction < 20) + daysModifier = -GRAD_DAYS_PENALTY_SEVERE; + else if (satisfaction < 50) + daysModifier = -GRAD_DAYS_PENALTY; + + if (daysModifier != 0) + __instance.Graduation_Date = __instance.Graduation_Date.AddDays(daysModifier); + } + } + + // In hard mode, 10-fame idols expect AT LEAST 10% of average earnings. + // Never lower a larger salary expectation produced by vanilla or another mod. + [HarmonyPatch(typeof(data_girls.girls), "GetExpectedSalary_Total")] + public class data_girls_girls_GetExpectedSalary_Total + { + public static void Postfix(data_girls.girls __instance, ref long __result) + { + if (__instance == null || !staticVars.IsHard() || __instance.GetFameLevel() != 10) + return; + + float earning = __instance.GetAverageEarnings(); + if (float.IsNaN(earning) || float.IsInfinity(earning) || earning <= 0f) + return; + + long floor = (long)Mathf.Round(earning * MAXFAME_SALARY_EARN_COEFF); + if (floor > __result) + __result = floor; + } + } + + [HarmonyPatch(typeof(data_girls), "GenerateGirl")] + public class data_girls_GenerateGirl + { + public static void Postfix(ref data_girls.girls __result) + { + if (__result != null) + __result.salary = DEF_SALARY; + } + } +} diff --git a/mods/Worker Rights/Worker Rights.csproj b/mods/Worker Rights/Worker Rights.csproj index 888ab2b..45cbea8 100755 --- a/mods/Worker Rights/Worker Rights.csproj +++ b/mods/Worker Rights/Worker Rights.csproj @@ -1,25 +1,25 @@ - - - - Worker Rights - com.tel.workerrights - Employee rights! Idols will demand fair wages! Brand new staff cannot be blamed for scandals! - Tel - 1.0.0 - ["gameplay"] - - - 0 - - $(HarmonyID) - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - + + + + Worker Rights + com.tel.workerrights + Employee rights! Idols will demand fair wages! Brand new staff cannot be blamed for scandals! + Tel + 1.0.1 + ["gameplay"] + + + 0 + + $(HarmonyID) + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + From 0e3890d66ae8059f2251403f296c41440cbb87a3 Mon Sep 17 00:00:00 2001 From: ExSlam Date: Sat, 15 Aug 2026 13:57:47 -0400 Subject: [PATCH 18/33] Added a changelog.md --- changelog.md | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 changelog.md diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..d7985e6 --- /dev/null +++ b/changelog.md @@ -0,0 +1,76 @@ +# Going Viral 1.0.1 + +## Crash/data-corruption fixes + +- Removed the single-sales integer divide-by-zero path when a trending release has zero casual new fans. +- Reworked the save marker so the private trending value is consumed instead of being written into vanilla Buzz. New markers cannot collide with vanilla Buzz, old `10000 + trend` saves (including negative trends) are migrated, and loading a markerless save resets stale static trend state. +- Replaced hard-coded fan-tooltip child indices with named lines, preventing UI hierarchy changes from producing index errors/writing to the wrong line. + +## Math/gameplay fixes + +- Negative trending now uses a positive churn multiplier instead of a sign-flipping coefficient that could turn fan loss into fan gain. +- Fixed integer-division errors in TV trending chance calculations (`/20`, `/5`, and days/365). +- Removed the broken fame interpolation from fan-acquisition weights; new-fan distribution now follows non-negative appeal with the documented 3x casual weighting and ignores opinion. +- Fan churn uses non-negative appeal, clamped opinion, and a stable 3x casual weighting. Crisis strength scales the total churn instead of reversing or distorting the demographic weights. +- Singles and shows now apply the trending multiplier to total new fans, with the bonus allocated primarily to casual buckets, matching the mod description. +- Weekly tooltip totals now include cafe fans. +- TV genre tooltips now compute the most recent TV show per displayed genre button, not from the currently selected genre for every button. +- Trend/crisis duration notifications use the clamped saved duration; negative trends are discarded if Fan Attrition is not installed. + +## Compatibility hardening + +- Replaced the brittle show-sales IL-local transpiler with scoped patches around `SetSales`, `AddFans_Equally`, and `SetNewFans`. +- Replaced fan distribution IL-local transpilers with scoped Harmony contexts. Vanilla allocation/rounding remains in place while only weighting calls are substituted. +- Added exception-safe context cleanup and null guards throughout show, theater, tooltip, save/load, contract and marketing-roll hooks. +- Preserved explicit integration with `com.tel.fanattrition` and `com.tel.unofficialpatch`. + +--- +# Worker Rights 1.0.1 + +## Fixes + +- Fixed the low-salary graduation penalty: `DateTime.AddDays()` is immutable, so the original call discarded the new date. The returned date is now assigned. +- Applies the complete advertised 10x low-salary penalty (-10 or -30 days), because the supplied vanilla `Graduation_Date_Update()` has the same discarded-`AddDays` no-op for its own -1/-3 salary adjustment. +- Kept this adjustment on the game's existing weekly `Graduation_Date_Update()` cadence. It is not silently converted into a daily 7x balance change. +- The hard-mode max-fame 10%-of-earnings rule is now a salary floor and can no longer lower a larger vanilla/third-party expected salary. +- The low-fame expected salary is likewise enforced as a floor rather than lowering a larger result. +- Added null/invalid-value guards for staff, policies, generated idols and earnings. + +--- +# Going Viral 1.0.1 + + +## Crash/data-corruption fixes + +- Removed the single-sales integer divide-by-zero path when a trending release has zero casual new fans. +- Reworked the save marker so the private trending value is consumed instead of being written into vanilla Buzz. New markers cannot collide with vanilla Buzz, old `10000 + trend` saves (including negative trends) are migrated, and loading a markerless save resets stale static trend state. +- Replaced hard-coded fan-tooltip child indices with named lines, preventing UI hierarchy changes from producing index errors/writing to the wrong line. + +## Math/gameplay fixes + +- Negative trending now uses a positive churn multiplier instead of a sign-flipping coefficient that could turn fan loss into fan gain. +- Fixed integer-division errors in TV trending chance calculations (`/20`, `/5`, and days/365). +- Removed the broken fame interpolation from fan-acquisition weights; new-fan distribution now follows non-negative appeal with the documented 3x casual weighting and ignores opinion. +- Fan churn uses non-negative appeal, clamped opinion, and a stable 3x casual weighting. Crisis strength scales the total churn instead of reversing or distorting the demographic weights. +- Singles and shows now apply the trending multiplier to total new fans, with the bonus allocated primarily to casual buckets, matching the mod description. +- Weekly tooltip totals now include cafe fans. +- TV genre tooltips now compute the most recent TV show per displayed genre button, not from the currently selected genre for every button. +- Trend/crisis duration notifications use the clamped saved duration; negative trends are discarded if Fan Attrition is not installed. + +## Compatibility hardening + +- Replaced the brittle show-sales IL-local transpiler with scoped patches around `SetSales`, `AddFans_Equally`, and `SetNewFans`. +- Replaced fan distribution IL-local transpilers with scoped Harmony contexts. Vanilla allocation/rounding remains in place while only weighting calls are substituted. +- Added exception-safe context cleanup and null guards throughout show, theater, tooltip, save/load, contract and marketing-roll hooks. +- Preserved explicit integration with `com.tel.fanattrition` and `com.tel.unofficialpatch`. +*** + +# Never Graduate 1.2.0 + +- Harmony patches data_girls.girls.Graduation_Date_Update and always skips the original method. +- Graduation_Date values are therefore inert while Never Graduate is enabled. +- Worker Rights can no longer turn low salary satisfaction into an automatic graduation. +- Traits Expansion / Job Hopper can no longer turn its shortened default graduation date into an automatic graduation. +- Already-announced idols cannot complete date-driven graduation while the mod is enabled. +- Direct firing still works, preserving the original "unless the girl is fired" behavior. +--- \ No newline at end of file From 947f28915c8116e9e7ad8260dbe3b2e7ab4f6281 Mon Sep 17 00:00:00 2001 From: ExSlam Date: Sat, 15 Aug 2026 15:01:18 -0400 Subject: [PATCH 19/33] Made correctness and math fixes --- mods/Extended SSK/Extended SSK.cs | 11 +++-------- mods/Fan Attrition/Fan Attrition.cs | 2 +- mods/Growing Distant/Growing Distant.cs | 3 ++- mods/MBTI Personalities/MBTI Personalities.cs | 2 +- mods/ModMenus/ModMenus.cs | 2 +- mods/Traits Expansion/Traits Expansion.cs | 12 ++++++------ 6 files changed, 14 insertions(+), 18 deletions(-) diff --git a/mods/Extended SSK/Extended SSK.cs b/mods/Extended SSK/Extended SSK.cs index e450901..046ed33 100755 --- a/mods/Extended SSK/Extended SSK.cs +++ b/mods/Extended SSK/Extended SSK.cs @@ -69,7 +69,7 @@ public static int Infix(int i) [HarmonyPatch("RecalcFameBonus")] public static class SSK_RecalcFameBonusPatch { - static Func GetFameBaseValDelegate; + static readonly MethodInfo GetFameBaseValInfo = AccessTools.Method(typeof(SEvent_SSK._SSK), "GetFameBaseVal"); /// /// Postfix method to recalculate fame bonuses after the original method execution. @@ -90,13 +90,8 @@ public static void Postfix(ref SEvent_SSK._SSK __instance) int limit = int.Parse(variables.Get(varID) ?? defaultRankingsStr); List list = __instance.FameBonus; - if(GetFameBaseValDelegate == null) - { - MethodInfo GetFameBaseValInfo = AccessTools.Method(typeof(SEvent_SSK._SSK), "GetFameBaseVal"); - GetFameBaseValDelegate = AccessTools.MethodDelegate>(GetFameBaseValInfo, __instance); - } - - int num = Mathf.RoundToInt(GetFameBaseValDelegate() * 0.056f); + int fameBaseVal = (int)GetFameBaseValInfo.Invoke(__instance, null); + int num = Mathf.RoundToInt(fameBaseVal * 0.056f); for (int i = 10; i < Math.Min(girlCount, limit); i++) { list.Add(num); diff --git a/mods/Fan Attrition/Fan Attrition.cs b/mods/Fan Attrition/Fan Attrition.cs index 1c6d082..cd2d2d5 100755 --- a/mods/Fan Attrition/Fan Attrition.cs +++ b/mods/Fan Attrition/Fan Attrition.cs @@ -62,7 +62,7 @@ public static int Infix(Shows._show __this, int num6) { if (__this.mc != null) { - float mcCoeff = Mathf.Max(1f, 1f + __this.mc.fame * __this.mc.fame / 10); + float mcCoeff = Mathf.Max(1f, 1f + __this.mc.fame * __this.mc.fame / 10f); if (__this.mc.fame >= 10) { mcCoeff += MC_MAX_FAME_BONUS; diff --git a/mods/Growing Distant/Growing Distant.cs b/mods/Growing Distant/Growing Distant.cs index 2f5310b..b579949 100755 --- a/mods/Growing Distant/Growing Distant.cs +++ b/mods/Growing Distant/Growing Distant.cs @@ -19,6 +19,7 @@ public class data_girls_girls_UpdateRelationshipBasedOnSalary public const int SALARY_LOWER_THR = 50; public const int ROMANCE_PENALTY = -1; public const int FRIEND_PENALTY = -2; + public const int RELATIONSHIP_MAX_POINTS = 512; /// /// Harmony patch for the UpdateRelationshipBasedOnSalary method in data_girls.girls. @@ -31,7 +32,7 @@ public static void Postfix(ref data_girls.girls __instance) int salarySatisfaction_Percentage = __instance.GetSalarySatisfaction_Percentage(); if (salarySatisfaction_Percentage >= SALARY_UPPER_THR) { - __instance.Rel_Influence_Points += INFLUENCE_BONUS; + __instance.Rel_Influence_Points = Math.Min(RELATIONSHIP_MAX_POINTS, __instance.Rel_Influence_Points + INFLUENCE_BONUS); } else if (salarySatisfaction_Percentage <= SALARY_LOWER_THR && __instance.Rel_Influence_Points >= -INFLUENCE_PENALTYCRIT) { diff --git a/mods/MBTI Personalities/MBTI Personalities.cs b/mods/MBTI Personalities/MBTI Personalities.cs index ce0ce39..e90f20b 100755 --- a/mods/MBTI Personalities/MBTI Personalities.cs +++ b/mods/MBTI Personalities/MBTI Personalities.cs @@ -166,7 +166,7 @@ public static void Postfix(ref int __result) mBTI = GetGirlMBTI(_girls); if(mBTI == MBTI.ISTP) { - __result = Mathf.RoundToInt(ISTPBonus + __result / 100 * (1 - ISTPBonus)); + __result = Mathf.RoundToInt(__result + (100 - __result) * ISTPBonus); return; } } diff --git a/mods/ModMenus/ModMenus.cs b/mods/ModMenus/ModMenus.cs index 3e62eea..bd0686b 100755 --- a/mods/ModMenus/ModMenus.cs +++ b/mods/ModMenus/ModMenus.cs @@ -522,7 +522,7 @@ public static void AddMenuItems(Transform parentTransform) minValue = item[JSON_FIELD_MIN].AsFloat; maxValue = item[JSON_FIELD_MAX].AsFloat; } - float defaultFloat = maxValue + minValue / 2; + float defaultFloat = (maxValue + minValue) / 2f; if (!string.IsNullOrEmpty(item[JSON_FIELD_DEF])) { defaultFloat = item[JSON_FIELD_DEF].AsFloat; diff --git a/mods/Traits Expansion/Traits Expansion.cs b/mods/Traits Expansion/Traits Expansion.cs index 53788a8..0920167 100755 --- a/mods/Traits Expansion/Traits Expansion.cs +++ b/mods/Traits Expansion/Traits Expansion.cs @@ -180,7 +180,7 @@ public static void Postfix() if (member.IsSick()) continue; - if (member.trait == (traits._trait._type)NewTraits.Sadistic && clique.IsBullied(member)) + if (member.trait == (traits._trait._type)NewTraits.Sadistic && clique.IsBully(member)) { sadistic++; } @@ -377,15 +377,15 @@ public static void Prefix() [HarmonyPriority(Priority.VeryLow)] public static void Postfix(data_girls.girls _girl, ref float __result, business._proposal __instance) { - // Girls with Wooden Acting receive penalty + // Girls with Wooden Acting receive a 50% drama reward penalty. if (__instance.type == business._type.tv_drama && _girl.trait == (traits._trait._type)NewTraits.Wooden_Acting) { - __result -= WOODACTING_PENALTY; + __result *= 1f - WOODACTING_PENALTY; } - // Girls with Wooden Acting receive penalty + // Girls with Quick Wit receive a 50% variety reward bonus. else if (__instance.type == business._type.variety && _girl.trait == (traits._trait._type)NewTraits.Quick_Wit) { - __result += QUICKWIT_BONUS; + __result *= 1f + QUICKWIT_BONUS; } patchGetVal = false; } @@ -506,7 +506,7 @@ public class TraitsExpansion public const int HOMELY_PENALTY = 10; public const int TONEDEAF_PENALTY = 30; - public const float WOODACTING_PENALTY = 0.2f; + public const float WOODACTING_PENALTY = 0.5f; public const float QUICKWIT_BONUS = 0.5f; public const float RECKLESS_THR_UPPER = 60f; public const float RECKLESS_THR_LOWER = 5f; From c82b61e03a71e75fd95d6244e831122fe6fb7ae3 Mon Sep 17 00:00:00 2001 From: ExSlam Date: Sat, 15 Aug 2026 15:02:36 -0400 Subject: [PATCH 20/33] Fixed op codes and run order to properly apply multipliers --- mods/Stale Theater Shows/Stale Theater Shows.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/mods/Stale Theater Shows/Stale Theater Shows.cs b/mods/Stale Theater Shows/Stale Theater Shows.cs index 4b9ffd5..90c85f1 100755 --- a/mods/Stale Theater Shows/Stale Theater Shows.cs +++ b/mods/Stale Theater Shows/Stale Theater Shows.cs @@ -109,9 +109,17 @@ public static IEnumerable Transpiler(IEnumerable Date: Sat, 15 Aug 2026 15:05:18 -0400 Subject: [PATCH 21/33] Fixed Audition idol age exceeding set target limit by one year, made target age not affect Unique Idols, fixed target auditions affecting generation of rivals --- mods/Targeted Auditions/Targeted Auditions.cs | 99 ++++++++++++++----- .../assets/steam description.txt | 4 +- 2 files changed, 76 insertions(+), 27 deletions(-) diff --git a/mods/Targeted Auditions/Targeted Auditions.cs b/mods/Targeted Auditions/Targeted Auditions.cs index ef7167d..0928ee1 100755 --- a/mods/Targeted Auditions/Targeted Auditions.cs +++ b/mods/Targeted Auditions/Targeted Auditions.cs @@ -279,8 +279,9 @@ public class Auditions_GenerateGirls /// Prefix method to set audition parameters before generating girls. /// /// The instance of Auditions being patched. - public static void Prefix(Auditions __instance) + public static void Prefix(Auditions __instance, out bool __state) { + __state = false; LoadConfiguredAgeRange(); // Set sexual orientation @@ -308,11 +309,16 @@ public static void Prefix(Auditions __instance) // Set girl count __instance.NumberOfGirls = int.Parse(variables.Get(VARID_COUNT) ?? DEF_COUNT); - + BeginAuditionGeneration(); + __state = true; } - public static Exception Finalizer(Exception __exception) + public static Exception Finalizer(Exception __exception, bool __state) { + if (__state) + { + EndAuditionGeneration(); + } if (__exception != null) { Debug.LogError( @@ -332,15 +338,31 @@ public static Exception Finalizer(Exception __exception) public class data_girls_GenerateGirl { /// - /// Postfix method to set the sexuality of a generated girl. + /// Before an audition candidate is generated, allow body IDs to repeat only after + /// every currently eligible body ID has been used once in this audition. + /// + public static void Prefix(bool genTextures, data_girls_textures._textureAsset BodyAsset) + { + if (!IsGeneratingAudition || !genTextures || BodyAsset != null) + { + return; + } + + if (!HasUnusedEligibleBody()) + { + Auditions.UsedBodyIDs.Clear(); + } + } + + /// + /// Postfix method to set the sexuality of a generated audition candidate. /// /// The generated girl data. public static void Postfix(ref data_girls.girls __result) { - int girlCount = int.Parse(variables.Get(VARID_COUNT) ?? DEF_COUNT); - if (girlCount > 12) + if (!IsGeneratingAudition || __result == null) { - Auditions.UsedBodyIDs.Clear(); + return; } data_girls.girls._sexuality sexuality = data_girls.girls._sexuality.straight; @@ -398,6 +420,11 @@ public static IEnumerable Transpiler(IEnumerableA new list of assigned stat values. public static List Infix(List statValues) { + if (!IsGeneratingAudition) + { + return statValues; + } + List output = new(statValues); Dictionary priorityDictTemp = new(priorityDict); List remainingParamTypes = priorityDictTemp.Keys.ToList(); @@ -441,19 +468,10 @@ public class data_girls_girls_GenerateBirthday { public static void Postfix(ref data_girls.girls __instance) { - ApplyRandomBirthdayInConfiguredRange(__instance); - } - } - - /// - /// Loads Targeted Auditions settings before scripted unique-idol auditions generate candidates. - /// - [HarmonyPatch(typeof(Auditions), "CustomAudition", new Type[] { typeof(string) })] - public class Auditions_CustomAudition_String - { - public static void Prefix() - { - LoadConfiguredAgeRange(); + if (IsGeneratingAudition) + { + ApplyRandomBirthdayInConfiguredRange(__instance); + } } } @@ -566,6 +584,37 @@ public static bool IsInputValid(string ageRange) public static Dictionary priorityDict = new(); + private static int auditionGenerationDepth = 0; + public static bool IsGeneratingAudition => auditionGenerationDepth > 0; + + public static void BeginAuditionGeneration() + { + auditionGenerationDepth++; + } + + public static void EndAuditionGeneration() + { + if (auditionGenerationDepth > 0) + { + auditionGenerationDepth--; + } + } + + public static bool HasUnusedEligibleBody() + { + if (data_girls_textures.textureAssets == null) + { + return false; + } + + return data_girls_textures.textureAssets.Any(asset => + asset != null && + !asset.Add_To_Default && + asset.type == data_girls_textures._spriteType.body && + !Auditions.UsedBodyIDs.Contains(asset.body_id) && + asset.CanBeHired()); + } + public static void LoadConfiguredAgeRange() { // The former per-audition age popup is retired. Always use the Mod Menu range. @@ -593,11 +642,11 @@ public static void ApplyRandomBirthdayInConfiguredRange(data_girls.girls girl) return; } - DateTime dateTime = staticVars.dateTime - .AddYears(-maxAge - 1) - .AddYears(UnityEngine.Random.Range(0, maxAge - minAge + 1)) - .AddMonths(UnityEngine.Random.Range(0, 12)) - .AddDays(UnityEngine.Random.Range(0, 31)); + int age = UnityEngine.Random.Range(minAge, maxAge + 1); + DateTime latestBirthday = staticVars.dateTime.AddYears(-age); + DateTime earliestBirthday = staticVars.dateTime.AddYears(-age - 1).AddDays(1); + int possibleDays = (latestBirthday - earliestBirthday).Days + 1; + DateTime dateTime = earliestBirthday.AddDays(UnityEngine.Random.Range(0, possibleDays)); girl.SetBirthday(dateTime); } diff --git a/mods/Targeted Auditions/assets/steam description.txt b/mods/Targeted Auditions/assets/steam description.txt index 350dbe5..6651496 100755 --- a/mods/Targeted Auditions/assets/steam description.txt +++ b/mods/Targeted Auditions/assets/steam description.txt @@ -7,10 +7,10 @@ This mod was formerly called "Audition Age Limits". Using the ModMenu menu, you will be able to customise: [list] -[*][b]Age limits[/b]: Only idols within the desired age range will be selected. Does not apply to unique idols. +[*][b]Age limits[/b]: Generated audition candidates stay within the desired inclusive age range. Does not apply to unique idols or other non-audition girl generation. [*][b]Prioritised Skills[/b]: The higher the prioritisation, the higher the chance that the skill will be the girl's best skill. [*][b]Sexual Orientation[/b]: Increase to make girls easier to date if you are female or decrease to reduce in-group dating. -[*][b]Number of Candidates[/b]: Have any number of candidates per audition. Click and drag to scroll through cards in the audition. +[*][b]Number of Candidates[/b]: Have any number of candidates per audition. Body IDs stay unique until the available audition body pool is exhausted, then repeat as needed. Click and drag to scroll through cards in the audition. [/list] [url=https://github.com/ui3TD/Tel-Mod-Library]source code[/url] \ No newline at end of file From 210339fbf0af807fe707d0285c116eded6afb79c Mon Sep 17 00:00:00 2001 From: ExSlam Date: Sat, 15 Aug 2026 15:05:26 -0400 Subject: [PATCH 22/33] Updated changelog --- changelog.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index d7985e6..b8334a8 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,4 @@ + # Going Viral 1.0.1 ## Crash/data-corruption fixes @@ -73,4 +74,17 @@ - Traits Expansion / Job Hopper can no longer turn its shortened default graduation date into an automatic graduation. - Already-announced idols cannot complete date-driven graduation while the mod is enabled. - Direct firing still works, preserving the original "unless the girl is fired" behavior. ---- \ No newline at end of file +--- + +## Correctness fixes + +- **MBTI Personalities**: fixed ISTP concert accident handling so the trait halves the remaining failure chance while keeping AccidentSuccessChance in 0-100 percentage units. +- **Extended SSK**: fame bonuses for ranks beyond 10 now read GetFameBaseVal from the current election instance instead of a delegate permanently bound to the first election. +- **Fan Attrition**: restored floating-point division in the MC fame coefficient so fame 1-3 and other low-fame values receive the intended quadratic boost. +- **Growing Distant**: salary-based positive influence is now capped at the vanilla 512-point relationship maximum, preventing hidden overflow points that delay later decay. +- **ModMenus**: corrected the omitted slider default to the arithmetic midpoint, (min + max) / 2. +- **Stale Theater Shows**: repaired the attendance transpiler branch target so normal ticket-price execution passes through the custom attendance multiplier. +- **Traits Expansion**: Sadistic now detects active bullies rather than bullied victims, and Wooden Acting / Quick Wit now apply true -50% / +50% multiplicative business reward modifiers. +- **Targeted Auditions**: settings are now scoped to Auditions.GenerateGirls, preventing age/stat/sexuality settings from leaking into rival, story, unique, or other non-audition girl generation. +- **Targeted Auditions**: body IDs remain unique until the currently eligible audition body pool is exhausted; only then are IDs recycled for larger candidate counts. +- **Targeted Auditions**: birthday generation now produces ages exactly within the configured inclusive minimum/maximum range, eliminating the max-age + 1 boundary case. \ No newline at end of file From 2cbb7e308ad4f6892e987d80a6eb6aa0d53b53fa Mon Sep 17 00:00:00 2001 From: ExSlam Date: Sat, 15 Aug 2026 15:15:41 -0400 Subject: [PATCH 23/33] Fixed an incorrect reference to properly use Harmony for accessing the game's textureAssets --- mods/Targeted Auditions/Targeted Auditions.cs | 1313 +++++++++-------- 1 file changed, 659 insertions(+), 654 deletions(-) diff --git a/mods/Targeted Auditions/Targeted Auditions.cs b/mods/Targeted Auditions/Targeted Auditions.cs index 0928ee1..e9fad68 100755 --- a/mods/Targeted Auditions/Targeted Auditions.cs +++ b/mods/Targeted Auditions/Targeted Auditions.cs @@ -1,654 +1,659 @@ -using HarmonyLib; -using System; -using System.Collections.Generic; -using System.Reflection.Emit; -using UnityEngine; -using UnityEngine.UI; -using System.Linq; -using static CustomAuditions.CustomAuditions; - -namespace CustomAuditions -{ - - /// - /// Patches the Popup_Audition class to allow scrolling cards in the audition popup. - /// - // Set up audition popup to allow scrolling cards - [HarmonyPatch(typeof(Popup_Audition), "Start")] - public class Popup_Audition_Start - { - - /// - /// Sets the properties of a RectTransform. - /// - /// The RectTransform to modify. - /// The minimum anchor point. - /// The maximum anchor point. - /// The minimum offset. - /// The maximum offset. - private static void SetRectTransform(RectTransform rt, Vector2 anchorMin, Vector2 anchorMax, Vector2 offsetMin, Vector2 offsetMax) - { - rt.anchorMin = anchorMin; - rt.anchorMax = anchorMax; - rt.offsetMin = offsetMin; - rt.offsetMax = offsetMax; - } - - /// - /// Postfix method to set up the scrollable audition popup. - /// - /// The instance of Popup_Audition being patched. - public static void Postfix(Popup_Audition __instance) - { - if (__instance == null || __instance.Cards_Container == null) - return; - - Transform currentParent = __instance.Cards_Container.transform.parent; - if (currentParent == null) - return; - - if (currentParent.GetComponent() != null) - return; - - // Create ScrollRect container and attach to panel - GameObject scrollContainer = new(AUD_SCROLLRECT_NAME, typeof(RectTransform), typeof(ScrollRect)); - RectTransform scrollRectTransform = scrollContainer.GetComponent(); - SetRectTransform(scrollRectTransform, Vector2.zero, Vector2.one, Vector2.zero, Vector2.zero); - - // Configure the ScrollRect - ScrollRect scrollRect = scrollContainer.GetComponent(); - scrollRect.content = __instance.Cards_Container.GetComponent(); // attach content - scrollRect.viewport = scrollRectTransform; - scrollRect.vertical = false; - scrollRect.horizontal = true; - scrollRect.movementType = ScrollRect.MovementType.Elastic; - scrollRect.elasticity = 0.1f; - scrollRect.inertia = false; - scrollRect.scrollSensitivity = 20; - - // Configure hierarchy - scrollContainer.transform.SetParent(currentParent, false); - __instance.Cards_Container.transform.SetParent(scrollContainer.transform, false); - - // Reuse existing fitter if one exists to avoid duplicate component warnings. - ContentSizeFitter fitter = __instance.Cards_Container.GetComponent(); - if (fitter == null) - { - fitter = __instance.Cards_Container.AddComponent(); - } - fitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize; - } - } - - /// - /// Tracks audition popup loading from the moment Set begins. - /// Starting before the original Set keeps the watchdog state aligned with - /// Assistant Manager's delayed per-manager audition handoff. - /// - [HarmonyPatch(typeof(Popup_Audition), "Set", new Type[] { typeof(Auditions.data), typeof(bool) })] - public class Popup_Audition_Set - { - public static void Prefix(Popup_Audition __instance) - { - if (__instance == null) - { - return; - } - - auditionLoadStartedAt[__instance.GetInstanceID()] = Time.unscaledTime; - } - - public static Exception Finalizer(Popup_Audition __instance, Exception __exception) - { - if (__exception == null) - { - return null; - } - - if (__instance != null) - { - auditionLoadStartedAt.Remove(__instance.GetInstanceID()); - } - - Debug.LogError( - "[Targeted Auditions] Popup_Audition.Set failed:\n" + - __exception); - - // Preserve the original exception. - return __exception; - } - } - - /// - /// Clears load watchdog state when audition popup is reset. - /// - [HarmonyPatch(typeof(Popup_Audition), "Reset")] - public class Popup_Audition_Reset - { - /// - /// Postfix method that removes stale watchdog entries. - /// - /// Popup instance. - public static void Postfix(Popup_Audition __instance) - { - if (__instance == null) - { - return; - } - - auditionLoadStartedAt.Remove(__instance.GetInstanceID()); - } - } - - /// - /// Clears load watchdog state when audition popup is closed. - /// - [HarmonyPatch(typeof(Popup_Audition), "Close")] - public class Popup_Audition_Close - { - /// - /// Prefix method that removes stale watchdog entries before close logic runs. - /// - /// Popup instance. - public static void Prefix(Popup_Audition __instance) - { - if (__instance == null) - { - return; - } - - auditionLoadStartedAt.Remove(__instance.GetInstanceID()); - } - } - - /// - /// Prevents recruitment popup deadlocks when one portrait never resolves. - /// - [HarmonyPatch(typeof(Popup_Audition), "PortraitsLoaded")] - public class Popup_Audition_PortraitsLoaded - { - /// - /// Postfix method that applies a timeout fallback for stuck portrait loading. - /// - /// Popup instance. - /// Original readiness result. - public static void Postfix(Popup_Audition __instance, ref bool __result) - { - if (__result || __instance == null || __instance.Cards_Container == null) - { - return; - } - - int popupId = __instance.GetInstanceID(); - if (!auditionLoadStartedAt.TryGetValue(popupId, out float startedAt)) - { - return; - } - - float elapsed = Time.unscaledTime - startedAt; - if (elapsed < PORTRAIT_LOAD_TIMEOUT_SECONDS) - { - return; - } - - // The vanilla coroutine waits indefinitely for all portraits. With large candidate counts this can - // deadlock the popup (blur shown, cards never become interactive). After timeout, continue anyway. - EnsurePopupIsVisible(__instance); - Sprite fallbackSprite = FindFallbackPortraitSprite(__instance); - bool missingPortraits = FillMissingPortraits(__instance, fallbackSprite); - if (missingPortraits) - { - Debug.Log("[Targeted Auditions] Portrait load timed out. Continuing with fallback portraits."); - } - - __result = true; - } - - private static void EnsurePopupIsVisible(Popup_Audition popup) - { - CanvasGroup cg = popup.GetComponent(); - if (cg != null) - { - cg.alpha = 1f; - cg.blocksRaycasts = true; - cg.interactable = true; - } - - RectTransform rt = popup.GetComponent(); - if (rt != null) - { - rt.localScale = Vector3.one; - } - } - - private static Sprite FindFallbackPortraitSprite(Popup_Audition popup) - { - foreach (Transform child in popup.Cards_Container.transform) - { - Audition_Closed_Card closedCard = child.GetComponent(); - if (closedCard == null || closedCard.Portrait == null) - { - continue; - } - - Image image = closedCard.Portrait.GetComponent(); - if (image != null && image.sprite != null) - { - return image.sprite; - } - } - - return null; - } - - private static bool FillMissingPortraits(Popup_Audition popup, Sprite fallback) - { - bool hadMissing = false; - foreach (Transform child in popup.Cards_Container.transform) - { - Audition_Closed_Card closedCard = child.GetComponent(); - if (closedCard == null || closedCard.Portrait == null) - { - continue; - } - - Image image = closedCard.Portrait.GetComponent(); - if (image == null || image.sprite != null) - { - continue; - } - - hadMissing = true; - if (fallback != null) - { - image.sprite = fallback; - } - } - - return hadMissing; - } - } - - /// - /// Patches the Auditions class to set variables at the start of an audition. - /// - [HarmonyPatch(typeof(Auditions), "GenerateGirls")] - public class Auditions_GenerateGirls - { - /// - /// Prefix method to set audition parameters before generating girls. - /// - /// The instance of Auditions being patched. - public static void Prefix(Auditions __instance, out bool __state) - { - __state = false; - LoadConfiguredAgeRange(); - - // Set sexual orientation - float varLesbian = float.Parse(variables.Get(VARID_LESCHANCE) ?? DEF_CHANCE_LES_STR); - float varBi = float.Parse(variables.Get(VARID_BICHANCE) ?? DEF_CHANCE_BI_STR); - - if (varLesbian + varBi > 100) - { - varLesbian = Mathf.Floor(varLesbian / (varLesbian + varBi) * 100); - varBi = 100 - varLesbian; - variables.Set(VARID_LESCHANCE, varLesbian.ToString()); - variables.Set(VARID_BICHANCE, varBi.ToString()); - } - chanceLesbian = (int)varLesbian; - chanceBi = (int)Mathf.Floor(varBi / (100 - chanceLesbian) * 100); - - - // Set stat priorities - foreach (data_girls._paramType param in paramTypes) - { - int value = int.Parse(variables.Get($"{VARID_PRIO_PREFIX}{param}") ?? DEF_PRIO); - priorityDict[param] = value; - } - - // Set girl count - __instance.NumberOfGirls = int.Parse(variables.Get(VARID_COUNT) ?? DEF_COUNT); - - BeginAuditionGeneration(); - __state = true; - } - - public static Exception Finalizer(Exception __exception, bool __state) - { - if (__state) - { - EndAuditionGeneration(); - } - if (__exception != null) - { - Debug.LogError( - "[Targeted Auditions] Auditions.GenerateGirls failed:\n" + - __exception); - } - - // Preserve the original exception. - return __exception; - } - } - - /// - /// Patches the data_girls class to apply girl sexuality. - /// - [HarmonyPatch(typeof(data_girls), "GenerateGirl")] - public class data_girls_GenerateGirl - { - /// - /// Before an audition candidate is generated, allow body IDs to repeat only after - /// every currently eligible body ID has been used once in this audition. - /// - public static void Prefix(bool genTextures, data_girls_textures._textureAsset BodyAsset) - { - if (!IsGeneratingAudition || !genTextures || BodyAsset != null) - { - return; - } - - if (!HasUnusedEligibleBody()) - { - Auditions.UsedBodyIDs.Clear(); - } - } - - /// - /// Postfix method to set the sexuality of a generated audition candidate. - /// - /// The generated girl data. - public static void Postfix(ref data_girls.girls __result) - { - if (!IsGeneratingAudition || __result == null) - { - return; - } - - data_girls.girls._sexuality sexuality = data_girls.girls._sexuality.straight; - if (mainScript.chance(chanceLesbian)) - { - sexuality = data_girls.girls._sexuality.lesbian; - } - else if (mainScript.chance(chanceBi)) - { - sexuality = data_girls.girls._sexuality.bi; - } - __result.sexuality = sexuality; - } - } - - /// - /// Patches the data_girls class to apply custom girl stats. - /// - [HarmonyPatch(typeof(data_girls), "GenerateParams")] - public static class data_girls_GenerateParams - { - /// - /// Transpiler method to modify the IL code for generating girl parameters. - /// - /// The original IL instructions. - /// The modified IL instructions. - public static IEnumerable Transpiler(IEnumerable instructions) - { - List instructionList = new(instructions); - - int index = -1; - for (int i = 0; i < instructionList.Count - 1; i++) - { - if (instructionList[i].opcode == OpCodes.Ldloc_1 && instructionList[i + 1].opcode == OpCodes.Call) - { - index = i + 1; - break; - } - } - - if (index != -1) - { - instructionList.Insert(index + 1, new CodeInstruction(OpCodes.Ldloc_1)); - instructionList.Insert(index + 2, new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(data_girls_GenerateParams), "Infix"))); - instructionList.Insert(index + 3, new CodeInstruction(OpCodes.Stloc_1)); - } - - return instructionList.AsEnumerable(); - } - - /// - /// Infix method to assign stat values based on priorities. - /// - /// The list of stat values to assign. - /// A new list of assigned stat values. - public static List Infix(List statValues) - { - if (!IsGeneratingAudition) - { - return statValues; - } - - List output = new(statValues); - Dictionary priorityDictTemp = new(priorityDict); - List remainingParamTypes = priorityDictTemp.Keys.ToList(); - - List sortedStatValues = statValues.OrderByDescending(v => v).ToList(); - - // Assign stats based on priority - foreach (int statValue in sortedStatValues) - { - - // Calculate total priority - int totalPriority = remainingParamTypes.Sum(p => priorityDictTemp[p]); - - // Roll a random number - int roll = UnityEngine.Random.Range(1, totalPriority + 1); - - // Find which param "wins" this roll - int cumulativePriority = 0; - for (int i = 0; i < remainingParamTypes.Count; i++) - { - cumulativePriority += priorityDict[remainingParamTypes[i]]; - if (roll <= cumulativePriority) - { - output[paramTypes.IndexOf(remainingParamTypes[i])] = statValue; - remainingParamTypes.RemoveAt(i); - break; - } - } - } - - return output; - } - - } - - /// - /// Patches the data_girls.girls class to apply age limits. - /// - [HarmonyPatch(typeof(data_girls.girls), "GenerateBirthday")] - public class data_girls_girls_GenerateBirthday - { - public static void Postfix(ref data_girls.girls __instance) - { - if (IsGeneratingAudition) - { - ApplyRandomBirthdayInConfiguredRange(__instance); - } - } - } - - /// - /// Contains utility methods and variables for custom auditions. - /// - class CustomAuditions - { - public const string DEF_MINAGE_STR = "12"; - public const string DEF_MAXAGE_STR = "23"; - public const string DEF_CHANCE_LES_STR = "7"; - public const string DEF_CHANCE_BI_STR = "14"; - public const string DEF_PRIO = "50"; - public const string DEF_COUNT = "5"; - - public const string VARID_MINAGE = "AuditionAgeLimit_MinAge"; - public const string VARID_MAXAGE = "AuditionAgeLimit_MaxAge"; - public const string VARID_BICHANCE = "CustomAudition_Bi"; - public const string VARID_LESCHANCE = "CustomAudition_Gay"; - public const string VARID_PRIO_PREFIX = "CustomAudition_Prio_"; - public const string VARID_COUNT = "CustomAudition_Count"; - - - public const string AUD_SCROLLRECT_NAME = "ScrollContainer"; - public const float PORTRAIT_LOAD_TIMEOUT_SECONDS = 6f; - - public const string VARID_AGELIMIT_POPUP_TOGGLE = "AuditionAgeLimit_TogglePopup"; - public const string DEF_AGELIMIT_POPUP_TOGGLE = "0"; - - public static int defaultMinAge = 12; - public static int defaultMaxAge = 23; - public static int minAge = defaultMinAge; - public static int maxAge = defaultMaxAge; - - public static int chanceLesbian = 7; - public static int chanceBi = 14; - - public static bool agePopup = false; - public static bool inputValid = false; - public static Dictionary auditionLoadStartedAt = new(); - - /// - /// Parses the age range string and sets the minAge and maxAge values. - /// - /// The age range string to parse. - public static void ParseAgeRange(string ageRange) - { - if (IsInputValid(ageRange)) - { - string[] ageLimits = ageRange.Split('-'); - minAge = int.Parse(ageLimits[0].Trim()); - maxAge = int.Parse(ageLimits[1].Trim()); - } - else - { - minAge = defaultMinAge; - maxAge = defaultMaxAge; - } - } - - /// - /// Validates the input age range string. - /// - /// The age range string to validate. - /// True if the input is valid, false otherwise. - public static bool IsInputValid(string ageRange) - { - - string[] ageLimits = ageRange.Split('-'); - - if (ageLimits == null || ageLimits.Length != 2) - { - return false; - } - - if (!int.TryParse(ageLimits[0].Trim(), out int min)) - { - return false; - } - if (!int.TryParse(ageLimits[1].Trim(), out int max)) - { - return false; - } - - if (max < 1 || min < 1) - { - return false; - } - - if (max < min) - { - return false; - } - - return true; - } - - - public static List paramTypes = new() - { - data_girls._paramType.cute, - data_girls._paramType.cool, - data_girls._paramType.sexy, - data_girls._paramType.pretty, - data_girls._paramType.vocal, - data_girls._paramType.dance, - data_girls._paramType.funny, - data_girls._paramType.smart - }; - - public static Dictionary priorityDict = new(); - - private static int auditionGenerationDepth = 0; - public static bool IsGeneratingAudition => auditionGenerationDepth > 0; - - public static void BeginAuditionGeneration() - { - auditionGenerationDepth++; - } - - public static void EndAuditionGeneration() - { - if (auditionGenerationDepth > 0) - { - auditionGenerationDepth--; - } - } - - public static bool HasUnusedEligibleBody() - { - if (data_girls_textures.textureAssets == null) - { - return false; - } - - return data_girls_textures.textureAssets.Any(asset => - asset != null && - !asset.Add_To_Default && - asset.type == data_girls_textures._spriteType.body && - !Auditions.UsedBodyIDs.Contains(asset.body_id) && - asset.CanBeHired()); - } - - public static void LoadConfiguredAgeRange() - { - // The former per-audition age popup is retired. Always use the Mod Menu range. - variables.Set(VARID_AGELIMIT_POPUP_TOGGLE, DEF_AGELIMIT_POPUP_TOGGLE); - - minAge = int.Parse(variables.Get(VARID_MINAGE) ?? DEF_MINAGE_STR); - maxAge = int.Parse(variables.Get(VARID_MAXAGE) ?? DEF_MAXAGE_STR); - if (maxAge < minAge) - { - int originalMinAge = minAge; - minAge = maxAge; - maxAge = originalMinAge; - - defaultMaxAge = maxAge; - defaultMinAge = minAge; - variables.Set(VARID_MAXAGE, maxAge.ToString()); - variables.Set(VARID_MINAGE, minAge.ToString()); - } - } - - public static void ApplyRandomBirthdayInConfiguredRange(data_girls.girls girl) - { - if (girl == null) - { - return; - } - - int age = UnityEngine.Random.Range(minAge, maxAge + 1); - DateTime latestBirthday = staticVars.dateTime.AddYears(-age); - DateTime earliestBirthday = staticVars.dateTime.AddYears(-age - 1).AddDays(1); - int possibleDays = (latestBirthday - earliestBirthday).Days + 1; - DateTime dateTime = earliestBirthday.AddDays(UnityEngine.Random.Range(0, possibleDays)); - girl.SetBirthday(dateTime); - } - - } -} +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Reflection.Emit; +using UnityEngine; +using UnityEngine.UI; +using System.Linq; +using static CustomAuditions.CustomAuditions; + +namespace CustomAuditions +{ + + /// + /// Patches the Popup_Audition class to allow scrolling cards in the audition popup. + /// + // Set up audition popup to allow scrolling cards + [HarmonyPatch(typeof(Popup_Audition), "Start")] + public class Popup_Audition_Start + { + + /// + /// Sets the properties of a RectTransform. + /// + /// The RectTransform to modify. + /// The minimum anchor point. + /// The maximum anchor point. + /// The minimum offset. + /// The maximum offset. + private static void SetRectTransform(RectTransform rt, Vector2 anchorMin, Vector2 anchorMax, Vector2 offsetMin, Vector2 offsetMax) + { + rt.anchorMin = anchorMin; + rt.anchorMax = anchorMax; + rt.offsetMin = offsetMin; + rt.offsetMax = offsetMax; + } + + /// + /// Postfix method to set up the scrollable audition popup. + /// + /// The instance of Popup_Audition being patched. + public static void Postfix(Popup_Audition __instance) + { + if (__instance == null || __instance.Cards_Container == null) + return; + + Transform currentParent = __instance.Cards_Container.transform.parent; + if (currentParent == null) + return; + + if (currentParent.GetComponent() != null) + return; + + // Create ScrollRect container and attach to panel + GameObject scrollContainer = new(AUD_SCROLLRECT_NAME, typeof(RectTransform), typeof(ScrollRect)); + RectTransform scrollRectTransform = scrollContainer.GetComponent(); + SetRectTransform(scrollRectTransform, Vector2.zero, Vector2.one, Vector2.zero, Vector2.zero); + + // Configure the ScrollRect + ScrollRect scrollRect = scrollContainer.GetComponent(); + scrollRect.content = __instance.Cards_Container.GetComponent(); // attach content + scrollRect.viewport = scrollRectTransform; + scrollRect.vertical = false; + scrollRect.horizontal = true; + scrollRect.movementType = ScrollRect.MovementType.Elastic; + scrollRect.elasticity = 0.1f; + scrollRect.inertia = false; + scrollRect.scrollSensitivity = 20; + + // Configure hierarchy + scrollContainer.transform.SetParent(currentParent, false); + __instance.Cards_Container.transform.SetParent(scrollContainer.transform, false); + + // Reuse existing fitter if one exists to avoid duplicate component warnings. + ContentSizeFitter fitter = __instance.Cards_Container.GetComponent(); + if (fitter == null) + { + fitter = __instance.Cards_Container.AddComponent(); + } + fitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize; + } + } + + /// + /// Tracks audition popup loading from the moment Set begins. + /// Starting before the original Set keeps the watchdog state aligned with + /// Assistant Manager's delayed per-manager audition handoff. + /// + [HarmonyPatch(typeof(Popup_Audition), "Set", new Type[] { typeof(Auditions.data), typeof(bool) })] + public class Popup_Audition_Set + { + public static void Prefix(Popup_Audition __instance) + { + if (__instance == null) + { + return; + } + + auditionLoadStartedAt[__instance.GetInstanceID()] = Time.unscaledTime; + } + + public static Exception Finalizer(Popup_Audition __instance, Exception __exception) + { + if (__exception == null) + { + return null; + } + + if (__instance != null) + { + auditionLoadStartedAt.Remove(__instance.GetInstanceID()); + } + + Debug.LogError( + "[Targeted Auditions] Popup_Audition.Set failed:\n" + + __exception); + + // Preserve the original exception. + return __exception; + } + } + + /// + /// Clears load watchdog state when audition popup is reset. + /// + [HarmonyPatch(typeof(Popup_Audition), "Reset")] + public class Popup_Audition_Reset + { + /// + /// Postfix method that removes stale watchdog entries. + /// + /// Popup instance. + public static void Postfix(Popup_Audition __instance) + { + if (__instance == null) + { + return; + } + + auditionLoadStartedAt.Remove(__instance.GetInstanceID()); + } + } + + /// + /// Clears load watchdog state when audition popup is closed. + /// + [HarmonyPatch(typeof(Popup_Audition), "Close")] + public class Popup_Audition_Close + { + /// + /// Prefix method that removes stale watchdog entries before close logic runs. + /// + /// Popup instance. + public static void Prefix(Popup_Audition __instance) + { + if (__instance == null) + { + return; + } + + auditionLoadStartedAt.Remove(__instance.GetInstanceID()); + } + } + + /// + /// Prevents recruitment popup deadlocks when one portrait never resolves. + /// + [HarmonyPatch(typeof(Popup_Audition), "PortraitsLoaded")] + public class Popup_Audition_PortraitsLoaded + { + /// + /// Postfix method that applies a timeout fallback for stuck portrait loading. + /// + /// Popup instance. + /// Original readiness result. + public static void Postfix(Popup_Audition __instance, ref bool __result) + { + if (__result || __instance == null || __instance.Cards_Container == null) + { + return; + } + + int popupId = __instance.GetInstanceID(); + if (!auditionLoadStartedAt.TryGetValue(popupId, out float startedAt)) + { + return; + } + + float elapsed = Time.unscaledTime - startedAt; + if (elapsed < PORTRAIT_LOAD_TIMEOUT_SECONDS) + { + return; + } + + // The vanilla coroutine waits indefinitely for all portraits. With large candidate counts this can + // deadlock the popup (blur shown, cards never become interactive). After timeout, continue anyway. + EnsurePopupIsVisible(__instance); + Sprite fallbackSprite = FindFallbackPortraitSprite(__instance); + bool missingPortraits = FillMissingPortraits(__instance, fallbackSprite); + if (missingPortraits) + { + Debug.Log("[Targeted Auditions] Portrait load timed out. Continuing with fallback portraits."); + } + + __result = true; + } + + private static void EnsurePopupIsVisible(Popup_Audition popup) + { + CanvasGroup cg = popup.GetComponent(); + if (cg != null) + { + cg.alpha = 1f; + cg.blocksRaycasts = true; + cg.interactable = true; + } + + RectTransform rt = popup.GetComponent(); + if (rt != null) + { + rt.localScale = Vector3.one; + } + } + + private static Sprite FindFallbackPortraitSprite(Popup_Audition popup) + { + foreach (Transform child in popup.Cards_Container.transform) + { + Audition_Closed_Card closedCard = child.GetComponent(); + if (closedCard == null || closedCard.Portrait == null) + { + continue; + } + + Image image = closedCard.Portrait.GetComponent(); + if (image != null && image.sprite != null) + { + return image.sprite; + } + } + + return null; + } + + private static bool FillMissingPortraits(Popup_Audition popup, Sprite fallback) + { + bool hadMissing = false; + foreach (Transform child in popup.Cards_Container.transform) + { + Audition_Closed_Card closedCard = child.GetComponent(); + if (closedCard == null || closedCard.Portrait == null) + { + continue; + } + + Image image = closedCard.Portrait.GetComponent(); + if (image == null || image.sprite != null) + { + continue; + } + + hadMissing = true; + if (fallback != null) + { + image.sprite = fallback; + } + } + + return hadMissing; + } + } + + /// + /// Patches the Auditions class to set variables at the start of an audition. + /// + [HarmonyPatch(typeof(Auditions), "GenerateGirls")] + public class Auditions_GenerateGirls + { + /// + /// Prefix method to set audition parameters before generating girls. + /// + /// The instance of Auditions being patched. + public static void Prefix(Auditions __instance, out bool __state) + { + __state = false; + LoadConfiguredAgeRange(); + + // Set sexual orientation + float varLesbian = float.Parse(variables.Get(VARID_LESCHANCE) ?? DEF_CHANCE_LES_STR); + float varBi = float.Parse(variables.Get(VARID_BICHANCE) ?? DEF_CHANCE_BI_STR); + + if (varLesbian + varBi > 100) + { + varLesbian = Mathf.Floor(varLesbian / (varLesbian + varBi) * 100); + varBi = 100 - varLesbian; + variables.Set(VARID_LESCHANCE, varLesbian.ToString()); + variables.Set(VARID_BICHANCE, varBi.ToString()); + } + chanceLesbian = (int)varLesbian; + chanceBi = (int)Mathf.Floor(varBi / (100 - chanceLesbian) * 100); + + + // Set stat priorities + foreach (data_girls._paramType param in paramTypes) + { + int value = int.Parse(variables.Get($"{VARID_PRIO_PREFIX}{param}") ?? DEF_PRIO); + priorityDict[param] = value; + } + + // Set girl count + __instance.NumberOfGirls = int.Parse(variables.Get(VARID_COUNT) ?? DEF_COUNT); + + BeginAuditionGeneration(); + __state = true; + } + + public static Exception Finalizer(Exception __exception, bool __state) + { + if (__state) + { + EndAuditionGeneration(); + } + if (__exception != null) + { + Debug.LogError( + "[Targeted Auditions] Auditions.GenerateGirls failed:\n" + + __exception); + } + + // Preserve the original exception. + return __exception; + } + } + + /// + /// Patches the data_girls class to apply girl sexuality. + /// + [HarmonyPatch(typeof(data_girls), "GenerateGirl")] + public class data_girls_GenerateGirl + { + /// + /// Before an audition candidate is generated, allow body IDs to repeat only after + /// every currently eligible body ID has been used once in this audition. + /// + public static void Prefix(bool genTextures, data_girls_textures._textureAsset BodyAsset) + { + if (!IsGeneratingAudition || !genTextures || BodyAsset != null) + { + return; + } + + if (!HasUnusedEligibleBody()) + { + Auditions.UsedBodyIDs.Clear(); + } + } + + /// + /// Postfix method to set the sexuality of a generated audition candidate. + /// + /// The generated girl data. + public static void Postfix(ref data_girls.girls __result) + { + if (!IsGeneratingAudition || __result == null) + { + return; + } + + data_girls.girls._sexuality sexuality = data_girls.girls._sexuality.straight; + if (mainScript.chance(chanceLesbian)) + { + sexuality = data_girls.girls._sexuality.lesbian; + } + else if (mainScript.chance(chanceBi)) + { + sexuality = data_girls.girls._sexuality.bi; + } + __result.sexuality = sexuality; + } + } + + /// + /// Patches the data_girls class to apply custom girl stats. + /// + [HarmonyPatch(typeof(data_girls), "GenerateParams")] + public static class data_girls_GenerateParams + { + /// + /// Transpiler method to modify the IL code for generating girl parameters. + /// + /// The original IL instructions. + /// The modified IL instructions. + public static IEnumerable Transpiler(IEnumerable instructions) + { + List instructionList = new(instructions); + + int index = -1; + for (int i = 0; i < instructionList.Count - 1; i++) + { + if (instructionList[i].opcode == OpCodes.Ldloc_1 && instructionList[i + 1].opcode == OpCodes.Call) + { + index = i + 1; + break; + } + } + + if (index != -1) + { + instructionList.Insert(index + 1, new CodeInstruction(OpCodes.Ldloc_1)); + instructionList.Insert(index + 2, new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(data_girls_GenerateParams), "Infix"))); + instructionList.Insert(index + 3, new CodeInstruction(OpCodes.Stloc_1)); + } + + return instructionList.AsEnumerable(); + } + + /// + /// Infix method to assign stat values based on priorities. + /// + /// The list of stat values to assign. + /// A new list of assigned stat values. + public static List Infix(List statValues) + { + if (!IsGeneratingAudition) + { + return statValues; + } + + List output = new(statValues); + Dictionary priorityDictTemp = new(priorityDict); + List remainingParamTypes = priorityDictTemp.Keys.ToList(); + + List sortedStatValues = statValues.OrderByDescending(v => v).ToList(); + + // Assign stats based on priority + foreach (int statValue in sortedStatValues) + { + + // Calculate total priority + int totalPriority = remainingParamTypes.Sum(p => priorityDictTemp[p]); + + // Roll a random number + int roll = UnityEngine.Random.Range(1, totalPriority + 1); + + // Find which param "wins" this roll + int cumulativePriority = 0; + for (int i = 0; i < remainingParamTypes.Count; i++) + { + cumulativePriority += priorityDict[remainingParamTypes[i]]; + if (roll <= cumulativePriority) + { + output[paramTypes.IndexOf(remainingParamTypes[i])] = statValue; + remainingParamTypes.RemoveAt(i); + break; + } + } + } + + return output; + } + + } + + /// + /// Patches the data_girls.girls class to apply age limits. + /// + [HarmonyPatch(typeof(data_girls.girls), "GenerateBirthday")] + public class data_girls_girls_GenerateBirthday + { + public static void Postfix(ref data_girls.girls __instance) + { + if (IsGeneratingAudition) + { + ApplyRandomBirthdayInConfiguredRange(__instance); + } + } + } + + /// + /// Contains utility methods and variables for custom auditions. + /// + class CustomAuditions + { + public const string DEF_MINAGE_STR = "12"; + public const string DEF_MAXAGE_STR = "23"; + public const string DEF_CHANCE_LES_STR = "7"; + public const string DEF_CHANCE_BI_STR = "14"; + public const string DEF_PRIO = "50"; + public const string DEF_COUNT = "5"; + + public const string VARID_MINAGE = "AuditionAgeLimit_MinAge"; + public const string VARID_MAXAGE = "AuditionAgeLimit_MaxAge"; + public const string VARID_BICHANCE = "CustomAudition_Bi"; + public const string VARID_LESCHANCE = "CustomAudition_Gay"; + public const string VARID_PRIO_PREFIX = "CustomAudition_Prio_"; + public const string VARID_COUNT = "CustomAudition_Count"; + + + public const string AUD_SCROLLRECT_NAME = "ScrollContainer"; + public const float PORTRAIT_LOAD_TIMEOUT_SECONDS = 6f; + + public const string VARID_AGELIMIT_POPUP_TOGGLE = "AuditionAgeLimit_TogglePopup"; + public const string DEF_AGELIMIT_POPUP_TOGGLE = "0"; + + public static int defaultMinAge = 12; + public static int defaultMaxAge = 23; + public static int minAge = defaultMinAge; + public static int maxAge = defaultMaxAge; + + public static int chanceLesbian = 7; + public static int chanceBi = 14; + + public static bool agePopup = false; + public static bool inputValid = false; + public static Dictionary auditionLoadStartedAt = new(); + + /// + /// Parses the age range string and sets the minAge and maxAge values. + /// + /// The age range string to parse. + public static void ParseAgeRange(string ageRange) + { + if (IsInputValid(ageRange)) + { + string[] ageLimits = ageRange.Split('-'); + minAge = int.Parse(ageLimits[0].Trim()); + maxAge = int.Parse(ageLimits[1].Trim()); + } + else + { + minAge = defaultMinAge; + maxAge = defaultMaxAge; + } + } + + /// + /// Validates the input age range string. + /// + /// The age range string to validate. + /// True if the input is valid, false otherwise. + public static bool IsInputValid(string ageRange) + { + + string[] ageLimits = ageRange.Split('-'); + + if (ageLimits == null || ageLimits.Length != 2) + { + return false; + } + + if (!int.TryParse(ageLimits[0].Trim(), out int min)) + { + return false; + } + if (!int.TryParse(ageLimits[1].Trim(), out int max)) + { + return false; + } + + if (max < 1 || min < 1) + { + return false; + } + + if (max < min) + { + return false; + } + + return true; + } + + + public static List paramTypes = new() + { + data_girls._paramType.cute, + data_girls._paramType.cool, + data_girls._paramType.sexy, + data_girls._paramType.pretty, + data_girls._paramType.vocal, + data_girls._paramType.dance, + data_girls._paramType.funny, + data_girls._paramType.smart + }; + + public static Dictionary priorityDict = new(); + + private static int auditionGenerationDepth = 0; + public static bool IsGeneratingAudition => auditionGenerationDepth > 0; + + public static void BeginAuditionGeneration() + { + auditionGenerationDepth++; + } + + public static void EndAuditionGeneration() + { + if (auditionGenerationDepth > 0) + { + auditionGenerationDepth--; + } + } + + private static readonly System.Reflection.FieldInfo textureAssetsField = + AccessTools.Field(typeof(data_girls_textures), "textureAssets"); + + public static bool HasUnusedEligibleBody() + { + List textureAssets = + textureAssetsField?.GetValue(null) as List; + if (textureAssets == null) + { + return false; + } + + return textureAssets.Any(asset => + asset != null && + !asset.Add_To_Default && + asset.type == data_girls_textures._spriteType.body && + !Auditions.UsedBodyIDs.Contains(asset.body_id) && + asset.CanBeHired()); + } + + public static void LoadConfiguredAgeRange() + { + // The former per-audition age popup is retired. Always use the Mod Menu range. + variables.Set(VARID_AGELIMIT_POPUP_TOGGLE, DEF_AGELIMIT_POPUP_TOGGLE); + + minAge = int.Parse(variables.Get(VARID_MINAGE) ?? DEF_MINAGE_STR); + maxAge = int.Parse(variables.Get(VARID_MAXAGE) ?? DEF_MAXAGE_STR); + if (maxAge < minAge) + { + int originalMinAge = minAge; + minAge = maxAge; + maxAge = originalMinAge; + + defaultMaxAge = maxAge; + defaultMinAge = minAge; + variables.Set(VARID_MAXAGE, maxAge.ToString()); + variables.Set(VARID_MINAGE, minAge.ToString()); + } + } + + public static void ApplyRandomBirthdayInConfiguredRange(data_girls.girls girl) + { + if (girl == null) + { + return; + } + + int age = UnityEngine.Random.Range(minAge, maxAge + 1); + DateTime latestBirthday = staticVars.dateTime.AddYears(-age); + DateTime earliestBirthday = staticVars.dateTime.AddYears(-age - 1).AddDays(1); + int possibleDays = (latestBirthday - earliestBirthday).Days + 1; + DateTime dateTime = earliestBirthday.AddDays(UnityEngine.Random.Range(0, possibleDays)); + girl.SetBirthday(dateTime); + } + + } +} From 56fc6d627982bd029820ab1a6f934c870723f48d Mon Sep 17 00:00:00 2001 From: ExSlam Date: Sat, 15 Aug 2026 16:39:45 -0400 Subject: [PATCH 24/33] included cafeFans in fan change, excluded internet shows --- mods/Fan Attrition/Fan Attrition.cs | 5 +++++ mods/Fan Attrition/FanTooltip.cs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/mods/Fan Attrition/Fan Attrition.cs b/mods/Fan Attrition/Fan Attrition.cs index cd2d2d5..4cc95bf 100755 --- a/mods/Fan Attrition/Fan Attrition.cs +++ b/mods/Fan Attrition/Fan Attrition.cs @@ -123,6 +123,11 @@ public static IEnumerable Transpiler(IEnumerableThe modified fan count. public static float Infix(Shows._show __this, float num2) { + if (__this.medium != null && __this.medium.media_type == Shows._param._media_type.internet) + { + return num2; + } + if (staticVars.IsHard()) { num2 *= 1f - __this.GetFatigue() * __this.GetFatigue() / SHOW_FATIGUE_COEFF_HARD; diff --git a/mods/Fan Attrition/FanTooltip.cs b/mods/Fan Attrition/FanTooltip.cs index 044fe5b..ae715fa 100755 --- a/mods/Fan Attrition/FanTooltip.cs +++ b/mods/Fan Attrition/FanTooltip.cs @@ -303,7 +303,7 @@ public class tooltip_fans_RenderFanChange public static void Postfix(tooltip_fans __instance) { long baseChange = resources.FansChange * 7; - long totalChange = adFans + dramaFans + netFans + tvFans + radioFans + baseChange; + long totalChange = adFans + dramaFans + netFans + tvFans + radioFans + cafeFans + baseChange; string changeStr = ExtensionMethods.formatNumber(totalChange, false, false) + " " + Language.Data["PER_WEEK"]; if (totalChange > 0) From 19f442e2738d0ddc13d8dad2ad3bd19fc71a1497 Mon Sep 17 00:00:00 2001 From: ExSlam Date: Sat, 15 Aug 2026 16:54:16 -0400 Subject: [PATCH 25/33] Fixed a clamp value for tv medium trend to 15% instead of 100% clap (could have become 37% in best case scenario if not clamped below that) --- mods/Going Viral/TrendingManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mods/Going Viral/TrendingManager.cs b/mods/Going Viral/TrendingManager.cs index b942094..84496d4 100755 --- a/mods/Going Viral/TrendingManager.cs +++ b/mods/Going Viral/TrendingManager.cs @@ -193,7 +193,7 @@ public static float GetTrendingChance(Shows._show show) int days = Math.Max(0, (staticVars.dateTime - lastShowDate.Value).Days); daysSinceCoeff = Math.Min(365, days) / 365f; } - return Mathf.Clamp(15f * daysSinceCoeff * fameCoeff * levelCoeff, 0f, 100f); + return Mathf.Clamp(15f * daysSinceCoeff * fameCoeff * levelCoeff, 0f, 15f); } public static long GetTrendingMagnitude(Shows._show show) From cd89321a37a9d6ddf1b9585377353a999e39b7af Mon Sep 17 00:00:00 2001 From: ExSlam Date: Sat, 15 Aug 2026 17:09:44 -0400 Subject: [PATCH 26/33] Made TV fame progression more balanced. Assuming cast, MC, and genre are approximately equal: | Fame/genre | 3 months | 6 months | 1 year | | ---------: | -------: | -------: | --------: | | 3 | ~1.1% | ~2.2% | 4.5% | | 5 | ~1.9% | ~3.7% | 7.5% | | 7 | ~2.6% | ~5.2% | 10.5% | | 8 | ~3.0% | ~6.0% | 12.0% | | 10 | ~3.7% | ~7.5% | **15.0%** | --- mods/Going Viral/TrendingManager.cs | 49 ++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/mods/Going Viral/TrendingManager.cs b/mods/Going Viral/TrendingManager.cs index 84496d4..d2edde3 100755 --- a/mods/Going Viral/TrendingManager.cs +++ b/mods/Going Viral/TrendingManager.cs @@ -147,32 +147,49 @@ public static float GetTrendingChance(Shows._show show) if (show == null || show.genre == null || show.medium == null || show.medium.media_type != Shows._param._media_type.tv) return 0f; - float fameCoeff = 0f; - if (show.fame != null && show.fame.Count > 0) - fameCoeff += show.fame[0] / 20f; - - float fameSum = 0f; - int count = 0; + // Normalize cast fame from vanilla's 0-10 fame scale to 0-1. + float castCoeff = 0f; if (show.castType == Shows._show._castType.entireGroup) { - fameSum = resources.GetFameLevel(); - count = 1; + castCoeff = Mathf.Clamp01(resources.GetFameLevel() / 10f); } else if (show.girls != null) { + float fameSum = 0f; + int count = 0; foreach (data_girls.girls girl in show.girls) { - if (girl == null) continue; + if (girl == null) + continue; + fameSum += girl.GetFameLevel(); count++; } + + if (count > 0) + castCoeff = Mathf.Clamp01((fameSum / count) / 10f); } - if (count > 0) - fameCoeff += (fameSum / count) / 20f; + + // Normalize genre level from the vanilla 0-10 level scale to 0-1. + float genreCoeff = Mathf.Clamp01(show.genre.GetLevel() / 10f); + + // Combine the applicable quality factors instead of multiplying them. + // This mirrors Going Viral's single-trend design: build one normalized + // quality score first, then apply the maximum trend chance. + float qualityTotal = castCoeff + genreCoeff; + int qualityFactors = 2; + + // Only count MC fame when the show actually has an MC. A show without + // an MC therefore isn't automatically penalized by a zero-valued factor. if (show.mc != null) - fameCoeff += show.mc.fame / 20f; + { + float mcCoeff = Mathf.Clamp01(show.mc.fame / 10f); + qualityTotal += mcCoeff; + qualityFactors++; + } + + float qualityCoeff = qualityTotal / qualityFactors; - float levelCoeff = show.genre.GetLevel() / 5f + 0.5f; DateTime? lastShowDate = null; if (Shows.shows != null) { @@ -187,13 +204,17 @@ public static float GetTrendingChance(Shows._show show) } } + // No previous TV show of this genre means maximum freshness. float daysSinceCoeff = 1f; if (lastShowDate.HasValue) { int days = Math.Max(0, (staticVars.dateTime - lastShowDate.Value).Days); daysSinceCoeff = Math.Min(365, days) / 365f; } - return Mathf.Clamp(15f * daysSinceCoeff * fameCoeff * levelCoeff, 0f, 15f); + + // Both coefficients are bounded to 0-1, so 15f is now a true maximum + // rather than a base multiplier that can balloon above 15%. + return Mathf.Clamp(15f * daysSinceCoeff * qualityCoeff, 0f, 15f); } public static long GetTrendingMagnitude(Shows._show show) From d9dd4a09aff9602f6f0303ced9b673f67dc32aaa Mon Sep 17 00:00:00 2001 From: ExSlam Date: Sat, 15 Aug 2026 18:20:37 -0400 Subject: [PATCH 27/33] Fixed ModMenus button injection to detect when the setting tab is selected and reduced lag --- mods/ModMenus/ModMenus.cs | 635 +++++++++++++++++--------------------- 1 file changed, 283 insertions(+), 352 deletions(-) diff --git a/mods/ModMenus/ModMenus.cs b/mods/ModMenus/ModMenus.cs index bd0686b..6ea6104 100755 --- a/mods/ModMenus/ModMenus.cs +++ b/mods/ModMenus/ModMenus.cs @@ -13,128 +13,28 @@ namespace ModMenus { /// - /// Ensures the mod menu popup is available when the game starts. + /// Lazily installs ModMenus only when the player opens the in-game Settings tab. + /// This intentionally does no work on the main menu or while a game is loading. /// - [HarmonyPatch(typeof(PopupManager), "Start")] - public class PopupManager_Start - { - /// - /// Postfix method that generates the mod menu popup. - /// - public static void Postfix() - { - GenerateMenuPopup(); - ModMenusBootstrap.EnsureButtonInstalled(); - } - } + [HarmonyPatch(typeof(Tabs_Manager), nameof(Tabs_Manager.OpenTab))] + public class Tabs_Manager_OpenTab + { + public static void Postfix(Tabs_Manager._tab._type __0) + { + if (__0 != Tabs_Manager._tab._type.settings || !ModMenusUtils.IsGameplayReady()) + { + return; + } - /// - /// Integrates the mod menu access point into the game's existing UI. - /// - [HarmonyPatch(typeof(Tabs_Manager), "Awake")] - public class Tabs_Manager_Awake - { - /// - /// Postfix method that adds a mod menu button to the settings panel. - /// - public static void Postfix() - { - ModMenusBootstrap.EnsureButtonInstalled(); - } - } - - /// - /// Re-runs button install when opening settings tab to survive late UI rebuilds. - /// - [HarmonyPatch(typeof(Tabs_Manager), nameof(Tabs_Manager.OpenTab))] - public class Tabs_Manager_OpenTab - { - public static void Postfix(Tabs_Manager._tab._type __0) - { - if (__0 == Tabs_Manager._tab._type.settings) - { - ModMenusBootstrap.EnsureButtonInstalled(); - } - } - } - - /// - /// Retries Mod Settings button installation until settings UI hierarchy is ready. - /// - public sealed class ModMenusBootstrap : MonoBehaviour - { - private const int MaxInstallAttempts = 240; - private const float RetryIntervalSeconds = 0.10f; - private const string LogPrefix = "[ModMenus] "; - private static ModMenusBootstrap instance; - private int attempts; - private float nextAttemptAt; - - public static void EnsureButtonInstalled() - { - if (ModMenusUtils.TryInstallSettingsButton()) - { - Debug.Log(LogPrefix + "Mod Settings button installed."); - DestroyInstance(); - return; - } - - if (instance != null) - { - return; - } - - Camera camera = Camera.main; - if (camera == null) - { - return; - } - - instance = camera.gameObject.GetComponent(); - if (instance == null) - { - instance = camera.gameObject.AddComponent(); - } - - instance.attempts = 0; - instance.nextAttemptAt = Time.unscaledTime; - } - - private static void DestroyInstance() - { - if (instance == null) - { - return; - } - - ModMenusBootstrap cached = instance; - instance = null; - if (cached != null) - { - UnityEngine.Object.Destroy(cached); - } - } - - private void Update() - { - if (Time.unscaledTime < nextAttemptAt) - { - return; - } - - nextAttemptAt = Time.unscaledTime + RetryIntervalSeconds; - attempts++; - if (ModMenusUtils.TryInstallSettingsButton() || attempts >= MaxInstallAttempts) - { - if (attempts >= MaxInstallAttempts) - { - Debug.LogWarning(LogPrefix + "Failed to install Mod Settings button after retries."); - } - - DestroyInstance(); - } - } - } + // The Settings hierarchy is active by the time this postfix runs, so install + // the button once without any background retry loop. The heavier mod-menu + // popup is generated only if the player actually clicks Mod Settings. + if (!ModMenusUtils.TryInstallSettingsButton()) + { + Debug.LogWarning("[ModMenus] Could not install the Mod Settings button in the active in-game Settings tab."); + } + } + } /// /// Utility class containing methods for creating and managing mod menu elements. @@ -178,238 +78,269 @@ class ModMenusUtils public const string MENUCONTENT_OBJ_NAME = "MenuContainer"; public const string SCROLLHANDLE_OBJ_NAME = "VerticalHandle"; public const string SCROLLBAR_OBJ_NAME = "VerticalScrollBar"; - public const string SCROLLRECT_OBJ_NAME = "ScrollContainer"; - public const string VIEWPORT_OBJ_NAME = "Viewport"; - - /// - /// Installs or repairs the Mod Settings button on the settings tab. - /// - public static bool TryInstallSettingsButton() - { - mainScript main = Camera.main != null ? Camera.main.GetComponent() : null; - if (main == null || main.Data == null) - { - return false; - } - - Tabs_Manager tabsManager = main.Data.GetComponent(); - if (tabsManager == null) - { - return false; - } - - Tabs_Manager._tab settingsTab = tabsManager.GetTab(Tabs_Manager._tab._type.settings); - if (settingsTab == null || settingsTab.Tab == null) - { - return false; - } - - Transform settingsContainer = FindSettingsContainer(settingsTab.Tab.transform); - if (settingsContainer == null) - { - return false; - } - - Transform existingButton = FindNamedChild(settingsTab.Tab.transform, BUTTON_OBJ_NAME); - if (existingButton != null) - { - ConfigureModMenuButton(existingButton.gameObject); - return true; - } - - GameObject templateButton = FindButtonTemplate(settingsContainer); - if (templateButton == null) - { - templateButton = FindButtonTemplate(settingsTab.Tab.transform); - if (templateButton == null || templateButton.transform.parent == null) - { - return false; - } - - settingsContainer = templateButton.transform.parent; - } - - GameObject modMenuButton = CloneButton(templateButton, settingsContainer, BUTTON_OBJ_NAME, BUTTON_LABEL, false, false); - if (modMenuButton == null) - { - return false; - } - - int maxIndex = Mathf.Max(0, settingsContainer.childCount - 1); - int targetIndex = Mathf.Clamp(settingsContainer.childCount - 2, 0, maxIndex); - modMenuButton.transform.SetSiblingIndex(targetIndex); - ConfigureModMenuButton(modMenuButton); - - RectTransform settingsRect = settingsContainer as RectTransform; - if (settingsRect != null) - { - LayoutRebuilder.ForceRebuildLayoutImmediate(settingsRect); - } - - return true; - } - - /// - /// Finds the settings list container using strict path first, then robust fallback. - /// - private static Transform FindSettingsContainer(Transform settingsRoot) - { - if (settingsRoot == null) - { - return null; - } - - Transform container = settingsRoot.Find("ScrollRect/Container"); - if (container != null) - { - return container; - } - - Transform[] descendants = settingsRoot.GetComponentsInChildren(true); - for (int i = 0; i < descendants.Length; i++) - { - Transform candidate = descendants[i]; - if (candidate == null) - { - continue; - } - - if (!string.Equals(candidate.name, "Container", StringComparison.Ordinal)) - { - continue; - } - - if (candidate.GetComponent() != null || candidate.GetComponent() != null) - { - return candidate; - } - } - - Button[] buttons = settingsRoot.GetComponentsInChildren