Freedom Planet 2 Modding Guide

Section 0: Prerequisites

Alternatively, any other C#/.NET IDE will do fine as well.

If you have VS already installed, you can add it by running Visual Studio Installer.
If you don’t want to install Visual Studio, you can download just the SDK itself, available HERE
You can verify installed .NET SDK by running terminal command: dotnet --list-sdks

You can use Powershell, Windows Terminal, or Visual Studio’s built-in terminal.
You can also choose to install them from within VS, by adding the NuGet repo there.

dotnet new -i BepInEx.Templates --nuget-source https://nuget.bepinex.dev/v3/index.json

You can also use other Unity asset extractors, as long as they produce a proper Unity project

Best option is to download just the editor using the “Downloads (Win)” tab, without using Unity Hub

Also recommended but not technically required:

Section 1: Creating a project

All these instructions are also available on official BepInEx documentation, following is a shortened version with included FP2 specific options.

Project can be created in two ways:

  1. Using your IDE
    Visual Studio will automatically load installed BepInEx Templates. Simply select “BepInEx 5 Mono Plugin Template” while creating new project and name it. On following menu select:

Fill the project name and version to your own preference, personally i would recommend using major.minor.patch (x.x.x) version format.

  1. Using the Terminal (Powershell or Windows Terminal will do):

In both cases, you will now have a ready-made template project, with main file called “Project.cs”

Once project is set, you will need to add the game’s core file as Assembly Reference

In Visual Studio, it is performed by right-clicking Dependencies > Assemblies tab within Solution Explorer. Select “Add Assembly Reference”, and in newly opened window go to “Browse”, click Browse… and navigate to your game’s installation directory. Within it, open FP2_Data/Managed, and select “Assembly-CSharp.dll”. Remember to make sure the checkmark appears next to the “Assembly-CSharp.dll” in the listing.

If you want to create code-only mod, you are now set up! The project is ready to go! Proceed to Section 3 to learn basic code examples

Section 2: Decompiling the game

Open Asset Ripper (or preferred tool) and point it to the FP2_Data directory within the game’s install directory.

You will want to decompile the latest possible build of the game - while assets made in older versions are usually compatible, you might run into some issues later on, especially after FP2’s language update comes out (no set date yet).

Rewired is a paid plug-in, and therefore cannot be decompiled properly (or legally). As such you will lack some options in the editor, and some parts of it might not function properly.

Similar case can be made for Ferr2D, however lack of it does not prevent the game from functioning, it does however limit Your capabilities due to lack of some tools for editing the geometry.

If you believe you will need these tools, You can consider purchasing them at a later date

If using Asset Ripper make sure you run the latest version.

After all the assets get parsed and loaded, select the Export > Export all Files option, and point it to a new folder which will become the root directory for the decompiled game.

Then wait. The process might take a long time. You might want to use a faster SSD drive for it.


By default, modern versions of Asset Ripper will do the following for you, however if your using an older version or another program, you might need to do these:

If your decompiler tool does not automatically move plugins (like Rewired .dll’s) to dedicated folder and instead decompiles them:
Once it’s done cooking, navigate into the output folder and copy following files from AuxiliaryFiles/GameAssemblies folder into ExportedProject/Assets:

  1. If you are using FP2 1.0.1:
  1. If you are using current FP2 build:

Then, navigate to ExportedProject\Assets\Scripts and delete folders named after files you just copied.


Open your Unity 5.6 and select Open. Navigate into directory where you decompiled the game, and select ExportedProject folder.
Then wait. Again. But longer this time! Unity needs to process every. single. file. within the project.

You might want to get a snack or something. Maybe watch a movie.

This step is both HDD and CPU bound - even a modern Ryzen 5800X will take a while at 100% CPU usage!

Once the project is loaded you are almost there!
Unity will now complain about few C# files within its console window.

There will be 4 issues to fix:

