Thursday, December 13, 2018

Stats Editor for Feed and Grow Fish

Preface


Today we're going to build a CSV based stats editor for Feed and Grow Fish. This editor will allow you to trivially edit the stats using an external CSV file.

Can I see it in action?

Here's s companion video that shows off all of this in action:


Let's build a mod

Let's load Feed and Grow Fish into dnSpy so that we can begin to add the CSV editor into the game.

We need to get our code inserted into the right place.

Jump into your Assembly-CSharp.dll and click on the "FishSelectFilterButton" class which is within Assets.Code.UI.Extensions:


Within the FishSelectFitlerButton class you need to use the search functionality (control + f) to search for "NPCS". You are looking for the "LoadAll" function as seen below:


You need to click on the "LoadAll" orange text which will jump you over to that function. Upon clicking on it you should see something like this:


We need to edit this code. Right click anywhere in the page and select "Edit Class (C#)" as seen below:


Remove everything from within the "LoadAll" function (in other words, just the line that starts with return Resources.ConvertObjects), and replace it with the following block of code:

// Load in the resources that were requested
T[] array = Resources.ConvertObjects<T>(Resources.LoadAll(path, typeof(T)));

// Did we request the NPCS?
if (path == "NPCS")
{
 // Does a stats file already exist?
 if (File.Exists("stats.csv"))
 {
  // Yes a stats file exists

  // Read in the stats CSV file and split it up into an array with one line as each entry
  string[] array2 = File.ReadAllText("stats.csv").Replace("\"", "").Split(new char[]
  {
   '\n'
  });

  // Loop over each NPC
  foreach (LivingEntity livingEntity in array as LivingEntity[])
  {
   // Search the CSV for the NPC we are currently dealing with
   string[] array3 = null;
   string[] array4 = array2;
   // Loop over each line in the CSV
   for (int i = 0; i < array4.Length; i++)
   {
    // Split the line based on a comma
    string[] array5 = array4[i].Split(new char[]
    {
     ','
    });
    // Was this the fish we are currently dealing with?
    if (array5[0] == livingEntity.name)
    {
     // Yep, grab it, and stop looping
     array3 = array5;
     break;
    }
   }
   // Did we find the fish in our CSV?
   if (array3 != null)
   {
    // Yes we found the fish

    // Apply all of the properties from the CSV
    // Some are floats, some are ints, we are just parsing the CSV
    livingEntity.AttackSpeed = float.Parse(array3[1]);
    livingEntity.AdditiveMaxEggs = int.Parse(array3[2]);
    livingEntity.Anger = float.Parse(array3[3]);
    livingEntity.CurrentSpeed = float.Parse(array3[4]);
    livingEntity.DamageStat = float.Parse(array3[5]);
    livingEntity.DefaultFocus = float.Parse(array3[6]);
    livingEntity.EggsInside = int.Parse(array3[7]);
    livingEntity.Health = float.Parse(array3[8]);
    livingEntity.HealthStat = float.Parse(array3[9]);
    livingEntity.MaxTemp = float.Parse(array3[10]);
    livingEntity.MinTemp = float.Parse(array3[11]);
    livingEntity.MinSize = float.Parse(array3[12]);

    // Handle the mouth, just need to apply it correctly
    string a = array3[13];
    if (!(a == "Beak"))
    {
     if (!(a == "Teeth"))
     {
      if (a == "Empty")
      {
       livingEntity.Mouth = ActiveThing.MouthType.Empty;
      }
     }
     else
     {
      livingEntity.Mouth = ActiveThing.MouthType.Teeth;
     }
    }
    else
    {
     livingEntity.Mouth = ActiveThing.MouthType.Beak;
    }
    // Handle some more attributes:
    livingEntity.MovemSpeed = float.Parse(array3[14]);
    livingEntity.RoamStrenght = float.Parse(array3[15]);
    if (livingEntity is Fish)
    {
     (livingEntity as Fish).RotationSpeed = float.Parse(array3[16]);
    }
    // Handle the skin type
    a = array3[17];
    if (!(a == "Leather"))
    {
     if (!(a == "Plate"))
     {
      if (a == "Scale")
      {
       livingEntity.Skin = ActiveThing.SkinType.Scale;
      }
     }
     else
     {
      livingEntity.Skin = ActiveThing.SkinType.Plate;
     }
    }
    else
    {
     livingEntity.Skin = ActiveThing.SkinType.Leather;
    }
   }
  }
 }
 else
 {
  // No, a stats file doesn't exist

  // Create the header for the CSV file
  string text = "Name,";
  text += "AttackSpeed,";
  text += "AdditiveMaxEggs,";
  text += "Anger,";
  text += "CurrentSpeed,";
  text += "DamageStat,";
  text += "DefaultFocus,";
  text += "EggsInside,";
  text += "Health,";
  text += "HealthStat,";
  text += "MaxTemp,";
  text += "MinTemp,";
  text += "MinSize,";
  text += "Mouth,";
  text += "MovemSpeed,";
  text += "RoamStrenght,";
  text += "RotationSpeed,";
  text += "Skin";
  text += Environment.NewLine;

  // Loop over every entity that we loaded from above
  foreach (LivingEntity livingEntity2 in array as LivingEntity[])
  {
   // Add the fields into the CSV
   text = text + livingEntity2.name + ",";
   text = text + livingEntity2.AttackSpeed + ",";
   text = text + livingEntity2.AdditiveMaxEggs + ",";
   text = text + livingEntity2.Anger + ",";
   text = text + livingEntity2.CurrentSpeed + ",";
   text = text + livingEntity2.DamageStat + ",";
   text = text + livingEntity2.DefaultFocus + ",";
   text = text + livingEntity2.EggsInside + ",";
   text = text + livingEntity2.Health + ",";
   text = text + livingEntity2.HealthStat + ",";
   text = text + livingEntity2.MaxTemp + ",";
   text = text + livingEntity2.MinTemp + ",";
   text = text + livingEntity2.MinSize + ",";
   text = text + livingEntity2.Mouth + ",";
   text = text + livingEntity2.MovemSpeed + ",";
   text = text + livingEntity2.RoamStrenght + ",";

   // Are we dealing with a fish?
   if (livingEntity2 is Fish)
   {
    // Yep, we are dealing with a fish, allow rotation speed to be edited
    text = text + (livingEntity2 as Fish).RotationSpeed + ",";
   }
   else
   {
    // Nope, we are not dealing with a fish, this isnt applicable
    text += "N/A,";
   }

   // Add the remaining fields and a new line
   text += livingEntity2.Skin;
   text += Environment.NewLine;
  }
  
  // Save the CSV
  File.WriteAllText("stats.csv", text);
 }
}
return array;


