Skip to content

Instantly share code, notes, and snippets.

@M3ales
Last active July 10, 2020 03:43
Show Gist options
  • Save M3ales/ba14a27c6a62a1ffafcec546043156d6 to your computer and use it in GitHub Desktop.
Save M3ales/ba14a27c6a62a1ffafcec546043156d6 to your computer and use it in GitHub Desktop.
Stardew Valley Modding Notes (What I've found)

Stardew Valley

Skip down to the Specific Datastorage Formats if you want the useful stuff. It's mostly just what I'm thinking so lots of speculation and development of reasoning (or me making lots of assumptions).

Notes on Game layout

  • Entrypoint is StardewValley.Program.cs:39
  • Game1 is a godclass holding the gamestate and most used Constants
  • Content is loaded using Microsoft.XNA.Framework.Content

The Dictionaries below are used to index most objects and are accessible under StardewValley.Game1.

public static IDictionary<int, string> objectInformation;
public static IDictionary<int, string> bigCraftablesInformation;
public static IDictionary<string, string> NPCGiftTastes;

Notes on Content Pipeline

  • XNB is a binary format used to pack assets. Apparently decompressable with this.[[Link is dead]]
  • Stardew Valley makes use of [Microsoft XNA's Content Pipeline](https://docs.microsoft.com/en-us/previous-versions/windows/xna/bb195587(v%3dxnagamestudio.41) for loading game assets.Microsoft.XNA.Framework.Content.ContentManager is wrapped by StardewValley.LocalizedContentManager to provide localization support on Assets. (-es/-fr/-ru/etc. suffixed to string queries) when referring to XNBs.
  • XNB file contains xml/yaml, and I will be referring it to the decompressed YAML for the time being. (This may be inaccurate but is assumed hereforth. (Based on what I've read from xnbNode xml is stored in xnb but when you extract it is simplified to yaml, this may be the authors choice but it's probably likely the xnb stores compressed xml which roughly equates to yaml)))*
  • Each class which loads from the content pipeline implements their own method for reading the string stored in the xml/yaml. It seems to be the constructor in most cases.
  • The Content pipeline makes use of a string path format relative to the game's Content directory. So something such as Strings\someAsset will load <Stardew Valley Path>\Strings\someAsset.xnb. When stored in files there is an escaped backslash used, so it will be presented as Strings\\someAsset rather than Strings\someAsset.

Brief note on Asset/Content References

Content references are both used in YAML and in raw code. Examples of code usage follow:

Game1.content.LoadString("Strings\\StringsFromCSFiles:Farmer.cs.1954"); Game1.content.LoadString("Strings\\StringsFromCSFiles:MoneyMadeScreen.cs.3854", (object) this.total) Dictionary<string, string> dictionary = Game1.content.Load<Dictionary<string, string>>("Data\\animationDescriptions"); - StardewValley.NPC.cs:2558

Strings\\someAsset will look for <stardew valley path>\Content\Strings\someAsset.xnb. More specific elements may be referenced such as: Strings\\someAsset:someElement in this case the yaml structure accessed would be something along the lines of

someAsset.yaml in Strings\someAsset.xnb

xnbdata:
...
...
content: #!Dictionary<String,String>
    someElement: "foo" #!String
    ...

Worth noting that string dictionaries are not the only form of storage in xnbs, and it may vary. Most of the game data however is stored in this format. And later processed manually by the class Constructor. A third kind of reference is seen in: Strings\\StringsFromCSFiles:Farmer.cs.1954 Is not different from before, the actual keys reflect the same format. ie.

BarnDweller.cs.386: "{0} is sleeping." #!String
    BarnDweller.cs.387: "{0} is looking happy today!" #!String
    BarnDweller.cs.388: "{0} seems to be in a bad mood..." #!String
    BluePrint.cs.1: "Use to see information about your animals." #!String
    Buff.cs.453: "Goblin's Curse" #!String
    Buff.cs.454: "-3 Speed" #!String
    Buff.cs.455: "-3 Defense" #!String

XNB YAML/XML

YAML stored inside the content XNBs are usually found as thus:

snippet from animationDescriptions.yaml

xnbData: 
    target: "w"
    compressed: true
    hiDef: true
    readerData: 
        - 
            type: "Microsoft.Xna.Framework.Content.DictionaryReader`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]"
            version: 0

        - 
            type: "Microsoft.Xna.Framework.Content.StringReader"
            version: 0


    numSharedResources: 0

content:  #!Dictionary<String,String>
    abigail_videogames: "22 21 20/20/20 21 22/Strings\\animationDescriptions:abigail_videogames" #!String
    abigail_sit_down: "23/23/23" #!String

From this we can see a few things:

  • target:w implies the xnb is intended to be run on windows. Guessing this, will need to check with an xnb from linux/mac, but will predict it will change to l and m respectively
  • The readers required to load the content tag are specified under the readerData tag.
  • Types are prefixed with #! (ie. #!String) which doesn't seem to be part of the yaml markup so it's likely internal.
  • The Content tag has it's type specified as Dictionary<String, String>
  • Each element in the content tag consists of a key-value pair.
  • The type of the value is also specified as String, this is likely to help a reader find which elements should be parsed by the StringReader specified earlier. Looking closer at the content tag:
content:  #!Dictionary<String,String>
    abigail_videogames: "22 21 20/20/20 21 22/Strings\\animationDescriptions:abigail_videogames" #!String

Key: abigail_videogames Value: 22 21 20/20/20 21 22/Strings\\animationDescriptions:abigail_videogames

Value holds type specific data for the class. Looking at value:

  • / separator
  • separator (Single Whitespace)
  • Strings\\animationDescriptions:abigail_videogames is an asset reference to a string value that will be displayed during the animation. abigail_videogames: "Urghh... I still can't beat the first level of 'Journey of the Prairie King'!$u"
    • This is supported by abigail_flute: "0/16 16 17 17 18 18 19 19/0/silent" which is specified as being 'silent'. At this time I'm still working on figuring out how these animations are loaded, and what each of the fields actually refer to. It's likely an internal XNA format, but I may be wrong.

Specific Datastorage Formats

Route End Animations

Animations stored in Data\animationDescriptions.xnb. Likely cutscene/special animations based on content. Parsing code located in StardewValley.NPC.loadEndOfRouteBehaviour(string name):2553:

      Dictionary<string, string> dictionary = Game1.content.Load<Dictionary<string, string>>("Data\\animationDescriptions");
      if (!dictionary.ContainsKey(name))
        return;
      string[] strArray = dictionary[name].Split('/');
      this.routeEndIntro = Utility.parseStringToIntArray(strArray[0], ' ');
      this.routeEndAnimation = Utility.parseStringToIntArray(strArray[1], ' ');
      this.routeEndOutro = Utility.parseStringToIntArray(strArray[2], ' ');
      if (strArray.Length <= 3)
        return;
      this.nextEndOfRouteMessage = strArray[3];

Sample data: abigail_videogames: "22 21 20/20/20 21 22/Strings\\animationDescriptions:abigail_videogames"

Suggested values: this.routeEndIntro/this.routeEndAnimation/this.routeEndOutro/this.nextEndOfRouteMessage (The last field is optional)

or by types: int[]/int[]/int[]/ContentReference as string (The last field is optional)

Declaration of variables:

    private int[] routeEndIntro; //intro anim/start frame?
    private int[] routeEndAnimation; //animation?
    private int[] routeEndOutro; //outro animation?
    [XmlIgnore]
    public string nextEndOfRouteMessage;

What the variable names mean or do isn't something I've looked into heavily yet but their declaration is on the NPC, and based on code I've read in AnimatedSprite and FarmerSprite, I'd wager it's to do with animation transitions into and out of the animation itself.

Crafting/Cooking Recipes

Crafting Recipies are loaded along with Cooking Recipies in StardewValley.CraftingRecipe.InitShared()

public static void InitShared()
    {
      CraftingRecipe.craftingRecipes = Game1.content.Load<Dictionary<string, string>>("Data//CraftingRecipes");
      CraftingRecipe.cookingRecipes = Game1.content.Load<Dictionary<string, string>>("Data//CookingRecipes");
    }

Data\CookingRecipe.xnb elements:

Fried Egg: "-5 1/10 10/194/default"
Omelet: "-5 1 -6 1/1 10/195/l 10"
Salad: "20 1 22 1 419 1/25 5/196/f Emily 3"
Cheese Cauli.: "190 1 424 1/5 5/197/f Pam 3"

Data\CraftingRecipe.xnb elements:

Wood Fence: "388 2/Field/322/false/l 0" #!String
Stone Fence: "390 2/Field/323/false/Farming 2" #!String
Iron Fence: "335 1/Field/324 10/false/Farming 4" #!String
Hardwood Fence: "709 1/Field/298/false/Farming 6" #!String
Gate: "388 10/Home/325/false/l 0" #!String

Key is once again an identifier as well as recipe name, with elements containing spaces and other characters without issue (no : can be used, or \n) to keep with the format. Reserved characters may also be found in the yaml reference.

    public CraftingRecipe(string name, bool isCookingRecipe)
    {
      this.isCookingRecipe = isCookingRecipe;
      this.name = name;
      string str1 = !isCookingRecipe || !CraftingRecipe.cookingRecipes.ContainsKey(name) ? (CraftingRecipe.craftingRecipes.ContainsKey(name) ? CraftingRecipe.craftingRecipes[name] : (string) null) : CraftingRecipe.cookingRecipes[name];//determine if its cooking or crafting recipe, null if it's neither
      if (str1 == null)//this seems to be a construction which forces a crafting recipe selection if null, probably just a specific
      {
        this.name = "Torch";
        name = "Torch";
        str1 = CraftingRecipe.craftingRecipes[name];
      }
      string[] strArray1 = str1.Split('/');//split on /
      string[] strArray2 = strArray1[0].Split(' ');//first segment
      //strange that they did not use Utility.stringToInt32Array()
      int index1 = 0;
      while (index1 < strArray2.Length)
      {
        this.recipeList.Add(Convert.ToInt32(strArray2[index1]), Convert.ToInt32(strArray2[index1 + 1]));
        index1 += 2;//ah this is why, the format requires pairs be read, not singles
      }
      string[] strArray3 = strArray1[2].Split(' ');//split the third segment
      int index2 = 0;
      while (index2 < strArray3.Length)
      {
        this.itemToProduce.Add(Convert.ToInt32(strArray3[index2]));
        this.numberProducedPerCraft = strArray3.Length > 1 ? Convert.ToInt32(strArray3[index2 + 1]) : 1;
        index2 += 2;//do the same as before with pairs
      }
      this.bigCraftable = !isCookingRecipe && Convert.ToBoolean(strArray1[3]);
      try
      {
        string str2;
        if (!this.bigCraftable)
          str2 = Game1.objectInformation[this.itemToProduce[0]].Split('/')[5];
        else
          str2 = Game1.bigCraftablesInformation[this.itemToProduce[0]].Split('/')[4];
        this.description = str2;
      }
      catch (Exception ex)
      {
        this.description = "";
      }
      this.timesCrafted = Game1.player.craftingRecipes.ContainsKey(name) ? Game1.player.craftingRecipes[name] : 0;
      if (name.Equals("Crab Pot") && Game1.player.professions.Contains(7))
      {
        this.recipeList = new Dictionary<int, int>();
        this.recipeList.Add(388, 25);
        this.recipeList.Add(334, 2);
      }
      if (LocalizedContentManager.CurrentLanguageCode != LocalizedContentManager.LanguageCode.en)
        this.DisplayName = strArray1[strArray1.Length - 1];//for non english
      else
        this.DisplayName = name; //the key value in the xnb
    }

Worth noting: because of this.numberProducedPerCraft = strArray3.Length > 1 ? Convert.ToInt32(strArray3[index2 + 1]) : 1; The last element in an ingredient list does not have to have an amount, and it will default to 1.

Format for crafting/cooking we can see from just this class is: name: "recipeList/???/itemsToProduce/bigCraftable/???"

Types: string: "<space separated integer pairs>/string/<space separated integer pairs>/string

But since we can guess that the last unknown ???, has something to do with requirements based on information from the wiki - we'll start looking in Skills and Quests for the ContentReference Data\\CraftingRecipe or Data\\CookingRecipe.

Working on this at the moment, though Quests doesn't seem to have direct link.

TODO

ObjectInformation

content:  #!Dictionary<Int32,String>
    0: "Weeds/0/-1/Basic/Weeds/A bunch of obnoxious weeds." #!String
    2: "Stone/0/-300/Basic/Stone/A useful material when broken with the Pickaxe." #!String
    4: "Stone/0/-300/Basic/Stone/A useful material when chopped with the axe." #!String

Key: int ObjectID Value: "Stone/0/-300/Basic/Stone/A useful material when broken with the Pickaxe."

In StardewValley.CraftingRecipe.CraftingRecipe(string name, bool isCookingRecipe) We can see

string str2;
if (!this.bigCraftable)
    str2 = Game1.objectInformation[this.itemToProduce[0]].Split('/')[5];
else
    str2 = Game1.bigCraftablesInformation[this.itemToProduce[0]].Split('/')[4];
this.description = str2;

The 6th element is used as a description.

Which co-incides in our case with A useful material when broken with the Pickaxe. which fits the idea of a description.

In StardewValley.Crop.harvest(int xTile, int yTile, HoeDirt soil, JunimoHarvester junimoHarvester = null) We can see

float num7 = (float) (16.0 * Math.Log(0.018 * (double) Convert.ToInt32(Game1.objectInformation[(int) ((NetFieldBase<int, NetInt>) this.indexOfHarvest)].Split('/')[1]) + 1.0, Math.E));
          if (junimoHarvester == null)
            Game1.player.gainExperience(0, (int) Math.Round((double) num7));

Perhaps suggesting that the 2nd element is a level or experience modifier. Though it's unclear.

To further confuse matters, In StardewValley.Debris(int debrisType, int numberOfChunks, Vector2 debrisOrigin, Vector2 playerPosition, float velocityMultiplyer)

It seems that the 4th element is checked for two different kinds of information, specifically a String literal and an integer, which doesn't help figure out what the field is intended to be. Though I'd offer a category/type classification as a suggestion.

      int num1;
      if (Game1.objectInformation.ContainsKey(debrisType))
        num1 = Game1.objectInformation[debrisType].Split('/')[3].Contains("-4") ? 1 : 0;
      else
        num1 = 0;
      floppingFish.Value = num1 != 0;
      int num2;
      if (Game1.objectInformation.ContainsKey(debrisType))
        num2 = Game1.objectInformation[debrisType].Split('/')[3].Contains("Fish") ? 1 : 0;
      else
        num2 = 0;

However,

 string str;
      if (objectIndex <= 0)
        str = "Crafting";
      else
        str = Game1.objectInformation[objectIndex].Split('/')[3].Split(' ')[0];

This snippet from StardewValley.Debris.Debris(int objectIndex, Vector2 debrisOrigin, Vector2 playerPosition, mentions "Crafting" as a default type, and suggests that all indexes <= 0 are "Crafting" objects. So I'd be happy to say the 4th elements first part is indicative of what 'category' the object falls under.

There also seems to be references which suggest the 5th element may hold description/name information in certain cases, evidenced by:

if (descriptionElement1.param[index1] is StardewValley.Object)
{
    string str;
    Game1.objectInformation.TryGetValue((int) ((NetFieldBase<int, NetInt>) (descriptionElement1.param[index1] as StardewValley.Object).parentSheetIndex), out str);
    descriptionElement1.param[index1] = (object) str.Split('/')[4];
}

In StardewValley.Quests.DescriptionElement.loadDescriptionElement().

In StardewValley.Farmer.doneEating()

else if (this.IsLocalPlayer)
      {
        string[] strArray1 = Game1.objectInformation[itemToEat.ParentSheetIndex].Split('/');
        if (Convert.ToInt32(strArray1[2]) > 0)
        {
          string[] strArray2;
          if (strArray1.Length <= 7)
            strArray2 = new string[12]
            {
              "0",
              "0",
              "0",
              "0",
              "0",
              "0",
              "0",
              "0",
              "0",
              "0",
              "0",
              "0"
            };
          else
            strArray2 = strArray1[7].Split(' ');
          string[] strArray3 = strArray2;
          if (strArray1.Length > 6 && strArray1[6].Equals("drink"))
          {
            if (!Game1.buffsDisplay.tryToAddDrinkBuff(new Buff(Convert.ToInt32(strArray3[0]), Convert.ToInt32(strArray3[1]), Convert.ToInt32(strArray3[2]), Convert.ToInt32(strArray3[3]), Convert.ToInt32(strArray3[4]), Convert.ToInt32(strArray3[5]), Convert.ToInt32(strArray3[6]), Convert.ToInt32(strArray3[7]), Convert.ToInt32(strArray3[8]), Convert.ToInt32(strArray3[9]), Convert.ToInt32(strArray3[10]), strArray3.Length > 10 ? Convert.ToInt32(strArray3[10]) : 0, strArray1.Length > 8 ? Convert.ToInt32(strArray1[8]) : -1, strArray1[0], strArray1[4])))
              ;
          }
          else if (Convert.ToInt32(strArray1[2]) > 0)
            Game1.buffsDisplay.tryToAddFoodBuff(new Buff(Convert.ToInt32(strArray3[0]), Convert.ToInt32(strArray3[1]), Convert.ToInt32(strArray3[2]), Convert.ToInt32(strArray3[3]), Convert.ToInt32(strArray3[4]), Convert.ToInt32(strArray3[5]), Convert.ToInt32(strArray3[6]), Convert.ToInt32(strArray3[7]), Convert.ToInt32(strArray3[8]), Convert.ToInt32(strArray3[9]), Convert.ToInt32(strArray3[10]), strArray3.Length > 11 ? Convert.ToInt32(strArray3[11]) : 0, strArray1.Length > 8 ? Convert.ToInt32(strArray1[8]) : -1, strArray1[0], strArray1[4]), Math.Min(120000, (int) ((double) Convert.ToInt32(strArray1[2]) / 20.0 * 30000.0)));
        }

Whole stack of things are mentioned, especially past the 6th element, namely with regards to buffs and if the item eaten is infact food or drink (stored in the 7th element). Will need to take a longer look at the binary-if statement mess there to figure out what it's expecting. More drink stuff is seen later in the same file:

      this.itemToEat = (Item) o;
      this.mostRecentlyGrabbedItem = (Item) o;
      string[] strArray = Game1.objectInformation[o.ParentSheetIndex].Split('/');
      this.forceCanMove();
      this.completelyStopAnimatingOrDoingAction();
      if (strArray.Length > 6 && strArray[6].Equals("drink"))
      {
        if (this.IsLocalPlayer && Game1.buffsDisplay.hasBuff(7) && !overrideFullness)
        {
          Game1.addHUDMessage(new HUDMessage(Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.2898"), Color.OrangeRed, 3500f));
          return;
        }
        this.drinkAnimationEvent.Fire(o.getOne() as Object);
      }
      else if (Convert.ToInt32(strArray[2]) != -300)
      {
        if (Game1.buffsDisplay.hasBuff(6) && !overrideFullness)
        {
          Game1.addHUDMessage(new HUDMessage(Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.2899"), Color.OrangeRed, 3500f));
          return;
        }
        this.eatAnimationEvent.Fire(o.getOne() as Object);
      }

StardewValley.Tools.FishingRod.draw(SpriteBatch b) gets the caught fish's name using an object lookup string text = Game1.objectInformation[this.whichFish].Split('/')[4]; Therefore suggesting Fish that can be caught will have a name in the 5th element. (This complements evidence found earlier)

StardewValley.GameLocation.digUpArtifactSpot(int xLocation, int yLocation, Farmer who) makes use of the 4th element to determine if an object is archeological. If it is, Then the 7th element is a set of integer pairs, likely an objectID and amount.

 foreach (KeyValuePair<int, string> keyValuePair in (IEnumerable<KeyValuePair<int, string>>) Game1.objectInformation)
      {
        string[] strArray1 = keyValuePair.Value.Split('/');
        if (strArray1[3].Contains("Arch"))
        {
          string[] strArray2 = strArray1[6].Split(' ');
          int index = 0;
          while (index < strArray2.Length)
          {
            if (strArray2[index].Equals((string) ((NetFieldBase<string, NetString>) this.name)) && random.NextDouble() < Convert.ToDouble(strArray2[index + 1], (IFormatProvider) CultureInfo.InvariantCulture))
            {
              objectIndex = keyValuePair.Key;
              break;
            }
            index += 2;
          }
        }
        if (objectIndex != -1)
          break;
      }

Additionally later in the same class, it mentions: if(Game1.objectInformation[index2].Split('/')[3].Contains("Arch") || index2 == 102) item index 102 is seemingly an exception to the "Arch" rule.

The recurring value of -300 on items may be because they are marked as inedible, as evidenced by StardewValley.Game1.parseDebufInput(string debufInput)

  case "makeInedible":
            if (Game1.player.ActiveObject != null)
            {
              Game1.player.ActiveObject.edibility.Value = -300;
              break;
            }

This is supported by StardewValley.Object.Object(Vector2 tileLocation, int parentSheetIndex, string Givenname...)

Game1.objectInformation.TryGetValue(parentSheetIndex, out str);
      try
      {
        if (str != null)
        {
          string[] strArray1 = str.Split('/');
          this.name = strArray1[0];
          this.price.Value = Convert.ToInt32(strArray1[1]);
          this.edibility.Value = Convert.ToInt32(strArray1[2]);
          string[] strArray2 = strArray1[3].Split(' ');
          this.type.Value = strArray2[0];
          if (strArray2.Length > 1)
            this.Category = Convert.ToInt32(strArray2[1]);
        }
      }

which lists specifically that:

  • Element 1 is the name
  • Element 2 is the price
  • Element 3 is the edibility
  • Element 4's first part (split on space) is the ObjectType
  • Element 4's second part (split on space) is the Category (optional)
  • Element 5 is a DisplayedName
  • Element 6 is a Description
  • The subsequent Elements are variable based on the type.

Applying what we've figured out so far to: "Stone/0/-300/Basic/Stone/A useful material when broken with the Pickaxe."

Each field represents: Name/Price/Edibility/ObjectType/DisplayedName/Description

Or optionally:

Name/Price/Edibility/ObjectType Category/DisplayedName/Description

Quests

Dictionary<int, string> dictionary = Game1.temporaryContent.Load<Dictionary<int, string>>("Data\\Quests");
if (dictionary != null && dictionary.ContainsKey((int) ((NetFieldBase<int, NetInt>) this.id)))
{
    string[] strArray = dictionary[(int) ((NetFieldBase<int, NetInt>) this.id)].Split('/');
    if (strArray[3].Length > 1)
    this._currentObjective = strArray[3];
}
this.reloadObjective();
if (this._currentObjective == null)
    this._currentObjective = "";
return this._currentObjective;
public static Quest getQuestFromId(int id)
    {
      Dictionary<int, string> dictionary = Game1.temporaryContent.Load<Dictionary<int, string>>("Data\\Quests");
      if (dictionary == null || !dictionary.ContainsKey(id))
        return (Quest) null;
      string[] strArray1 = dictionary[id].Split('/');
      string str1 = strArray1[0];
      Quest quest = (Quest) null;
      string[] strArray2 = strArray1[4].Split(' ');
      switch (str1)
      {
        case "Basic":
          quest = new Quest();
          quest.questType.Value = 1;
          break;
        case "Building":
          quest = new Quest();
          quest.questType.Value = 8;
          quest.completionString.Value = strArray2[0];
          break;
        case "Crafting":
          quest = (Quest) new CraftingQuest(Convert.ToInt32(strArray2[0]), strArray2[1].ToLower().Equals("true"));
          quest.questType.Value = 2;
          break;
        case "ItemDelivery":
          quest = (Quest) new ItemDeliveryQuest();
          (quest as ItemDeliveryQuest).target.Value = strArray2[0];
          (quest as ItemDeliveryQuest).item.Value = Convert.ToInt32(strArray2[1]);
          (quest as ItemDeliveryQuest).targetMessage = strArray1[9];
          if (strArray2.Length > 2)
            (quest as ItemDeliveryQuest).number.Value = Convert.ToInt32(strArray2[2]);
          quest.questType.Value = 3;
          break;
        case "ItemHarvest":
          quest = (Quest) new ItemHarvestQuest(Convert.ToInt32(strArray2[0]), strArray2.Length > 1 ? Convert.ToInt32(strArray2[1]) : 1);
          break;
        case "Location":
          quest = (Quest) new GoSomewhereQuest(strArray2[0]);
          quest.questType.Value = 6;
          break;
        case "LostItem":
          quest = (Quest) new LostItemQuest(strArray2[0], strArray2[2], Convert.ToInt32(strArray2[1]), Convert.ToInt32(strArray2[3]), Convert.ToInt32(strArray2[4]));
          break;
        case "Monster":
          quest = (Quest) new SlayMonsterQuest();
          (quest as SlayMonsterQuest).loadQuestInfo();
          (quest as SlayMonsterQuest).monster.Value.Name = strArray2[0].Replace('_', ' ');
          (quest as SlayMonsterQuest).monsterName.Value = (quest as SlayMonsterQuest).monster.Value.Name;
          (quest as SlayMonsterQuest).numberToKill.Value = Convert.ToInt32(strArray2[1]);
          if (strArray2.Length > 2)
            (quest as SlayMonsterQuest).target.Value = strArray2[2];
          else
            (quest as SlayMonsterQuest).target.Value = "null";
          quest.questType.Value = 4;
          break;
        case "Social":
          quest = (Quest) new SocializeQuest();
          (quest as SocializeQuest).loadQuestInfo();
          break;
      }
      quest.id.Value = id;
      quest.questTitle = strArray1[1];
      quest.questDescription = strArray1[2];
      if (strArray1[3].Length > 1)
        quest.currentObjective = strArray1[3];
      string str2 = strArray1[5];
      char[] chArray = new char[1]{ ' ' };
      foreach (string str3 in str2.Split(chArray))
      {
        if (str3.StartsWith("h"))
        {
          if (Game1.IsMasterGame)
            str3 = str3.Substring(1);
          else
            continue;
        }
        quest.nextQuests.Add(Convert.ToInt32(str3));
      }
      quest.showNew.Value = true;
      quest.moneyReward.Value = Convert.ToInt32(strArray1[6]);
      quest.rewardDescription.Value = strArray1[6].Equals("-1") ? (string) null : strArray1[7];
      if (strArray1.Length > 8)
        quest.canBeCancelled.Value = strArray1[8].Equals("true");
      return quest;
    }

NPC

NPC Name Translation (Names Hardcoded)

protected override string translateName(string name)
    {
      switch (name)
      {
        case "Bear":
          return Game1.content.LoadString("Strings\\NPCNames:Bear");
        case "Bouncer":
          return Game1.content.LoadString("Strings\\NPCNames:Bouncer");
        case "Gil":
          return Game1.content.LoadString("Strings\\NPCNames:Gil");
        case "Governor":
          return Game1.content.LoadString("Strings\\NPCNames:Governor");
        case "Grandpa":
          return Game1.content.LoadString("Strings\\NPCNames:Grandpa");
        case "Gunther":
          return Game1.content.LoadString("Strings\\NPCNames:Gunther");
        case "Henchman":
          return Game1.content.LoadString("Strings\\NPCNames:Henchman");
        case "Kel":
          return Game1.content.LoadString("Strings\\NPCNames:Kel");
        case "Marlon":
          return Game1.content.LoadString("Strings\\NPCNames:Marlon");
        case "Mister Qi":
          return Game1.content.LoadString("Strings\\NPCNames:MisterQi");
        case "Morris":
          return Game1.content.LoadString("Strings\\NPCNames:Morris");
        case "Old Mariner":
          return Game1.content.LoadString("Strings\\NPCNames:OldMariner");
        case "Welwick":
          return Game1.content.LoadString("Strings\\NPCNames:Welwick");
        default:
          Dictionary<string, string> dictionary = Game1.content.Load<Dictionary<string, string>>("Data\\NPCDispositions");
          if (!dictionary.ContainsKey(name))
            return name;
          string[] strArray = dictionary[name].Split('/');
          return strArray[strArray.Length - 1];
      }
    }

NPC Dialogue

public Dictionary<string, string> Dialogue
    {
      get
      {
        if (this is Monster)
          return (Dictionary<string, string>) null;
        if (this.dialogue == null)
        {
          try
          {
            IEnumerable<KeyValuePair<string, string>> source = Game1.content.Load<Dictionary<string, string>>("Characters\\Dialogue\\" + this.Name).Select<KeyValuePair<string, string>, KeyValuePair<string, string>>((Func<KeyValuePair<string, string>, KeyValuePair<string, string>>) (pair =>
            {
              string key = pair.Key;
              string str1 = pair.Value;
              if (str1.Contains("¦"))
                str1 = !Game1.player.IsMale ? str1.Substring(str1.IndexOf("¦") + 1) : str1.Substring(0, str1.IndexOf("¦"));
              string str2 = str1;
              return new KeyValuePair<string, string>(key, str2);
            }));
            Func<KeyValuePair<string, string>, string> func = (Func<KeyValuePair<string, string>, string>) (p => p.Key);
            Func<KeyValuePair<string, string>, string> keySelector;
            this.dialogue = source.ToDictionary<KeyValuePair<string, string>, string, string>(keySelector, (Func<KeyValuePair<string, string>, string>) (p => p.Value));
          }
          catch (ContentLoadException ex)
          {
            this.dialogue = new Dictionary<string, string>();
          }
        }
        return this.dialogue;
      }
    }

NPC Arrival Dialogue

    string[] strArray = this.Dialogue[(string) ((NetFieldBase<string, NetString>) l.name) + "_Entry"].Split('/');

NPC Ambient Dialogue

Under 10 minute update:

if (Game1.random.NextDouble() < 0.1 && this.Dialogue != null && this.Dialogue.ContainsKey((string) ((NetFieldBase<string, NetString>) l.name) + "_Ambient"))
      {
        string[] strArray = this.Dialogue[(string) ((NetFieldBase<string, NetString>) l.name) + "_Ambient"].Split('/');
        int preTimer = Game1.random.Next(4) * 1000;
        this.showTextAboveHead(strArray[Game1.random.Next(strArray.Length)], -1, 2, 3000, preTimer);
      }

NPC Gift Tastes

Dictionary<string, string> dictionary2 = Game1.content.Load<Dictionary<string, string>>("Data\\NPCGiftTastes");

StardewValley.NPC.loadCurrentDialogue() : 1896

NPC Schedules

See StardewValley.NPC.parseMasterSchedule(string rawData) : 2991 Game1.content.Load<Dictionary<string, string>>("Characters\\schedules\\" + this.Name)[currentSeason].Split('/')

NPC Disposition

public NPC(AnimatedSprite sprite, Vector2 position, string defaultMap, int facingDir, string name, Dictionary<int, int[]> schedule, Texture2D portrait, bool eventActor)
      : base(sprite, position, 2, name)
    {
      this.portrait = portrait;
      this.faceDirection(facingDir);
      this.defaultPosition.Value = position;
      this.defaultMap.Value = defaultMap;
      this.currentLocation = Game1.getLocationFromName(defaultMap);
      this.defaultFacingDirection = facingDir;
      if (!eventActor)
        this.lastCrossroad = new Microsoft.Xna.Framework.Rectangle((int) position.X, (int) position.Y + 64, 64, 64);
      try
      {
        Dictionary<string, string> source = Game1.content.Load<Dictionary<string, string>>("Data\\NPCDispositions");
        if (!source.ContainsKey(name))
          return;
        string[] strArray = source[name].Split('/');
        string str1 = strArray[0];
        if (!(str1 == nameof (teen)))
        {
          if (str1 == nameof (child))
            this.Age = 2;
        }
        else
          this.Age = 1;
        string str2 = strArray[1];
        if (!(str2 == nameof (rude)))
        {
          if (str2 == nameof (polite))
            this.Manners = 1;
        }
        else
          this.Manners = 2;
        string str3 = strArray[2];
        if (!(str3 == nameof (shy)))
        {
          if (str3 == nameof (outgoing))
            this.SocialAnxiety = 0;
        }
        else
          this.SocialAnxiety = 1;
        string str4 = strArray[3];
        if (!(str4 == nameof (positive)))
        {
          if (str4 == nameof (negative))
            this.Optimism = 1;
        }
        else
          this.Optimism = 0;
        string str5 = strArray[4];
        if (!(str5 == nameof (female)))
        {
          if (str5 == nameof (undefined))
            this.Gender = 2;
        }
        else
          this.Gender = 1;
        string str6 = strArray[5];
        if (!(str6 == nameof (datable)))
        {
          if (str6 == "not-datable")
            this.datable.Value = false;
        }
        else
          this.datable.Value = true;
        this.loveInterest = strArray[6];
        string str7 = strArray[7];
        if (!(str7 == "Desert"))
        {
          if (!(str7 == "Other"))
          {
            if (str7 == "Town")
              this.homeRegion = 2;
          }
          else
            this.homeRegion = 0;
        }
        else
          this.homeRegion = 1;
        if (strArray.Length > 8)
        {
          this.Birthday_Season = strArray[8].Split(' ')[0];
          this.Birthday_Day = Convert.ToInt32(strArray[8].Split(' ')[1]);
        }
        for (int index = 0; index < source.Count; ++index)
        {
          if (source.ElementAt<KeyValuePair<string, string>>(index).Key.Equals(name))
          {
            this.id = index;
            break;
          }
        }
        this.displayName = strArray[11];
      }
      catch (Exception ex)
      {
      }
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment