Modding - Gamepiaynmo/BallanceModLoader GitHub Wiki

Modding

转到中文

One of the best way to learn is to reference other people's works. For example BML Built-in Mods.

To follow this tutorial you need basic knowledge of C++ programming, Virtools and Ballance.

The documentation of Virtools SDK would also be very helpful when modding.

Mod Structure

All mods should be put in ModLoader/Mods folder to work. There are three ways to structure your mod:

  • One single .bmod file, when no external assets are needed (models, textures, sounds etc.).

  • One folder containing your .bmod file with other assets in it, used for debugging.

  • One .zip file containing the .bmod file and other assets, used for released mods.

The mod structure should look like this:

Your Mod Folder
    3D Entities
        PH
            Modul NMO Files
        Other NMO Files
    Textures
        Texture Files
    Sounds
        Sound Files
    Your Mod Name.bmod
    Other Dlls

BML will add these assets paths into the Path Manager of Virtools. So you can access them just like original files of Ballance.

Modding Environment Setup

I would personally recommand Visual Studio 2019 as your IDE, but others may also work.

  • First download the dev package from the Release Page;

  • Create a new project with template Dynamic Link Library (DLL);

Some of the following steps are not necessary. You can use your own settings if you clearly understand how it works.

  • Delete all automatically created code files (.cpp, .h), and create one .cpp and one .h for your own mod;

  • In the Project Properties:

    • Set the output path to ModLoader/Mods, or ModLoader/Mods/YourModName if there are external assets;

    • Set the output file extension to .bmod;

    • Set character set to ANSI (or Not Set);

    • Set debugging command to Ballance/Bin/Player.exe. So you can debug the mod in VS;

    • Set debugging workdir to Ballance/Bin;

    • Add the downloaded dev package "include" and "lib" paths into your project;

    • Disable precompiled header in C/C++;

    • Add BML.lib as one of the linker's input.

  • In your .h file, input the following code:

#pragma once

#include <BML/BMLAll.h>

extern "C" {
	__declspec(dllexport) IMod* BMLEntry(IBML* bml);
}

class ExampleMod : public IMod {
public:
	ExampleMod(IBML* bml) : IMod(bml) {}

	virtual CKSTRING GetID() override { return "ExampleMod"; }
	virtual CKSTRING GetVersion() override { return BML_VERSION; }
	virtual CKSTRING GetName() override { return "BML Example Mod"; }
	virtual CKSTRING GetAuthor() override { return "Gamepiaynmo"; }
	virtual CKSTRING GetDescription() override { return "Example Mod of BML."; }
	DECLARE_BML_VERSION;
};
  • You can customize these info to suit your own mod, but DO NOT use Chinese. Chinese characters cannot be properly displayed ingame.

    • The class name can be whatever you want.
    • ID should comprize of letters only, and be as concise as possible.
    • You can use your own version system.
    • Name should comprize of letters and spaces, and be concise.
    • Description should be a sentence describing what this mod do.
  • In your .cpp file, input the following code:

#include "ExampleMod.h"

IMod* BMLEntry(IBML* bml) {
	return new ExampleMod(bml);
}
  • Build the project and launch the game, you will be able to find your mod entry.

Implementing Your Mod

A mod generally do two kinds of things: subscribing and registering.

What is Subscribing ?

Subscribing means subscribing to some ingame messages, which would be triggered with some conditions (loading a level, touching the flame of a check point, dying, etc.), so that your mod can do some customized behavior when the messages are triggered.

How to subscribe ?

To subscribe to a message, you need to override a function in your C++ class.

For example, you can subscribe the Post Load Level message in your class:

	virtual void OnPostLoadLevel() override;

And implement this function to output some word to the ingame console:

void ExampleMod::OnPostLoadLevel() {
	m_bml->SendIngameMessage("Hello World !");
}

Build and launch the game, this message will appear each time you enter a level.

Some important messages:

  • OnLoad: Invoked when virtools has finished initialization, and it's time to initialize your mod.
  • OnProcess: Invoked in each game loop. Never do extremely time-consuming things here.
  • OnRender: Invoked in each render frame. Same principle with OnProcess.
  • OnUnload: The game is going to exit. Time to do some cleaning.
What is Registering ?

Registering means the encapsulated functions from BML to easily add your own contents into the game, including moduls, ball types, etc. Registering allows you to add new moduls with only one line of C++ code.

How to register ?

All registering codes should be written in Load message:

void ExampleMod::OnLoad() {
	m_bml->RegisterFloorType("Phys_Floor_E0", 0.7f, 0.0f, 1.0f, "Floor", true);
}

The code above registered a new floor type with zero elasticity. It means that if there is a virtools group named "Phys_Floor_E0" in a custom map, all objects in this group will be physicalized as zero elasiticity.

Mod Configuration

BML offers a way to conveniently manage adjustable options (Shortcut Keys for example). The configuration is structured by categories and entries.

First declare instances of IProperty class:

	IProperty* m_props[2];

Then initialize them in Load message:

void ExampleMod::OnLoad() {
	GetConfig()->SetCategoryComment("Integers", "Here are Integers");
	m_props[0] = GetConfig()->GetProperty("Integers", "Integer1");
	m_props[0]->SetComment("Here is Integer 1");
	m_props[0]->SetDefaultInteger(1);

	GetConfig()->SetCategoryComment("Strings", "Here are Strings");
	m_props[1] = GetConfig()->GetProperty("Strings", "String1");
	m_props[1]->SetComment("Here is String One");
	m_props[1]->SetDefaultString("One");
}

Build and launch the game, you will find these options in your mod entry.

BML uses a .cfg file to store the configuration for every mod. In ModLoader/Config folder, you can find ExampleMod.cfg, with the following content:

