-
-
Notifications
You must be signed in to change notification settings - Fork 0
5) Modding Guide
- Visual Studio 2022
Download here
Alternatively, any other C#/.NET IDE will do fine as well.
- .NET SDK for .NET 3.5 (While installing)
While installing VS, click to “Individual components” and check the “.NET Framework 3.5 development tools” checkbox.
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
- BepInEx 5 Templates
You can install them from official NuGet repository running the Powershell command:
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 install BepInEx.Templates::2.0.0-be.4 --nuget-source https://nuget.bepinex.dev/v3/index.json
- Asset Ripper in version 1.3.5 or older (Later builds lock shader decomp behind a monthly sub) Download Latest Builds Here
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.
- 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:
- Target Framework:
net35 - Unity Version:
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.
- Using the Terminal (Powershell or Windows Terminal will do):
- Create a project folder
- Within the project folder run the following:
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.
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
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.
- Most of the options can be left on their defaults, as they are the most safe choices. You can try them out, to find out what options suit you best - most of them are fully described within the app itself.
- You do, however, want to set Shader Export Format to Decompile. _Otherwise you will not be able to export scenes properly and see a lot of purple squares while in editor. You might need DirectX SDK in some edge-cases when decompiling prerelase builds with just DX9 shaders.
_ - Make sure the Script Export Format is set to Hybrid - it will make running the decompile much easier.
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.
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.
You really want to fix them, and do so before you load the project into Unity. Not only will it make editing much much easier, but *exporting a scene would include broken shaders*. So any level you export would then have broken shaders in-game.
Which is bad.
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. Opening the exported project in Visual Studio works well, for example.
If you see any comments speaking about DX9 disassembly in the shader files, you likely set asset ripper to Disassembly instead of Decompile for shaders. Disassembled shaders will not compile back in the editor and will instead show lovely shades of pink everywhere.
In all files within Shaders folder, you will want to remove following lines:
Comp Disabled
Replace:
Fog {
Mode 0
}
with
Fog {
Mode Off
}
These changes will make the shaders compile (there might be exceptions with other tools and versions), but the Stencil shaders will still be broken. If you want to see how they are broken, load up Paradise Prime or Lightning Tower.
Unity logs shader issues in separate tab, when having the shader itself selected in the editor. This is where you can look for other issues, if Unity is fussy.
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
And in WaterCaustic shader:
In v2f vert(appdata_full v):
#if defined(SHADER_API_GLCORE) o.texcoord.y = 1 - o.texcoord.y; #endif
Just before return o;
This will ensure the Y coordinate is properly flipped on OpenGL to account for difference between HLSL and GLSL.
There is one more shader which i have not fixed for OpenGL (i am not a shader dev, and im working with a decompiled DX11 shaders here), which warrants the effects for Bakunawa Rush and Lunar Cannon.
This being said that shader is *already broken* in the release builds of the game! Try launching these stages in DirectX9 mode, you will see them being very much broken.
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:
- 2 lines complaining about invalid
refkeyword. To fix them, simply modify the lines of code the error points you at and removing the two wrongly decompiledrefin each line.
You can double-click an error within the console to open the file in question, on the problematic line.
The errors are:
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 neighbors = ref Neighbors;
ref FixedArray3 points = ref Points;
to:
FixedArray3 neighbors = Neighbors;
FixedArray3 points = Points;
- 2 methods which have been wrongly generated by the decompiler. Error will complain about accessor not being able to use such method. Simply remove whole method, as it should not be there (i believe so at least).
These errors are:
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; }
Depending on decompiler used, it might mess up Rewired and saving. Rewired dll's are obfuscated and decompiling them makes a mess.
New Versions of Asset Ripper resolve this by themselves in hybrid decompile mode (they just keep Rewired in compiled state)
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.
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:
- Filtering: Point (No filter)
- Compression: None
It will take a while to process, but it will un-jpeg all the sprites.
You will want to do the same for audio. Select all audio files and set some sane quality (90% is fine). Default is 1% quality and it *will* make the characters sound like Duke Nukem in DN3D.
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)]
- Replace
PluginInfo.PLUGIN_GUIDwith 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.
Other modders might need to use this value to set dependencies for their mod, or mark them as incompatible.
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 likepreloaderandscriptthat 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_NAMEwith 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_VERSIONwith 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 isx.x.xas 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(),"",)]
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:
-
Directory structure:
Within the .zip archive you plan to upload, there should be following directory structure: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
-
Mod manifest file
When packaging mods, you will want to includemodinfo.jsonfile, which contains information about your mod.
Contents of this file allow Freedom Manager to display proper information about your mod, as well as notify user about updates (work in progress) and link them to your mod’s page.
Typical file has contents of:{ "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.