You need to edit both of these lines from:
ref FixedArray3<DelaunayTriangle> neighbors = ref Neighbors;
ref FixedArray3<TriangulationPoint> points = ref Points;
to:
FixedArray3<DelaunayTriangle> neighbors = Neighbors;
FixedArray3<TriangulationPoint> points = Points;

In both of these files, completely remove the method (usually at the very bottom):
string IFerr2DTMaterial.get_name() { return base.name; }

You can now load a scene, like Adventure Square, and see if the game runs. If the game starts and you can move as Lilac, you are all set!
Below are extra steps to do if the Unity console fills with thousands of errors instead.

Depending on decompiler used, it might mess up Rewired and Saving.
New Versions of Asset Ripper resolve this by themselves and these steps are no longer needed.

Here are the old steps to fix it, in case you encounter this issue:

a) Hotwire the game to use old input system:

Since we have a lacking Rewired setup, we will need to disable it.

or
b) Buy Rewired/Use its free demo (Untested, and you will lack few data files requiring manual setup)

Additionally there are some other methods that might need to be fixed from decompile within FPSaveManager
You will need to correct these yourself, both issues with FileStream can be worked around by swapping FileOptions.WriteThrough to true

As mentioned above, modern Asset Ripper seems to be able to decode these just fine.


If your sprites seem very jpeg-y and with blur on them, open the Texture2D folder, select All files within it, unselect “Font Texture” file, and on the Import menu on the right set the following:

It will take a while to process, but it will un-jpeg all the sprites.


Next step will be fixing the decompiled shaders. Decompiler boldly assumes you are going to use “modern” version of Unity from 2020 and as such it generates shaders in too new of a format.

Since you will be editing multiple files at once, you might want to use Notepad++ or similar tool for editing, instead of built-in MonoDevelop.

In all files within Shaders folder, you will want to remove following lines:

Comp Disabled
ZClip Off
GpuProgramID <digits>

Replace:

Fog {
	Mode 0
}

with

Fog {
	Mode Off
}

These changes will make the shaders compile, but the Stencil shaders will still be broken. If you want to see how they are broken, load up Paradise Prime or Lightning Tower.

To fix the Stencil Shaders you will need to edit the following:
Replace following section in StencilDraw (might look different per decompile, but it will have same heading):

fout frag(v2f inp)
{
	fout o;
	float4 tmp0;
	float4 tmp1;
	tmp0 = tex2D(_MainTex, inp.texcoord.xy);
	tmp1.x = _AlphaSplitEnabled != 0.0;
	if (tmp1.x) {
	  tmp1 = tex2D(_AlphaTex, inp.texcoord.xy);
	  tmp0.x = tmp1.x;
	}
	tmp0 = tmp0.yzwx * inp.color;
	o.sv_target.xyz = tmp0.www * tmp0.xyz;
	o.sv_target.w = tmp0.w;
	return o;
}

With this (This is basically the same code but not scrunkled by the decompiler):

fixed4 frag(v2f IN) : SV_Target
{
  fixed4 c = tex2D(_MainTex, IN.texcoord.xy) * IN.color;
  if (c.a<0.1) clip (-1);
  if (c.w<0.2) discard;
  c.xyz = c.www * c.xyz;
  c.rgb *= c.a;
  return c;
}

And in StencilInvert with this:

fixed4 frag(v2f IN) : SV_Target
{
	fixed4 c = tex2D(_MainTex, IN.texcoord.xy) * IN.color;
	if (_AlphaSplitEnabled != 0.0) c.x = tex2D(_AlphaTex, IN.texcoord.xy).x;
	if (c.w<0.2) discard;
	c.xyz = c.www * c.xyz;
	c.rgb *= c.a;
	return c;
}

Do not replace it in StencilMask.

If you are interested in OpenGl support for shaders, an extra step will be needed:

You will need to modify GrabPass Distortion shader:
In fout frag(v2f inp) method, paste following lines before o.sv_target = tex2D(_GrabTexture, tmp0.xy);

