Support for Json Asset Mod

This commit is contained in:
yangzhi 2019-04-12 23:40:59 +08:00
parent a22263b70a
commit e18ac2a111
34 changed files with 2668 additions and 28 deletions

View File

@ -26,3 +26,48 @@ 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)
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

120
Mods/JsonAssets/Api.cs Normal file
View File

@ -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<string, int> GetAllObjectIds();
IDictionary<string, int> GetAllCropIds();
IDictionary<string, int> GetAllFruitTreeIds();
IDictionary<string, int> GetAllBigCraftableIds();
IDictionary<string, int> GetAllHatIds();
IDictionary<string, int> GetAllWeaponIds();
event EventHandler IdsAssigned;
event EventHandler AddedItemsToShop;
}
public class Api : IApi
{
private readonly Action<string> loadFolder;
public Api(Action<string> 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<string, int> GetAllObjectIds()
{
return new Dictionary<string, int>(Mod.instance.objectIds);
}
public IDictionary<string, int> GetAllCropIds()
{
return new Dictionary<string, int>(Mod.instance.cropIds);
}
public IDictionary<string, int> GetAllFruitTreeIds()
{
return new Dictionary<string, int>(Mod.instance.fruitTreeIds);
}
public IDictionary<string, int> GetAllBigCraftableIds()
{
return new Dictionary<string, int>(Mod.instance.bigCraftableIds);
}
public IDictionary<string, int> GetAllHatIds()
{
return new Dictionary<string, int>(Mod.instance.hatIds);
}
public IDictionary<string, int> GetAllWeaponIds()
{
return new Dictionary<string, int>(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);
}
}
}

View File

@ -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<T>(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<T>(IAssetData asset)
{
if (asset.AssetNameEquals("Data\\ObjectInformation"))
{
IDictionary<int, string> data = asset.AsDictionary<int, string>().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<int, string> data = asset.AsDictionary<int, string>().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<int, string> data = asset.AsDictionary<int, string>().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<string, string> data = asset.AsDictionary<string, string>().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<string, string> data = asset.AsDictionary<string, string>().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<int, string> data = asset.AsDictionary<int, string>().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<int, string> data = asset.AsDictionary<int, string>().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<int, string> data = asset.AsDictionary<int, string>().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<string, string> data = asset.AsDictionary<string, string>().Data;
// TODO: This could be optimized from mn to... m + n?
// Basically, iterate through objects and create Dictionary<NPC name, GiftData[]>
// Iterate through objects, each section and add to dict[npc][approp. section]
// Point is, I'm doing this the lazy way right now
Dictionary<string, string> newData = new Dictionary<string, string>(data);
foreach (KeyValuePair<string, string> 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<string> loveIds = new List<string>(sections[1].Split(' '));
string likeStr = sections[2];
List<string> likeIds = new List<string>(sections[3].Split(' '));
string dislikeStr = sections[4];
List<string> dislikeIds = new List<string>(sections[5].Split(' '));
string hateStr = sections[6];
List<string> hateIds = new List<string>(sections[7].Split(' '));
string neutralStr = sections[8];
List<string> neutralIds = new List<string>(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);
}
}
}

View File

@ -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<Ingredient> Ingredients { get; set; } = new List<Ingredient>();
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<string> PurchaseRequirements { get; set; } = new List<string>();
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<string> PurchaseRequirements { get; set; } = new List<string>();
public Dictionary<string, string> NameLocalization = new Dictionary<string, string>();
public Dictionary<string, string> DescriptionLocalization = new Dictionary<string, string>();
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;
}
}
}

View File

@ -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<string> UpdateKeys { get; set; } = new List<string>();
}
}

View File

@ -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<string> Seasons { get; set; } = new List<string>();
public IList<int> Phases { get; set; } = new List<int>();
public int RegrowthPhase { get; set; } = -1;
public bool HarvestWithScythe { get; set; } = false;
public bool TrellisCrop { get; set; } = false;
public IList<Color> Colors { get; set; } = new List<Color>();
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<string> SeedPurchaseRequirements { get; set; } = new List<string>();
public int SeedPurchasePrice { get; set; }
public string SeedPurchaseFrom { get; set; } = "Pierre";
public Dictionary<string, string> SeedNameLocalization = new Dictionary<string, string>();
public Dictionary<string, string> SeedDescriptionLocalization = new Dictionary<string, string>();
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;
}
}
}