It should look something like this:


Let's scroll to the very top of the code, and replace the entire section that has "using xxx" with the following:

using System;
using System.IO;
using System.Runtime.CompilerServices;
using Assets.Code;
using Assets.Code.Things;
using UnityEngineInternal;


This will end up looking something like this:


The final thing we need to do as add some assembly references. Click the add assembly reference in the bottom left hand corner of the editor:


This will open a browse to window. You need to add the "Assembly-CSharp.dll" and "UnityEngine.Networking.dll", this can be done using the button twice:


Finally press the compile button in the lower right hand corner of the code editor.

We can now save our changes. Select "File" and then "Save All...", and then press "OK" to override the DLL and save our changes.

How do I use the mod?

With the mod installed, you need to launch the game, load a map, and access the menu where you select a fish.

You can now close the game and there will be a stats.csv file in the root directory of your Feed and Grow Fish game.

Edit the stats.csv file using Microsoft Excel or any other text editor, save it, and make sure to close the editor. If you don't close the editor then Feed and Grow Fish may error out and not let you select any fish.

All of the stat changed you made will be applied to the fish.


Once again, if you have any questions, let us know below, or on the video.

Sunday, December 9, 2018

Mega Whale Gang in Feed and Grow Fish Tutorial

Preface


Today I was playing around in Feed and Grow Fish and noticed that the Bibos has a Gang feature that can be toggled to make other Bibos work as a group to attack things.


Naturally my first question is "How can we take that to the EXTREME?", let's investigate!

Can I see it in action?

Of course! We've made a companion video that shows the mod in action, and exactly what it does, the video also walks through the tutorial from below, so you can see exactly how this all works.

Lets go exploring

Let's load Feed and Grow Fish into dnSpy so that we can begin to search for gangs and see what we can find.

My first step was to try and find the gang ability, and find it I did:


It's called "CallOfBrotherhood" internally.