# Configuration File for Mod: BML Example Mod - 0.3.24

# Here are Integers
Integers {

	# Here is Integer 1
	I Integer1 1

}

# Here are Strings
Strings {

	# Here is String One
	S String1 One

}

This file can be edited when the game is not running, and be loaded by BML the next time game launches.

When you would like to access the value of these options, use the Get function series:

void ExampleMod::OnPostLoadLevel() {
	if (m_props[0]->GetInteger() == 1) {
		m_bml->SendIngameMessage(m_props[1]->GetString());
	}
}

Sometimes you may also need to know when the options has been modified. Here is a message named Modify Config for you:

void ExampleMod::OnModifyConfig(CKSTRING category, CKSTRING key, IProperty* prop) {
	if (prop == m_props[1]) {
		// m_props[1] has been modified.
		m_bml->SendIngameMessage(m_props[1]->GetString());
	}
}

Ingame Commands

BML has implemented some commands like /bml, /help. And it allows every mod to create its own command.

To create your own command, you need to implement the interface ICommand:

class CommandExample : public ICommand {
public:
	virtual std::string GetName() override { return "example"; };
	virtual std::string GetAlias() override { return "exp"; };
	virtual std::string GetDescription() override { return "An example command."; };
	virtual bool IsCheat() override { return false; };

	virtual void Execute(IBML* bml, const std::vector<std::string>& args) override {
		if (args.size() > 1) {
			if (args[1] == "plus")
				m_number++;
			if (args[1] == "minus")
				m_number--;
		}

		std::string msg = "Number is now: " + std::to_string(m_number);
		bml->SendIngameMessage(msg.c_str());
	}
	virtual const std::vector<std::string> GetTabCompletion(IBML* bml, const std::vector<std::string>& args) override {
		return args.size() == 2 ? std::vector<std::string>{ "plus", "minus" } : std::vector<std::string>{};
	};

private:
	int m_number = 0;
};
  • Name is the word after character '/' when invoking a command. It should comprize of only letters in lower case.
  • Alias is another way to invoke this command. For example, /help has an alias /?.
  • Description is a sentence describing what this command does.
  • IsCheat if true, this command cannot be invoked when cheat mode is off.
  • Execute is what happens after the player pressed enter.
  • GetTabCompletion gives a list of possible next words. args.size() indicates the index. It is invoked when the player pressed Tab while inputing a command line.

After implementing the interface, register it in Load message:

void ExampleMod::OnLoad() {
	m_bml->RegisterCommand(new CommandExample());
}

Then you can use this command ingame.

Final Code

// ExampleMod.h

#pragma once

#include <BML/BMLAll.h>

extern "C" {
	__declspec(dllexport) IMod* BMLEntry(IBML* bml);
}

class CommandExample : public ICommand {
public:
	virtual std::string GetName() override { return "example"; };
	virtual std::string GetAlias() override { return "exp"; };
	virtual std::string GetDescription() override { return "An example command."; };
	virtual bool IsCheat() override { return false; };

	virtual void Execute(IBML* bml, const std::vector<std::string>& args) override {
		if (args.size() > 1) {
			if (args[1] == "plus")
				m_number++;
			if (args[1] == "minus")
				m_number--;
		}

		std::string msg = "Number is now: " + std::to_string(m_number);
		bml->SendIngameMessage(msg.c_str());
	}
	virtual const std::vector<std::string> GetTabCompletion(IBML* bml, const std::vector<std::string>& args) override {
		return args.size() == 2 ? std::vector<std::string>{ "plus", "minus" } : std::vector<std::string>{};
	};

private:
	int m_number = 0;
};

class ExampleMod : public IMod {
public:
	ExampleMod(IBML* bml) : IMod(bml) {}

	virtual CKSTRING GetID() override { return "ExampleMod"; }
	virtual CKSTRING GetVersion() override { return BML_VERSION; }
	virtual CKSTRING GetName() override { return "BML Example Mod"; }
	virtual CKSTRING GetAuthor() override { return "Gamepiaynmo"; }
	virtual CKSTRING GetDescription() override { return "Example Mod of BML."; }
	DECLARE_BML_VERSION;

private:
	virtual void OnLoad() override;
	virtual void OnPostLoadLevel() override;
	virtual void OnModifyConfig(CKSTRING category, CKSTRING key, IProperty* prop) override;

	IProperty* m_props[2];
};
// ExampleMod.cpp

#include "ExampleMod.h"

IMod* BMLEntry(IBML* bml) {
	return new ExampleMod(bml);
}

void ExampleMod::OnLoad() {
	GetConfig()->SetCategoryComment("Integers", "Here are Integers");
	m_props[0] = GetConfig()->GetProperty("Integers", "Integer1");
	m_props[0]->SetComment("Here is Integer 1");
	m_props[0]->SetDefaultInteger(1);

	GetConfig()->SetCategoryComment("Strings", "Here are Strings");
	m_props[1] = GetConfig()->GetProperty("Strings", "String1");
	m_props[1]->SetComment("Here is String One");
	m_props[1]->SetDefaultString("One");

	m_bml->RegisterFloorType("Phys_Floor_E0", 0.7f, 0.0f, 1.0f, "Floor", true);

	m_bml->RegisterCommand(new CommandExample());
}

void ExampleMod::OnPostLoadLevel() {
	if (m_props[0]->GetInteger() == 1) {
		m_bml->SendIngameMessage(m_props[1]->GetString());
	}
}

void ExampleMod::OnModifyConfig(CKSTRING category, CKSTRING key, IProperty* prop) {
	if (prop == m_props[1]) {
		m_bml->SendIngameMessage(m_props[1]->GetString());
	}
}