View File

@ -0,0 +1,9 @@
namespace JsonAssets.Data
{
public abstract class DataNeedsId
{
public string Name { get; set; }
internal int id = -1;
}
}

View File

@ -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<string> SaplingPurchaseRequirements { get; set; } = new List<string>();
public int SaplingPurchasePrice { get; set; }
public string SaplingPurchaseFrom { get; set; } = "Pierre";
public Dictionary<string, string> SaplingNameLocalization = new Dictionary<string, string>();
public Dictionary<string, string> SaplingDescriptionLocalization = new Dictionary<string, string>();
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?";
}
}
}

View File

@ -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<string, string> NameLocalization = new Dictionary<string, string>();
public Dictionary<string, string> DescriptionLocalization = new Dictionary<string, string>();
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()}";
}
}
}

View File

@ -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<Ingredient> Ingredients { get; set; } = new List<Ingredient>();
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<string> PurchaseRequirements { get; set; } = new List<string>();
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<string> PurchaseRequirements { get; set; } = new List<string>();
public class GiftTastes_
{
public IList<string> Love = new List<string>();
public IList<string> Like = new List<string>();
public IList<string> Neutral = new List<string>();
public IList<string> Dislike = new List<string>();
public IList<string> Hate = new List<string>();
}
public GiftTastes_ GiftTastes;
public Dictionary<string, string> NameLocalization = new Dictionary<string, string>();
public Dictionary<string, string> DescriptionLocalization = new Dictionary<string, string>();
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;
}
}
}

View File

@ -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<string> PurchaseRequirements { get; set; } = new List<string>();
public Dictionary<string, string> NameLocalization = new Dictionary<string, string>();
public Dictionary<string, string> DescriptionLocalization = new Dictionary<string, string>();
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;
}
}
}

View File

@ -0,0 +1,174 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{F56B5F8E-0069-4029-8DCD-89002B7285E3}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>JsonAssets</RootNamespace>
<AssemblyName>JsonAssets</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="StardewModdingAPI">
<HintPath>..\assemblies\StardewModdingAPI.dll</HintPath>
</Reference>
<Reference Include="StardewValley">
<HintPath>..\assemblies\StardewValley.dll</HintPath>
</Reference>
<Reference Include="BmFont, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\BmFont.dll</HintPath>
</Reference>
<Reference Include="Google.Android.Vending.Expansion.Downloader, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Google.Android.Vending.Expansion.Downloader.dll</HintPath>
</Reference>
<Reference Include="Google.Android.Vending.Expansion.ZipFile, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Google.Android.Vending.Expansion.ZipFile.dll</HintPath>
</Reference>
<Reference Include="Google.Android.Vending.Licensing, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Google.Android.Vending.Licensing.dll</HintPath>
</Reference>
<Reference Include="Java.Interop, Version=0.1.0.0, Culture=neutral, PublicKeyToken=84e04ff9cfb79065, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Java.Interop.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AppCenter, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Microsoft.AppCenter.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AppCenter.Analytics, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Microsoft.AppCenter.Analytics.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AppCenter.Analytics.Android.Bindings, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Microsoft.AppCenter.Analytics.Android.Bindings.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AppCenter.Android.Bindings, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Microsoft.AppCenter.Android.Bindings.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AppCenter.Crashes, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Microsoft.AppCenter.Crashes.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AppCenter.Crashes.Android.Bindings, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Microsoft.AppCenter.Crashes.Android.Bindings.dll</HintPath>
</Reference>
<Reference Include="Mono.Android, Version=0.0.0.0, Culture=neutral, PublicKeyToken=84e04ff9cfb79065, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Mono.Android.dll</HintPath>
</Reference>
<Reference Include="Mono.Security, Version=2.0.5.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Mono.Security.dll</HintPath>
</Reference>
<Reference Include="MonoGame.Framework, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\MonoGame.Framework.dll</HintPath>
</Reference>
<Reference Include="mscorlib, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e">
<HintPath>..\assemblies\mscorlib.dll</HintPath>
</Reference>
<Reference Include="System, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e">
<HintPath>..\assemblies\System.dll</HintPath>
</Reference>
<Reference Include="System.Xml, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e">
<HintPath>..\assemblies\System.Xml.dll</HintPath>
</Reference>
<Reference Include="System.Net.Http, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<HintPath>..\assemblies\System.Net.Http.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.Serialization, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e">
<HintPath>..\assemblies\System.Runtime.Serialization.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Arch.Core.Common, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Arch.Core.Common.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Arch.Lifecycle.Common, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Arch.Lifecycle.Common.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Arch.Lifecycle.Runtime, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Arch.Lifecycle.Runtime.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.Annotations, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.Annotations.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.Compat, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.Compat.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.Core.UI, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.Core.UI.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.Core.Utils, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.Core.Utils.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.Fragment, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.Fragment.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.Media.Compat, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.Media.Compat.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.v4, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.v4.dll</HintPath>
</Reference>
<Reference Include="xTile, Version=1.0.7033.16602, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\xTile.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Api.cs" />
<Compile Include="ContentInjector.cs" />
<Compile Include="Data\BigCraftableData.cs" />
<Compile Include="Data\ContentPackData.cs" />
<Compile Include="Data\CropData.cs" />
<Compile Include="Data\DataNeedsId.cs" />
<Compile Include="Data\FruitTreeData.cs" />
<Compile Include="Data\HatData.cs" />
<Compile Include="Data\ObjectData.cs" />
<Compile Include="Data\WeaponData.cs" />
<Compile Include="Log.cs" />
<Compile Include="Mod.cs" />
<Compile Include="Overrides\Object.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Util.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

