From da4e95f15c52f2e456a52247a768661a78b501dd Mon Sep 17 00:00:00 2001 From: SinZ Date: Wed, 20 Mar 2024 17:44:19 +1100 Subject: [PATCH] Add a bunch of new mods --- SinZational Scene Setup/ModEntry.cs | 32 ++ SinZational Scene Setup/manifest.json | 2 +- SinZational Science Showcase/ModEntry.cs | 120 +++++++ .../SinZational Science Showcase.csproj | 18 + SinZational Science Showcase/cp/content.json | 14 + SinZational Science Showcase/manifest.json | 16 + SinZational Several Spouse Spots/ModEntry.cs | 310 +++++++++++++++++ .../SinZational Several Spouse Spots.csproj | 20 ++ .../assets/baseSpousePatio.pdn | Bin 0 -> 8094 bytes .../assets/baseSpousePatio.png | Bin 0 -> 1928 bytes SinZational Shared Spaces/ModEntry.cs | 319 ++++++++++++++++++ .../SinZational Shared Spaces.csproj | 22 ++ .../assets/elevatorButton.png | Bin 0 -> 421 bytes SinZational Spending Services/ModEntry.cs | 55 +++ .../SinZational Spending Services.csproj | 26 ++ SinZational Sporadic Shifts/ModEntry.cs | 33 ++ .../SinZational Sporadic Shifts.csproj | 21 ++ SinZationalSeleneSupport.Support/LuaMod.cs | 18 + .../SinZationalSeleneSupport.Support.csproj | 17 + SinZationalSeleneSupport/LuaCompatManifest.cs | 46 +++ SinZationalSeleneSupport/ModEntry.cs | 197 +++++++++++ .../SinZationalSeleneSupport.csproj | 27 ++ SinZationalSeleneSupport/manifest.json | 17 + SinZationalSixSupport/ModEntry.cs | 42 ++- StardewMods.sln | 38 ++- 25 files changed, 1392 insertions(+), 18 deletions(-) create mode 100644 SinZational Science Showcase/ModEntry.cs create mode 100644 SinZational Science Showcase/SinZational Science Showcase.csproj create mode 100644 SinZational Science Showcase/cp/content.json create mode 100644 SinZational Science Showcase/manifest.json create mode 100644 SinZational Several Spouse Spots/ModEntry.cs create mode 100644 SinZational Several Spouse Spots/SinZational Several Spouse Spots.csproj create mode 100644 SinZational Several Spouse Spots/assets/baseSpousePatio.pdn create mode 100644 SinZational Several Spouse Spots/assets/baseSpousePatio.png create mode 100644 SinZational Shared Spaces/ModEntry.cs create mode 100644 SinZational Shared Spaces/SinZational Shared Spaces.csproj create mode 100644 SinZational Shared Spaces/assets/elevatorButton.png create mode 100644 SinZational Spending Services/ModEntry.cs create mode 100644 SinZational Spending Services/SinZational Spending Services.csproj create mode 100644 SinZational Sporadic Shifts/ModEntry.cs create mode 100644 SinZational Sporadic Shifts/SinZational Sporadic Shifts.csproj create mode 100644 SinZationalSeleneSupport.Support/LuaMod.cs create mode 100644 SinZationalSeleneSupport.Support/SinZationalSeleneSupport.Support.csproj create mode 100644 SinZationalSeleneSupport/LuaCompatManifest.cs create mode 100644 SinZationalSeleneSupport/ModEntry.cs create mode 100644 SinZationalSeleneSupport/SinZationalSeleneSupport.csproj create mode 100644 SinZationalSeleneSupport/manifest.json diff --git a/SinZational Scene Setup/ModEntry.cs b/SinZational Scene Setup/ModEntry.cs index 9b1fe36..5527e16 100644 --- a/SinZational Scene Setup/ModEntry.cs +++ b/SinZational Scene Setup/ModEntry.cs @@ -5,6 +5,7 @@ using StardewValley.Menus; using System; using System.Collections.Generic; +using xTile.Dimensions; namespace SinZational_Scene_Setup { @@ -15,6 +16,7 @@ public class ModEntry : Mod public override void Entry(IModHelper helper) { helper.Events.GameLoop.UpdateTicked += GameLoop_UpdateTicked; + helper.Events.GameLoop.DayStarted += GameLoop_DayStarted; helper.ConsoleCommands.Add("sinz.playevents", "Auto plays events in the current location. If arguments are given is treated as a specific, or all if location is 'ALL'", (command, args) => { @@ -49,6 +51,36 @@ public override void Entry(IModHelper helper) }); } + private void GameLoop_DayStarted(object sender, StardewModdingAPI.Events.DayStartedEventArgs e) + { + foreach (var location in Game1.locations) + { + Dictionary events; + try + { + events = Helper.GameContent.Load>($"Data/Events/{location.Name}"); + } + catch (ContentLoadException) + { + Monitor.Log($"Location {location.Name} does not have events?", LogLevel.Info); + continue; + } + Monitor.Log($"Location {location.Name} has {events.Count} events", LogLevel.Info); + foreach (var key in events.Keys) + { + var split = key.Split('/'); + if (split.Length < 2) continue; + var eventId = split[0]; + var args = split[1..]; + Monitor.Log("Testing event " + key); + foreach (var arg in args) + { + Event.CheckPrecondition(location, eventId, arg); + } + } + } + } + private void GameLoop_UpdateTicked(object sender, StardewModdingAPI.Events.UpdateTickedEventArgs e) { if (!Context.IsWorldReady) return; diff --git a/SinZational Scene Setup/manifest.json b/SinZational Scene Setup/manifest.json index 3990745..0b646ea 100644 --- a/SinZational Scene Setup/manifest.json +++ b/SinZational Scene Setup/manifest.json @@ -10,7 +10,7 @@ "UniqueID": "SinZ.SceneSetup", "Name": "SinZational Scene Setup", "Author": "SinZ", - "Version": "0.0.1", + "Version": "0.0.1-Debug", "Description": "A mod that exposes commands to autoplay events for a given or all locations", "MinimumApiVersion": "3.18", "EntryDll": "SinZational Scene Setup.dll" diff --git a/SinZational Science Showcase/ModEntry.cs b/SinZational Science Showcase/ModEntry.cs new file mode 100644 index 0000000..d7c0b63 --- /dev/null +++ b/SinZational Science Showcase/ModEntry.cs @@ -0,0 +1,120 @@ +using HarmonyLib; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI; +using StardewValley; +using StardewValley.ItemTypeDefinitions; +using StardewValley.Objects; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection.Emit; + +namespace SinZational_Science_Showcase +{ + public class ModEntry : Mod + { + public override void Entry(IModHelper helper) + { + //helper.Events.Input.ButtonReleased += Input_ButtonReleased; + var harmony = new Harmony(ModManifest.UniqueID); + harmony.Patch(AccessTools.Method(typeof(Hat), nameof(Hat.draw)), transpiler: new HarmonyMethod(typeof(HarmonyPatches).GetMethod(nameof(HarmonyPatches.Hat__Draw__Transpiler)))); + } + + /* + private void Input_ButtonReleased(object sender, StardewModdingAPI.Events.ButtonReleasedEventArgs e) + { + if (!Context.IsWorldReady) return; + if (Game1.activeClickableMenu != null) return; + if (e.Button != SButton.F5) return; + + var coop = Game1.getFarm().buildings.Where(b => b.buildingType.Value == "Deluxe Coop").First(); + var coopInterior = coop.indoors.Value as AnimalHouse; + var autoGrabber = coopInterior.objects.Values.Where(o => o.QualifiedItemId == "(BC)165").First(); + var autoGrabberStorage = autoGrabber.heldObject.Value as Chest; + var dayCounter = 0; + var season = Game1.season; + do + { + Monitor.Log($"Day: {Game1.stats.DaysPlayed} ({season} {Game1.dayOfMonth}), Duck Feather count: {autoGrabberStorage.Items.CountId("(O)444")}, Rabbits Feet count: {autoGrabberStorage.Items.CountId("(O)446")}", LogLevel.Info); + Game1.stats.DaysPlayed++; + Game1.dayOfMonth++; + if (Game1.dayOfMonth > 28) + { + Game1.dayOfMonth = 1; + season++; + if (season > Season.Winter) + { + season = Season.Spring; + } + } + Game1.getFarm().tryToAddHay(20); + foreach (var animal in coopInterior.animals.Values) + { + animal.wasPet.Value = true; + // Eating outside + if (season != Season.Winter) + { + animal.friendshipTowardFarmer.Value += 8; + } + } + coopInterior.DayUpdate((int)((Game1.stats.DaysPlayed % 28) + 1)); + if (dayCounter++ > 1000) + { + Monitor.Log("After 1000 simulations, did not show??", LogLevel.Warn); + autoGrabber.checkForAction(Game1.player); + break; + } + } + while (autoGrabberStorage.Items.CountId("(O)444") == 0); + } + */ + + } + + public static class HarmonyPatches + { + public static IEnumerable Hat__Draw__Transpiler(ILGenerator generator, IEnumerable instructions) + { + var output = new List(); + var skipCount = 0; + foreach (var instruction in instructions) + { + if (skipCount-- > 0) continue; + /* + * Replace the following + * IL_001f: ldsfld class StardewValley.LocalizedContentManager StardewValley.Game1::content + * IL_0024: ldstr "Characters\\Farmer\\hats_animals" + * IL_0029: callvirt instance !!0 StardewValley.LocalizedContentManager::Load(string) + * with + * ldloc_0 + * call HarmonyPatches::Hat__Draw__GetAnimalHat + */ + if (instruction.opcode == OpCodes.Ldsfld) + { + var load = new CodeInstruction(OpCodes.Ldloc_0) + { + labels = instruction.labels + }; + output.Add(load); + output.Add(new CodeInstruction(OpCodes.Call, typeof(HarmonyPatches).GetMethod(nameof(Hat__Draw__GetAnimalHat)))); + skipCount = 2; + continue; + } + output.Add(instruction); + } + return output; + } + + public static Texture2D Hat__Draw__GetAnimalHat(ParsedItemData itemData) + { + var rawData = itemData.RawData as string[]; + ArgUtility.TryGetOptional(rawData, 8, out var textureName, out _, itemData.TextureName); + if (textureName == "Characters\\Farmer\\hats") + { + textureName = "Characters\\Farmer\\hats_animals"; + } + return Game1.content.Load(textureName); + } + } +} diff --git a/SinZational Science Showcase/SinZational Science Showcase.csproj b/SinZational Science Showcase/SinZational Science Showcase.csproj new file mode 100644 index 0000000..05606c9 --- /dev/null +++ b/SinZational Science Showcase/SinZational Science Showcase.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + SinZational_Science_Showcase + true + C:\Users\acerP\Downloads\depotdownloader-2.4.7\stardew1.6-beta + + + + + Always + + + + + + diff --git a/SinZational Science Showcase/cp/content.json b/SinZational Science Showcase/cp/content.json new file mode 100644 index 0000000..18e0751 --- /dev/null +++ b/SinZational Science Showcase/cp/content.json @@ -0,0 +1,14 @@ +{ + "Format": "1.28.0", + "Changes": [ + { + "Action": "EditData", + "Target": "Data/Achievements", + "Fields": { + "1": { + "0": "Achievement name" + } + } + } + ] +} \ No newline at end of file diff --git a/SinZational Science Showcase/manifest.json b/SinZational Science Showcase/manifest.json new file mode 100644 index 0000000..5dac1b9 --- /dev/null +++ b/SinZational Science Showcase/manifest.json @@ -0,0 +1,16 @@ +{ + "Name": "SinZational Science Showcase", + "Author": "SinZ", + "Version": "0.1.0-alpha", + "Description": "Visualizes Schedule information on the map to assist NPC creators", + "UniqueID": "SinZ.Science", + "EntryDll": "SinZational Science Showcase.dll", + "MinimumApiVersion": "3.0.0", + "UpdateKeys": [ "Nexus:-1" ], + "Dependencies": [ + { + "UniqueID": "Pathoschild.ContentPatcher", + "IsRequired": false + } + ] +} \ No newline at end of file diff --git a/SinZational Several Spouse Spots/ModEntry.cs b/SinZational Several Spouse Spots/ModEntry.cs new file mode 100644 index 0000000..9850ddc --- /dev/null +++ b/SinZational Several Spouse Spots/ModEntry.cs @@ -0,0 +1,310 @@ +using HarmonyLib; +using Microsoft.Xna.Framework; +using StardewModdingAPI; +using StardewModdingAPI.Events; +using StardewValley; +using StardewValley.Buildings; +using StardewValley.Extensions; +using StardewValley.GameData.Buildings; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Threading; + +namespace SeveralSpouseSpots; + +public class ModEntry : Mod +{ + public const string SpousePatioBuildingId = "SinZ.SpousePatio"; + public const string SpousePatioAllocationKey = "SinZ.SpousePatio.Allocation"; + + public static ModEntry Instance; + + public static Dictionary SpousePatioAllocations { get; } = new(); + public static Dictionary SpousePatioStandingSpots { get; } = new(); + + public override void Entry(IModHelper helper) + { + Instance = this; + var harmony = new Harmony(ModManifest.UniqueID); + HarmonyPatches.Setup(Monitor, harmony); + helper.Events.Content.AssetRequested += Content_AssetRequested; + helper.Events.World.BuildingListChanged += World_BuildingListChanged; + } + + public void RecalculateSpousePatioAllocations() + { + var spousePatioCount = 0; + SpousePatioAllocations.Clear(); + SpousePatioStandingSpots.Clear(); + var spouses = Game1.getAllFarmers().Where(f => f != Game1.MasterPlayer).Select(f => f.spouse).Where(s => s != null); + + var unallocatedPatios = new List(); + var unallocatedSpouses = new List(spouses); + + // TODO: Handle divorce + + Utility.ForEachBuilding(b => + { + if (b.buildingType.Value != SpousePatioBuildingId) return true; + + spousePatioCount++; + if (b.modData.TryGetValue(SpousePatioAllocationKey, out string allocation)) + { + SpousePatioAllocations[allocation] = b; + unallocatedSpouses.Remove(allocation); + } + else + { + unallocatedPatios.Add(b); + } + if (SpousePatioAllocations.Count == spouses.Count()) return false; + return true; + }); + while (unallocatedPatios.Count > 0 && unallocatedSpouses.Count > 0) + { + var patio = unallocatedPatios.First(); + unallocatedPatios.RemoveAt(0); + var spouse = unallocatedSpouses.First(); + unallocatedSpouses.RemoveAt(0); + patio.modData[SpousePatioAllocationKey] = spouse; + SpousePatioAllocations[spouse] = patio; + } + // Force Farm to be reloaded + Monitor.Log("Invalidating Farm: " + Game1.getFarm().Map.assetPath, LogLevel.Alert); + Helper.GameContent.InvalidateCache(Game1.getFarm().Map.assetPath); + } + + public void ApplyMapModifications() + { + // Code is based on Farm.addSpouseOutdoorArea + foreach (var (spouse, building) in SpousePatioAllocations) + { + var npc = Game1.getCharacterFromName(spouse, true); + if (npc == null) continue; + var patioData = npc.GetData()?.SpousePatio; + if (patioData == null) continue; + + string assetName = patioData.MapAsset ?? "spousePatios"; + Rectangle sourceArea = patioData.MapSourceRect; + int width = Math.Min(sourceArea.Width, 4); + int height = Math.Min(sourceArea.Height, 4); + Rectangle areaToRefurbish = new Rectangle(building.tileX.Value, building.tileY.Value - 2, width, height); + var location = building.GetParentLocation(); + location.ApplyMapOverride(assetName, "SinZ.SpousePatios_" + spouse, new Rectangle(sourceArea.Location.X, sourceArea.Location.Y, areaToRefurbish.Width, areaToRefurbish.Height), areaToRefurbish); + foreach (Point tile in areaToRefurbish.GetPoints()) + { + if (location.getTileIndexAt(tile, "Paths") == 7) + { + SpousePatioStandingSpots[spouse] = new Vector2(tile.X, tile.Y); + // Reload the patio activity to the new location + if (npc.shouldPlaySpousePatioAnimation.Value) + { + npc.setUpForOutdoorPatioActivity(); + } + break; + } + } + } + } + + private void World_BuildingListChanged(object sender, StardewModdingAPI.Events.BuildingListChangedEventArgs e) + { + var addedSpousePatios = e.Added.Where(b => b.buildingType.Value == SpousePatioBuildingId); + var removedSpousePatios = e.Removed.Where(b => b.buildingType.Value == SpousePatioBuildingId); + if (addedSpousePatios.Any() || removedSpousePatios.Any()) + { + RecalculateSpousePatioAllocations(); + } + } + + private void Content_AssetRequested(object sender, StardewModdingAPI.Events.AssetRequestedEventArgs e) + { + if (e.NameWithoutLocale.IsEquivalentTo("Data/Buildings")) + { + e.Edit(data => + { + var dict = data.AsDictionary(); + dict.Data[SpousePatioBuildingId] = new() + { + Name = "Spouse Patio", // TODO: i18n? + Description = "Spouse Patio", // TODO: i18n? + + Builder = "Robin", + BuildDays = 0, + // TODO: Test this actually works + BuildCondition = "LOCATION_NAME Target Farm", + BuildCost = 500, + BuildMaterials = new() + { + new() + { + ItemId = "(O)388", + Amount = 20 + } + }, + + Texture = Helper.ModContent.GetInternalAssetName("assets/baseSpousePatio.png").Name, + Size = new() + { + X = 4, + Y = 2 + }, + FadeWhenBehind = false, + DrawShadow = false, + }; + }, AssetEditPriority.Default - 1); + } + } +} + +public static class HarmonyPatches +{ + private static IMonitor monitor; + public static void Setup(IMonitor monitor, Harmony harmony) + { + HarmonyPatches.monitor = monitor; + var marriageDuties = typeof(NPC).GetMethod(nameof(NPC.marriageDuties)); + if (marriageDuties != null) + { + harmony.Patch(marriageDuties, transpiler: new HarmonyMethod(typeof(HarmonyPatches).GetMethod(nameof(marriageDuties__Transpiler)))); + } + var getSpousePatioPosition = typeof(NPC).GetMethod(nameof(NPC.GetSpousePatioPosition)); + if (getSpousePatioPosition != null) + { + harmony.Patch(getSpousePatioPosition, postfix: new HarmonyMethod(typeof(HarmonyPatches).GetMethod(nameof(GetSpousePatioPosition__Postfix)))); + } + var performActionOnBuildingPlacement = AccessTools.Method(typeof(Building), nameof(Building.performActionOnBuildingPlacement)); + if (performActionOnBuildingPlacement != null) + { + harmony.Patch(performActionOnBuildingPlacement, postfix: new HarmonyMethod(typeof(HarmonyPatches).GetMethod(nameof(performActionOnBuildingPlacement__Postfix)))); + } + harmony.Patch(AccessTools.Method(typeof(Building), nameof(Building.isTilePassable)), prefix: new HarmonyMethod(typeof(HarmonyPatches).GetMethod(nameof(isTilePassable__Prefix)))); + harmony.Patch(AccessTools.Method(typeof(Building), nameof(Building.draw)), prefix: new HarmonyMethod(typeof(HarmonyPatches).GetMethod(nameof(Draw__prefix)))); + harmony.Patch(AccessTools.Method(typeof(Farm), nameof(Farm.OnMapLoad)), postfix: new HarmonyMethod(typeof(HarmonyPatches).GetMethod(nameof(OnMapLoad__Postfix)))); + harmony.Patch(AccessTools.Method(typeof(Farmer), nameof(Farmer.doDivorce)), prefix: new HarmonyMethod(typeof(HarmonyPatches).GetMethod(nameof(doDivorce__Prefix))), postfix: new HarmonyMethod(typeof(HarmonyPatches).GetMethod(nameof(doDivorce__Postfix)))); + harmony.Patch(AccessTools.Method(typeof(Game1), nameof(Game1.OnDayStarted)), prefix: new HarmonyMethod(typeof(HarmonyPatches).GetMethod(nameof(OnDayStarted__Prefix)))); + } + + /// + /// Harmony patch to make allocated patios passable, due to the lack of conditional CollisionMaps + /// + /// + /// + /// + public static bool isTilePassable__Prefix(Building __instance, ref bool __result) + { + if (__instance.buildingType.Value == ModEntry.SpousePatioBuildingId && __instance.modData.ContainsKey(ModEntry.SpousePatioAllocationKey)) + { + __result = true; + return false; + } + return true; + } + + public static bool Draw__prefix(Building __instance) + { + // Override drawing if its my building and allocated + if (__instance.buildingType.Value == ModEntry.SpousePatioBuildingId && __instance.modData.ContainsKey(ModEntry.SpousePatioAllocationKey)) + { + return false; + } + return true; + } + + public static void doDivorce__Prefix(Farmer __instance, out string? __state) + { + // Set state if the farmer is married to an npc, not roommates + __state = null; + if (__instance.spouse != null && __instance.friendshipData.TryGetValue(__instance.spouse, out var friendship) && friendship.IsMarried() && !friendship.IsRoommate()) + { + __state = __instance.spouse; + } + } + public static void doDivorce__Postfix(string? __state) + { + if (__state != null) + { + if (ModEntry.SpousePatioAllocations.TryGetValue(__state, out var patio)) + { + patio.modData.Remove(ModEntry.SpousePatioAllocationKey); + } + ModEntry.Instance.RecalculateSpousePatioAllocations(); + } + } + + public static void OnMapLoad__Postfix() + { + ModEntry.Instance.ApplyMapModifications(); + } + + public static void performActionOnBuildingPlacement__Postfix(Building __instance) + { + if (__instance.buildingType.Value == ModEntry.SpousePatioBuildingId) + { + ModEntry.Instance.RecalculateSpousePatioAllocations(); + } + } + + /// + /// Marriage Duties run before game loop DayStarted, so prefix hooking into Game1.OnDayStarted which eventually calls marriage duties + /// + public static void OnDayStarted__Prefix() + { + ModEntry.Instance.RecalculateSpousePatioAllocations(); + } + + public static void GetSpousePatioPosition__Postfix(NPC __instance, ref Vector2 __result) + { + if (ModEntry.SpousePatioStandingSpots.TryGetValue(__instance.Name, out var spot)) + { + __result = spot; + } + } + + public static IEnumerable marriageDuties__Transpiler(ILGenerator generator, IEnumerable instructions) + { + var output = new List(); + var huntBNE = false; + foreach (var instruction in instructions) + { + /* + * IL_03ea: ldloc.0 + * IL_03eb: call class StardewValley.Farmer StardewValley.Game1::get_MasterPlayer() + * IL_03f0: bne.un.s IL_040b + */ + if (instruction.opcode == OpCodes.Call && instruction.operand is MethodInfo operandMethod && operandMethod.Name == "get_MasterPlayer") + { + // remove ldloc.0 and replace it with Ldarg_0 (this) + output.RemoveAt(output.Count - 1); + output.Add(new CodeInstruction(OpCodes.Ldarg_0)); + output.Add(new CodeInstruction(OpCodes.Call, typeof(HarmonyPatches).GetMethod(nameof(IsValidSpousePatio)))); + huntBNE = true; + continue; + } + // We are no longer bne.un.s, and doing brfalse.s instead + if (huntBNE && instruction.opcode == OpCodes.Bne_Un_S) + { + huntBNE = false; + output.Add(new CodeInstruction(OpCodes.Brfalse_S, instruction.operand)); + continue; + } + output.Add(instruction); + } + return output; + } + /// + /// This returns the spouses name if its valid. + /// This is a harmony swap out of a call to Game1.MasterPlayer from NPC.marriageDuties but returns + /// + /// + /// + public static bool IsValidSpousePatio(NPC npc) + { + if (Game1.MasterPlayer.spouse == npc.Name) return true; + if (ModEntry.SpousePatioStandingSpots.ContainsKey(npc.Name)) return true; + return false; + } +} \ No newline at end of file diff --git a/SinZational Several Spouse Spots/SinZational Several Spouse Spots.csproj b/SinZational Several Spouse Spots/SinZational Several Spouse Spots.csproj new file mode 100644 index 0000000..578b56e --- /dev/null +++ b/SinZational Several Spouse Spots/SinZational Several Spouse Spots.csproj @@ -0,0 +1,20 @@ + + + net6.0 + SeveralSpouseSpots + true + + + SinZational Several Spouse Spots + SinZ + 1.0.0 + A mod that implements multiplayer spouse patios + SinZ.SeveralSpouseSpots + 4.0 + Nexus:20655 + + + + + + \ No newline at end of file diff --git a/SinZational Several Spouse Spots/assets/baseSpousePatio.pdn b/SinZational Several Spouse Spots/assets/baseSpousePatio.pdn new file mode 100644 index 0000000000000000000000000000000000000000..68eb2a2991f6f46f4033287e54b9cf81848d5838 GIT binary patch literal 8094 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 literal 0 HcmV?d00001 diff --git a/SinZational Several Spouse Spots/assets/baseSpousePatio.png b/SinZational Several Spouse Spots/assets/baseSpousePatio.png new file mode 100644 index 0000000000000000000000000000000000000000..e2808ce8c84ec85309973b27c06a5215c3dede32 GIT binary patch literal 1928 zcmV;32Y2|1P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!TN6>DqJKutR@IlVS?)s(rRplyk8!0wRQIc~kBhD<;^9?cL?W?XLIM>)o~0 zk{tLijdq@ydFJN-%W0`DrvgyzDc8`zFZN+%gLmxq}f3;~D<*+?rO3`6~c0OZCdsBNfo%5D1#Uwy#U`lCk}FNfU8r;nLe z-hI*K3Y&Kv;=oe}Ljy3>Ke#MGKnNk_#q5NvyRTmAhOWdFAtx@3DfRa?$emx=Gv)5< zqed97k^tCwn@DbKB2oZuWrXnxT>(JZ2Eiup$IIqavIzpT)=KBdz#>1?KWGm150;#z zw=(SUx+l)d%SOywa6V@&X_$I@2CR;)qN^whB;Wg<;Tm+zS^bwI6{$1tl;U9izPF;`54KY4(>Vd*J@!B`GI`-bP1);UqI`+}Fkx+j- zZ}+ovA0$!*5yT7c3fQ^NZNZ=AVssmuwgWJ56tw2gPtWg8>wIcMOaRKa<2UYlZOQcscl$xp_Hj zA~!bS6nZg`u3EAQDvHzzxteRd=C(I!ZmPv@ICu` zgq@=J`ypWLPMxqH72mzB(>X6&YbED%2Ag*rV#iF6?<29%+~(xP@V)%~_2;cURgn1F zWw^81-Mkd}jJG5hs#a=8@Q=z%VjU5+4Rth)Y@=yp8`Cd;nx>I$-abnp|Mg!)So z?MV{t_mF6>w#f@?g(=p`hy&|@_1?ZBU{(H6cnSM``Tq8kXwSY>QkgmOeQwKiEL{Pr z_Aqd_Gv!MYK6&?b!Ma98aTi#De|qES@hH3^odQ0vXCrRSj$+NW`!+FcT>#Lnv=C+R zK3KJX7%z!+ZvZ^IIqRgc``-(4c0WHTt{Jl7-hAwVV+Cl=RTmYj@{htxVksm5+3`zG zvdiqN2fqMjfsMc!;23ZXr~qCCUa~}zb(Vd~b$p#6pE=?5l+T<9wWIjE{j6(rUaHnH z)IVrG^Jvn%&{bnz=&CWFc{Ev8FF?MCz{}YKx(`2C^0LrcE9pM`pkptGmcGeNEq#-B zwDe8h4$yhJbA9LO&UokPPVa4TQxL|h8h!Lbk)^?bk#MA4=^`+_V>*9 z?C;UNZEqHWc$EYoJ333_?g|=rSCAc@byt-&f_N2=Emi9{dFZj@LCVQ$G1NbJ^EAN- zyqo}}H;x{E=<#k?MmsA@SOLJr32R>{RfVkpG}c!;jj)c7haT@P+aR2L`k49FD~Ivp z<@?77KH6DXYl1JHdBQI9^0M(it_j}bl=<<(F93dx8k@G09l1#A?U@oyuzKoxY<|OA zmL~Y!Y1Nof3gZTQ&;C>FYEKq5!N1P&+f#pv@#L@Ph`C4mLbIhx>|Kz`h# z34Q>XQ@K!od#r9vkm8?GunaH!0=(mJ&9bX zzYVYcXJa{DOG^A2m9r*j!;cq!0XUyCyhTvvnjitZ1TKBd??VEX`BKDaZmYDY>TM;Yb*NfoP0zt9{iE=5G{NlnCEq8euC`=ZYl30C zoUqu9-O2lHjSFR?35M~KxRys_eYKMU$Wv~bU=T0ix54>g$w{DU-vPD)uaM7NLDjwm zsKCqQGt;QrV=nvOsMXCbGC_R+In$ O0000 + { + if (whichAnswer == "Cancel") return; + who.modData[Constants.SelectedFloor] = whichAnswer; + Game1.warpFarmer(whichAnswer, 0, 0, 0); + }; + List options = new(); + foreach (var farmer in Game1.getAllFarmers()) + { + if (!farmer.isUnclaimedFarmhand) + { + options.Add(new(farmer.homeLocation.Value, farmer.Name)); + } + } + options.Add(new Response("Cancel", Game1.content.LoadString("Strings\\Locations:ManorHouse_LedgerBook_TransferCancel"))); + Game1.currentLocation.createQuestionDialogue("Which floor do you want to enter?", options.ToArray(), "SinZ.SharedSpaces_Elevator"); + return true; + } + + private void Content_AssetRequested(object? sender, StardewModdingAPI.Events.AssetRequestedEventArgs e) + { + if (e.NameWithoutLocale.IsEquivalentTo("Data/Buildings")) + { + e.Edit(asset => + { + var data = asset.AsDictionary().Data; + var farmhouse = data["Farmhouse"]; + farmhouse.ActionTiles.Add(new() + { + Id = "SinZ.SharedSpaces_ElevatorButton", + Tile = new() + { + X = 6, + Y = 2 + }, + Action = "SinZ.SharedSpaces_ElevatorButton" + }); + farmhouse.DrawLayers.Add(new() + { + Id = "SinZ.SharedSpaces_ElevatorButton", + Texture = Helper.ModContent.GetInternalAssetName("assets/elevatorButton.png").Name, + SourceRect = new() + { + Width = 16, + Height = 16 + }, + DrawInBackground = false, + DrawPosition = new() + { + X = 6 * 16, + Y = 5 * 16 + }, + }); + }, AssetEditPriority.Default - 1); + } + } +} + +public static class Patches +{ + public static IMonitor monitor; + public static void Init(Harmony harmony, IModHelper helper, IMonitor monitor) + { + Patches.monitor = monitor; + + harmony.Patch(AccessTools.Method(typeof(GameLocation), nameof(GameLocation.performAction), new Type[] { typeof(string[]), typeof(Farmer), typeof(Location) }), transpiler: new HarmonyMethod(typeof(Patches).GetMethod(nameof(GameLocation__performAction__Transpiler)))); + harmony.Patch(AccessTools.Method(typeof(Building), nameof(Building.OnUseHumanDoor)), postfix: new HarmonyMethod(typeof(Patches).GetMethod(nameof(Building__OnUseHumanDoor__Postfix)))); + harmony.Patch(AccessTools.Method(typeof(FarmhandMenu.FarmhandSlot), nameof(FarmhandMenu.FarmhandSlot.Activate)), transpiler: new HarmonyMethod(typeof(Patches).GetMethod(nameof(FarmhandSlot__Activate__Transpiler)))); + harmony.Patch(AccessTools.Method(typeof(Game1), "_newDayAfterFade"), postfix: new HarmonyMethod(typeof(Patches).GetMethod(nameof(Game1___newDaysAfterFade__Postfix)))); + harmony.Patch(AccessTools.Method(typeof(GameServer), "unclaimedFarmhandsExist"), prefix: new HarmonyMethod(typeof(Patches).GetMethod(nameof(GameServer__unclaimedFarmhandsExist__Prefix)))); + harmony.Patch(AccessTools.Method(typeof(GameServer), nameof(GameServer.sendAvailableFarmhands)), transpiler: new HarmonyMethod(typeof(Patches).GetMethod(nameof(GameServer__sendAvailableFarmhands__Transpiler)))); + harmony.Patch(AccessTools.Method(typeof(NetWorldState), nameof(NetWorldState.TryAssignFarmhandHome)), prefix: new HarmonyMethod(typeof(Patches).GetMethod(nameof(NetWorldState__TryAssignFarmhandHome__Prefix)))); + harmony.Patch(AccessTools.Method(typeof(SaveGame), nameof(SaveGame.loadDataToLocations)), transpiler: new HarmonyMethod(typeof(Patches).GetMethod(nameof(SaveGame__loadDataToLocations__Transpiler)))); + } + + public static IEnumerable GameLocation__performAction__Transpiler(ILGenerator generator, IEnumerable instructions) + { + var output = new List(); + foreach (var instruction in instructions) + { + // for the purposes of mailbox, the farmhouse is owned by everyone + if (instruction.opcode == OpCodes.Callvirt && (instruction.operand as MethodInfo)?.Name == "get_IsOwnedByCurrentPlayer") + { + /** + * Replace the following + * IL_4016: ldloc.s 102 + * IL_4018: callvirt instance bool StardewValley.Locations.FarmHouse::get_IsOwnedByCurrentPlayer() + * with + * ldc.i4.1 + * so its always "owned" + */ + // Remove the ldloc.s + output.RemoveAt(output.Count - 1); + // Replace the callvirt + output.Add(new CodeInstruction(OpCodes.Ldc_I4_1)); + continue; + } + output.Add(instruction); + } + return output; + } + + public static IEnumerable SaveGame__loadDataToLocations__Transpiler(ILGenerator generator, IEnumerable instructions) + { + var instructionArray = instructions.ToArray(); + var newLabel = generator.DefineLabel(); + var output = new List(); + + bool found = false; + + CodeInstruction stloc8 = default; + + foreach (var instruction in instructionArray) + { + if (stloc8 == null && instruction.opcode == OpCodes.Stloc_S && (instruction.operand as LocalBuilder).LocalIndex == 8) + { + stloc8 = instruction; + } + output.Add(instruction); + if (!found && instruction.opcode == OpCodes.Ldloc_S && (instruction.operand as LocalBuilder).LocalIndex == 9 && instruction.labels.Count == 2) + { + found = true; + var brTrue = instructionArray[output.Count]; + var ldLoc8 = instructionArray[output.Count + 3]; + var stLoc9 = instructionArray[output.Count - 2]; + output.Add(brTrue); + output.Add(ldLoc8); + output.Add(new CodeInstruction(OpCodes.Call, typeof(Patches).GetMethod(nameof(SaveGame_loadDataToLocations__FixCabin)))); + output.Add(stLoc9); + output.Add(instruction); //ldLoc9 + } + } + return output; + } + + public static GameLocation? SaveGame_loadDataToLocations__FixCabin(GameLocation location) + { + if (location is Cabin oldCabin) + { + // Need to duplicate it due to NetCollection.Set clears the destination before iterating the argument. + // so if it was setting with itself as the argument it is a clear + var newCabin = new Cabin("Maps/Farmhouse"); + newCabin.name.Value = location.name.Value; + newCabin.isFarm.Value = true; + newCabin.isAlwaysActive.Value = true; + newCabin.IsOutdoors = false; + + newCabin.fridge.Value = oldCabin.fridge.Value; + newCabin.farmhandReference.Value = oldCabin.farmhandReference.Value; + Game1.locations.Add(newCabin); + return newCabin; + } + monitor.Log("Help!"); + return null; + } + + public static void Building__OnUseHumanDoor__Postfix(Building __instance, Farmer who, ref bool __result) + { + if (__instance.buildingType.Value != "Farmhouse") return; + if (!who.modData.TryGetValue(Constants.SelectedFloor, out var floor)) return; + // Default behaviour + if (floor == "Farmhouse") return; + __result = false; + + who.currentLocation.playSound("doorClose", who.Tile); + Game1.warpFarmer(floor, 0, 0, false); + } + + public static IEnumerable FarmhandSlot__Activate__Transpiler(ILGenerator generator, IEnumerable instructions) + { + var output = new List(); + var skipCount = 0; + foreach (var instruction in instructions) + { + if (skipCount-- > 0) continue; + + if (instruction.opcode == OpCodes.Ldnull) + { + output.Add(new CodeInstruction(OpCodes.Call, typeof(Patches).GetMethod(nameof(FarmhandSlot__Activate__AddCabins)))); + skipCount = 1; + continue; + } + output.Add(instruction); + } + return output; + } + public static void FarmhandSlot__Activate__AddCabins(Client client) + { + // Inject the Cabin locations as they are dynamic + foreach (var farmhand in client.availableFarmhands) + { + var cabin = new Cabin("Maps\\FarmHouse"); + cabin.name.Value = farmhand.homeLocation.Value; + cabin.isFarm.Value = true; + cabin.isAlwaysActive.Value = true; + Game1.locations.Add(cabin); + } + + // Preserve the code we replaced + client.availableFarmhands = null; + } + + public static void Game1___newDaysAfterFade__Postfix() + { + Game1.player.slotCanHost = true; + } + + public static IEnumerable GameServer__sendAvailableFarmhands__Transpiler(ILGenerator generator, IEnumerable instructions) + { + var output = new List(); + foreach (var instruction in instructions) + { + if (instruction.opcode == OpCodes.Newobj && ((ConstructorInfo)instruction.operand).DeclaringType.Name == "MemoryStream") + { + var firstInstruction = new CodeInstruction(OpCodes.Ldloc_1) + { + labels = instruction.labels + }; + instruction.labels = new List