Stuck? MyFirstCard.md is outdated yet again? Join and get help in modding Discord
Prerequisites: basic C#, basic Object Oriented Programming, resolve to debug errors (and ability to read the bloody error messages).
-
[One time] Get Visual Studio 2022.
-
[One time] Setup BepInEx with Sideloader.
-
[One time] After installing Sideloader run the game once and close it after it loads to main menu. There should .cfg file generated
BepInEx\config\neo.lbol.frameworks.entitySideloader.cfg
. Open it with a text editor and changeDevMode
totrue
. Else hot-reloading and extra logging will be disabled. -
[One time] Setup scriptengine and create
BepInEx/scripts
folder. -
[One time] Get DebugMode
-
[One time] Get MODDED Watermark
-
[One time] Setup VS project template.
-
Create new project using Sidleoader template. Wait a moment for nuget packages to finish downloading. If GameFolder was set correctly there should be no errors. Else adjust the GameFolder and clean the project.
- Create a new class, call it FirstCardDef. Make it
public sealed
and extendCardTemplate
.
- Class name should be underlined red. Alt+Enter while cursor is on class name, select 'Implement abstract class'.
- This should have generated a bunch of methods. These methods are like slots to be filled up with components which define a card. Disregard them for the moment.
- Create a new class for convenience placed in the same file:
public sealed class FirstCard : Card
. This class is going to define card's behavior.
- Put
[EntityLogic(typeof(FirstCardDef))]
on top ofFirstCard
class.
- Now come back to the generated methods. Make
GetId()
return name of the entity logic classreturn nameof(FirstCard)
(string). For now it's a strict requirement that Id is the same as the logic type name.
- Crop image to 743x512 , call it same as the Id,
FirstCard.png
, paste it into theResources
folder. Any files or folder in theResources
will be automatically embedded in the compiled dll. It's the most convenient way to include files, for now.
-
In
LoadCardImages
method create a new instance ofCardImages
specifying the embedded source. It acts as container to hold any images a card might need and eventually will be passed to Sideloader. -
Call
imgs.AutoLoad
method and specify ".png" extension. AutoLoad is possible because the card image is named the same as the Id. Return images. Alternatively,return null
can be used skip loading images for now and leave the card without an image.
public override CardImages LoadCardImages()
{
var imgs = new CardImages(BepinexPlugin.embeddedSource);
imgs.AutoLoad(this, extension: ".png");
return imgs;
}
- In
Resources
folder createCardsEn.yaml
file.
LoadLocalization
can return eitherLocalizationFiles
orGlobalLocalization
.GlobalLocalization
is just a better choice. When setting up global localization only one CardTemplate should specify the localization files withloc.LocalizationFiles.AddLocaleFile
method. Other cards should justreturn new GlobalLocalization(BepinexPlugin.embeddedSource);
to indicate that they use global localization.
public override LocalizationOption LoadLocalization()
{
var loc = new GlobalLocalization(BepinexPlugin.embeddedSource);
loc.LocalizationFiles.AddLocaleFile(LBoL.Core.Locale.En, "CardsEn.yaml");
return loc;
}
- Open
CardsEn.yaml
. In the end it, should look very similar to game's localization files, found at<LBoL_install_folder>\LBoL_Data\StreamingAssets\Localization\en
. Make sure yaml is formatted legally.
CardId:
Name: SomeName
Description: "Description"
-
For now make
MakeConfig()
return DefaultConfig();
. You can worry about specifics in a bit. -
If everything was done correctly, you should have a skeleton of the card. Build the project, the dll should be automatically copied to
BepInEx/scripts
folder, run the game.
-
If BepInEx has [Logging.Console] enabled in
BepInEx\config\BepInEx.cfg
you should see various messages (or errors) related to sideloader. -
In game, go to collection, scroll to the very bottom without filtering and there should be your card, sitting, not doing much.
-
Now comes the difficult part, actually implementing the card. Let's start small and say we want to create a stronger version of Youkai Buster.
-
Go back to
FirstCardDef.MakeConfig
. You don't have to close the game just close the collection. -
Unfortunately, for now, the fastest way to fill out config is to copy code. Copy CardConfig constructor with named parameters from here.
-
The more detailed explanation of each config property can be found in Entity Reference. For now change
Index
,GunName
,GunNameBurst
,Type
,IsPooled
,TargetType
,Colors
,Cost
andDamage
.sequenceTable.Next
is convenient way to keep track of indexes but any positive number can be assigned as index as well.
Index: BepinexPlugin.sequenceTable.Next(typeof(CardConfig)),
...
GunName: "Simple1",
GunNameBurst: "Simple2",
...
IsPooled: true,
...
Rarity: Rarity.Common,
Type: CardType.Attack,
TargetType: TargetType.SingleEnemy,
Colors: new List<ManaColor>() { ManaColor.Red, ManaColor.White },
...
Cost: new ManaGroup() { Red = 1, White = 1 },
...
Damage: 7,
...
- Compile the project again and go back to the game. Close Collection, press F6 to refresh the mods. The card should be an
Attack
and have cost and colors.
-
[One time] Create 'Debug' profile in-game for testing stuff.
-
[One time] Download dnSpyEx.
-
Open these LBoL assemblies in dnSpy.
LBoL.EntityLib.dll
is the one where the game entities are implemented.
-
Find the Id of Youkai Buster. To do that either search Card.yaml localization files or use DebugMode menu option
Output All Config
and checkCardConfig.txt
. Id of Youkai Buster isYaoguaiBuster
. -
In dnSpy search for a Type named
YaoguaiBuster
. Make sure Files in Same Folder option is selected.
-
Investigating
YaoguaiBuster
type we can see that it extendsCard
. It seems thatSetGuns
method can be used to increase number of hits performed. Let's copy the method and paste it in our ownFirstCard
class. -
Let's increase number of hits performed to 3/4 by adding an item to each array.
[EntityLogic(typeof(FirstCardDef))]
public sealed class FirstCard : Card
{
protected override void SetGuns()
{
if (this.IsUpgraded)
{
CardGuns = new Guns(new string[]
{
Config.GunNameBurst,
Config.GunName,
Config.GunNameBurst,
Config.GunName,
});
return;
}
CardGuns = new Guns(new string[]
{
Config.GunName,
Config.GunNameBurst,
Config.GunName,
});
}
}
- Time to test it! Compile the project, reload mods and start a debug run with F5 (while in main menu). After adding the cards to the deck, giving yourself some shining exhibits you can click one of these buttons to quickly advance the map node. This will save the game and you won't have to redo your setup when restarting the level.
- Both upgraded and unupgraded card versions seems to function as intended and console is clean of errors. The only thing left to do is to finish writing the description. I've chosen to differentiate upgraded card description by using 'UpgradedDescription:' node.
Check out example project/template by rmrfmaxxc. The intended way to organize Sidloader templates for larger projects, i.e., new characters. Also contains example code for many generic card types like Teammates, Abilities, XCost etc.
Join modding Discord