Let's take a look at the code for when this ability is equipped:


Looks like exactly what we want, we can see there is a "Group" property on your player which gets created if you're part of a group, and then it sets your fish to be the "Leader" and configures the "PermanentLeader" property to true, and then shows a nice little notification (That's cool, we can use that notification code elsewhere).

Our past mods that we've made on on our YouTube channel have added extra key binds by editing the PlayerController class -- This is where all the logic for when you're doing stuff as a Fish is actually housed, there is also a SpectatorController, but we don't care too much about that for our use cases.

Let's build a mod

I've written a chunk of code that can be directly copy and pasted into the PlayerController so that we can have custom binds for adding every single fish into our gang, and a custom bind for spawning new fish and placing those into our gang.

I've put a bunch of comments (the green lines) in there for you, they start with a "//" which means they are a comment in C#, so you can just paste that directly in, exactly as it is, and dnSpy will just ignore the comments.

It's easy enough to change the key bindings, if you just delete the letter (e.g. P) and then delete the "dot" before it, you can type a dot, and then dnSpy will automatically try to help you and autocomplete, showing you a range of other options, allowing you to see all the keys that are available.

// Did we just press the letter "P"?
if (Input.GetKeyDown(KeyCode.P))
{
 // Are we in a group?
 if (this.CurrentLivingEntity.Group == null)
 {
  // We are not in a group, better create one!
  LivingEntityGroup.CreateGroup(this.CurrentLivingEntity, this.CurrentLivingEntity);
 }

 // Loop over every single LivingEntity (fish, whales, sharks, crabs, EVERYTHING)
 foreach (LivingEntity thisEnt in UnityEngine.Object.FindObjectsOfType<LivingEntity>())
 {
  // Add this LivingEntity to our new group
  this.CurrentLivingEntity.Group.AddLivingEntity(thisEnt);
 }

 // Set the intensity (how close everything is) to be really low
 this.CurrentLivingEntity.Group.DensityIntensity = 0.1f;

 // Make it so there is a leader, and make the leader us
 this.CurrentLivingEntity.Group.PermanentLeader = true;
 this.CurrentLivingEntity.Group.Leader = this.CurrentLivingEntity;

 // Tell everything who falls behind to come catch up
 this.CurrentLivingEntity.Group.DontLeaveBehind = true;

 // Show a pretty message
 NotifyDialogue.ShowNotify("Super Gang Updated", 1.5f);
}

// Are we currently holding the letter "O"
if (Input.GetKey(KeyCode.O))
{
 // Are we in a group?
 if (this.CurrentLivingEntity.Group == null)
 {
  // We are not in a group, better create one!
  LivingEntityGroup.CreateGroup(this.CurrentLivingEntity, this.CurrentLivingEntity);
 }

 // Grab a list of EVERY template fish in the entire game
 LivingEntity[] array2 = Resources.LoadAll<LivingEntity>("NPCS");

 // Loop over every template, we are looking for one that matches our fish
 LivingEntity foundEnt = null;
 foreach (LivingEntity anEnt in array2)
 {
  // Is the name of this template the same as the name of our fish?
  if (anEnt.name == this.CurrentLivingEntity.name)
  {
   // Woot, we found the right template!
   foundEnt = anEnt;

   // Exit the loop, no need to continue
   break;
  }
 }

 // Did we manage to find the template for our fish?
 if (foundEnt != null)
 {
  // We found the template!

  // Create a copy of that template, and spawn it at the same position as our fish
  LivingEntity newEnt = (LivingEntity)NpcGenerator.Instance.Spawn(foundEnt, this.CurrentLivingEntity.Position, false);
  newEnt.IsMyPlayer = false;

  // Add that new fish we created to our group
  this.CurrentLivingEntity.Group.AddLivingEntity(newEnt);

  // Set the intensity (how close everything is) to be really low
  this.CurrentLivingEntity.Group.DensityIntensity = 0.1f;

  // Make it so there is a leader, and make the leader us
  this.CurrentLivingEntity.Group.PermanentLeader = true;
  this.CurrentLivingEntity.Group.Leader = this.CurrentLivingEntity;

  // Tell everything who falls behind to come catch up
  this.CurrentLivingEntity.Group.DontLeaveBehind = true;
 }
}

Pretty cool, but what do we do with that chunk?