33
Mods/JsonAssets/Log.cs Normal file
View File

@ -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);
}
}
}

925
Mods/JsonAssets/Mod.cs Normal file
View File

@ -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;
/// <summary>The mod entry point, called after the mod is first loaded.</summary>
/// <param name="helper">Provides simplified APIs for writing mods.</param>
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<ContentPackData>("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<string, IContentPack> dupObjects = new Dictionary<string, IContentPack>();
private Dictionary<string, IContentPack> dupCrops = new Dictionary<string, IContentPack>();
private Dictionary<string, IContentPack> dupFruitTrees = new Dictionary<string, IContentPack>();
private Dictionary<string, IContentPack> dupBigCraftables = new Dictionary<string, IContentPack>();
private Dictionary<string, IContentPack> dupHats = new Dictionary<string, IContentPack>();
private Dictionary<string, IContentPack> dupWeapons = new Dictionary<string, IContentPack>();
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<ObjectData>($"{relativePath}/object.json");
if (obj == null)
continue;
// save object
obj.texture = contentPack.LoadAsset<Texture2D>($"{relativePath}/object.png");
if (obj.IsColored)
obj.textureColor = contentPack.LoadAsset<Texture2D>($"{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<CropData>($"{relativePath}/crop.json");
if (crop == null)
continue;
// save crop
crop.texture = contentPack.LoadAsset<Texture2D>($"{relativePath}/crop.png");
this.crops.Add(crop);
// save seeds
crop.seed = new ObjectData
{
texture = contentPack.LoadAsset<Texture2D>($"{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<string>(),
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<FruitTreeData>($"{relativePath}/tree.json");
if (tree == null)
continue;
// save fruit tree
tree.texture = contentPack.LoadAsset<Texture2D>($"{relativePath}/tree.png");
this.fruitTrees.Add(tree);
// save seed
tree.sapling = new ObjectData
{
texture = contentPack.LoadAsset<Texture2D>($"{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<BigCraftableData>($"{relativePath}/big-craftable.json");
if (craftable == null)
continue;
// save craftable
craftable.texture = contentPack.LoadAsset<Texture2D>($"{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<HatData>($"{relativePath}/hat.json");
if (hat == null)
continue;
// save object
hat.texture = contentPack.LoadAsset<Texture2D>($"{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<WeaponData>($"{relativePath}/weapon.json");
if (weapon == null)
continue;
// save object
weapon.texture = contentPack.LoadAsset<Texture2D>($"{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<DataNeedsId>());
this.clearIds(out this.cropIds, this.crops.ToList<DataNeedsId>());
this.clearIds(out this.fruitTreeIds, this.fruitTrees.ToList<DataNeedsId>());
this.clearIds(out this.bigCraftableIds, this.bigCraftables.ToList<DataNeedsId>());
this.clearIds(out this.hatIds, this.hats.ToList<DataNeedsId>());
this.clearIds(out this.weaponIds, this.weapons.ToList<DataNeedsId>());
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 );
}
}
/// <summary>Raised after a game menu is opened, closed, or replaced.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
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<Item> forSale = this.Helper.Reflection.GetField<List<Item>>(menu, "forSale").GetValue();
int count = forSale.Count;
Dictionary<Item, int[]> itemPriceAndStock = this.Helper.Reflection.GetField<Dictionary<Item, int[]>>(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<int>(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<int>(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<int>(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<int>(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<int>(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<int>(menu, "currency").GetValue(), this.Helper.Reflection.GetField<string>(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<TKey, TValue> LoadDictionary<TKey, TValue>(string filename)
{
string path = Path.Combine(Constants.CurrentSavePath, "JsonAssets", filename);
return File.Exists(path)
? JsonConvert.DeserializeObject<Dictionary<TKey, TValue>>(File.ReadAllText(path))
: new Dictionary<TKey, TValue>();
}
Directory.CreateDirectory(Path.Combine(Constants.CurrentSavePath, "JsonAssets"));
this.oldObjectIds = LoadDictionary<string, int>("ids-objects.json");
this.oldCropIds = LoadDictionary<string, int>("ids-crops.json");
this.oldFruitTreeIds = LoadDictionary<string, int>("ids-fruittrees.json");
this.oldBigCraftableIds = LoadDictionary<string, int>("ids-big-craftables.json");
this.oldHatIds = LoadDictionary<string, int>("ids-hats.json");
this.oldWeaponIds = LoadDictionary<string, int>("ids-weapons.json");
Log.trace("OLD IDS START");
foreach (KeyValuePair<string, int> id in this.oldObjectIds)
Log.trace("\tObject " + id.Key + " = " + id.Value);
foreach (KeyValuePair<string, int> id in this.oldCropIds)
Log.trace("\tCrop " + id.Key + " = " + id.Value);
foreach (KeyValuePair<string, int> id in this.oldFruitTreeIds)
Log.trace("\tFruit Tree " + id.Key + " = " + id.Value);
foreach (KeyValuePair<string, int> id in this.oldBigCraftableIds)
Log.trace("\tBigCraftable " + id.Key + " = " + id.Value);
foreach (KeyValuePair<string, int> id in this.oldHatIds)
Log.trace("\tHat " + id.Key + " = " + id.Value);
foreach (KeyValuePair<string, int> 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<DataNeedsId>());
this.cropIds = this.AssignIds("crops", StartingCropId, this.crops.ToList<DataNeedsId>());
this.fruitTreeIds = this.AssignIds("fruittrees", StartingFruitTreeId, this.fruitTrees.ToList<DataNeedsId>());
this.bigCraftableIds = this.AssignIds("big-craftables", StartingBigCraftableId, this.bigCraftables.ToList<DataNeedsId>());
this.hatIds = this.AssignIds("hats", StartingHatId, this.hats.ToList<DataNeedsId>());
this.weaponIds = this.AssignIds("weapons", StartingWeaponId, this.weapons.ToList<DataNeedsId>());
this.api.InvokeIdsAssigned();
// init
this.Helper.Content.AssetEditors.Add(new ContentInjector());
}
/// <summary>Raised after the game finishes writing data to the save file (except the initial save creation).</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
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<ObjectData> myRings = new List<ObjectData>();
/// <summary>Raised after items are added or removed to a player's inventory. NOTE: this event is currently only raised for the current player.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void onInventoryChanged(object sender, InventoryChangedEventArgs e)
{
if (!e.IsLocalPlayer)
return;
IList<int> ringIds = new List<int>();
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<ObjectData> objects = new List<ObjectData>();
internal IList<CropData> crops = new List<CropData>();
internal IList<FruitTreeData> fruitTrees = new List<FruitTreeData>();
internal IList<BigCraftableData> bigCraftables = new List<BigCraftableData>();
internal IList<HatData> hats = new List<HatData>();
internal IList<WeaponData> weapons = new List<WeaponData>();
internal IDictionary<string, int> objectIds;
internal IDictionary<string, int> cropIds;
internal IDictionary<string, int> fruitTreeIds;
internal IDictionary<string, int> bigCraftableIds;
internal IDictionary<string, int> hatIds;
internal IDictionary<string, int> weaponIds;
internal IDictionary<string, int> oldObjectIds;
internal IDictionary<string, int> oldCropIds;
internal IDictionary<string, int> oldFruitTreeIds;
internal IDictionary<string, int> oldBigCraftableIds;
internal IDictionary<string, int> oldHatIds;
internal IDictionary<string, int> oldWeaponIds;
internal IDictionary<int, string> origObjects;
internal IDictionary<int, string> origCrops;
internal IDictionary<int, string> origFruitTrees;
internal IDictionary<int, string> origBigCraftables;
internal IDictionary<int, string> origHats;
internal IDictionary<int, string> 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<int, string> 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<string, int> AssignIds(string type, int starting, IList<DataNeedsId> data)
{
Dictionary<string, int> ids = new Dictionary<string, int>();
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<string, int> ids, List<DataNeedsId> objs)
{
ids = null;
foreach ( DataNeedsId obj in objs )
{
obj.id = -1;
}
}
private IDictionary<int, string> cloneIdDictAndRemoveOurs( IDictionary<int, string> full, IDictionary<string, int> ours )
{
Dictionary<int, string> ret = new Dictionary<int, string>(full);
foreach (KeyValuePair<string, int> 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<Dictionary<int, string>>("Data\\Crops"), this.cropIds);
this.origFruitTrees = this.cloneIdDictAndRemoveOurs(Game1.content.Load<Dictionary<int, string>>("Data\\fruitTrees"), this.fruitTreeIds);
this.origBigCraftables = this.cloneIdDictAndRemoveOurs(Game1.bigCraftablesInformation, this.bigCraftableIds);
this.origHats = this.cloneIdDictAndRemoveOurs(Game1.content.Load<Dictionary<int, string>>("Data\\hats"), this.hatIds);
this.origWeapons = this.cloneIdDictAndRemoveOurs(Game1.content.Load<Dictionary<int, string>>("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<Vector2> toRemove = new List<Vector2>();
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<string, int> oldIds, IDictionary<string, int> newIds, NetInt id, IDictionary<int, string> 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;
}
}
}

View File

@ -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;
}
}
}
}
}

View File

@ -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")]

28
Mods/JsonAssets/Util.cs Normal file
View File

@ -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<Delegate> handlers, object sender)
{
EventArgs args = new EventArgs();
foreach (EventHandler handler in handlers.Cast<EventHandler>())
{
try
{
handler.Invoke(sender, args);
}
catch (Exception e)
{
Log.error($"Exception while handling event {name}:\n{e}");
}
}
}
}
}

View File

@ -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);
}

View File

@ -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,

View File

@ -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,

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -443,6 +443,7 @@
<Compile Include="SMAPI\Events\DayStartedEventArgs.cs" />
<Compile Include="SMAPI\Events\DebrisListChangedEventArgs.cs" />
<Compile Include="SMAPI\Events\GameLaunchedEventArgs.cs" />
<Compile Include="SMAPI\Events\IHookEvents.cs" />
<Compile Include="SMAPI\Events\IDisplayEvents.cs" />
<Compile Include="SMAPI\Events\IGameLoopEvents.cs" />
<Compile Include="SMAPI\Events\IInputEvents.cs" />
@ -462,6 +463,8 @@
<Compile Include="SMAPI\Events\ModMessageReceivedEventArgs.cs" />
<Compile Include="SMAPI\Events\MouseWheelScrolledEventArgs.cs" />
<Compile Include="SMAPI\Events\NpcListChangedEventArgs.cs" />
<Compile Include="SMAPI\Events\ObjectCheckForActionEventArgs.cs" />
<Compile Include="SMAPI\Events\ObjectIsIndexOkForBasicShippedCategoryEventArgs.cs" />
<Compile Include="SMAPI\Events\ObjectListChangedEventArgs.cs" />
<Compile Include="SMAPI\Events\OneSecondUpdateTickedEventArgs.cs" />
<Compile Include="SMAPI\Events\OneSecondUpdateTickingEventArgs.cs" />
@ -488,6 +491,7 @@
<Compile Include="SMAPI\Events\UpdateTickedEventArgs.cs" />
<Compile Include="SMAPI\Events\UpdateTickingEventArgs.cs" />
<Compile Include="SMAPI\Events\WarpedEventArgs.cs" />
<Compile Include="SMAPI\Events\ObjectCanBePlacedHereEventArgs.cs" />
<Compile Include="SMAPI\Events\WindowResizedEventArgs.cs" />
<Compile Include="SMAPI\Framework\Command.cs" />
<Compile Include="SMAPI\Framework\CommandManager.cs" />
@ -513,6 +517,7 @@
<Compile Include="SMAPI\Framework\Events\ModEvents.cs" />
<Compile Include="SMAPI\Framework\Events\ModEventsBase.cs" />
<Compile Include="SMAPI\Framework\Events\ModGameLoopEvents.cs" />
<Compile Include="SMAPI\Framework\Events\ModHookEvents.cs" />
<Compile Include="SMAPI\Framework\Events\ModInputEvents.cs" />
<Compile Include="SMAPI\Framework\Events\ModMultiplayerEvents.cs" />
<Compile Include="SMAPI\Framework\Events\ModPlayerEvents.cs" />

View File

@ -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);
}
}
}

