From e18ac2a111d5a9ea604a37e71044e1b229e182c1 Mon Sep 17 00:00:00 2001 From: yangzhi <@4F!xZpJwly&KbWq> Date: Fri, 12 Apr 2019 23:40:59 +0800 Subject: [PATCH] Support for Json Asset Mod --- Install/PatchStep.txt | 47 +- Mods/JsonAssets/Api.cs | 120 +++ Mods/JsonAssets/ContentInjector.cs | 412 ++++++++ Mods/JsonAssets/Data/BigCraftableData.cs | 103 ++ Mods/JsonAssets/Data/ContentPackData.cs | 13 + Mods/JsonAssets/Data/CropData.cs | 73 ++ Mods/JsonAssets/Data/DataNeedsId.cs | 9 + Mods/JsonAssets/Data/FruitTreeData.cs | 33 + Mods/JsonAssets/Data/HatData.cs | 48 + Mods/JsonAssets/Data/ObjectData.cs | 175 ++++ Mods/JsonAssets/Data/WeaponData.cs | 82 ++ Mods/JsonAssets/JsonAssets.csproj | 174 ++++ Mods/JsonAssets/Log.cs | 33 + Mods/JsonAssets/Mod.cs | 925 ++++++++++++++++++ Mods/JsonAssets/Overrides/Object.cs | 71 ++ Mods/JsonAssets/Properties/AssemblyInfo.cs | 36 + Mods/JsonAssets/Util.cs | 28 + Mods/UI Info Suite/IconHandler.cs | 2 +- .../UIElements/ShowBirthdayIcon.cs | 4 +- .../UIElements/ShowQueenOfSauceIcon.cs | 12 +- .../UIElements/ShowToolUpgradeStatus.cs | 6 +- .../UIElements/ShowTravelingMerchant.cs | 6 +- src/Mod.csproj | 5 + src/ModEntry.cs | 23 +- src/SMAPI/Events/IHookEvents.cs | 18 + src/SMAPI/Events/IModEvents.cs | 1 + .../Events/ObjectCanBePlacedHereEventArgs.cs | 34 + .../Events/ObjectCheckForActionEventArgs.cs | 24 + ...IndexOkForBasicShippedCategoryEventArgs.cs | 29 + src/SMAPI/Framework/Events/EventManager.cs | 9 + src/SMAPI/Framework/Events/ManagedEvent.cs | 74 +- src/SMAPI/Framework/Events/ModEvents.cs | 2 + src/SMAPI/Framework/Events/ModHookEvents.cs | 43 + src/SMAPI/Framework/SGame.cs | 22 + 34 files changed, 2668 insertions(+), 28 deletions(-) create mode 100644 Mods/JsonAssets/Api.cs create mode 100644 Mods/JsonAssets/ContentInjector.cs create mode 100644 Mods/JsonAssets/Data/BigCraftableData.cs create mode 100644 Mods/JsonAssets/Data/ContentPackData.cs create mode 100644 Mods/JsonAssets/Data/CropData.cs create mode 100644 Mods/JsonAssets/Data/DataNeedsId.cs create mode 100644 Mods/JsonAssets/Data/FruitTreeData.cs create mode 100644 Mods/JsonAssets/Data/HatData.cs create mode 100644 Mods/JsonAssets/Data/ObjectData.cs create mode 100644 Mods/JsonAssets/Data/WeaponData.cs create mode 100644 Mods/JsonAssets/JsonAssets.csproj create mode 100644 Mods/JsonAssets/Log.cs create mode 100644 Mods/JsonAssets/Mod.cs create mode 100644 Mods/JsonAssets/Overrides/Object.cs create mode 100644 Mods/JsonAssets/Properties/AssemblyInfo.cs create mode 100644 Mods/JsonAssets/Util.cs create mode 100644 src/SMAPI/Events/IHookEvents.cs create mode 100644 src/SMAPI/Events/ObjectCanBePlacedHereEventArgs.cs create mode 100644 src/SMAPI/Events/ObjectCheckForActionEventArgs.cs create mode 100644 src/SMAPI/Events/ObjectIsIndexOkForBasicShippedCategoryEventArgs.cs create mode 100644 src/SMAPI/Framework/Events/ModHookEvents.cs diff --git a/Install/PatchStep.txt b/Install/PatchStep.txt index f6dd9eaa..b6171e53 100644 --- a/Install/PatchStep.txt +++ b/Install/PatchStep.txt @@ -25,4 +25,49 @@ ret ldsfld StardewValley.ModHooks StardewValley.Game1::hooks ldarg.1 ldnull -callvirt System.Void StardewValley.ModHooks::OnGame1_Draw(Microsoft.Xna.Framework.GameTime,Microsoft.Xna.Framework.Graphics.RenderTarget2D) \ No newline at end of file +callvirt System.Void StardewValley.ModHooks::OnGame1_Draw(Microsoft.Xna.Framework.GameTime,Microsoft.Xna.Framework.Graphics.RenderTarget2D) + + +Optional Section + +Fix back button + +Modify class StardewValley.Game1, modify method updateAndroidMenus(), modify Instructions at beginning: +ldsfld StardewValley.InputState StardewValley.Game1::input +callvirt Microsoft.Xna.Framework.Input.GamePadState StardewValley.InputState::GetGamePadState() + + + +Json Asset Support + +Modify class StardewValley.Object, modify method checkForAction,insert instructions at beginning: +ldsfld StardewValley.ModHooks StardewValley.Game1::hooks +ldarg.0 +callvirt System.Boolean StardewValley.ModHooks::OnObject_checkForAction(StardewValley.Object) +brtrue.s -> (6) ldarg.2 +ldc.i4.0 +ret + +modify method isIndexOkForBasicShippedCategory,replace instructions: +ldarg.0 +ldc.i4 434 +bne.un.s -> (5) ldsfld StardewValley.ModHooks StardewValley.Game1::hooks +ldc.i4.0 +ret +ldsfld StardewValley.ModHooks StardewValley.Game1::hooks +ldarg.0 +ldloca.s -> (0) (System.Boolean) +callvirt System.Void StardewValley.ModHooks::OnObject_isIndexOkForBasicShippedCategory(System.Int32,System.Boolean&) +ldloc.0 +ret + +modify method canBePlacedHere insert instructions at beginning: +ldsfld StardewValley.ModHooks StardewValley.Game1::hooks +ldarg.0 +ldarg.1 +ldarg.2 +ldloca.s -> (1) (System.Boolean) +callvirt System.Boolean StardewValley.ModHooks::OnObject_canBePlacedHere(StardewValley.Object,StardewValley.GameLocation,Microsoft.Xna.Framework.Vector2,System.Boolean&) +brtrue.s -> (9) ldarg.1 +ldloc.1 +ret \ No newline at end of file diff --git a/Mods/JsonAssets/Api.cs b/Mods/JsonAssets/Api.cs new file mode 100644 index 00000000..dc46c49a --- /dev/null +++ b/Mods/JsonAssets/Api.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; + +namespace JsonAssets +{ + public interface IApi + { + void LoadAssets(string path); + + int GetObjectId(string name); + int GetCropId(string name); + int GetFruitTreeId(string name); + int GetBigCraftableId(string name); + int GetHatId(string name); + int GetWeaponId(string name); + + IDictionary GetAllObjectIds(); + IDictionary GetAllCropIds(); + IDictionary GetAllFruitTreeIds(); + IDictionary GetAllBigCraftableIds(); + IDictionary GetAllHatIds(); + IDictionary GetAllWeaponIds(); + + event EventHandler IdsAssigned; + event EventHandler AddedItemsToShop; + } + + public class Api : IApi + { + private readonly Action loadFolder; + + public Api(Action loadFolder) + { + this.loadFolder = loadFolder; + } + + public void LoadAssets(string path) + { + this.loadFolder(path); + } + + public int GetObjectId(string name) + { + return Mod.instance.objectIds.ContainsKey(name) ? Mod.instance.objectIds[name] : -1; + } + + public int GetCropId(string name) + { + return Mod.instance.cropIds.ContainsKey(name) ? Mod.instance.cropIds[name] : -1; + } + + public int GetFruitTreeId(string name) + { + return Mod.instance.fruitTreeIds.ContainsKey(name) ? Mod.instance.fruitTreeIds[name] : -1; + } + + public int GetBigCraftableId(string name) + { + return Mod.instance.bigCraftableIds.ContainsKey(name) ? Mod.instance.bigCraftableIds[name] : -1; + } + + public int GetHatId(string name) + { + return Mod.instance.hatIds.ContainsKey(name) ? Mod.instance.hatIds[name] : -1; + } + + public int GetWeaponId(string name) + { + return Mod.instance.weaponIds.ContainsKey(name) ? Mod.instance.weaponIds[name] : -1; + } + + public IDictionary GetAllObjectIds() + { + return new Dictionary(Mod.instance.objectIds); + } + + public IDictionary GetAllCropIds() + { + return new Dictionary(Mod.instance.cropIds); + } + + public IDictionary GetAllFruitTreeIds() + { + return new Dictionary(Mod.instance.fruitTreeIds); + } + + public IDictionary GetAllBigCraftableIds() + { + return new Dictionary(Mod.instance.bigCraftableIds); + } + + public IDictionary GetAllHatIds() + { + return new Dictionary(Mod.instance.hatIds); + } + + public IDictionary GetAllWeaponIds() + { + return new Dictionary(Mod.instance.weaponIds); + } + + public event EventHandler IdsAssigned; + internal void InvokeIdsAssigned() + { + Log.trace("Event: IdsAssigned"); + if (IdsAssigned == null) + return; + Util.invokeEvent("JsonAssets.Api.IdsAssigned", IdsAssigned.GetInvocationList(), null); + } + + public event EventHandler AddedItemsToShop; + internal void InvokeAddedItemsToShop() + { + Log.trace("Event: AddedItemsToShop"); + if (AddedItemsToShop == null) + return; + Util.invokeEvent("JsonAssets.Api.AddedItemsToShop", AddedItemsToShop.GetInvocationList(), null); + } + } +} diff --git a/Mods/JsonAssets/ContentInjector.cs b/Mods/JsonAssets/ContentInjector.cs new file mode 100644 index 00000000..ae4daedd --- /dev/null +++ b/Mods/JsonAssets/ContentInjector.cs @@ -0,0 +1,412 @@ +using JsonAssets.Data; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI; +using StardewValley; +using System; +using System.Collections.Generic; + +namespace JsonAssets +{ + public class ContentInjector : IAssetEditor + { + public bool CanEdit(IAssetInfo asset) + { + if (asset.AssetNameEquals("Data\\ObjectInformation")) + return true; + if (asset.AssetNameEquals("Data\\Crops")) + return true; + if (asset.AssetNameEquals("Data\\fruitTrees")) + return true; + if (asset.AssetNameEquals("Data\\CookingRecipes")) + return true; + if (asset.AssetNameEquals("Data\\CraftingRecipes")) + return true; + if (asset.AssetNameEquals("Data\\BigCraftablesInformation")) + return true; + if (asset.AssetNameEquals("Data\\hats")) + return true; + if (asset.AssetNameEquals("Data\\weapons")) + return true; + if (asset.AssetNameEquals("Data\\NPCGiftTastes")) + return true; + if (asset.AssetNameEquals("Maps\\springobjects")) + return true; + if (asset.AssetNameEquals("TileSheets\\crops")) + return true; + if (asset.AssetNameEquals("TileSheets\\fruitTrees")) + return true; + if (asset.AssetNameEquals("TileSheets\\Craftables") || asset.AssetNameEquals("TileSheets\\Craftables_indoor") || asset.AssetNameEquals("TileSheets\\Craftables_outdoor")) + return true; // _indoor/_outdoor for Seasonal Immersion compat + if (asset.AssetNameEquals("Characters\\Farmer\\hats")) + return true; + if (asset.AssetNameEquals("TileSheets\\weapons")) + return true; + return false; + } + + public void Edit(IAssetData asset) + { + if (asset.AssetNameEquals("Data\\ObjectInformation")) + { + IDictionary data = asset.AsDictionary().Data; + foreach (ObjectData obj in Mod.instance.objects) + { + try + { + Log.trace($"Injecting to objects: {obj.GetObjectId()}: {obj.GetObjectInformation()}"); + data.Add(obj.GetObjectId(), obj.GetObjectInformation()); + } + catch (Exception e) + { + Log.error($"Exception injecting object information for {obj.Name}: {e}"); + } + } + } + else if (asset.AssetNameEquals("Data\\Crops")) + { + IDictionary data = asset.AsDictionary().Data; + foreach (CropData crop in Mod.instance.crops) + { + try + { + Log.trace($"Injecting to crops: {crop.GetSeedId()}: {crop.GetCropInformation()}"); + data.Add(crop.GetSeedId(), crop.GetCropInformation()); + } + catch (Exception e) + { + Log.error($"Exception injecting crop for {crop.Name}: {e}"); + } + } + } + else if (asset.AssetNameEquals("Data\\fruitTrees")) + { + IDictionary data = asset.AsDictionary().Data; + foreach (FruitTreeData fruitTree in Mod.instance.fruitTrees) + { + try + { + Log.trace($"Injecting to fruit trees: {fruitTree.GetSaplingId()}: {fruitTree.GetFruitTreeInformation()}"); + data.Add(fruitTree.GetSaplingId(), fruitTree.GetFruitTreeInformation()); + } + catch (Exception e) + { + Log.error($"Exception injecting fruit tree for {fruitTree.Name}: {e}"); + } + } + } + else if (asset.AssetNameEquals("Data\\CookingRecipes")) + { + IDictionary data = asset.AsDictionary().Data; + foreach (ObjectData obj in Mod.instance.objects) + { + try + { + if (obj.Recipe == null) + continue; + if (obj.Category != ObjectData.Category_.Cooking) + continue; + Log.trace($"Injecting to cooking recipes: {obj.Name}: {obj.Recipe.GetRecipeString(obj)}"); + data.Add(obj.Name, obj.Recipe.GetRecipeString(obj)); + } + catch (Exception e) + { + Log.error($"Exception injecting cooking recipe for {obj.Name}: {e}"); + } + } + } + else if (asset.AssetNameEquals("Data\\CraftingRecipes")) + { + IDictionary data = asset.AsDictionary().Data; + foreach (ObjectData obj in Mod.instance.objects) + { + try + { + if (obj.Recipe == null) + continue; + if (obj.Category == ObjectData.Category_.Cooking) + continue; + Log.trace($"Injecting to crafting recipes: {obj.Name}: {obj.Recipe.GetRecipeString(obj)}"); + data.Add(obj.Name, obj.Recipe.GetRecipeString(obj)); + } + catch (Exception e) + { + Log.error($"Exception injecting crafting recipe for {obj.Name}: {e}"); + } + } + foreach (BigCraftableData big in Mod.instance.bigCraftables) + { + try + { + if (big.Recipe == null) + continue; + Log.trace($"Injecting to crafting recipes: {big.Name}: {big.Recipe.GetRecipeString(big)}"); + data.Add(big.Name, big.Recipe.GetRecipeString(big)); + } + catch (Exception e) + { + Log.error($"Exception injecting crafting recipe for {big.Name}: {e}"); + } + } + } + else if (asset.AssetNameEquals("Data\\BigCraftablesInformation")) + { + IDictionary data = asset.AsDictionary().Data; + foreach (BigCraftableData big in Mod.instance.bigCraftables) + { + try + { + Log.trace($"Injecting to big craftables: {big.GetCraftableId()}: {big.GetCraftableInformation()}"); + data.Add(big.GetCraftableId(), big.GetCraftableInformation()); + } + catch (Exception e) + { + Log.error($"Exception injecting object information for {big.Name}: {e}"); + } + } + } + else if (asset.AssetNameEquals("Data\\hats")) + { + IDictionary data = asset.AsDictionary().Data; + foreach (HatData hat in Mod.instance.hats) + { + try + { + Log.trace($"Injecting to hats: {hat.GetHatId()}: {hat.GetHatInformation()}"); + data.Add(hat.GetHatId(), hat.GetHatInformation()); + } + catch (Exception e) + { + Log.error($"Exception injecting hat information for {hat.Name}: {e}"); + } + } + } + else if (asset.AssetNameEquals("Data\\weapons")) + { + IDictionary data = asset.AsDictionary().Data; + foreach (WeaponData weapon in Mod.instance.weapons) + { + try + { + Log.trace($"Injecting to weapons: {weapon.GetWeaponId()}: {weapon.GetWeaponInformation()}"); + data.Add(weapon.GetWeaponId(), weapon.GetWeaponInformation()); + } + catch (Exception e) + { + Log.error($"Exception injecting weapon information for {weapon.Name}: {e}"); + } + } + } + else if (asset.AssetNameEquals("Data\\NPCGiftTastes")) + { + IDictionary data = asset.AsDictionary().Data; + // TODO: This could be optimized from mn to... m + n? + // Basically, iterate through objects and create Dictionary + // Iterate through objects, each section and add to dict[npc][approp. section] + // Point is, I'm doing this the lazy way right now + Dictionary newData = new Dictionary(data); + foreach (KeyValuePair npc in data) + { + if (npc.Key.StartsWith("Universal_")) + continue; + + string[] sections = npc.Value.Split('/'); + if ( sections.Length != 11 ) + { + Log.warn($"Bad gift taste data for {npc.Key}!"); + continue; + } + + string loveStr = sections[0]; + List loveIds = new List(sections[1].Split(' ')); + string likeStr = sections[2]; + List likeIds = new List(sections[3].Split(' ')); + string dislikeStr = sections[4]; + List dislikeIds = new List(sections[5].Split(' ')); + string hateStr = sections[6]; + List hateIds = new List(sections[7].Split(' ')); + string neutralStr = sections[8]; + List neutralIds = new List(sections[9].Split(' ')); + + foreach (ObjectData obj in Mod.instance.objects ) + { + if (obj.GiftTastes == null) + continue; + if (obj.GiftTastes.Love != null && obj.GiftTastes.Love.Contains(npc.Key)) + loveIds.Add(obj.GetObjectId().ToString()); + if (obj.GiftTastes.Like != null && obj.GiftTastes.Like.Contains(npc.Key)) + likeIds.Add(obj.GetObjectId().ToString()); + if (obj.GiftTastes.Neutral != null && obj.GiftTastes.Neutral.Contains(npc.Key)) + neutralIds.Add(obj.GetObjectId().ToString()); + if (obj.GiftTastes.Dislike != null && obj.GiftTastes.Dislike.Contains(npc.Key)) + dislikeIds.Add(obj.GetObjectId().ToString()); + if (obj.GiftTastes.Hate != null && obj.GiftTastes.Hate.Contains(npc.Key)) + hateIds.Add(obj.GetObjectId().ToString()); + } + + string loveIdStr = string.Join(" ", loveIds); + string likeIdStr = string.Join(" ", likeIds); + string dislikeIdStr = string.Join(" ", dislikeIds); + string hateIdStr = string.Join(" ", hateIds); + string neutralIdStr = string.Join(" ", neutralIds); + newData[npc.Key] = $"{loveStr}/{loveIdStr}/{likeStr}/{likeIdStr}/{dislikeStr}/{dislikeIdStr}/{hateStr}/{hateIdStr}/{neutralStr}/{neutralIdStr}/ "; + + Log.trace($"Adding gift tastes for {npc.Key}: {newData[npc.Key]}"); + } + asset.ReplaceWith(newData); + } + else if (asset.AssetNameEquals("Maps\\springobjects")) + { + Texture2D oldTex = asset.AsImage().Data; + Texture2D newTex = new Texture2D(Game1.graphics.GraphicsDevice, oldTex.Width, Math.Max(oldTex.Height, 4096)); + asset.ReplaceWith(newTex); + asset.AsImage().PatchImage(oldTex); + + foreach (ObjectData obj in Mod.instance.objects) + { + try + { + Log.trace($"Injecting {obj.Name} sprites @ {this.objectRect(obj.GetObjectId())}"); + asset.AsImage().PatchImage(obj.texture, null, this.objectRect(obj.GetObjectId())); + if (obj.IsColored) + { + Log.trace($"Injecting {obj.Name} color sprites @ {this.objectRect(obj.GetObjectId() + 1)}"); + asset.AsImage().PatchImage(obj.textureColor, null, this.objectRect(obj.GetObjectId() + 1)); + } + } + catch ( Exception e ) + { + Log.error($"Exception injecting sprite for {obj.Name}: {e}"); + } + } + } + else if (asset.AssetNameEquals("TileSheets\\crops")) + { + Texture2D oldTex = asset.AsImage().Data; + Texture2D newTex = new Texture2D(Game1.graphics.GraphicsDevice, oldTex.Width, Math.Max(oldTex.Height, 4096)); + asset.ReplaceWith(newTex); + asset.AsImage().PatchImage(oldTex); + + foreach (CropData crop in Mod.instance.crops) + { + try + { + Log.trace($"Injecting {crop.Name} crop images @ {this.cropRect(crop.GetCropSpriteIndex())}"); + asset.AsImage().PatchImage(crop.texture, null, this.cropRect(crop.GetCropSpriteIndex())); + } + catch (Exception e) + { + Log.error($"Exception injecting crop sprite for {crop.Name}: {e}"); + } + } + } + else if (asset.AssetNameEquals("TileSheets\\fruitTrees")) + { + Texture2D oldTex = asset.AsImage().Data; + Texture2D newTex = new Texture2D(Game1.graphics.GraphicsDevice, oldTex.Width, Math.Max(oldTex.Height, 4096)); + asset.ReplaceWith(newTex); + asset.AsImage().PatchImage(oldTex); + + foreach (FruitTreeData fruitTree in Mod.instance.fruitTrees) + { + try + { + Log.trace($"Injecting {fruitTree.Name} fruit tree images @ {this.fruitTreeRect(fruitTree.GetFruitTreeIndex())}"); + asset.AsImage().PatchImage(fruitTree.texture, null, this.fruitTreeRect(fruitTree.GetFruitTreeIndex())); + } + catch (Exception e) + { + Log.error($"Exception injecting fruit tree sprite for {fruitTree.Name}: {e}"); + } + } + } + else if (asset.AssetNameEquals("TileSheets\\Craftables") || asset.AssetNameEquals("TileSheets\\Craftables_indoor") || asset.AssetNameEquals("TileSheets\\Craftables_outdoor")) + { + Texture2D oldTex = asset.AsImage().Data; + Texture2D newTex = new Texture2D(Game1.graphics.GraphicsDevice, oldTex.Width, Math.Max(oldTex.Height, 4096)); + asset.ReplaceWith(newTex); + asset.AsImage().PatchImage(oldTex); + Log.trace($"Big craftables are now ({oldTex.Width}, {Math.Max(oldTex.Height, 4096)})"); + + foreach (BigCraftableData big in Mod.instance.bigCraftables) + { + try + { + Log.trace($"Injecting {big.Name} sprites @ {this.bigCraftableRect(big.GetCraftableId())}"); + asset.AsImage().PatchImage(big.texture, null, this.bigCraftableRect(big.GetCraftableId())); + } + catch (Exception e) + { + Log.error($"Exception injecting sprite for {big.Name}: {e}"); + } + } + } + else if (asset.AssetNameEquals("Characters\\Farmer\\hats")) + { + Texture2D oldTex = asset.AsImage().Data; + Texture2D newTex = new Texture2D(Game1.graphics.GraphicsDevice, oldTex.Width, Math.Max(oldTex.Height, 4096)); + asset.ReplaceWith(newTex); + asset.AsImage().PatchImage(oldTex); + Log.trace($"Hats are now ({oldTex.Width}, {Math.Max(oldTex.Height, 4096)})"); + + foreach (HatData hat in Mod.instance.hats) + { + try + { + Log.trace($"Injecting {hat.Name} sprites @ {this.hatRect(hat.GetHatId())}"); + asset.AsImage().PatchImage(hat.texture, null, this.hatRect(hat.GetHatId())); + } + catch (Exception e) + { + Log.error($"Exception injecting sprite for {hat.Name}: {e}"); + } + } + } + else if (asset.AssetNameEquals("TileSheets\\weapons")) + { + Texture2D oldTex = asset.AsImage().Data; + Texture2D newTex = new Texture2D(Game1.graphics.GraphicsDevice, oldTex.Width, Math.Max(oldTex.Height, 4096)); + asset.ReplaceWith(newTex); + asset.AsImage().PatchImage(oldTex); + Log.trace($"Weapons are now ({oldTex.Width}, {Math.Max(oldTex.Height, 4096)})"); + + foreach (WeaponData weapon in Mod.instance.weapons) + { + try + { + Log.trace($"Injecting {weapon.Name} sprites @ {this.weaponRect(weapon.GetWeaponId())}"); + asset.AsImage().PatchImage(weapon.texture, null, this.weaponRect(weapon.GetWeaponId())); + } + catch (Exception e) + { + Log.error($"Exception injecting sprite for {weapon.Name}: {e}"); + } + } + } + } + private Rectangle objectRect(int index) + { + return new Rectangle(index % 24 * 16, index / 24 * 16, 16, 16); + } + private Rectangle cropRect(int index) + { + return new Rectangle(index % 2 * 128, index / 2 * 32, 128, 32); + } + private Rectangle fruitTreeRect(int index) + { + return new Rectangle(0, index * 80, 432, 80); + } + private Rectangle bigCraftableRect(int index) + { + return new Rectangle(index % 8 * 16, index / 8 * 32, 16, 32); + } + private Rectangle hatRect(int index) + { + return new Rectangle(index % 12 * 20, index / 12 * 80, 20, 80); + } + private Rectangle weaponRect(int index) + { + return new Rectangle(index % 8 * 16, index / 8 * 16, 16, 16); + } + } +} diff --git a/Mods/JsonAssets/Data/BigCraftableData.cs b/Mods/JsonAssets/Data/BigCraftableData.cs new file mode 100644 index 00000000..c44a57d9 --- /dev/null +++ b/Mods/JsonAssets/Data/BigCraftableData.cs @@ -0,0 +1,103 @@ +using Microsoft.Xna.Framework.Graphics; +using Newtonsoft.Json; +using StardewValley; +using System.Collections.Generic; + +namespace JsonAssets.Data +{ + public class BigCraftableData : DataNeedsId + { + [JsonIgnore] + internal Texture2D texture; + + public class Recipe_ + { + public class Ingredient + { + public object Object { get; set; } + public int Count { get; set; } + } + // Possibly friendship option (letters, like vanilla) and/or skill levels (on levelup?) + public int ResultCount { get; set; } = 1; + public IList Ingredients { get; set; } = new List(); + + public bool IsDefault { get; set; } = false; + public bool CanPurchase { get; set; } = false; + public int PurchasePrice { get; set; } + public string PurchaseFrom { get; set; } = "Gus"; + public IList PurchaseRequirements { get; set; } = new List(); + + internal string GetRecipeString( BigCraftableData parent ) + { + string str = ""; + foreach (Ingredient ingredient in this.Ingredients) + str += Mod.instance.ResolveObjectId(ingredient.Object) + " " + ingredient.Count + " "; + str = str.Substring(0, str.Length - 1); + str += $"/what is this for?/{parent.id}/true/null"; + return str; + } + + internal string GetPurchaseRequirementString() + { + string str = $"1234567890"; + foreach (string cond in this.PurchaseRequirements) + str += $"/{cond}"; + return str; + } + } + + public string Description { get; set; } + + public int Price { get; set; } + + public bool ProvidesLight { get; set; } = false; + + public Recipe_ Recipe { get; set; } + + public bool CanPurchase { get; set; } = false; + public int PurchasePrice { get; set; } + public string PurchaseFrom { get; set; } = "Pierre"; + public IList PurchaseRequirements { get; set; } = new List(); + + public Dictionary NameLocalization = new Dictionary(); + public Dictionary DescriptionLocalization = new Dictionary(); + + public string LocalizedName() + { + LocalizedContentManager.LanguageCode currLang = LocalizedContentManager.CurrentLanguageCode; + if (currLang == LocalizedContentManager.LanguageCode.en) + return this.Name; + if (this.NameLocalization == null || !this.NameLocalization.ContainsKey(currLang.ToString())) + return this.Name; + return this.NameLocalization[currLang.ToString()]; + } + + public string LocalizedDescription() + { + LocalizedContentManager.LanguageCode currLang = LocalizedContentManager.CurrentLanguageCode; + if (currLang == LocalizedContentManager.LanguageCode.en) + return this.Description; + if (this.DescriptionLocalization == null || !this.DescriptionLocalization.ContainsKey(currLang.ToString())) + return this.Description; + return this.DescriptionLocalization[currLang.ToString()]; + } + + public int GetCraftableId() { return this.id; } + + internal string GetCraftableInformation() + { + string str = $"{this.Name}/{this.Price}/-300/Crafting -9/{this.LocalizedDescription()}/true/true/0/{this.LocalizedName()}"; + if (this.ProvidesLight) + str += "/true"; + return str; + } + + internal string GetPurchaseRequirementString() + { + string str = $"1234567890"; + foreach (string cond in this.PurchaseRequirements) + str += $"/{cond}"; + return str; + } + } +} diff --git a/Mods/JsonAssets/Data/ContentPackData.cs b/Mods/JsonAssets/Data/ContentPackData.cs new file mode 100644 index 00000000..8e50ee21 --- /dev/null +++ b/Mods/JsonAssets/Data/ContentPackData.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace JsonAssets.Data +{ + public class ContentPackData + { + public string Name { get; set; } + public string Description { get; set; } + public string Version { get; set; } + public string Author { get; set; } + public IList UpdateKeys { get; set; } = new List(); + } +} diff --git a/Mods/JsonAssets/Data/CropData.cs b/Mods/JsonAssets/Data/CropData.cs new file mode 100644 index 00000000..9dfb34fe --- /dev/null +++ b/Mods/JsonAssets/Data/CropData.cs @@ -0,0 +1,73 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace JsonAssets.Data +{ + public class CropData : DataNeedsId + { + [JsonIgnore] + internal Texture2D texture; + + public object Product { get; set; } + public string SeedName { get; set; } + public string SeedDescription { get; set; } + + public IList Seasons { get; set; } = new List(); + public IList Phases { get; set; } = new List(); + public int RegrowthPhase { get; set; } = -1; + public bool HarvestWithScythe { get; set; } = false; + public bool TrellisCrop { get; set; } = false; + public IList Colors { get; set; } = new List(); + public class Bonus_ + { + public int MinimumPerHarvest { get; set; } + public int MaximumPerHarvest { get; set; } + public int MaxIncreasePerFarmLevel { get; set; } + public double ExtraChance { get; set; } + } + public Bonus_ Bonus { get; set; } = null; + + public IList SeedPurchaseRequirements { get; set; } = new List(); + public int SeedPurchasePrice { get; set; } + public string SeedPurchaseFrom { get; set; } = "Pierre"; + + public Dictionary SeedNameLocalization = new Dictionary(); + public Dictionary SeedDescriptionLocalization = new Dictionary(); + + internal ObjectData seed; + public int GetSeedId() { return this.seed.id; } + public int GetCropSpriteIndex() { return this.id; } + internal string GetCropInformation() + { + string str = ""; + //str += GetProductId() + "/"; + foreach (int phase in this.Phases ) + { + str += phase + " "; + } + str = str.Substring(0, str.Length - 1) + "/"; + foreach (string season in this.Seasons) + { + str += season + " "; + } + str = str.Substring(0, str.Length - 1) + "/"; + str += $"{this.GetCropSpriteIndex()}/{Mod.instance.ResolveObjectId(this.Product)}/{this.RegrowthPhase}/"; + str += (this.HarvestWithScythe ? "1" : "0") + "/"; + if (this.Bonus != null) + str += $"true {this.Bonus.MinimumPerHarvest} {this.Bonus.MaximumPerHarvest} {this.Bonus.MaxIncreasePerFarmLevel} {this.Bonus.ExtraChance}/"; + else str += "false/"; + str += (this.TrellisCrop ? "true" : "false") + "/"; + if (this.Colors != null && this.Colors.Count > 0) + { + str += "true"; + foreach (Color color in this.Colors) + str += $" {color.R} {color.G} {color.B}"; + } + else + str += "false"; + return str; + } + } +} diff --git a/Mods/JsonAssets/Data/DataNeedsId.cs b/Mods/JsonAssets/Data/DataNeedsId.cs new file mode 100644 index 00000000..a18f2ec3 --- /dev/null +++ b/Mods/JsonAssets/Data/DataNeedsId.cs @@ -0,0 +1,9 @@ +namespace JsonAssets.Data +{ + public abstract class DataNeedsId + { + public string Name { get; set; } + + internal int id = -1; + } +} diff --git a/Mods/JsonAssets/Data/FruitTreeData.cs b/Mods/JsonAssets/Data/FruitTreeData.cs new file mode 100644 index 00000000..97a5e19b --- /dev/null +++ b/Mods/JsonAssets/Data/FruitTreeData.cs @@ -0,0 +1,33 @@ +using Microsoft.Xna.Framework.Graphics; +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace JsonAssets.Data +{ + public class FruitTreeData : DataNeedsId + { + [JsonIgnore] + internal Texture2D texture; + + public object Product { get; set; } + public string SaplingName { get; set; } + public string SaplingDescription { get; set; } + + public string Season { get; set; } + + public IList SaplingPurchaseRequirements { get; set; } = new List(); + public int SaplingPurchasePrice { get; set; } + public string SaplingPurchaseFrom { get; set; } = "Pierre"; + + public Dictionary SaplingNameLocalization = new Dictionary(); + public Dictionary SaplingDescriptionLocalization = new Dictionary(); + + internal ObjectData sapling; + public int GetSaplingId() { return this.sapling.id; } + public int GetFruitTreeIndex() { return this.id; } + internal string GetFruitTreeInformation() + { + return $"{this.GetFruitTreeIndex()}/{this.Season}/{Mod.instance.ResolveObjectId(this.Product)}/what goes here?"; + } + } +} diff --git a/Mods/JsonAssets/Data/HatData.cs b/Mods/JsonAssets/Data/HatData.cs new file mode 100644 index 00000000..c1142d22 --- /dev/null +++ b/Mods/JsonAssets/Data/HatData.cs @@ -0,0 +1,48 @@ +using Microsoft.Xna.Framework.Graphics; +using Newtonsoft.Json; +using StardewValley; +using System.Collections.Generic; + +namespace JsonAssets.Data +{ + class HatData : DataNeedsId + { + [JsonIgnore] + internal Texture2D texture; + + public string Description { get; set; } + public int PurchasePrice { get; set; } + public bool ShowHair { get; set; } + public bool IgnoreHairstyleOffset { get; set; } + + public Dictionary NameLocalization = new Dictionary(); + public Dictionary DescriptionLocalization = new Dictionary(); + + public string LocalizedName() + { + LocalizedContentManager.LanguageCode currLang = LocalizedContentManager.CurrentLanguageCode; + if (currLang == LocalizedContentManager.LanguageCode.en) + return this.Name; + if (this.NameLocalization == null || !this.NameLocalization.ContainsKey(currLang.ToString())) + return this.Name; + return this.NameLocalization[currLang.ToString()]; + } + + public string LocalizedDescription() + { + LocalizedContentManager.LanguageCode currLang = LocalizedContentManager.CurrentLanguageCode; + if (currLang == LocalizedContentManager.LanguageCode.en) + return this.Description; + if (this.DescriptionLocalization == null || !this.DescriptionLocalization.ContainsKey(currLang.ToString())) + return this.Description; + return this.DescriptionLocalization[currLang.ToString()]; + } + + public int GetHatId() { return this.id; } + + internal string GetHatInformation() + { + return $"{this.Name}/{this.LocalizedDescription()}/" + (this.ShowHair ? "true" : "false" ) + "/" + (this.IgnoreHairstyleOffset ? "true" : "false") + $"/{this.LocalizedName()}"; + } + } +} diff --git a/Mods/JsonAssets/Data/ObjectData.cs b/Mods/JsonAssets/Data/ObjectData.cs new file mode 100644 index 00000000..82bc9168 --- /dev/null +++ b/Mods/JsonAssets/Data/ObjectData.cs @@ -0,0 +1,175 @@ +using Microsoft.Xna.Framework.Graphics; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using StardewValley; +using System.Collections.Generic; +using SObject = StardewValley.Object; + +namespace JsonAssets.Data +{ + public class ObjectData : DataNeedsId + { + [JsonIgnore] + internal Texture2D texture; + [JsonIgnore] + internal Texture2D textureColor; + + [JsonConverter(typeof(StringEnumConverter))] + public enum Category_ + { + // SDV Patcher made these static readonly, so I can't use them in the enum + Vegetable = -75, //SObject.VegetableCategory, + Fruit = -79, //SObject.FruitsCategory, + Flower = -80, //SObject.flowersCategory, + Gem = -2, //SObject.GemCategory, + Fish = -4, //SObject.FishCategory, + Egg = -5, //SObject.EggCategory, + Milk = -6, //SObject.MilkCategory, + Cooking = -7, //SObject.CookingCategory, + Crafting = -8, //SObject.CraftingCategory, + Mineral = -12, //SObject.mineralsCategory, + Meat = -14, //SObject.meatCategory, + Metal = -15, //SObject.metalResources, + Junk = -20, //SObject.junkCategory, + Syrup = -27, //SObject.syrupCategory, + MonsterLoot = -28, //SObject.monsterLootCategory, + ArtisanGoods = -26, //SObject.artisanGoodsCategory, + Seeds = -74, //SObject.SeedsCategory, + Ring = -96, //SObject.ringCategory, + AnimalGoods = -18, //SObject.sellAtPierresAndMarnies + } + + public class Recipe_ + { + public class Ingredient + { + public object Object { get; set; } + public int Count { get; set; } + } + // Possibly friendship option (letters, like vanilla) and/or skill levels (on levelup?) + public int ResultCount { get; set; } = 1; + public IList Ingredients { get; set; } = new List(); + + public bool IsDefault { get; set; } = false; + public bool CanPurchase { get; set; } = false; + public int PurchasePrice { get; set; } + public string PurchaseFrom { get; set; } = "Gus"; + public IList PurchaseRequirements { get; set; } = new List(); + + internal string GetRecipeString( ObjectData parent ) + { + string str = ""; + foreach (Ingredient ingredient in this.Ingredients) + str += Mod.instance.ResolveObjectId(ingredient.Object) + " " + ingredient.Count + " "; + str = str.Substring(0, str.Length - 1); + str += $"/what is this for?/{parent.id}/"; + if (parent.Category != Category_.Cooking) + str += "false/"; + str += "/null"; // TODO: Requirement + return str; + } + + internal string GetPurchaseRequirementString() + { + string str = $"1234567890"; + foreach (string cond in this.PurchaseRequirements) + str += $"/{cond}"; + return str; + } + } + + public class FoodBuffs_ + { + public int Farming { get; set; } = 0; + public int Fishing { get; set; } = 0; + public int Mining { get; set; } = 0; + public int Luck { get; set; } = 0; + public int Foraging { get; set; } = 0; + public int MaxStamina { get; set; } = 0; + public int MagnetRadius { get; set; } = 0; + public int Speed { get; set; } = 0; + public int Defense { get; set; } = 0; + public int Attack { get; set; } = 0; + public int Duration { get; set; } = 0; + } + + public string Description { get; set; } + public Category_ Category { get; set; } + public bool IsColored { get; set; } = false; + + public int Price { get; set; } + + public Recipe_ Recipe { get; set; } + + public int Edibility { get; set; } = SObject.inedible; + public bool EdibleIsDrink { get; set; } = false; + public FoodBuffs_ EdibleBuffs = new FoodBuffs_(); + + public bool CanPurchase { get; set; } = false; + public int PurchasePrice { get; set; } + public string PurchaseFrom { get; set; } = "Pierre"; + public IList PurchaseRequirements { get; set; } = new List(); + + public class GiftTastes_ + { + public IList Love = new List(); + public IList Like = new List(); + public IList Neutral = new List(); + public IList Dislike = new List(); + public IList Hate = new List(); + } + public GiftTastes_ GiftTastes; + + public Dictionary NameLocalization = new Dictionary(); + public Dictionary DescriptionLocalization = new Dictionary(); + + public string LocalizedName() + { + LocalizedContentManager.LanguageCode currLang = LocalizedContentManager.CurrentLanguageCode; + if (currLang == LocalizedContentManager.LanguageCode.en) + return this.Name; + if (this.NameLocalization == null || !this.NameLocalization.ContainsKey(currLang.ToString())) + return this.Name; + return this.NameLocalization[currLang.ToString()]; + } + + public string LocalizedDescription() + { + LocalizedContentManager.LanguageCode currLang = LocalizedContentManager.CurrentLanguageCode; + if (currLang == LocalizedContentManager.LanguageCode.en) + return this.Description; + if (this.DescriptionLocalization == null || !this.DescriptionLocalization.ContainsKey(currLang.ToString())) + return this.Description; + return this.DescriptionLocalization[currLang.ToString()]; + } + + public int GetObjectId() { return this.id; } + + internal string GetObjectInformation() + { + if (this.Edibility != SObject.inedible) + { + int itype = (int)this.Category; + string str = $"{this.Name}/{this.Price}/{this.Edibility}/{this.Category} {itype}/{this.LocalizedName()}/{this.LocalizedDescription()}/"; + str += (this.EdibleIsDrink ? "drink" : "food") + "/"; + if (this.EdibleBuffs == null) + this.EdibleBuffs = new FoodBuffs_(); + str += $"{this.EdibleBuffs.Farming} {this.EdibleBuffs.Fishing} {this.EdibleBuffs.Mining} 0 {this.EdibleBuffs.Luck} {this.EdibleBuffs.Foraging} 0 {this.EdibleBuffs.MaxStamina} {this.EdibleBuffs.MagnetRadius} {this.EdibleBuffs.Speed} {this.EdibleBuffs.Defense} {this.EdibleBuffs.Attack}/{this.EdibleBuffs.Duration}"; + return str; + } + else + { + int itype = (int)this.Category; + return $"{this.Name}/{this.Price}/{this.Edibility}/Basic {itype}/{this.LocalizedName()}/{this.LocalizedDescription()}"; + } + } + + internal string GetPurchaseRequirementString() + { + string str = $"1234567890"; + foreach (string cond in this.PurchaseRequirements) + str += $"/{cond}"; + return str; + } + } +} diff --git a/Mods/JsonAssets/Data/WeaponData.cs b/Mods/JsonAssets/Data/WeaponData.cs new file mode 100644 index 00000000..726eb91d --- /dev/null +++ b/Mods/JsonAssets/Data/WeaponData.cs @@ -0,0 +1,82 @@ +using Microsoft.Xna.Framework.Graphics; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using StardewValley; +using StardewValley.Tools; +using System.Collections.Generic; +using SObject = StardewValley.Object; + +namespace JsonAssets.Data +{ + public class WeaponData : DataNeedsId + { + [JsonIgnore] + internal Texture2D texture; + + [JsonConverter(typeof(StringEnumConverter))] + public enum Type_ + { + Dagger = MeleeWeapon.dagger, + Club = MeleeWeapon.club, + Sword = MeleeWeapon.defenseSword, + } + + public string Description { get; set; } + public Type_ Type { get; set; } + + public int MinimumDamage { get; set; } + public int MaximumDamage { get; set; } + public double Knockback { get; set; } + public int Speed { get; set; } + public int Accuracy { get; set; } + public int Defense { get; set; } + public int MineDropVar { get; set; } + public int MineDropMinimumLevel { get; set; } + public int ExtraSwingArea { get; set; } + public double CritChance { get; set; } + public double CritMultiplier { get; set; } + + public bool CanPurchase { get; set; } = false; + public int PurchasePrice { get; set; } + public string PurchaseFrom { get; set; } = "Pierre"; + public IList PurchaseRequirements { get; set; } = new List(); + + public Dictionary NameLocalization = new Dictionary(); + public Dictionary DescriptionLocalization = new Dictionary(); + + public string LocalizedName() + { + LocalizedContentManager.LanguageCode currLang = LocalizedContentManager.CurrentLanguageCode; + if (currLang == LocalizedContentManager.LanguageCode.en) + return this.Name; + if (this.NameLocalization == null || !this.NameLocalization.ContainsKey(currLang.ToString())) + return this.Name; + return this.NameLocalization[currLang.ToString()]; + } + + public string LocalizedDescription() + { + LocalizedContentManager.LanguageCode currLang = LocalizedContentManager.CurrentLanguageCode; + if (currLang == LocalizedContentManager.LanguageCode.en) + return this.Description; + if (this.DescriptionLocalization == null || !this.DescriptionLocalization.ContainsKey(currLang.ToString())) + return this.Description; + return this.DescriptionLocalization[currLang.ToString()]; + } + + public int GetWeaponId() { return this.id; } + + internal string GetWeaponInformation() + { + return $"{this.Name}/{this.LocalizedDescription()}/{this.MinimumDamage}/{this.MaximumDamage}/{this.Knockback}/{this.Speed}/{this.Accuracy}/{this.Defense}/{(int)this.Type}/{this.MineDropVar}/{this.MineDropMinimumLevel}/{this.ExtraSwingArea}/{this.CritChance}/{this.CritMultiplier}/{this.LocalizedName()}"; + } + + internal string GetPurchaseRequirementString() + { + string str = $"1234567890"; + foreach (string cond in this.PurchaseRequirements) + str += $"/{cond}"; + return str; + } + } +} diff --git a/Mods/JsonAssets/JsonAssets.csproj b/Mods/JsonAssets/JsonAssets.csproj new file mode 100644 index 00000000..9209ea54 --- /dev/null +++ b/Mods/JsonAssets/JsonAssets.csproj @@ -0,0 +1,174 @@ + + + + + Debug + AnyCPU + {F56B5F8E-0069-4029-8DCD-89002B7285E3} + Library + Properties + JsonAssets + JsonAssets + v4.5.2 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\assemblies\StardewModdingAPI.dll + + + ..\assemblies\StardewValley.dll + + + False + ..\assemblies\BmFont.dll + + + False + ..\assemblies\Google.Android.Vending.Expansion.Downloader.dll + + + False + ..\assemblies\Google.Android.Vending.Expansion.ZipFile.dll + + + False + ..\assemblies\Google.Android.Vending.Licensing.dll + + + False + ..\assemblies\Java.Interop.dll + + + False + ..\assemblies\Microsoft.AppCenter.dll + + + False + ..\assemblies\Microsoft.AppCenter.Analytics.dll + + + False + ..\assemblies\Microsoft.AppCenter.Analytics.Android.Bindings.dll + + + False + ..\assemblies\Microsoft.AppCenter.Android.Bindings.dll + + + False + ..\assemblies\Microsoft.AppCenter.Crashes.dll + + + False + ..\assemblies\Microsoft.AppCenter.Crashes.Android.Bindings.dll + + + False + ..\assemblies\Mono.Android.dll + + + False + ..\assemblies\Mono.Security.dll + + + False + ..\assemblies\MonoGame.Framework.dll + + + ..\assemblies\mscorlib.dll + + + ..\assemblies\System.dll + + + ..\assemblies\System.Xml.dll + + + ..\assemblies\System.Net.Http.dll + + + ..\assemblies\System.Runtime.Serialization.dll + + + False + ..\assemblies\Xamarin.Android.Arch.Core.Common.dll + + + False + ..\assemblies\Xamarin.Android.Arch.Lifecycle.Common.dll + + + False + ..\assemblies\Xamarin.Android.Arch.Lifecycle.Runtime.dll + + + False + ..\assemblies\Xamarin.Android.Support.Annotations.dll + + + False + ..\assemblies\Xamarin.Android.Support.Compat.dll + + + False + ..\assemblies\Xamarin.Android.Support.Core.UI.dll + + + False + ..\assemblies\Xamarin.Android.Support.Core.Utils.dll + + + False + ..\assemblies\Xamarin.Android.Support.Fragment.dll + + + False + ..\assemblies\Xamarin.Android.Support.Media.Compat.dll + + + False + ..\assemblies\Xamarin.Android.Support.v4.dll + + + False + ..\assemblies\xTile.dll + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Mods/JsonAssets/Log.cs b/Mods/JsonAssets/Log.cs new file mode 100644 index 00000000..1f31e159 --- /dev/null +++ b/Mods/JsonAssets/Log.cs @@ -0,0 +1,33 @@ +using StardewModdingAPI; +using System; + +namespace JsonAssets +{ + class Log + { + public static void trace(String str) + { + Mod.instance.Monitor.Log(str, LogLevel.Trace); + } + + public static void debug(String str) + { + Mod.instance.Monitor.Log(str, LogLevel.Debug); + } + + public static void info(String str) + { + Mod.instance.Monitor.Log(str, LogLevel.Info); + } + + public static void warn(String str) + { + Mod.instance.Monitor.Log(str, LogLevel.Warn); + } + + public static void error(String str) + { + Mod.instance.Monitor.Log(str, LogLevel.Error); + } + } +} diff --git a/Mods/JsonAssets/Mod.cs b/Mods/JsonAssets/Mod.cs new file mode 100644 index 00000000..5e95e11b --- /dev/null +++ b/Mods/JsonAssets/Mod.cs @@ -0,0 +1,925 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using JsonAssets.Data; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI; +using StardewModdingAPI.Events; +using StardewValley; +using StardewValley.Menus; +using StardewValley.Locations; +using StardewValley.TerrainFeatures; +using StardewValley.Objects; +using System.Reflection; +using Netcode; +using StardewValley.Buildings; +using Harmony; +using System.Text.RegularExpressions; +using JsonAssets.Overrides; +using Newtonsoft.Json; +using StardewValley.Tools; + +// TODO: Refactor recipes + +namespace JsonAssets +{ + public class Mod : StardewModdingAPI.Mod + { + public static Mod instance; + + /// The mod entry point, called after the mod is first loaded. + /// Provides simplified APIs for writing mods. + public override void Entry(IModHelper helper) + { + instance = this; + + helper.Events.Display.MenuChanged += this.onMenuChanged; + helper.Events.GameLoop.Saved += this.onSaved; + helper.Events.Player.InventoryChanged += this.onInventoryChanged; + helper.Events.GameLoop.SaveCreated += this.onCreated; + helper.Events.Specialised.LoadStageChanged += this.onLoadStageChanged; + helper.Events.Multiplayer.PeerContextReceived += this.clientConnected; + + Log.info("Loading content packs..."); + foreach (IContentPack contentPack in this.Helper.ContentPacks.GetOwned()) + this.loadData(contentPack); + if (Directory.Exists(Path.Combine(this.Helper.DirectoryPath, "ContentPacks"))) + { + foreach (string dir in Directory.EnumerateDirectories(Path.Combine(this.Helper.DirectoryPath, "ContentPacks"))) + this.loadData(dir); + } + + this.resetAtTitle(); + + try + { + helper.Events.Hook.ObjectCanBePlacedHere += (args) => ObjectCanPlantHereOverride.Prefix(args.__instance, args.location, args.tile, ref args.__result); + helper.Events.Hook.ObjectCheckForAction += (args) => ObjectNoActionHook.Prefix(args.__instance); + helper.Events.Hook.ObjectIsIndexOkForBasicShippedCategory += (args) => { ObjectCollectionShippingHook.Postfix(args.index, ref args.__result); return true; }; + } + catch (Exception e) + { + Log.error($"Exception doing harmony stuff: {e}"); + } + } + + private Api api; + public override object GetApi() + { + return this.api ?? (this.api = new Api(this.loadData)); + } + + private void loadData(string dir) + { + // read initial info + IContentPack temp = this.Helper.ContentPacks.CreateFake(dir); + ContentPackData info = temp.ReadJsonFile("content-pack.json"); + if (info == null) + { + Log.warn($"\tNo {dir}/content-pack.json!"); + return; + } + + // load content pack + IContentPack contentPack = this.Helper.ContentPacks.CreateTemporary(dir, id: Guid.NewGuid().ToString("N"), name: info.Name, description: info.Description, author: info.Author, version: new SemanticVersion(info.Version)); + this.loadData(contentPack); + } + + private Dictionary dupObjects = new Dictionary(); + private Dictionary dupCrops = new Dictionary(); + private Dictionary dupFruitTrees = new Dictionary(); + private Dictionary dupBigCraftables = new Dictionary(); + private Dictionary dupHats = new Dictionary(); + private Dictionary dupWeapons = new Dictionary(); + + private readonly Regex SeasonLimiter = new Regex("(z(?: spring| summer| fall| winter){2,4})", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private void loadData(IContentPack contentPack) + { + Log.info($"\t{contentPack.Manifest.Name} {contentPack.Manifest.Version} by {contentPack.Manifest.Author} - {contentPack.Manifest.Description}"); + + // load objects + DirectoryInfo objectsDir = new DirectoryInfo(Path.Combine(contentPack.DirectoryPath, "Objects")); + if (objectsDir.Exists) + { + foreach (DirectoryInfo dir in objectsDir.EnumerateDirectories()) + { + string relativePath = $"Objects/{dir.Name}"; + + // load data + ObjectData obj = contentPack.ReadJsonFile($"{relativePath}/object.json"); + if (obj == null) + continue; + + // save object + obj.texture = contentPack.LoadAsset($"{relativePath}/object.png"); + if (obj.IsColored) + obj.textureColor = contentPack.LoadAsset($"{relativePath}/color.png"); + this.objects.Add(obj); + + // save ring + if (obj.Category == ObjectData.Category_.Ring) + this.myRings.Add(obj); + + // Duplicate check + if (this.dupObjects.ContainsKey(obj.Name)) + Log.error($"Duplicate object: {obj.Name} just added by {contentPack.Manifest.Name}, already added by {this.dupObjects[obj.Name].Manifest.Name}!"); + else + this.dupObjects[obj.Name] = contentPack; + } + } + + // load crops + DirectoryInfo cropsDir = new DirectoryInfo(Path.Combine(contentPack.DirectoryPath, "Crops")); + if (cropsDir.Exists) + { + foreach (DirectoryInfo dir in cropsDir.EnumerateDirectories()) + { + string relativePath = $"Crops/{dir.Name}"; + + // load data + CropData crop = contentPack.ReadJsonFile($"{relativePath}/crop.json"); + if (crop == null) + continue; + + // save crop + crop.texture = contentPack.LoadAsset($"{relativePath}/crop.png"); + this.crops.Add(crop); + + // save seeds + crop.seed = new ObjectData + { + texture = contentPack.LoadAsset($"{relativePath}/seeds.png"), + Name = crop.SeedName, + Description = crop.SeedDescription, + Category = ObjectData.Category_.Seeds, + Price = crop.SeedPurchasePrice, + CanPurchase = true, + PurchaseFrom = crop.SeedPurchaseFrom, + PurchasePrice = crop.SeedPurchasePrice, + PurchaseRequirements = crop.SeedPurchaseRequirements ?? new List(), + NameLocalization = crop.SeedNameLocalization, + DescriptionLocalization = crop.SeedDescriptionLocalization + }; + + // TODO: Clean up this chunk + // I copy/pasted it from the unofficial update decompiled + string str = ""; + string[] array = new[] { "spring", "summer", "fall", "winter" } + .Except(crop.Seasons) + .ToArray(); + foreach (string season in array) + { + str += $"/z {season}"; + } + string strtrimstart = str.TrimStart(new char[] { '/' }); + if (crop.SeedPurchaseRequirements != null && crop.SeedPurchaseRequirements.Count > 0) + { + for (int index = 0; index < crop.SeedPurchaseRequirements.Count; index++) + { + if (this.SeasonLimiter.IsMatch(crop.SeedPurchaseRequirements[index])) + { + crop.SeedPurchaseRequirements[index] = strtrimstart; + Log.warn($" Faulty season requirements for {crop.SeedName}!\n Fixed season requirements: {crop.SeedPurchaseRequirements[index]}"); + } + } + if (!crop.SeedPurchaseRequirements.Contains(str.TrimStart('/'))) + { + Log.trace($" Adding season requirements for {crop.SeedName}:\n New season requirements: {strtrimstart}"); + crop.seed.PurchaseRequirements.Add(strtrimstart); + } + } + else + { + Log.trace($" Adding season requirements for {crop.SeedName}:\n New season requirements: {strtrimstart}"); + crop.seed.PurchaseRequirements.Add(strtrimstart); + } + + this.objects.Add(crop.seed); + + // Duplicate check + if (this.dupCrops.ContainsKey(crop.Name)) + Log.error($"Duplicate crop: {crop.Name} just added by {contentPack.Manifest.Name}, already added by {this.dupCrops[crop.Name].Manifest.Name}!"); + else + this.dupCrops[crop.Name] = contentPack; + } + } + + // load fruit trees + DirectoryInfo fruitTreesDir = new DirectoryInfo(Path.Combine(contentPack.DirectoryPath, "FruitTrees")); + if (fruitTreesDir.Exists) + { + foreach (DirectoryInfo dir in fruitTreesDir.EnumerateDirectories()) + { + string relativePath = $"FruitTrees/{dir.Name}"; + + // load data + FruitTreeData tree = contentPack.ReadJsonFile($"{relativePath}/tree.json"); + if (tree == null) + continue; + + // save fruit tree + tree.texture = contentPack.LoadAsset($"{relativePath}/tree.png"); + this.fruitTrees.Add(tree); + + // save seed + tree.sapling = new ObjectData + { + texture = contentPack.LoadAsset($"{relativePath}/sapling.png"), + Name = tree.SaplingName, + Description = tree.SaplingDescription, + Category = ObjectData.Category_.Seeds, + Price = tree.SaplingPurchasePrice, + CanPurchase = true, + PurchaseRequirements = tree.SaplingPurchaseRequirements, + PurchaseFrom = tree.SaplingPurchaseFrom, + PurchasePrice = tree.SaplingPurchasePrice, + NameLocalization = tree.SaplingNameLocalization, + DescriptionLocalization = tree.SaplingDescriptionLocalization + }; + this.objects.Add(tree.sapling); + + // Duplicate check + if (this.dupFruitTrees.ContainsKey(tree.Name)) + Log.error($"Duplicate fruit tree: {tree.Name} just added by {contentPack.Manifest.Name}, already added by {this.dupFruitTrees[tree.Name].Manifest.Name}!"); + else + this.dupFruitTrees[tree.Name] = contentPack; + } + } + + // load big craftables + DirectoryInfo bigCraftablesDir = new DirectoryInfo(Path.Combine(contentPack.DirectoryPath, "BigCraftables")); + if (bigCraftablesDir.Exists) + { + foreach (DirectoryInfo dir in bigCraftablesDir.EnumerateDirectories()) + { + string relativePath = $"BigCraftables/{dir.Name}"; + + // load data + BigCraftableData craftable = contentPack.ReadJsonFile($"{relativePath}/big-craftable.json"); + if (craftable == null) + continue; + + // save craftable + craftable.texture = contentPack.LoadAsset($"{relativePath}/big-craftable.png"); + this.bigCraftables.Add(craftable); + + // Duplicate check + if (this.dupBigCraftables.ContainsKey(craftable.Name)) + Log.error($"Duplicate big craftable: {craftable.Name} just added by {contentPack.Manifest.Name}, already added by {this.dupBigCraftables[craftable.Name].Manifest.Name}!"); + else + this.dupBigCraftables[craftable.Name] = contentPack; + } + } + + // load hats + DirectoryInfo hatsDir = new DirectoryInfo(Path.Combine(contentPack.DirectoryPath, "Hats")); + if (hatsDir.Exists) + { + foreach (DirectoryInfo dir in hatsDir.EnumerateDirectories()) + { + string relativePath = $"Hats/{dir.Name}"; + + // load data + HatData hat = contentPack.ReadJsonFile($"{relativePath}/hat.json"); + if (hat == null) + continue; + + // save object + hat.texture = contentPack.LoadAsset($"{relativePath}/hat.png"); + this.hats.Add(hat); + + // Duplicate check + if (this.dupHats.ContainsKey(hat.Name)) + Log.error($"Duplicate hat: {hat.Name} just added by {contentPack.Manifest.Name}, already added by {this.dupHats[hat.Name].Manifest.Name}!"); + else + this.dupBigCraftables[hat.Name] = contentPack; + } + } + + // Load weapons + // load objects + DirectoryInfo weaponsDir = new DirectoryInfo(Path.Combine(contentPack.DirectoryPath, "Weapons")); + if (weaponsDir.Exists) + { + foreach (DirectoryInfo dir in weaponsDir.EnumerateDirectories()) + { + string relativePath = $"Weapons/{dir.Name}"; + + // load data + WeaponData weapon = contentPack.ReadJsonFile($"{relativePath}/weapon.json"); + if (weapon == null) + continue; + + // save object + weapon.texture = contentPack.LoadAsset($"{relativePath}/weapon.png"); + this.weapons.Add(weapon); + + // Duplicate check + if (this.dupWeapons.ContainsKey(weapon.Name)) + Log.error($"Duplicate weapon: {weapon.Name} just added by {contentPack.Manifest.Name}, already added by {this.dupWeapons[weapon.Name].Manifest.Name}!"); + else + this.dupBigCraftables[weapon.Name] = contentPack; + } + } + } + + private void resetAtTitle() + { + this.didInit = false; + // When we go back to the title menu we need to reset things so things don't break when + // going back to a save. + this.clearIds(out this.objectIds, this.objects.ToList()); + this.clearIds(out this.cropIds, this.crops.ToList()); + this.clearIds(out this.fruitTreeIds, this.fruitTrees.ToList()); + this.clearIds(out this.bigCraftableIds, this.bigCraftables.ToList()); + this.clearIds(out this.hatIds, this.hats.ToList()); + this.clearIds(out this.weaponIds, this.weapons.ToList()); + + IAssetEditor editor = this.Helper.Content.AssetEditors.FirstOrDefault(p => p is ContentInjector); + if (editor != null) + this.Helper.Content.AssetEditors.Remove(editor); + } + + private void onCreated(object sender, SaveCreatedEventArgs e) + { + Log.debug("Loading stuff early (creation)"); + this.initStuff( loadIdFiles: false ); + } + + private void onLoadStageChanged(object sender, LoadStageChangedEventArgs e) + { + if (e.NewStage == StardewModdingAPI.Enums.LoadStage.SaveParsed) + { + Log.debug("Loading stuff early (loading)"); + this.initStuff( loadIdFiles: true ); + } + else if ( e.NewStage == StardewModdingAPI.Enums.LoadStage.SaveLoadedLocations ) + { + Log.debug("Fixing IDs"); + this.fixIdsEverywhere(); + } + else if ( e.NewStage == StardewModdingAPI.Enums.LoadStage.Loaded ) + { + Log.debug("Adding default recipes"); + foreach (ObjectData obj in this.objects) + { + if (obj.Recipe != null && obj.Recipe.IsDefault && !Game1.player.knowsRecipe(obj.Name)) + { + if (obj.Category == ObjectData.Category_.Cooking) + { + Game1.player.cookingRecipes.Add(obj.Name, 0); + } + else + { + Game1.player.craftingRecipes.Add(obj.Name, 0); + } + } + } + foreach (BigCraftableData big in this.bigCraftables) + { + if (big.Recipe != null && big.Recipe.IsDefault && !Game1.player.knowsRecipe(big.Name)) + { + Game1.player.craftingRecipes.Add(big.Name, 0); + } + } + } + } + + private void clientConnected(object sender, PeerContextReceivedEventArgs e) + { + if (!Context.IsMainPlayer && !this.didInit) + { + Log.debug("Loading stuff early (MP client)"); + this.initStuff( loadIdFiles: false ); + } + } + + /// Raised after a game menu is opened, closed, or replaced. + /// The event sender. + /// The event arguments. + private void onMenuChanged(object sender, MenuChangedEventArgs e) + { + if ( e.NewMenu == null ) + return; + + if ( e.NewMenu is TitleMenu ) + { + this.resetAtTitle(); + return; + } + if (e.OldMenu is ShopMenu) + { + return; + } + + ShopMenu menu = e.NewMenu as ShopMenu; + bool hatMouse = menu != null && menu.potraitPersonDialogue == Game1.parseText(Game1.content.LoadString("Strings\\StringsFromCSFiles:ShopMenu.cs.11494"), Game1.dialogueFont, Game1.tileSize * 5 - Game1.pixelZoom * 4); + if (menu == null || menu.portraitPerson == null && !hatMouse) + return; + + //if (menu.portraitPerson.name == "Pierre") + { + Log.trace($"Adding objects to {menu.portraitPerson?.Name}'s shop"); + List forSale = this.Helper.Reflection.GetField>(menu, "forSale").GetValue(); + int count = forSale.Count; + Dictionary itemPriceAndStock = this.Helper.Reflection.GetField>(menu, "itemPriceAndStock").GetValue(); + + IReflectedMethod precondMeth = this.Helper.Reflection.GetMethod(Game1.currentLocation, "checkEventPrecondition"); + foreach (ObjectData obj in this.objects) + { + if (obj.Recipe != null && obj.Recipe.CanPurchase) + { + bool add = true; + // Can't use continue here or the item might not sell + if (obj.Recipe.PurchaseFrom != menu.portraitPerson?.Name || (obj.Recipe.PurchaseFrom == "HatMouse" && hatMouse) ) + add = false; + if (Game1.player.craftingRecipes.ContainsKey(obj.Name) || Game1.player.cookingRecipes.ContainsKey(obj.Name)) + add = false; + if (obj.Recipe.PurchaseRequirements != null && obj.Recipe.PurchaseRequirements.Count > 0 && + precondMeth.Invoke(new object[] { obj.Recipe.GetPurchaseRequirementString() }) == -1) + add = false; + if (add) + { + StardewValley.Object recipeObj = new StardewValley.Object(obj.id, 1, true, obj.Recipe.PurchasePrice, 0); + forSale.Add(recipeObj); + itemPriceAndStock.Add(recipeObj, new int[] { obj.Recipe.PurchasePrice, 1 }); + Log.trace($"\tAdding recipe for {obj.Name}"); + } + } + if (!obj.CanPurchase) + continue; + if (obj.PurchaseFrom != menu.portraitPerson?.Name || (obj.PurchaseFrom == "HatMouse" && hatMouse)) + continue; + if (obj.PurchaseRequirements != null && obj.PurchaseRequirements.Count > 0 && + precondMeth.Invoke(new object[] { obj.GetPurchaseRequirementString() }) == -1) + continue; + Item item = new StardewValley.Object(Vector2.Zero, obj.id, int.MaxValue); + forSale.Add(item); + itemPriceAndStock.Add(item, new int[] { obj.PurchasePrice, int.MaxValue }); + Log.trace($"\tAdding {obj.Name}"); + } + foreach (BigCraftableData big in this.bigCraftables) + { + if (big.Recipe != null && big.Recipe.CanPurchase) + { + bool add = true; + // Can't use continue here or the item might not sell + if (big.Recipe.PurchaseFrom != menu.portraitPerson?.Name || (big.Recipe.PurchaseFrom == "HatMouse" && hatMouse)) + add = false; + if (Game1.player.craftingRecipes.ContainsKey(big.Name) || Game1.player.cookingRecipes.ContainsKey(big.Name)) + add = false; + if (big.Recipe.PurchaseRequirements != null && big.Recipe.PurchaseRequirements.Count > 0 && + precondMeth.Invoke(new object[] { big.Recipe.GetPurchaseRequirementString() }) == -1) + add = false; + if (add) + { + StardewValley.Object recipeObj = new StardewValley.Object(new Vector2(0, 0), big.id, true); + forSale.Add(recipeObj); + itemPriceAndStock.Add(recipeObj, new int[] { big.Recipe.PurchasePrice, 1 }); + Log.trace($"\tAdding recipe for {big.Name}"); + } + } + if (!big.CanPurchase) + continue; + if (big.PurchaseFrom != menu.portraitPerson?.Name || (big.PurchaseFrom == "HatMouse" && hatMouse)) + continue; + if (big.PurchaseRequirements != null && big.PurchaseRequirements.Count > 0 && + precondMeth.Invoke(new object[] { big.GetPurchaseRequirementString() }) == -1) + continue; + Item item = new StardewValley.Object(Vector2.Zero, big.id, false); + forSale.Add(item); + itemPriceAndStock.Add(item, new int[] { big.PurchasePrice, int.MaxValue }); + Log.trace($"\tAdding {big.Name}"); + } + if ( hatMouse ) + { + foreach (HatData hat in this.hats ) + { + Item item = new Hat(hat.GetHatId()); + forSale.Add(item); + itemPriceAndStock.Add(item, new int[] { hat.PurchasePrice, int.MaxValue }); + Log.trace($"\tAdding {hat.Name}"); + } + } + foreach (WeaponData weapon in this.weapons) + { + if (!weapon.CanPurchase) + continue; + if (weapon.PurchaseFrom != menu.portraitPerson?.Name || (weapon.PurchaseFrom == "HatMouse" && hatMouse)) + continue; + if (weapon.PurchaseRequirements != null && weapon.PurchaseRequirements.Count > 0 && + precondMeth.Invoke(new object[] { weapon.GetPurchaseRequirementString() }) == -1) + continue; + Item item = new StardewValley.Tools.MeleeWeapon(weapon.id); + forSale.Add(item); + itemPriceAndStock.Add(item, new int[] { weapon.PurchasePrice, int.MaxValue }); + Log.trace($"\tAdding {weapon.Name}"); + } + if(count != forSale.Count) + { + Game1.activeClickableMenu = new ShopMenu(itemPriceAndStock, this.Helper.Reflection.GetField(menu, "currency").GetValue(), this.Helper.Reflection.GetField(menu, "personName").GetValue()); + } + } + + ( ( Api )this.api ).InvokeAddedItemsToShop(); + } + + private bool didInit = false; + private void initStuff( bool loadIdFiles ) + { + if (this.didInit) + return; + this.didInit = true; + + // load object ID mappings from save folder + if (loadIdFiles) + { + IDictionary LoadDictionary(string filename) + { + string path = Path.Combine(Constants.CurrentSavePath, "JsonAssets", filename); + return File.Exists(path) + ? JsonConvert.DeserializeObject>(File.ReadAllText(path)) + : new Dictionary(); + } + Directory.CreateDirectory(Path.Combine(Constants.CurrentSavePath, "JsonAssets")); + this.oldObjectIds = LoadDictionary("ids-objects.json"); + this.oldCropIds = LoadDictionary("ids-crops.json"); + this.oldFruitTreeIds = LoadDictionary("ids-fruittrees.json"); + this.oldBigCraftableIds = LoadDictionary("ids-big-craftables.json"); + this.oldHatIds = LoadDictionary("ids-hats.json"); + this.oldWeaponIds = LoadDictionary("ids-weapons.json"); + + Log.trace("OLD IDS START"); + foreach (KeyValuePair id in this.oldObjectIds) + Log.trace("\tObject " + id.Key + " = " + id.Value); + foreach (KeyValuePair id in this.oldCropIds) + Log.trace("\tCrop " + id.Key + " = " + id.Value); + foreach (KeyValuePair id in this.oldFruitTreeIds) + Log.trace("\tFruit Tree " + id.Key + " = " + id.Value); + foreach (KeyValuePair id in this.oldBigCraftableIds) + Log.trace("\tBigCraftable " + id.Key + " = " + id.Value); + foreach (KeyValuePair id in this.oldHatIds) + Log.trace("\tHat " + id.Key + " = " + id.Value); + foreach (KeyValuePair id in this.oldWeaponIds) + Log.trace("\tWeapon " + id.Key + " = " + id.Value); + Log.trace("OLD IDS END"); + } + + // assign IDs + this.objectIds = this.AssignIds("objects", StartingObjectId, this.objects.ToList()); + this.cropIds = this.AssignIds("crops", StartingCropId, this.crops.ToList()); + this.fruitTreeIds = this.AssignIds("fruittrees", StartingFruitTreeId, this.fruitTrees.ToList()); + this.bigCraftableIds = this.AssignIds("big-craftables", StartingBigCraftableId, this.bigCraftables.ToList()); + this.hatIds = this.AssignIds("hats", StartingHatId, this.hats.ToList()); + this.weaponIds = this.AssignIds("weapons", StartingWeaponId, this.weapons.ToList()); + + this.api.InvokeIdsAssigned(); + + // init + this.Helper.Content.AssetEditors.Add(new ContentInjector()); + } + + /// Raised after the game finishes writing data to the save file (except the initial save creation). + /// The event sender. + /// The event arguments. + private void onSaved(object sender, SavedEventArgs e) + { + if (!Directory.Exists(Path.Combine(Constants.CurrentSavePath, "JsonAssets"))) + Directory.CreateDirectory(Path.Combine(Constants.CurrentSavePath, "JsonAssets")); + + File.WriteAllText(Path.Combine(Constants.CurrentSavePath, "JsonAssets", "ids-objects.json"), JsonConvert.SerializeObject(this.objectIds)); + File.WriteAllText(Path.Combine(Constants.CurrentSavePath, "JsonAssets", "ids-crops.json"), JsonConvert.SerializeObject(this.cropIds)); + File.WriteAllText(Path.Combine(Constants.CurrentSavePath, "JsonAssets", "ids-fruittrees.json"), JsonConvert.SerializeObject(this.fruitTreeIds)); + File.WriteAllText(Path.Combine(Constants.CurrentSavePath, "JsonAssets", "ids-big-craftables.json"), JsonConvert.SerializeObject(this.bigCraftableIds)); + File.WriteAllText(Path.Combine(Constants.CurrentSavePath, "JsonAssets", "ids-hats.json"), JsonConvert.SerializeObject(this.hatIds)); + File.WriteAllText(Path.Combine(Constants.CurrentSavePath, "JsonAssets", "ids-weapons.json"), JsonConvert.SerializeObject(this.weaponIds)); + } + + internal IList myRings = new List(); + + /// Raised after items are added or removed to a player's inventory. NOTE: this event is currently only raised for the current player. + /// The event sender. + /// The event arguments. + private void onInventoryChanged(object sender, InventoryChangedEventArgs e) + { + if (!e.IsLocalPlayer) + return; + + IList ringIds = new List(); + foreach (ObjectData ring in this.myRings) + ringIds.Add(ring.id); + + for (int i = 0; i < Game1.player.Items.Count; ++i) + { + Item item = Game1.player.Items[i]; + if (item is StardewValley.Object obj && ringIds.Contains(obj.ParentSheetIndex)) + { + Log.trace($"Turning a ring-object of {obj.ParentSheetIndex} into a proper ring"); + Game1.player.Items[i] = new StardewValley.Objects.Ring(obj.ParentSheetIndex); + } + } + } + + private const int StartingObjectId = 2000; + private const int StartingCropId = 100; + private const int StartingFruitTreeId = 10; + private const int StartingBigCraftableId = 300; + private const int StartingHatId = 50; + private const int StartingWeaponId = 64; + + internal IList objects = new List(); + internal IList crops = new List(); + internal IList fruitTrees = new List(); + internal IList bigCraftables = new List(); + internal IList hats = new List(); + internal IList weapons = new List(); + + internal IDictionary objectIds; + internal IDictionary cropIds; + internal IDictionary fruitTreeIds; + internal IDictionary bigCraftableIds; + internal IDictionary hatIds; + internal IDictionary weaponIds; + + internal IDictionary oldObjectIds; + internal IDictionary oldCropIds; + internal IDictionary oldFruitTreeIds; + internal IDictionary oldBigCraftableIds; + internal IDictionary oldHatIds; + internal IDictionary oldWeaponIds; + + internal IDictionary origObjects; + internal IDictionary origCrops; + internal IDictionary origFruitTrees; + internal IDictionary origBigCraftables; + internal IDictionary origHats; + internal IDictionary origWeapons; + + public int ResolveObjectId(object data) + { + if (data.GetType() == typeof(long)) + return (int)(long)data; + else + { + if (this.objectIds.ContainsKey((string)data)) + return this.objectIds[(string)data]; + + foreach (KeyValuePair obj in Game1.objectInformation ) + { + if (obj.Value.Split('/')[0] == (string)data) + return obj.Key; + } + + Log.warn($"No idea what '{data}' is!"); + return 0; + } + } + + private Dictionary AssignIds(string type, int starting, IList data) + { + Dictionary ids = new Dictionary(); + + int currId = starting; + foreach (DataNeedsId d in data) + { + if (d.id == -1) + { + Log.trace($"New ID: {d.Name} = {currId}"); + ids.Add(d.Name, currId++); + if (type == "objects" && ((ObjectData)d).IsColored) + ++currId; + d.id = ids[d.Name]; + } + } + + return ids; + } + + private void clearIds(out IDictionary ids, List objs) + { + ids = null; + foreach ( DataNeedsId obj in objs ) + { + obj.id = -1; + } + } + + private IDictionary cloneIdDictAndRemoveOurs( IDictionary full, IDictionary ours ) + { + Dictionary ret = new Dictionary(full); + foreach (KeyValuePair obj in ours) + ret.Remove(obj.Value); + return ret; + } + + private void fixIdsEverywhere() + { + this.origObjects = this.cloneIdDictAndRemoveOurs(Game1.objectInformation, this.objectIds); + this.origCrops = this.cloneIdDictAndRemoveOurs(Game1.content.Load>("Data\\Crops"), this.cropIds); + this.origFruitTrees = this.cloneIdDictAndRemoveOurs(Game1.content.Load>("Data\\fruitTrees"), this.fruitTreeIds); + this.origBigCraftables = this.cloneIdDictAndRemoveOurs(Game1.bigCraftablesInformation, this.bigCraftableIds); + this.origHats = this.cloneIdDictAndRemoveOurs(Game1.content.Load>("Data\\hats"), this.hatIds); + this.origWeapons = this.cloneIdDictAndRemoveOurs(Game1.content.Load>("Data\\weapons"), this.weaponIds); + + this.fixItemList(Game1.player.Items); + foreach (GameLocation loc in Game1.locations ) + this.fixLocation(loc); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage( "SMAPI.CommonErrors", "AvoidNetField") ] + private void fixLocation( GameLocation loc ) + { + if (loc is FarmHouse fh) + { +#pragma warning disable AvoidImplicitNetFieldCast + if (fh.fridge.Value?.items != null) +#pragma warning restore AvoidImplicitNetFieldCast + this.fixItemList(fh.fridge.Value.items); + } + + IList toRemove = new List(); + foreach (Vector2 tfk in loc.terrainFeatures.Keys ) + { + TerrainFeature tf = loc.terrainFeatures[tfk]; + if ( tf is HoeDirt hd ) + { + if (hd.crop == null) + continue; + + if (this.fixId(this.oldCropIds, this.cropIds, hd.crop.rowInSpriteSheet, this.origCrops)) + hd.crop = null; + else + { + string key = this.cropIds.FirstOrDefault(x => x.Value == hd.crop.rowInSpriteSheet.Value).Key; + CropData c = this.crops.FirstOrDefault(x => x.Name == key); + if ( c != null ) // Non-JA crop + hd.crop.indexOfHarvest.Value = this.ResolveObjectId(c.Product); + } + } + else if ( tf is FruitTree ft ) + { + if (this.fixId(this.oldFruitTreeIds, this.fruitTreeIds, ft.treeType, this.origFruitTrees)) + toRemove.Add(tfk); + else + { + string key = this.oldFruitTreeIds.FirstOrDefault(x => x.Value == ft.treeType.Value).Key; + FruitTreeData ftt = this.fruitTrees.FirstOrDefault(x => x.Name == key); + if ( ftt != null ) // Non-JA fruit tree + ft.indexOfFruit.Value = this.ResolveObjectId(ftt.Product); + } + } + } + foreach (Vector2 rem in toRemove) + loc.terrainFeatures.Remove(rem); + + toRemove.Clear(); + foreach (Vector2 objk in loc.netObjects.Keys ) + { + StardewValley.Object obj = loc.netObjects[objk]; + if ( obj is Chest chest ) + { + this.fixItemList(chest.items); + } + else + { + if (!obj.bigCraftable.Value) + { + if (this.fixId(this.oldObjectIds, this.objectIds, obj.parentSheetIndex, this.origObjects)) + toRemove.Add(objk); + } + else + { + if (this.fixId(this.oldBigCraftableIds, this.bigCraftableIds, obj.parentSheetIndex, this.origBigCraftables)) + toRemove.Add(objk); + } + } + + if ( obj.heldObject.Value != null ) + { + if (this.fixId(this.oldObjectIds, this.objectIds, obj.heldObject.Value.parentSheetIndex, this.origObjects)) + obj.heldObject.Value = null; + + if ( obj.heldObject.Value is Chest chest2 ) + { + this.fixItemList(chest2.items); + } + } + } + foreach (Vector2 rem in toRemove) + loc.objects.Remove(rem); + + toRemove.Clear(); + foreach (Vector2 objk in loc.overlayObjects.Keys) + { + StardewValley.Object obj = loc.overlayObjects[objk]; + if (obj is Chest chest) + { + this.fixItemList(chest.items); + } + else + { + if (!obj.bigCraftable.Value) + { + if (this.fixId(this.oldObjectIds, this.objectIds, obj.parentSheetIndex, this.origObjects)) + toRemove.Add(objk); + } + else + { + if (this.fixId(this.oldBigCraftableIds, this.bigCraftableIds, obj.parentSheetIndex, this.origBigCraftables)) + toRemove.Add(objk); + } + } + + if (obj.heldObject.Value != null) + { + if (this.fixId(this.oldObjectIds, this.objectIds, obj.heldObject.Value.parentSheetIndex, this.origObjects)) + obj.heldObject.Value = null; + + if (obj.heldObject.Value is Chest chest2) + { + this.fixItemList(chest2.items); + } + } + } + foreach (Vector2 rem in toRemove) + loc.overlayObjects.Remove(rem); + + if (loc is BuildableGameLocation buildLoc) + foreach (Building building in buildLoc.buildings) + { + if (building.indoors.Value != null) + this.fixLocation(building.indoors.Value); + if ( building is Mill mill ) + { + this.fixItemList(mill.input.Value.items); + this.fixItemList(mill.output.Value.items); + } + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("SMAPI.CommonErrors", "AvoidNetField")] + private void fixItemList( IList< Item > items ) + { + for ( int i = 0; i < items.Count; ++i ) + { + Item item = items[i]; + if ( item is StardewValley.Object obj ) + { + if (!obj.bigCraftable.Value) + { + if (this.fixId(this.oldObjectIds, this.objectIds, obj.parentSheetIndex, this.origObjects)) + items[i] = null; + } + else + { + if (this.fixId(this.oldBigCraftableIds, this.bigCraftableIds, obj.parentSheetIndex, this.origBigCraftables)) + items[i] = null; + } + } + else if ( item is Hat hat ) + { + if (this.fixId(this.oldHatIds, this.hatIds, hat.which, this.origHats)) + items[i] = null; + } + else if ( item is MeleeWeapon weapon ) + { + if (this.fixId(this.oldWeaponIds, this.weaponIds, weapon.initialParentTileIndex, this.origWeapons)) + items[i] = null; + else if (this.fixId(this.oldWeaponIds, this.weaponIds, weapon.currentParentTileIndex, this.origWeapons)) + items[i] = null; + else if (this.fixId(this.oldWeaponIds, this.weaponIds, weapon.currentParentTileIndex, this.origWeapons)) + items[i] = null; + } + else if ( item is Ring ring ) + { + if (this.fixId(this.oldObjectIds, this.objectIds, ring.indexInTileSheet, this.origObjects)) + items[i] = null; + } + } + } + + // Return true if the item should be deleted, false otherwise. + // Only remove something if old has it but not new + private bool fixId(IDictionary oldIds, IDictionary newIds, NetInt id, IDictionary origData ) + { + if (origData.ContainsKey(id.Value)) + return false; + + if (oldIds.Values.Contains(id.Value)) + { + int id_ = id.Value; + string key = oldIds.FirstOrDefault(x => x.Value == id_).Key; + + if (newIds.ContainsKey(key)) + { + id.Value = newIds[key]; + return false; + } + else return true; + } + else return false; + } + } +} diff --git a/Mods/JsonAssets/Overrides/Object.cs b/Mods/JsonAssets/Overrides/Object.cs new file mode 100644 index 00000000..aa8cd480 --- /dev/null +++ b/Mods/JsonAssets/Overrides/Object.cs @@ -0,0 +1,71 @@ +using Microsoft.Xna.Framework; +using StardewValley; +using StardewValley.Objects; + +namespace JsonAssets.Overrides +{ + public class ObjectCanPlantHereOverride + { + public static bool Prefix(StardewValley.Object __instance, GameLocation l, Vector2 tile, ref bool __result) + { + if (!__instance.bigCraftable.Value && Mod.instance.objectIds.Values.Contains(__instance.ParentSheetIndex)) + { + if (__instance.Category == StardewValley.Object.SeedsCategory) + { + bool isTree = false; + foreach (Data.FruitTreeData tree in Mod.instance.fruitTrees) + { + if (tree.sapling.id == __instance.ParentSheetIndex) + { + isTree = true; + break; + } + } + + Object lobj = l.objects.ContainsKey(tile) ? l.objects[tile] : null; + if (isTree) + { + __result = lobj == null && !l.isTileOccupiedForPlacement(tile, __instance); + return false; + } + else + { + if (l.isTileHoeDirt(tile) || (lobj is IndoorPot)) + __result = l.isTileOccupiedForPlacement(tile); + else + __result = false; + return false; + } + } + return true; + } + else + return true; + } + } + + public static class ObjectNoActionHook + { + public static bool Prefix(StardewValley.Object __instance) + { + if (__instance.bigCraftable.Value && Mod.instance.bigCraftableIds.Values.Contains(__instance.ParentSheetIndex)) + return false; + return true; + } + } + + public static class ObjectCollectionShippingHook + { + public static void Postfix(int index, ref bool __result) + { + foreach (Data.ObjectData ring in Mod.instance.myRings) + { + if (ring.GetObjectId() == index) + { + __result = false; + break; + } + } + } + } +} diff --git a/Mods/JsonAssets/Properties/AssemblyInfo.cs b/Mods/JsonAssets/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..0879c7a9 --- /dev/null +++ b/Mods/JsonAssets/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// 有关程序集的一般信息由以下 +// 控制。更改这些特性值可修改 +// 与程序集关联的信息。 +[assembly: AssemblyTitle("JsonAssets")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("JsonAssets")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// 将 ComVisible 设置为 false 会使此程序集中的类型 +//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型 +//请将此类型的 ComVisible 特性设置为 true。 +[assembly: ComVisible(false)] + +// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID +[assembly: Guid("f56b5f8e-0069-4029-8dcd-89002b7285e3")] + +// 程序集的版本信息由下列四个值组成: +// +// 主版本 +// 次版本 +// 生成号 +// 修订号 +// +// 可以指定所有值,也可以使用以下所示的 "*" 预置版本号和修订号 +//通过使用 "*",如下所示: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Mods/JsonAssets/Util.cs b/Mods/JsonAssets/Util.cs new file mode 100644 index 00000000..d5bf3a2e --- /dev/null +++ b/Mods/JsonAssets/Util.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace JsonAssets +{ + // Copied from SpaceCore + // TODO: Add SC as a dependency instead + public class Util + { + // Stolen from SMAPI + public static void invokeEvent(string name, IEnumerable handlers, object sender) + { + EventArgs args = new EventArgs(); + foreach (EventHandler handler in handlers.Cast()) + { + try + { + handler.Invoke(sender, args); + } + catch (Exception e) + { + Log.error($"Exception while handling event {name}:\n{e}"); + } + } + } + } +} diff --git a/Mods/UI Info Suite/IconHandler.cs b/Mods/UI Info Suite/IconHandler.cs index 08295b99..53a0b8f9 100644 --- a/Mods/UI Info Suite/IconHandler.cs +++ b/Mods/UI Info Suite/IconHandler.cs @@ -28,7 +28,7 @@ namespace UIInfoSuite public Point GetNewIconPosition() { int yPos = Game1.options.zoomButtons ? 320 : 290; - int xPosition = (int)Tools.GetWidthInPlayArea() - 214 - 46 * this._amountOfVisibleIcons; + int xPosition = (int)Tools.GetWidthInPlayArea() - 214 - 69 * this._amountOfVisibleIcons; ++this._amountOfVisibleIcons; return new Point(xPosition, yPos); } diff --git a/Mods/UI Info Suite/UIElements/ShowBirthdayIcon.cs b/Mods/UI Info Suite/UIElements/ShowBirthdayIcon.cs index 74b479ba..62ca197e 100644 --- a/Mods/UI Info Suite/UIElements/ShowBirthdayIcon.cs +++ b/Mods/UI Info Suite/UIElements/ShowBirthdayIcon.cs @@ -93,13 +93,13 @@ namespace UIInfoSuite.UIElements private void OnRenderingHud(object sender, EventArgs e) { // draw birthday icon - if (!Game1.eventUp) + if (!Game1.eventUp && Game1.activeClickableMenu == null) { if (this._birthdayNPC != null) { Rectangle headShot = this._birthdayNPC.GetHeadShot(); Point iconPosition = IconHandler.Handler.GetNewIconPosition(); - float scale = 2.9f; + float scale = 4.35f; Game1.spriteBatch.Draw( Game1.mouseCursors, diff --git a/Mods/UI Info Suite/UIElements/ShowQueenOfSauceIcon.cs b/Mods/UI Info Suite/UIElements/ShowQueenOfSauceIcon.cs index d03cd217..ed24118f 100644 --- a/Mods/UI Info Suite/UIElements/ShowQueenOfSauceIcon.cs +++ b/Mods/UI Info Suite/UIElements/ShowQueenOfSauceIcon.cs @@ -146,24 +146,24 @@ namespace UIInfoSuite.UIElements private void OnRenderingHud(object sender, RenderingHudEventArgs e) { // draw icon - if (!Game1.eventUp) + if (!Game1.eventUp && Game1.activeClickableMenu == null) { if (this._drawQueenOfSauceIcon) { Point iconPosition = IconHandler.Handler.GetNewIconPosition(); this._queenOfSauceIcon = new ClickableTextureComponent( - new Rectangle(iconPosition.X, iconPosition.Y, 40, 40), + new Rectangle(iconPosition.X, iconPosition.Y, 10 * 6, 10 * 6), Game1.mouseCursors, new Rectangle(609, 361, 28, 28), - 1.3f); + 1.95f); this._queenOfSauceIcon.draw(Game1.spriteBatch); } if (this._drawDishOfDayIcon) { Point iconLocation = IconHandler.Handler.GetNewIconPosition(); - float scale = 2.9f; + float scale = 4.35f; Game1.spriteBatch.Draw( Game1.objectSpriteSheet, @@ -188,7 +188,7 @@ namespace UIInfoSuite.UIElements this._gus.Name, this._gus.Sprite.Texture, this._gus.GetHeadShot(), - 2f); + 3f); texture.draw(Game1.spriteBatch); @@ -210,7 +210,7 @@ namespace UIInfoSuite.UIElements { // draw hover text if (this._drawQueenOfSauceIcon && - this._queenOfSauceIcon.containsPoint(Game1.getMouseX(), Game1.getMouseY())) + this._queenOfSauceIcon.containsPoint((int)(Game1.getMouseX() * Game1.options.zoomLevel), (int)(Game1.getMouseY() * Game1.options.zoomLevel))) { IClickableMenu.drawHoverText( Game1.spriteBatch, diff --git a/Mods/UI Info Suite/UIElements/ShowToolUpgradeStatus.cs b/Mods/UI Info Suite/UIElements/ShowToolUpgradeStatus.cs index 318a8d06..98a359ac 100644 --- a/Mods/UI Info Suite/UIElements/ShowToolUpgradeStatus.cs +++ b/Mods/UI Info Suite/UIElements/ShowToolUpgradeStatus.cs @@ -120,15 +120,15 @@ namespace UIInfoSuite.UIElements private void OnRenderingHud(object sender, RenderingHudEventArgs e) { // draw tool upgrade status - if (!Game1.eventUp && this._toolBeingUpgraded != null) + if (!Game1.eventUp && this._toolBeingUpgraded != null && Game1.activeClickableMenu == null) { Point iconPosition = IconHandler.Handler.GetNewIconPosition(); this._toolUpgradeIcon = new ClickableTextureComponent( - new Rectangle(iconPosition.X, iconPosition.Y, 40, 40), + new Rectangle(iconPosition.X, iconPosition.Y, 60, 60), Game1.toolSpriteSheet, this._toolTexturePosition, - 2.5f); + 3.75f); this._toolUpgradeIcon.draw(Game1.spriteBatch); } } diff --git a/Mods/UI Info Suite/UIElements/ShowTravelingMerchant.cs b/Mods/UI Info Suite/UIElements/ShowTravelingMerchant.cs index 3ed2824d..d6c1244d 100644 --- a/Mods/UI Info Suite/UIElements/ShowTravelingMerchant.cs +++ b/Mods/UI Info Suite/UIElements/ShowTravelingMerchant.cs @@ -60,15 +60,15 @@ namespace UIInfoSuite.UIElements private void OnRenderingHud(object sender, RenderingHudEventArgs e) { // draw traveling merchant - if (!Game1.eventUp && this._travelingMerchantIsHere) + if (!Game1.eventUp && this._travelingMerchantIsHere && Game1.activeClickableMenu == null) { Point iconPosition = IconHandler.Handler.GetNewIconPosition(); this._travelingMerchantIcon = new ClickableTextureComponent( - new Rectangle(iconPosition.X, iconPosition.Y, 40, 40), + new Rectangle(iconPosition.X, iconPosition.Y, 60, 60), Game1.mouseCursors, new Rectangle(192, 1411, 20, 20), - 2f); + 3f); this._travelingMerchantIcon.draw(Game1.spriteBatch); } } diff --git a/src/Mod.csproj b/src/Mod.csproj index 914dc98b..2ad54b11 100644 --- a/src/Mod.csproj +++ b/src/Mod.csproj @@ -443,6 +443,7 @@ + @@ -462,6 +463,8 @@ + + @@ -488,6 +491,7 @@ + @@ -513,6 +517,7 @@ + diff --git a/src/ModEntry.cs b/src/ModEntry.cs index 08bf94ed..79e70e0d 100644 --- a/src/ModEntry.cs +++ b/src/ModEntry.cs @@ -1,19 +1,8 @@ using System; -using System.Collections.Generic; using Microsoft.Xna.Framework; using StardewValley; -using StardewValley.Menus; using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Events; -using StardewModdingAPI.Framework.ModHelpers; -using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI; -using StardewModdingAPI.Events; -using StardewModdingAPI.Framework.Logging; using System.Threading; -using StardewModdingAPI.Internal.ConsoleWriting; -using StardewModdingAPI.Toolkit.Serialisation; -using StardewModdingAPI.Framework.Input; using Microsoft.Xna.Framework.Graphics; namespace SMDroid @@ -68,5 +57,17 @@ namespace SMDroid this.core.GameInstance.OnNewDayAfterFade(); base.OnGame1_NewDayAfterFade(action); } + public override bool OnObject_canBePlacedHere(StardewValley.Object __instance, GameLocation location, Vector2 tile, ref bool __result) + { + return this.core.GameInstance.OnObjectCanBePlacedHere(__instance, location, tile, ref __result); + } + public override void OnObject_isIndexOkForBasicShippedCategory(int index, ref bool __result) + { + this.core.GameInstance.OnObjectIsIndexOkForBasicShippedCategory(index, ref __result); + } + public override bool OnObject_checkForAction(StardewValley.Object __instance) + { + return this.core.GameInstance.OnObjectCheckForAction(__instance); + } } } diff --git a/src/SMAPI/Events/IHookEvents.cs b/src/SMAPI/Events/IHookEvents.cs new file mode 100644 index 00000000..62a84716 --- /dev/null +++ b/src/SMAPI/Events/IHookEvents.cs @@ -0,0 +1,18 @@ +using System; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Events related to UI and drawing to the screen. + public interface IHookEvents + { + /// Object.canBePlacedHere hook. + event Func ObjectCanBePlacedHere; + + /// Object.checkForAction hook. + event Func ObjectCheckForAction; + + /// Object.isIndexOkForBasicShippedCategory hook. + event Func ObjectIsIndexOkForBasicShippedCategory; + } +} diff --git a/src/SMAPI/Events/IModEvents.cs b/src/SMAPI/Events/IModEvents.cs index bd7ab880..8d57f62c 100644 --- a/src/SMAPI/Events/IModEvents.cs +++ b/src/SMAPI/Events/IModEvents.cs @@ -23,5 +23,6 @@ namespace StardewModdingAPI.Events /// Events serving specialised edge cases that shouldn't be used by most mods. ISpecialisedEvents Specialised { get; } + IHookEvents Hook { get; } } } diff --git a/src/SMAPI/Events/ObjectCanBePlacedHereEventArgs.cs b/src/SMAPI/Events/ObjectCanBePlacedHereEventArgs.cs new file mode 100644 index 00000000..00364c13 --- /dev/null +++ b/src/SMAPI/Events/ObjectCanBePlacedHereEventArgs.cs @@ -0,0 +1,34 @@ +using StardewValley; +using Microsoft.Xna.Framework; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class ObjectCanBePlacedHereEventArgs : System.EventArgs + { + /********* + ** Accessors + *********/ + public Object __instance { get; } + + public GameLocation location { get; } + + public Vector2 tile { get; } + + public bool __result; + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous window size. + /// The current window size. + internal ObjectCanBePlacedHereEventArgs(Object __instance, GameLocation location, Vector2 tile, bool __result) + { + this.__instance = __instance; + this.location = location; + this.tile = tile; + this.__result = __result; + } + } +} diff --git a/src/SMAPI/Events/ObjectCheckForActionEventArgs.cs b/src/SMAPI/Events/ObjectCheckForActionEventArgs.cs new file mode 100644 index 00000000..0b4b96ff --- /dev/null +++ b/src/SMAPI/Events/ObjectCheckForActionEventArgs.cs @@ -0,0 +1,24 @@ +using StardewValley; +using Microsoft.Xna.Framework; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class ObjectCheckForActionEventArgs : System.EventArgs + { + /********* + ** Accessors + *********/ + public Object __instance { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + internal ObjectCheckForActionEventArgs(Object __instance) + { + this.__instance = __instance; + } + } +} diff --git a/src/SMAPI/Events/ObjectIsIndexOkForBasicShippedCategoryEventArgs.cs b/src/SMAPI/Events/ObjectIsIndexOkForBasicShippedCategoryEventArgs.cs new file mode 100644 index 00000000..39bee3c4 --- /dev/null +++ b/src/SMAPI/Events/ObjectIsIndexOkForBasicShippedCategoryEventArgs.cs @@ -0,0 +1,29 @@ +using System; +using Microsoft.Xna.Framework; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class ObjectIsIndexOkForBasicShippedCategoryEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The index + public int index { get; } + + public bool __result; + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous window size. + /// The current window size. + internal ObjectIsIndexOkForBasicShippedCategoryEventArgs(int index, bool __result) + { + this.index = index; + this.__result = __result; + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 23879f1d..b2926105 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -163,6 +163,11 @@ namespace StardewModdingAPI.Framework.Events /// Raised after the game performs its overall update tick (≈60 times per second). See notes on . public readonly ManagedEvent UnvalidatedUpdateTicked; + public readonly ManagedEvent ObjectCanBePlacedHere; + + public readonly ManagedEvent ObjectCheckForAction; + + public readonly ManagedEvent ObjectIsIndexOkForBasicShippedCategory; /********* ** Public methods @@ -226,6 +231,10 @@ namespace StardewModdingAPI.Framework.Events this.LoadStageChanged = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.LoadStageChanged)); this.UnvalidatedUpdateTicking = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.UnvalidatedUpdateTicking)); this.UnvalidatedUpdateTicked = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.UnvalidatedUpdateTicked)); + + this.ObjectCheckForAction = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.LoadStageChanged)); + this.ObjectCanBePlacedHere = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.UnvalidatedUpdateTicking)); + this.ObjectIsIndexOkForBasicShippedCategory = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.UnvalidatedUpdateTicked)); } } } diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index 2afe7a03..64634365 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -14,6 +14,8 @@ namespace StardewModdingAPI.Framework.Events /// The underlying event. private event EventHandler Event; + private event Func Func; + /// A human-readable name for the event. private readonly string EventName; @@ -26,8 +28,11 @@ namespace StardewModdingAPI.Framework.Events /// The display names for the mods which added each delegate. private readonly IDictionary, IModMetadata> SourceMods = new Dictionary, IModMetadata>(); + private readonly IDictionary, IModMetadata> SourceModsFunc = new Dictionary, IModMetadata>(); + /// The cached invocation list. private EventHandler[] CachedInvocationList; + private Func[] CachedInvocationListFunc; /********* @@ -57,6 +62,11 @@ namespace StardewModdingAPI.Framework.Events this.Add(handler, this.ModRegistry.GetFromStack()); } + public void Add(Func handler) + { + this.Add(handler, this.ModRegistry.GetFromStack()); + } + /// Add an event handler. /// The event handler. /// The mod which added the event handler. @@ -66,6 +76,12 @@ namespace StardewModdingAPI.Framework.Events this.AddTracking(mod, handler, this.Event?.GetInvocationList().Cast>()); } + public void Add(Func handler, IModMetadata mod) + { + this.Func += handler; + this.AddTracking(mod, handler, this.Func?.GetInvocationList().Cast>()); + } + /// Remove an event handler. /// The event handler. public void Remove(EventHandler handler) @@ -74,6 +90,12 @@ namespace StardewModdingAPI.Framework.Events this.RemoveTracking(handler, this.Event?.GetInvocationList().Cast>()); } + public void Remove(Func handler) + { + this.Func -= handler; + this.RemoveTracking(handler, this.Event?.GetInvocationList().Cast>()); + } + /// Raise the event and notify all handlers. /// The event arguments to pass. public void Raise(TEventArgs args) @@ -94,6 +116,31 @@ namespace StardewModdingAPI.Framework.Events } } + /// Raise the event and notify all handlers wait for a actively response. + /// The event arguments to pass. + public bool RaiseForChainRun(TEventArgs args) + { + if (this.Func == null) + return true; + + foreach (Func handler in this.CachedInvocationListFunc) + { + try + { + bool run = handler.Invoke(args); + if (!run) + { + return false; + } + } + catch (Exception ex) + { + this.LogError(handler, ex); + } + } + return true; + } + /// Raise the event and notify all handlers. /// The event arguments to pass. /// A lambda which returns true if the event should be raised for the given mod. @@ -132,6 +179,12 @@ namespace StardewModdingAPI.Framework.Events this.CachedInvocationList = invocationList?.ToArray() ?? new EventHandler[0]; } + protected void AddTracking(IModMetadata mod, Func handler, IEnumerable> invocationList) + { + this.SourceModsFunc[handler] = mod; + this.CachedInvocationListFunc = invocationList?.ToArray() ?? new Func[0]; + } + /// Remove tracking for an event handler. /// The event handler. /// The updated event invocation list. @@ -141,6 +194,12 @@ namespace StardewModdingAPI.Framework.Events if (!this.CachedInvocationList.Contains(handler)) // don't remove if there's still a reference to the removed handler (e.g. it was added twice and removed once) this.SourceMods.Remove(handler); } + protected void RemoveTracking(Func handler, IEnumerable> invocationList) + { + this.CachedInvocationListFunc = invocationList?.ToArray() ?? new Func[0]; + if (!this.CachedInvocationListFunc.Contains(handler)) // don't remove if there's still a reference to the removed handler (e.g. it was added twice and removed once) + this.SourceModsFunc.Remove(handler); + } /// Get the mod which registered the given event handler, if available. /// The event handler. @@ -150,7 +209,12 @@ namespace StardewModdingAPI.Framework.Events ? mod : null; } - + protected IModMetadata GetSourceModFunc(Func handler) + { + return this.SourceModsFunc.TryGetValue(handler, out IModMetadata mod) + ? mod + : null; + } /// Log an exception from an event handler. /// The event handler instance. /// The exception that was raised. @@ -162,5 +226,13 @@ namespace StardewModdingAPI.Framework.Events else this.Monitor.Log($"A mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); } + protected void LogError(Func handler, Exception ex) + { + IModMetadata mod = this.GetSourceModFunc(handler); + if (mod != null) + mod.LogAsMod($"This mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); + else + this.Monitor.Log($"A mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); + } } } diff --git a/src/SMAPI/Framework/Events/ModEvents.cs b/src/SMAPI/Framework/Events/ModEvents.cs index 8ad3936c..58b493fc 100644 --- a/src/SMAPI/Framework/Events/ModEvents.cs +++ b/src/SMAPI/Framework/Events/ModEvents.cs @@ -29,6 +29,7 @@ namespace StardewModdingAPI.Framework.Events /// Events serving specialised edge cases that shouldn't be used by most mods. public ISpecialisedEvents Specialised { get; } + public IHookEvents Hook { get; } /********* ** Public methods @@ -45,6 +46,7 @@ namespace StardewModdingAPI.Framework.Events this.Player = new ModPlayerEvents(mod, eventManager); this.World = new ModWorldEvents(mod, eventManager); this.Specialised = new ModSpecialisedEvents(mod, eventManager); + this.Hook = new ModHookEvents(mod, eventManager); } } } diff --git a/src/SMAPI/Framework/Events/ModHookEvents.cs b/src/SMAPI/Framework/Events/ModHookEvents.cs new file mode 100644 index 00000000..27311568 --- /dev/null +++ b/src/SMAPI/Framework/Events/ModHookEvents.cs @@ -0,0 +1,43 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Events raised when the player provides input using a controller, keyboard, or mouse. + internal class ModHookEvents : ModEventsBase, IHookEvents + { + /********* + ** Accessors + *********/ + /// Raised after the player presses a button on the keyboard, controller, or mouse. + public event Func ObjectCanBePlacedHere + { + add => this.EventManager.ObjectCanBePlacedHere.Add(value); + remove => this.EventManager.ObjectCanBePlacedHere.Remove(value); + } + + /// Raised after the player releases a button on the keyboard, controller, or mouse. + public event Func ObjectCheckForAction + { + add => this.EventManager.ObjectCheckForAction.Add(value); + remove => this.EventManager.ObjectCheckForAction.Remove(value); + } + + /// Raised after the player moves the in-game cursor. + public event Func ObjectIsIndexOkForBasicShippedCategory + { + add => this.EventManager.ObjectIsIndexOkForBasicShippedCategory.Add(value); + remove => this.EventManager.ObjectIsIndexOkForBasicShippedCategory.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModHookEvents(IModMetadata mod, EventManager eventManager) + : base(mod, eventManager) { } + } +} diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index cd9be025..bb7b4372 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -69,6 +69,28 @@ namespace StardewModdingAPI.Framework /// Skipping a few frames ensures the game finishes initialising the world before mods try to change it. private readonly Countdown AfterLoadTimer = new Countdown(5); + internal bool OnObjectCanBePlacedHere(SObject instance, GameLocation location, Vector2 tile, ref bool result) + { + ObjectCanBePlacedHereEventArgs args = new ObjectCanBePlacedHereEventArgs(instance, location, tile, result); + bool run =this.Events.ObjectCanBePlacedHere.RaiseForChainRun(args); + result = args.__result; + return run; + } + + internal void OnObjectIsIndexOkForBasicShippedCategory(int index, ref bool result) + { + ObjectIsIndexOkForBasicShippedCategoryEventArgs args = new ObjectIsIndexOkForBasicShippedCategoryEventArgs(index, result); + this.Events.ObjectIsIndexOkForBasicShippedCategory.RaiseForChainRun(args); + result = args.__result; + } + + internal bool OnObjectCheckForAction(SObject instance) + { + ObjectCheckForActionEventArgs args = new ObjectCheckForActionEventArgs(instance); + bool run = this.Events.ObjectCheckForAction.RaiseForChainRun(args); + return run; + } + /// Whether the game is saving and SMAPI has already raised . private bool IsBetweenSaveEvents;