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 0000000..68eb2a2 Binary files /dev/null and b/SinZational Several Spouse Spots/assets/baseSpousePatio.pdn differ diff --git a/SinZational Several Spouse Spots/assets/baseSpousePatio.png b/SinZational Several Spouse Spots/assets/baseSpousePatio.png new file mode 100644 index 0000000..e2808ce Binary files /dev/null and b/SinZational Several Spouse Spots/assets/baseSpousePatio.png differ diff --git a/SinZational Shared Spaces/ModEntry.cs b/SinZational Shared Spaces/ModEntry.cs new file mode 100644 index 0000000..dfb18a1 --- /dev/null +++ b/SinZational Shared Spaces/ModEntry.cs @@ -0,0 +1,319 @@ +using HarmonyLib; +using Microsoft.Xna.Framework; +using Netcode; +using StardewModdingAPI; +using StardewModdingAPI.Events; +using StardewValley; +using StardewValley.Buildings; +using StardewValley.GameData.Buildings; +using StardewValley.Locations; +using StardewValley.Menus; +using StardewValley.Network; +using System.Reflection; +using System.Reflection.Emit; +using xTile.Dimensions; +using static StardewValley.Minigames.MineCart; + +public static class Constants +{ + + public const string ElevatorTileAction = "SinZ.SharedSpaces_ElevatorButton"; + public const string SelectedFloor = "SinZ.SharedSpaces_SelectedFloor"; +} + +public class ModEntry : Mod +{ + public override void Entry(IModHelper helper) + { + helper.Events.Content.AssetRequested += Content_AssetRequested; + + GameLocation.RegisterTileAction(Constants.ElevatorTileAction, onElevatorPress); + + var harmony = new Harmony(ModManifest.UniqueID); + Patches.Init(harmony, helper, Monitor); + /** + * TODO: + * Changes to farmhouse front door to allow elevator style selection of which interior to go to + * + * Allow stable buildings up to player instance count + * + * Fix spouse standing spot to be building like Several Spouse Spots + * + * Prevent cabins spawning, preferably done in customization menu itself + * + * Allow manual cabin placement to migrate interiors over + * + * Test multiple farmhands joining for the first time at the same time + */ + } + + private bool onElevatorPress(GameLocation location, string[] arg2, Farmer who, Point point) + { + Game1.currentLocation.afterQuestion = (who, whichAnswer) => + { + 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