View File

@ -0,0 +1,18 @@
using System;
using StardewValley;
namespace StardewModdingAPI.Events
{
/// <summary>Events related to UI and drawing to the screen.</summary>
public interface IHookEvents
{
/// <summary>Object.canBePlacedHere hook.</summary>
event Func<ObjectCanBePlacedHereEventArgs, bool> ObjectCanBePlacedHere;
/// <summary>Object.checkForAction hook.</summary>
event Func<ObjectCheckForActionEventArgs, bool> ObjectCheckForAction;
/// <summary>Object.isIndexOkForBasicShippedCategory hook.</summary>
event Func<ObjectIsIndexOkForBasicShippedCategoryEventArgs, bool> ObjectIsIndexOkForBasicShippedCategory;
}
}

View File

@ -23,5 +23,6 @@ namespace StardewModdingAPI.Events
/// <summary>Events serving specialised edge cases that shouldn't be used by most mods.</summary>
ISpecialisedEvents Specialised { get; }
IHookEvents Hook { get; }
}
}

View File

@ -0,0 +1,34 @@
using StardewValley;
using Microsoft.Xna.Framework;
namespace StardewModdingAPI.Events
{
/// <summary>Event arguments for an <see cref="IHookEvents.ObjectCanBePlacedHere"/> event.</summary>
public class ObjectCanBePlacedHereEventArgs : System.EventArgs
{
/*********
** Accessors
*********/
public Object __instance { get; }
public GameLocation location { get; }
public Vector2 tile { get; }
public bool __result;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="oldSize">The previous window size.</param>
/// <param name="newSize">The current window size.</param>
internal ObjectCanBePlacedHereEventArgs(Object __instance, GameLocation location, Vector2 tile, bool __result)
{
this.__instance = __instance;
this.location = location;
this.tile = tile;
this.__result = __result;
}
}
}

View File

@ -0,0 +1,24 @@
using StardewValley;
using Microsoft.Xna.Framework;
namespace StardewModdingAPI.Events
{
/// <summary>Event arguments for an <see cref="IHookEvents.ObjectCheckForAction"/> event.</summary>
public class ObjectCheckForActionEventArgs : System.EventArgs
{
/*********
** Accessors
*********/
public Object __instance { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
internal ObjectCheckForActionEventArgs(Object __instance)
{
this.__instance = __instance;
}
}
}

View File

@ -0,0 +1,29 @@
using System;
using Microsoft.Xna.Framework;
namespace StardewModdingAPI.Events
{
/// <summary>Event arguments for an <see cref="IHookEvents.ObjectIsIndexOkForBasicShippedCategoryEventArgs"/> event.</summary>
public class ObjectIsIndexOkForBasicShippedCategoryEventArgs : EventArgs
{
/*********
** Accessors
*********/
/// <summary>The index</summary>
public int index { get; }
public bool __result;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="oldSize">The previous window size.</param>
/// <param name="newSize">The current window size.</param>
internal ObjectIsIndexOkForBasicShippedCategoryEventArgs(int index, bool __result)
{
this.index = index;
this.__result = __result;
}
}
}

View File

@ -163,6 +163,11 @@ namespace StardewModdingAPI.Framework.Events
/// <summary>Raised after the game performs its overall update tick (≈60 times per second). See notes on <see cref="ISpecialisedEvents.UnvalidatedUpdateTicked"/>.</summary>
public readonly ManagedEvent<UnvalidatedUpdateTickedEventArgs> UnvalidatedUpdateTicked;
public readonly ManagedEvent<ObjectCanBePlacedHereEventArgs> ObjectCanBePlacedHere;
public readonly ManagedEvent<ObjectCheckForActionEventArgs> ObjectCheckForAction;
public readonly ManagedEvent<ObjectIsIndexOkForBasicShippedCategoryEventArgs> ObjectIsIndexOkForBasicShippedCategory;
/*********
** Public methods
@ -226,6 +231,10 @@ namespace StardewModdingAPI.Framework.Events
this.LoadStageChanged = ManageEventOf<LoadStageChangedEventArgs>(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.LoadStageChanged));
this.UnvalidatedUpdateTicking = ManageEventOf<UnvalidatedUpdateTickingEventArgs>(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.UnvalidatedUpdateTicking));
this.UnvalidatedUpdateTicked = ManageEventOf<UnvalidatedUpdateTickedEventArgs>(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.UnvalidatedUpdateTicked));
this.ObjectCheckForAction = ManageEventOf<ObjectCheckForActionEventArgs>(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.LoadStageChanged));
this.ObjectCanBePlacedHere = ManageEventOf<ObjectCanBePlacedHereEventArgs>(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.UnvalidatedUpdateTicking));
this.ObjectIsIndexOkForBasicShippedCategory = ManageEventOf<ObjectIsIndexOkForBasicShippedCategoryEventArgs>(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.UnvalidatedUpdateTicked));
}
}
}

View File

@ -14,6 +14,8 @@ namespace StardewModdingAPI.Framework.Events
/// <summary>The underlying event.</summary>
private event EventHandler<TEventArgs> Event;
private event Func<TEventArgs, bool> Func;
/// <summary>A human-readable name for the event.</summary>
private readonly string EventName;
@ -26,8 +28,11 @@ namespace StardewModdingAPI.Framework.Events
/// <summary>The display names for the mods which added each delegate.</summary>
private readonly IDictionary<EventHandler<TEventArgs>, IModMetadata> SourceMods = new Dictionary<EventHandler<TEventArgs>, IModMetadata>();
private readonly IDictionary<Func<TEventArgs, bool>, IModMetadata> SourceModsFunc = new Dictionary<Func<TEventArgs, bool>, IModMetadata>();
/// <summary>The cached invocation list.</summary>
private EventHandler<TEventArgs>[] CachedInvocationList;
private Func<TEventArgs, bool>[] CachedInvocationListFunc;
/*********
@ -57,6 +62,11 @@ namespace StardewModdingAPI.Framework.Events
this.Add(handler, this.ModRegistry.GetFromStack());
}
public void Add(Func<TEventArgs, bool> handler)
{
this.Add(handler, this.ModRegistry.GetFromStack());
}
/// <summary>Add an event handler.</summary>
/// <param name="handler">The event handler.</param>
/// <param name="mod">The mod which added the event handler.</param>
@ -66,6 +76,12 @@ namespace StardewModdingAPI.Framework.Events
this.AddTracking(mod, handler, this.Event?.GetInvocationList().Cast<EventHandler<TEventArgs>>());
}
public void Add(Func<TEventArgs, bool> handler, IModMetadata mod)
{
this.Func += handler;
this.AddTracking(mod, handler, this.Func?.GetInvocationList().Cast<Func<TEventArgs, bool>>());
}
/// <summary>Remove an event handler.</summary>
/// <param name="handler">The event handler.</param>
public void Remove(EventHandler<TEventArgs> handler)
@ -74,6 +90,12 @@ namespace StardewModdingAPI.Framework.Events
this.RemoveTracking(handler, this.Event?.GetInvocationList().Cast<EventHandler<TEventArgs>>());
}
public void Remove(Func<TEventArgs, bool> handler)
{
this.Func -= handler;
this.RemoveTracking(handler, this.Event?.GetInvocationList().Cast<Func<TEventArgs, bool>>());
}
/// <summary>Raise the event and notify all handlers.</summary>
/// <param name="args">The event arguments to pass.</param>
public void Raise(TEventArgs args)
@ -94,6 +116,31 @@ namespace StardewModdingAPI.Framework.Events
}
}
/// <summary>Raise the event and notify all handlers wait for a actively response.</summary>
/// <param name="args">The event arguments to pass.</param>
public bool RaiseForChainRun(TEventArgs args)
{
if (this.Func == null)
return true;
foreach (Func<TEventArgs, bool> handler in this.CachedInvocationListFunc)
{
try
{
bool run = handler.Invoke(args);
if (!run)
{
return false;
}
}
catch (Exception ex)
{
this.LogError(handler, ex);
}
}
return true;
}
/// <summary>Raise the event and notify all handlers.</summary>
/// <param name="args">The event arguments to pass.</param>
/// <param name="match">A lambda which returns true if the event should be raised for the given mod.</param>
@ -132,6 +179,12 @@ namespace StardewModdingAPI.Framework.Events
this.CachedInvocationList = invocationList?.ToArray() ?? new EventHandler<TEventArgs>[0];
}
protected void AddTracking(IModMetadata mod, Func<TEventArgs, bool> handler, IEnumerable<Func<TEventArgs, bool>> invocationList)
{
this.SourceModsFunc[handler] = mod;
this.CachedInvocationListFunc = invocationList?.ToArray() ?? new Func<TEventArgs, bool>[0];
}
/// <summary>Remove tracking for an event handler.</summary>
/// <param name="handler">The event handler.</param>
/// <param name="invocationList">The updated event invocation list.</param>
@ -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<TEventArgs, bool> handler, IEnumerable<Func<TEventArgs, bool>> invocationList)
{
this.CachedInvocationListFunc = invocationList?.ToArray() ?? new Func<TEventArgs, bool>[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);
}
/// <summary>Get the mod which registered the given event handler, if available.</summary>
/// <param name="handler">The event handler.</param>
@ -150,7 +209,12 @@ namespace StardewModdingAPI.Framework.Events
? mod
: null;
}
protected IModMetadata GetSourceModFunc(Func<TEventArgs, bool> handler)
{
return this.SourceModsFunc.TryGetValue(handler, out IModMetadata mod)
? mod
: null;
}
/// <summary>Log an exception from an event handler.</summary>
/// <param name="handler">The event handler instance.</param>
/// <param name="ex">The exception that was raised.</param>
@ -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<TEventArgs, bool> 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);
}
}
}

View File

@ -29,6 +29,7 @@ namespace StardewModdingAPI.Framework.Events
/// <summary>Events serving specialised edge cases that shouldn't be used by most mods.</summary>
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);
}
}
}

View File

@ -0,0 +1,43 @@
using System;
using StardewModdingAPI.Events;
namespace StardewModdingAPI.Framework.Events
{
/// <summary>Events raised when the player provides input using a controller, keyboard, or mouse.</summary>
internal class ModHookEvents : ModEventsBase, IHookEvents
{
/*********
** Accessors
*********/
/// <summary>Raised after the player presses a button on the keyboard, controller, or mouse.</summary>
public event Func<ObjectCanBePlacedHereEventArgs, bool> ObjectCanBePlacedHere
{
add => this.EventManager.ObjectCanBePlacedHere.Add(value);
remove => this.EventManager.ObjectCanBePlacedHere.Remove(value);
}
/// <summary>Raised after the player releases a button on the keyboard, controller, or mouse.</summary>
public event Func<ObjectCheckForActionEventArgs, bool> ObjectCheckForAction
{
add => this.EventManager.ObjectCheckForAction.Add(value);
remove => this.EventManager.ObjectCheckForAction.Remove(value);
}
/// <summary>Raised after the player moves the in-game cursor.</summary>
public event Func<ObjectIsIndexOkForBasicShippedCategoryEventArgs, bool> ObjectIsIndexOkForBasicShippedCategory
{
add => this.EventManager.ObjectIsIndexOkForBasicShippedCategory.Add(value);
remove => this.EventManager.ObjectIsIndexOkForBasicShippedCategory.Remove(value);
}
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="mod">The mod which uses this instance.</param>
/// <param name="eventManager">The underlying event manager.</param>
internal ModHookEvents(IModMetadata mod, EventManager eventManager)
: base(mod, eventManager) { }
}
}

View File

@ -69,6 +69,28 @@ namespace StardewModdingAPI.Framework
/// <remarks>Skipping a few frames ensures the game finishes initialising the world before mods try to change it.</remarks>
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;
}
/// <summary>Whether the game is saving and SMAPI has already raised <see cref="IGameLoopEvents.Saving"/>.</summary>
private bool IsBetweenSaveEvents;