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
dnSpy/ilSpy:
dnSpy Download
Unity 5.6.3
Download Here
Best option is to download just the editor using the “Downloads (Win)” tab, without using Unity Hub
Also recommended but not technically required:
Already modded game (you kinda have to do it anyway later on~)
Recommended version of BepInEx 5 Mono is installed by Freedom Manager, or can be installed manually.
You can download it HERE, download the x86 build.
Freedom Planet 2
Unity Explorer mod installed. Mod can be installed like any other using FreedomManager, or manually.
Download Here
All these instructions are also available on official BepInEx documentation, following is a shortened version with included FP2 specific options.
net35
5.6.3
Fill the project name and version to your own preference, personally i would recommend using major.minor.patch (x.x.x) version format.
dotnet new bepinex5plugin -n MyFirstPlugin -T net35 -U 5.6.3
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.
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:
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:
ref
keyword. To fix them, simply modify the lines of code the error points you at and removing the two wrongly decompiled ref
in each line.Assets/Scripts/Assembly-CSharp/FerrPoly2Tri/DelaunayTriangle.cs(96,3): error CS1525: Unexpected symbol `ref'
Assets/Scripts/Assembly-CSharp/FerrPoly2Tri/DelaunayTriangle.cs(131,3): error CS1525: Unexpected symbol `ref'
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;
Assets/Scripts/Assembly-CSharp/Ferr2DT_Material.cs(199,26): error CS0683: `Ferr2DT_Material.IFerr2DTMaterial.get_name()' explicit method implementation cannot implement `IFerr2DTMaterial.name.get' because it is an accessor
Assets/Scripts/Assembly-CSharp/Ferr2DT_TerrainMaterial.cs(214,26): error CS0683: `Ferr2DT_TerrainMaterial.IFerr2DTMaterial.get_name()' explicit method implementation cannot implement `IFerr2DTMaterial.name.get' because it is an accessor
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.
Stop the stage from trying to setup nonexistent Rewired:
NullReferenceException: Object reference not set to an instance of an object FPStage.Start() (at Assets/Scripts/Assembly-CSharp/FPStage.cs:214)
NullReferenceException: Object reference not set to an instance of an object FPPlayer.Start () (at Assets/Scripts/Assembly-CSharp/FPPlayer.cs:789)
This can be fixed by replacing line pointed at the end of the message from:
rewiredMenuInput = ReInput.players.GetPlayer(0);
to
rewiredMenuInput = null;
There are other similar issues which might need fixing in this manner.
Force FPPlayer to use old input method:
FullReferenceException: Object reference not set to an instance of an object FPStage.UpdateMenuInput (Boolean pauseButtonOnly) (at Assets/Scripts/Assembly-CSharp/FPStage.cs:383) FPStage.Update () (at Assets/Scripts/Assembly-CSharp/FPStage.cs:264)
This can be fixed by adding:
FPSaveManager.inputSystem == 0;
at the very start of UpdateMenuInput()
like this:
public static void UpdateMenuInput(bool pauseButtonOnly = false) { FPSaveManager.inputSystem = 0; if (pauseButtonOnly) {
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
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
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.
dnSpy lets you view the decompiled source code of the game itself, in both c# form and pure IL assembly. You will use this tool to locate the methods you want to edit, as well as to understand the game’s code.
Unity Explorer, when installed, allows you to view objects and variables present in the game while it’s running. It’s perfect for finding specific objects you want to mess with, as well as change their parameters at runtime - many simple mods can be tested this way before even writing a single line of code!
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.
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)]
PluginInfo.PLUGIN_GUID
with unique GUID of your mod. This value is not shown to players but is instead used by BepInEx and other mods to identify your mod.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 aplugin
, there exist other mod types likepreloader
andscript
that exceed the scope of this tutorial.
Game would befp2
, 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.
Replace PluginInfo.PLUGIN_NAME
with the name you want your mod to show up as in console. This is the ‘nice’ name shown to users and in settings, it is recommended to not use spaces in this name (but not forbidden).
Replace PluginInfo.PLUGIN_VERSION
with your mod’s version. It will be shown within the console, as well as used by BepInEx to resolve version conflicts when 2 versions of your mod are installed. Other mods might also set requirement of a specific version. Format for this field is x.x.x
as in: “Major.Minor.Patch”
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!
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
modinfo.json
file, which contains information about your mod.{
"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
Put the code on GitHub
It is a very important step - not only for organising your work well, but also to support the community.
This way the mods might be also checked for bugs or other unwanted parts by other members of the community.
We all work together for a common goal, and sharing your knowledge with others is an important part of it.
It also allows people who do not trust random .dll on the internet (rightly so) to verify and compile the mod themselves.