#if defined(SHADER_API_GLCORE) 
tmp0.y = 1 - tmp0.y; 
#endif

This will ensure the Y coordinate is properly flipped on OpenGL to account for difference between HLSL and GLSL.

All of the shaders should work properly, if you encounter any shader issues and figure out a fix, send it over~ I will add it to this section of the guide.


Finally, we have animations to fix. Since current (as of time of writing - 2023) AssetRipper fails at properties that use custom objects, we will need to manually fix them. Luckily for us, it provides us CRC32 checksum of the variable it failed to decipher, and it can be reverse engineered.

I have written a simple PowerShell script which replaces 95% of these invalid scripts with proper values

It can be downloaded HERE

Place the script in ExportedProject folder and let it do it’s thing. It should show it’s progress in the console.

You can also replace the elements manually (if you don’t trust my scripts or it broke), in which case you will need to replace the following in all *.anim files :
format: (original - fixed)

script_1160310881 - hbAttack.top
script_1328475607 - hbAttack.bottom
script_993553639 - hbAttack.left
script_3381441582 - hbAttack.right
script_2570316445 - hbAttack.enabled
script_3011164480 - hbAttack.visible

script_4246489764 - hbTouch.visible
script_3612678521 - hbTouch.enabled
script_1257246864 - hbTouch.top
script_761158965 - hbTouch.bottom
script_4052579981 - hbTouch.left
script_1679919474 - hbTouch.Right

script_4241118996 - hbHurt.top
script_2992041647 - hbHurt.bottom
script_468585459 - hbHurt.left
script_3547772468 - hbHurt.right
script_3339742096 - hbHurt.enabled
script_3982047309 - hbHurt.visible

script_3273949789 - hbWeakpoint.enabled
script_3916288384 - hbWeakpoint.visible
script_857543501 - hbWeakpoint.top
script_1570321961 - hbWeakpoint.bottom
script_160839143 - hbWeakpoint.left
script_3384679031 - hbWeakpoint.right

script_3254587908 - hbBlock.enabled
script_3954459097 - hbBlock.visible
script_2678822360 - hbBlock.top
script_3919209962 - hbBlock.bottom
script_2304763050 - hbBlock.left
script_3243503243 - hbBlock.right

You are now done! If everything went right, you can now open any scene, click “Play” and the game will start in the editor!

Please note that the decompilation process is not perfect - some things might still be little bit off, but not to a point where it should be noticeable.

Remember to set the preview into 2D mode

Section 3: Writing mods

Most important thing in a mod, is the plan for what the mod will do in the game.
For the case in this tutorial we will be bringing Lilac’s scrapped wing power-up back, making it always active.

First step is finding out what needs to be done, and locating appropriate variables and methods.
dnSpy and Unity Explorer are used for this task - both serve two different functions in such.

For our example, we want to find out how to trigger scrapped wing effect. As we have seen in the trailers, effect is tied to Lilac’s boost - therefore looking there will be the wisest choice.
While browsing in UE, we can see that Player 1 is a GameObject containing FPPlayer - let’s then spin up dnSpy and see what it holds!

FPPlayer has 2 methods of interest: State_Lilac_DragonBoostPt1 and State_Lilac_DragonBoostPt2. Specifically within Pt2 we can see close to the bottom a section with following code:

if  (this.useSpecialItem)  
{  
	this.Action_BoostBreakerShockwave();  
}  
this.genericTimer  =  0f;  
this.angle  =  0f;  
if  (this.hasSpecialItem)  
{  
	this.state  =  new  FPObjectState(this.State_Lilac_Glide);  
}  
else  
{  
	this.SetPlayerAnimation("Jumping",  new  float?(0.25f),  new  float?(0.25f),  false,  true);  
	this.state  =  new  FPObjectState(this.State_InAir);  
}

There is a curious variable hasSpecialItem which seems to make the game fire an additional action, as well as making Lilac glide after the boost is over. Seems familiar?

Go back to Unity Explorer and change hasSpecialItem to True. You will see the power-up icon appear, and you can now use the wings!