Find the "Assets.Code.PlayerController" class (see the screenshot) and click on it to load the code for that class.


Clicking on the "PlayerController" class will load up the code for the class, we need to scroll down to find the "Update" method. The "Update" method is run every single frame of the game, and its where we do the logic for checking what controls are pressed, and it's where we will inject our code block.

We're looking for a block that starts with "protected void Update()" and then within there, a few lines below, there is a line that reads "if (this.CurrentLivingEntity != null)". We need to add our code just AFTER the curly bracket.

Right click anywhere in there, and select "Edit Method C#", this will pop open a code editor which will let us actually change the code.

Here's the position we're looking at:


And here's what it looks like after we added the code block within the editor:


You can see that our code begin on line 37 with "// Did we just press the letter P?". You'll also notice that the information lines have turned green, indicating that they are a comment, and don't actually do anything.

With our code change in place, we need to tell it to recompile, this could cause the code to change a fair bit due to the compiling making optimisations, however, the meaning will still be the same.

Press the "Compile" button in the bottom right hand corner of the screen.


Everything should be updated now, however, we need to tell dnSpy to write the changes to the DLL we are editing, so let's tell it to save.

Access the "File" menu and select "Save All...", you should get a popup with a bunch of options, simply select "OK" to confirm and it will override the original DLL, and the code changes will be saved.


That's it!The mod is now installed!

How do I use the mod?

Well! Assuming you've followed the tutorial from above, you should now have the mod installed :)

You can press the letter "P" to make every single fish in the entire map that is currently spawned join your gang and head towards you.

You can hold the letter "O" and clones of your current fish will spawn on top of you and head towards you.

Loading a Unity Game into dnSpy

Intro

This will be a quick walk through that explains the basics of loading a Unity based game into dnSpy for editing. This is intended as a common step for editing most Unity based games.

If you have any questions about the process then make sure to leave a comment below and we'll update the guide / clarify any points that are unclear.

1) Download dnSpy.zip

The first step is to download dnSpy. Visit the releases section of the dnSpy GitHub page and download the latest dnSpy.zip.

2) Extract dnSpy.zip

It's important to actually extract dnSpy.zip, do not open it and try to use it without extracting. Right click on the zip and select "Extract All..." from the menu. Press "Extract" on the window that pops up. A new window will popup which shows the extract files.

3) Open dnSpy.exe

With the files extracted, open dnSpy.exe. If you can't see the ".exe" on the end then you may need to change your view options. You may need to open "dnSpy-x86.exe" on a 32 bit computer (if dnSpy.exe isn't working, try dnSpy-x86.exe).

Windows 10:

Open the "View" tab at the top of the explorer window and then turn on "File name extensions" using the checkbox item.


Windows 7:

Press and release the "Alt" key on your keyboard. Select "Tools" and then navigate to "Folder Options". Open the "View" tab and then remove the check from the "Hide extensions for known file types" box.


4) Open Assembly-CSharp.dll

All Unity games generate a similar file structure. Start by opening the directory where the game was installed to. In the case of steam, you can simply right click on a game and then select "Properties", open the "LOCAL FILES" tab, and then press "BROWSE LOCAL FILES...".


All unity games have a folder with the name of the game that ends in "_Data", for example, for 60 seconds, it is called "60Seconds_Data". Open the "_Data" folder. Navigate into the "Managed" folder. The "Assembly-CSharp.dll" file is stored within the "Managed" folder.

The "Assembly-CSharp.dll" file contains the majority of the games code that we will be interested in modifying. Let's open it.

Drag and drop the "Assembly-CSharp.dll" directly into the "Assembly Explorer" within dnSpy.


If you can't see the assembly explorer then you can activate it by opening the "View" menu and then selecting "Assembly Explorer" (Ctrl + Alt + L).


If you are unable to drag and drop into the Assembly Explorer then you can alternately select "File" and then click on "Open" and directly browse to the "Assembly-CSharp.dll" file.

5) Expand the trees

Now that the main file is loaded, lets start exploring the code. The code is sorted into the namespaces and classes within dnSpy. You need to expand the trees in order to find the relevant piece of code to edit.


6) Follow one of our other tutorials

This was the basic setup that is used in many other tutorials. This tutorial was simply to get dnSpy up and running to load up a unity based Assembly-CSharp.dll.