We know what we want to do now, let us put it into a mod.

When you are ready to start coding:

Open the project from Step 1 in your IDE.
We will begin by setting up proper GUID, Name and Version for your mod.
The values can be manipulated in other ways, but for this tutorial the easiest method will be shown.

Edit the line:
[BepInPlugin(PluginInfo.PLUGIN_GUID, PluginInfo.PLUGIN_NAME, PluginInfo.PLUGIN_VERSION)]

Recommended naming uses Java convention of:
topdomain.creator.type.game.name.
Domain can be ‘com’, but it can be changed depending on preference.
Creator is your nick, or name.
Type defines what kind of mod it is. 99% of mods will be a plugin, there exist other mod types like preloader and script that exceed the scope of this tutorial.
Game would be fp2, unless your writing a mod for something else :D
Name is the name of your mod.
This naming scheme is not obligatory, it does simplify it for other modders however. You can put anything in there, as long as you are sure it will not collide with someone else’s GUID.

For example, my LilacWings mod will have GUID of com.kuborros.plugin.fp2.lilacwings
Specific libraries which need to load before anything else will append 000. at the start.

To ensure your mod loads only in FP2, you can optionally add this line right under [BepInPlugin…]
[BepInProcess("FP2.exe")]

The template file comes with method Awake() where your mod’s initialisation code should be put.
Here you can load assets, set up configuration values etc. Most importantly you will also want to define all the Harmony patches here.

Harmony is a patcher which allows you to dynamically edit any method within the game.
Full documentation is available here, in this tutorial we will cover the most basic Postfix patch.

The most common patches used in Harmony are Prefixes (which are run before the method you patch), and Postfixes (which are run after). Both have their own set of uses, for the needs of this tutorial we will be using a Postfix.

In your Awake() method create a new Harmony object, providing your mod’s GUID as its parameter, as well as tell newly created Harmony to load patches from a specific class:

var harmony = new Harmony("com.kuborro.plugins.fp2.lilacwings-tutorial");
harmony.PatchAll(typeof(PatchFPPlayer));

(Feel free to come up with your own GUID for this tutorial plug-in)

We will also go ahead and add a class called PatchFPPlayer, the one we also mentioned in the code above.
You can keep this small class within Plugin.cs, you can also put it into its own file.

The class currently will look like this:

class PatchFPPlayer 
{
}

As we found out before, we will want to set useSpecialItem to true when player loads into a level. For this case, we can hook into any method in FPPlayer which gets executed at the start of the level.
Good candidates are Start() and State_Init() - both are run then the level starts, Start gets called when player object initialises, while State_Init runs during the “Ready…” animation.
In this example we will use State_Init due to an issue it creates - more on that later.

Let us prepare a Harmony patch for this method, and add it to PatchFPPlayer we created earlier.
A basic Harmony postfix contains 2 annotations:
[HarmonyPostfix] - annotation defining what kind of patch are we performing, in this case a Postfix
[HarmonyPatch()] - annotation pointing at target method.
followed by a method, for example PatchStateInit(){}

[HarmonyPatch()] takes a set of parameters which allow Harmony to locate a proper method we are trying to patch. In most simple scenarios, it will have following structure:
[HarmonyPatch(typeof(<Class we are patching>),"<Method name>",<MethodType>)]
First two fields are quite self-explanatory, in the first one we put the Class within typeof(), in second the name of the method.
The third field is an enum which selects which type of method are we patching. The usual and most common choice will be MethodType.Normal which is used for most generic method. Another options include Constructor used for, well, constructors, as well as Getter and Setter used for inlined .get() and .set() methods.

In our example, the now expanded class will now look like this:

 class PatchFPPlayer 
 {
        [HarmonyPostfix]
        [HarmonyPatch(typeof(FPPlayer), nameof(FPPlayer.State_Init), MethodType.Normal)]
        static void PatchStateInit() 
	      {
	      }
}

Now we need to access hasSpecialItem to poke around with it.
Harmony exposes an easy method to obtain any private variable of a given Class (it cannot, however give us access to internal method variables).
To access a variable we want, we add it into the patch method’s definition, with it name prepended with ___ (that is 3x “_”). If we want to edit that value, we also need to make it a reference with a ‘ref’ keyword.

The result will be: static void PatchStateInit(ref bool ___hasSpecialItem){}

Now in the method body we need to set the variable to true. Before we do that however, we might want to check if we are playing as Lilac - we wouldn’t want to mess with other characters.

The easiest way to do so would be to refer to the FPPlayer object we are just patching - it exposes a public variable characterID which tells us which character we are playing as. We could use the same method as before - however, since this value is public we can also access it from the object itself.
To obtain an instance of object we are just patching, we can add another special harmony trick - __instance. By adding it to method definition we now have direct reference to the object - __instance is for all intents and purposes equivalent to this keyword in normal method.

We will now be able to run a simple if case to check which character are we playing as:
if (__instance.characterID == FPCharacterID.LILAC)

Here is how the whole example project might look like:

//These will be added by your IDE
using BepInEx;
using HarmonyLib;
using UnityEngine;

namespace LilacWingsTutorial
{
[BepInProcess("FP2.exe")]
[BepInPlugin("com.kuborro.plugins.fp2.lilacwings-tutorial", "LilacWingsTutorial", "1.0.0")]

//You can call your main class differently from what template generates.
public class Plugin : BaseUnityPlugin
{
	private void Awake()
	{
	var harmony = new Harmony("com.kuborro.plugins.fp2.lilacwings-tutorial");
	harmony.PatchAll(typeof(PatchFPPlayer));
	}
}

//Our patch class
class PatchFPPlayer
{
[HarmonyPostfix]
[HarmonyPatch(typeof(FPPlayer), nameof(FPPlayer.State_Init), MethodType.Normal)]
static void Postfix(ref bool ___hasSpecialItem, FPPlayer __instance)
	{
		if (__instance.characterID == FPCharacterID.LILAC)
		{
			___hasSpecialItem = true;
		}
	}
}
}

As for the issue with using State_Init, try playing Bakunawa Rush
Because the stage skips the usual loading in favour of a fancy animation, State_Init never gets fired!
As you mod the game, more situations like this might crop up, but do not let that discourage you - there always is another way as long as you look!

You are done!

Now it’s time to compile your project (F6 by default in VS) and test it out.
Your mod will be placed by default in your project directory, within /bin/Debug subfolder.
It will be named the same as you named your project at the start.
You don’t need to worry about any other .dll present in that folder - they are used as assembly references during compilation and export, and are not needed for the mod to run.

Before publishing the mod, remember to switch the compilation from “Debug” to “Release”. The resulting optimized file will be put in “bin/Release”

For testing, you can simply copy your mod’s .dll file into BepInEx/plugins folder within your modded game’s install directory. It will be automatically loaded during next launch of the game, Freedom Manager will also pick it up in its listing in ‘simple mode’.

When releasing the mod, you will want to properly package it for easy install.
Most important parts are:

   BepInEx
      > plugins
          > ModName   
              > ModName.dll 
              > modinfo.json (Optional)

This structure will ensure both manual installs, and these performed using Freedom Manager will go smoothly.
You can read more about Freedom Manager’s recommended directory structures HERE

{
	"ManifestVer":1,
	"Name":"Mod name",
	"Author":"Author",
	"Version":"1.0.0",
	"Loader":"bepinex",
	"HasAssets":false,
    "GitHub":"https://github.com/Author/Repo"
}

Most are self-explanatory, however:

HasAssets refers to mods that use external folders for AssetBundles and other loose files, not located in their mod folder. It allows to track mods which might leave loose files after removal.

GitHub is a link to the mod’s GitHub repo - used for updates, pulling changelog, and other update related details. Required for automatic mod updates

You can read more about modinfo.json HERE