Bootstrap completed, hello world in BSIPA logs confirmed
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
---
|
||||
prev: false
|
||||
next: false
|
||||
description: Learn how to create create mod configs for your Quest Mod!
|
||||
---
|
||||
|
||||
# Quest Mod Configuration
|
||||
|
||||
Most mods require a configuration to allow users to change the functionality of the mod.
|
||||
|
||||
This section will guide you through the basics of using `config-utils` to create configuration for your mod.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Install `config-utils` by running `qpm dependency add config-utils` in your project directory.
|
||||
|
||||
Make sure to restore after adding the dependencies.
|
||||
|
||||
## Declaring Your Configuration
|
||||
|
||||
First, you will need to define what your configuration will be. Create a `modconfig.hpp` header file, this will contain
|
||||
the definition.
|
||||
|
||||
In `modconfig.hpp`, you should put the following:
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
|
||||
#include "config-utils/shared/config-utils.hpp"
|
||||
|
||||
// Declare the mod config as "ModConfiguration" and declare all its values and functions.
|
||||
DECLARE_CONFIG(ModConfig,
|
||||
// Declare "VariableA"
|
||||
CONFIG_VALUE(VariableA, std::string, "Variable Name", "Variable Value");
|
||||
)
|
||||
```
|
||||
|
||||
Here is an example that uses all the types except `const char*` and `char*`
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
|
||||
#include "config-utils/shared/config-utils.hpp"
|
||||
#include "UnityEngine/Color.hpp"
|
||||
#include "UnityEngine/Vector2.hpp"
|
||||
#include "UnityEngine/Vector3.hpp"
|
||||
#include "UnityEngine/Vector4.hpp"
|
||||
|
||||
DECLARE_CONFIG(ModConfig,
|
||||
CONFIG_VALUE(VariableString, std::string, "String Example", "Var Value");
|
||||
CONFIG_VALUE(VariableInteger, int, "Integer Example", 5);
|
||||
CONFIG_VALUE(VariableFloat, float, "Float Example", 1.5f);
|
||||
CONFIG_VALUE(VariableBoolean, bool, "Bool Example", false);
|
||||
CONFIG_VALUE(VariableDouble, double, "Double Example", 0.39221);
|
||||
|
||||
// dividing by 255 in color constructor because UnityEngine::Color represents RGBA as values in the range of 0 to 1
|
||||
CONFIG_VALUE(VariableColor, UnityEngine::Color, "Color Example", UnityEngine::Color(10.0/255, 155.0/255, 90.0/255, 0));
|
||||
CONFIG_VALUE(VariableVector2, UnityEngine::Vector2, "Vector2 Example", UnityEngine::Vector2(1, 2));
|
||||
CONFIG_VALUE(VariableVector3, UnityEngine::Vector3, "Vector3 Example", UnityEngine::Vector3(1, 2, 3));
|
||||
CONFIG_VALUE(VariableVector4, UnityEngine::Vector4, "Vector4 Example", UnityEngine::Vector4(1, 2, 3, 4));
|
||||
)
|
||||
```
|
||||
|
||||
## Loading your Config
|
||||
|
||||
Make sure to initialize the config! If you attempt to get values from it before it's loaded, your game will crash.
|
||||
You can run this in `setup()`, `load()`, `late_load()`, or even anytime later if you really want to, but it only ever
|
||||
needs to be run once.
|
||||
|
||||
```cpp
|
||||
#include "modconfig.hpp"
|
||||
|
||||
// other code
|
||||
|
||||
extern "C" void late_load() {
|
||||
// Initialize and load the config
|
||||
getModConfig().Init(modInfo);
|
||||
|
||||
// other code.
|
||||
}
|
||||
```
|
||||
|
||||
## Using Your Configuration
|
||||
|
||||
In the following examples, we will be using the example that uses all the types
|
||||
from [Declaring Your Configuration](#declaring-your-configuration)
|
||||
|
||||
```cpp
|
||||
// Get VariableString
|
||||
getModConfig().VariableString.GetValue();
|
||||
|
||||
// Set VariableString to "Eris cute"
|
||||
getModConfig().VariableString.SetValue("Eris cute");
|
||||
|
||||
// Get VariableVector2 and store it as vec
|
||||
UnityEngine::Vector2 vec = getModConfig().VariableVector2.GetValue();
|
||||
|
||||
// Add 30 to the x value.
|
||||
vec = vec + UnityEngine::Vector2(30, 0, 0);
|
||||
|
||||
// Save VariableVector2 to the new vector
|
||||
getModConfig().VariableVector2.SetValue(vec);
|
||||
```
|
||||
|
||||
Setting a config variable will automatically save the configuration file.
|
||||
|
||||
The configuration file is usually stored at `~/ModData/com.beatgames.beatsaber/Configs/` on the Quest.
|
||||
Your mod id will be used to create the configuration file, eg: `qosmetics.json`.
|
||||
@@ -0,0 +1,338 @@
|
||||
---
|
||||
prev: false
|
||||
next: false
|
||||
description: Learn how to create C# macros for your Quest Mod!
|
||||
---
|
||||
|
||||
# Quest Custom Types
|
||||
|
||||
`custom-types` is a library that allows you to create (fake) C# types using macros. These types can extend classes such
|
||||
as `MonoBehaviour` and much more. `custom-types` also allows you to create [coroutines](https://docs.unity3d.com/Manual/Coroutines.html)
|
||||
and [delegates](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/delegates/).
|
||||
|
||||
Custom Types are complex and requires knowledge of basic C#.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Install `custom-types` by running `qpm dependency add custom-types` in your project directory.
|
||||
|
||||
Make sure to restore after adding the dependency.
|
||||
|
||||
## Basics
|
||||
|
||||
To create a custom type, create a header file for your type. In this example, we'll make a type called `Counter`
|
||||
that extends `MonoBehavior`.
|
||||
|
||||
In your header file, include the macros file.
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
|
||||
#include "custom-types/shared/macros.hpp"
|
||||
```
|
||||
|
||||
Since our `Counter` Custom Type will be extending `MonoBehaviour`, we need to include this too.
|
||||
|
||||
```cpp
|
||||
#include "UnityEngine/MonoBehaviour.hpp"
|
||||
```
|
||||
|
||||
### Declaring the Type
|
||||
|
||||
With those includes, we can now declare our `Counter` type. Types are declared using macros, similarly to hooking.
|
||||
|
||||
```cpp
|
||||
// parameters are (namespace, class name, parent class, contents)
|
||||
DECLARE_CLASS_CODEGEN(MyNamespace, Counter, UnityEngine::MonoBehaviour,
|
||||
// DECLARE_INSTANCE_METHOD creates methods
|
||||
DECLARE_INSTANCE_METHOD(void, Update);
|
||||
|
||||
// DECLARE_INSTANCE_FIELD creates fields
|
||||
DECLARE_INSTANCE_FIELD(int, counts);
|
||||
)
|
||||
```
|
||||
|
||||
In C#, this would translate to the following:
|
||||
|
||||
```csharp
|
||||
namespace MyNamespace
|
||||
{
|
||||
public class Counter : MonoBehaviour
|
||||
{
|
||||
public int counts;
|
||||
|
||||
public void Update()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that only basic types, such as `int`, `bool`, etc, and C# types can be used as instance
|
||||
fields and method parameters declared with these macros. If you need something like a `std::vector`
|
||||
or a c++ struct in your type, you can declare it after all the C# fields the same way you would
|
||||
in a regular c++ struct or class.
|
||||
|
||||
### Defining the Type
|
||||
|
||||
Create a new source file - name it accordingly - and include your Custom Type header.
|
||||
|
||||
To define the type, use the `DEFINE_TYPE(Namespace, Class)` macro.
|
||||
|
||||
For our `Counter` type, this will look like so:
|
||||
|
||||
```cpp
|
||||
#include "Counter.hpp"
|
||||
|
||||
DEFINE_TYPE(MyNamespace, Counter);
|
||||
```
|
||||
|
||||
We can now define the methods that we have declared:
|
||||
|
||||
- `Update` - Unity's update method, declared by `DECLARE_INSTANCE_METHOD(void, Update);`
|
||||
|
||||
Our `Counter.cpp` file now looks like this:
|
||||
|
||||
```cpp
|
||||
#include "Counter.hpp"
|
||||
|
||||
DEFINE_TYPE(MyNamespace, Counter);
|
||||
|
||||
// Unity update method - runs every frame this component is enabled
|
||||
void MyNamespace::Counter::Update() {
|
||||
// Add 5 to the counter field
|
||||
counter = counter + 5;
|
||||
}
|
||||
```
|
||||
|
||||
## Overriding methods
|
||||
|
||||
We can also define methods that override those on parent types or interfaces, but we are limited to only overriding
|
||||
methods explicitly defined as `virtual` or `abstract` in the C# code. For non interfaces, it's not always clear whether
|
||||
this is the case for any given method if you don't have access to a decompiler and the PC game files, but an example of
|
||||
a virtual method that is commonly overriden is `HMUI::ViewController::DidActivate`:
|
||||
|
||||
```cpp
|
||||
// don't forget to include the types you use!
|
||||
#include "HMUI/ViewController.hpp"
|
||||
|
||||
DECLARE_CLASS_CODEGEN(MyNamespace, CustomMenu, HMUI::ViewController,
|
||||
// to override a method, we need the MethodInfo* of the original
|
||||
// there are two common ways to get it, but unfortunately both of them make for relatively long lines
|
||||
DECLARE_OVERRIDE_METHOD(void, DidActivate,
|
||||
il2cpp_utils::il2cpp_type_check::MetadataGetter<&HMUI::ViewController::DidActivate>::get(),
|
||||
bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling);
|
||||
// OR
|
||||
DECLARE_OVERRIDE_METHOD(void, DidActivate,
|
||||
il2cpp_utils::FindMethodUnsafe("HMUI", "ViewController", "DidActivate", 3),
|
||||
bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling);
|
||||
// note that both of these seem to be calling methods at the global level, outside of any functions or hooks,
|
||||
// that you normally cannot call until at least after load() --
|
||||
// but actually, since these are macros, the code is actually moved inside of internal functions
|
||||
// that get called at the correct times for registration
|
||||
)
|
||||
```
|
||||
|
||||
### Using Interfaces
|
||||
|
||||
Sometimes you will want to have your custom type inherit from interfaces. Putting them as the parent type will not work,
|
||||
and instead there is a different macro for it:
|
||||
|
||||
```cpp
|
||||
#include "HMUI/TableView_IDataSource.hpp"
|
||||
|
||||
// if there is no required parent class, Il2CppObject can be used to equal a plain object with no parent
|
||||
// also, to inherit from multiple interfaces, they need to be wrapped with std::vector<Il2CppClass*>({ ... })
|
||||
// to prevent the macro from expanding them incorrectly
|
||||
DECLARE_CLASS_CODEGEN_INTERFACES(MyNamespace, TableData, Il2CppObject, { classof(HMUI::ISaberMovementData*) },
|
||||
// rest of the custom type as normal
|
||||
)
|
||||
```
|
||||
|
||||
## Constructors
|
||||
|
||||
Some simple custom types do not necessarily need constructors, but there are a lot of cases where one does
|
||||
need to be defined. You can create a fully custom one with the `DECLARE_CTOR` macro:
|
||||
|
||||
```cpp
|
||||
DECLARE_CLASS_CODEGEN(MyNamespace, Counter, UnityEngine::MonoBehaviour,
|
||||
// other members
|
||||
|
||||
// can have arguments the same as any other method
|
||||
// but the return type is always void so it is omitted from the macro
|
||||
DECLARE_CTOR(ctor);
|
||||
)
|
||||
```
|
||||
|
||||
And then define it just like any other method. However, in that definition, you should make sure to invoke the
|
||||
constructor of the base class with `INVOKE_BASE_CTOR`:
|
||||
|
||||
```cpp
|
||||
void MyNamespace::Counter::ctor() {
|
||||
INVOKE_BASE_CTOR(classof(UnityEngine::MonoBehaviour*), ...constructor arguments);
|
||||
// initialize other things
|
||||
}
|
||||
```
|
||||
|
||||
In the case of `MonoBehaviour`, this isn't necessary as it doesn't do anything in its constructor. If you inherit
|
||||
other types, though, not invoking their constructors can cause hard to track down bugs.
|
||||
|
||||
Another case where the constructor would be used is if you use `DECLARE_INSTANCE_FIELD_DEFAULT` or have c++ style fields
|
||||
in your class that need special initialization, such as `std::vector` or something with a default value, ex:
|
||||
|
||||
```cpp
|
||||
DECLARE_CLASS_CODEGEN(MyNamespace, Counter, UnityEngine::MonoBehaviour,
|
||||
// C# members
|
||||
public:
|
||||
int counts = 5;
|
||||
)
|
||||
```
|
||||
|
||||
In this case you define the constructor method the same way and include `INVOKE_CTOR()` in the method definition:
|
||||
|
||||
```cpp
|
||||
void MyNamespace::Counter::ctor() {
|
||||
// sets counts to 5
|
||||
INVOKE_CTOR();
|
||||
// initialize other things
|
||||
}
|
||||
```
|
||||
|
||||
If you want these macros but have nothing else to do in the constructor, you can skip the method definition and
|
||||
just use `DECLARE_DEFAULT_CTOR`:
|
||||
|
||||
```cpp
|
||||
DECLARE_CLASS_CODEGEN(MyNamespace, Counter, UnityEngine::MonoBehaviour,
|
||||
// C# members
|
||||
|
||||
// invokes the MonoBehaviour constructor and sets counts to 5
|
||||
DECLARE_DEFAULT_CTOR();
|
||||
|
||||
public:
|
||||
int counts = 5;
|
||||
)
|
||||
```
|
||||
|
||||
Destructors can be defined custom similarly to contructors with `DECLARE_DTOR`, and/or `DECLARE_SIMPLE_DTOR` to run
|
||||
the destructor for any c++ fields that need to have special behavior when being destroyed. You don't need to worry
|
||||
about running the base class destructor, though.
|
||||
|
||||
::: warning
|
||||
To create a new object, _do not_ run `ctor` yourself or create it in c++ with `new` or any similar operator,
|
||||
but instead use `il2cpp_utils::New<MyNamespace::Counter*>(...constructor arguments);`, `Counter::New_ctor(...constructor
|
||||
arguments);`, or any C# method that would
|
||||
create an object, such as `AddComponent`.
|
||||
:::
|
||||
|
||||
### Registering
|
||||
|
||||
You can register all the custom types you have created using the `custom_types::Register::AutoRegister()` method.
|
||||
|
||||
This method should be put in your `load()` or `late_load()` like so:
|
||||
|
||||
```cpp
|
||||
#include "custom-types/shared/register.hpp"
|
||||
|
||||
// other code
|
||||
|
||||
extern "C" void late_load() {
|
||||
// make sure this is after il2cpp_functions::Init()
|
||||
custom_types::Register::AutoRegister();
|
||||
|
||||
// other code
|
||||
}
|
||||
```
|
||||
|
||||
To ensure correct behavior, make sure you install hooks _after_ you register your Custom Types!
|
||||
|
||||
### Using the Type
|
||||
|
||||
Custom Types can be used as if they were conventional C# types like you would find in the base game - for our `Counter` type,
|
||||
we can add it as a component to a `GameObject` as it inherits `MonoBehaviour`.
|
||||
|
||||
```cpp
|
||||
#include "UnityEngine/GameObject.hpp"
|
||||
#include "Counter.hpp"
|
||||
|
||||
// in a hook somewhere
|
||||
UnityEngine::GameObject* gameObject = UnityEngine::GameObject::New_ctor("CounterObject");
|
||||
gameObject->AddComponent<MyNamespace::Counter*>();
|
||||
```
|
||||
|
||||
## Coroutines
|
||||
|
||||
In Unity, a coroutine is a method that can pause execution and return control to Unity but then continue where it left
|
||||
off on the following frame. [Unity Documentation](https://docs.unity3d.com/Manual/Coroutines.html)
|
||||
|
||||
### Creating a Coroutine
|
||||
|
||||
Using Custom Types, coroutines are pretty much the same as their C# counterparts. Take a look at this example:
|
||||
|
||||
```cpp
|
||||
#include "custom-types/shared/coroutine.hpp"
|
||||
#include "UnityEngine/WaitForSeconds.hpp"
|
||||
#include "System/Collections/IEnumerator.hpp"
|
||||
|
||||
custom_types::Helpers::Coroutine counterCoroutine() {
|
||||
|
||||
int secondsPassed = 0;
|
||||
|
||||
// loop 30 times
|
||||
for (int i = 0; i < 30; i++) {
|
||||
secondsPassed++;
|
||||
|
||||
// wait one second
|
||||
// arguments passed to co_yield must be cast to this type
|
||||
// you can also use co_yield nullptr; to wait a single frame
|
||||
co_yield reinterpret_cast<System::Collections::IEnumerator*>(UnityEngine::WaitForSeconds::New_ctor(1));
|
||||
}
|
||||
co_return;
|
||||
}
|
||||
```
|
||||
|
||||
| C# | C++ |
|
||||
| -------------- | ----------- |
|
||||
| `yield return` | `co_yield` |
|
||||
| `yield` | `co_yield` |
|
||||
| `yield break` | `co_return` |
|
||||
|
||||
`co_return` is used to end a coroutine. C# automatically handles this during compilation, but c++ does
|
||||
not, so make sure you have one at the end of all your coroutines.
|
||||
|
||||
You can also use `co_return` to exit a coroutine early, just like `return` would in a typical function.
|
||||
|
||||
Using normal `return` in a coroutine will not work.
|
||||
|
||||
### Using the Coroutine
|
||||
|
||||
You can start a coroutine on any `MonoBehaviour` using the `StartCoroutine` method just like in C#, however
|
||||
to create an actual coroutine from a function you need an extra call:
|
||||
|
||||
```cpp
|
||||
#include "UnityEngine/GameObject.hpp"
|
||||
#include "custom-types/shared/coroutine.hpp"
|
||||
|
||||
// in a hook somewhere
|
||||
auto gameObject = UnityEngine::GameObject::New_ctor("MyCoroutineRunner");
|
||||
// this is the example custom type we made earlier, but anything inheriting from a MonoBehaviour will work
|
||||
auto myMonoBehaviour = gameObject->AddComponent<MyNamespace::Counter*>();
|
||||
// create the object that we can pass to StartCoroutine from our function
|
||||
auto coroutine = custom_types::Helpers::CoroutineHelper::New(counterCoroutine());
|
||||
myMonoBehaviour->StartCoroutine(coroutine);
|
||||
```
|
||||
|
||||
You can use `SharedCoroutineStarter` to start a coroutine without the need of an instance like so:
|
||||
|
||||
```cpp
|
||||
#include "GlobalNamespace/SharedCoroutineStarter.hpp"
|
||||
#include "custom-types/shared/coroutine.hpp"
|
||||
|
||||
// in a hook somewhere
|
||||
auto coroutine = custom_types::Helpers::CoroutineHelper::New(counterCoroutine());
|
||||
GlobalNamespace::SharedCoroutineStarter::get_instance()->StartCoroutine(coroutine);
|
||||
```
|
||||
|
||||
## Other
|
||||
|
||||
Some extra information and recommended dos and don'ts can be found [here](https://github.com/sc2ad/Il2CppQuestTypePatching/wiki/FAQ).
|
||||
@@ -0,0 +1,433 @@
|
||||
---
|
||||
prev: false
|
||||
next: false
|
||||
description: Learn how to create your own Quest mods!
|
||||
---
|
||||
|
||||
# Quest Mod Development Intro
|
||||
|
||||
_Learn how to get started writing your own Quest Mods._
|
||||
|
||||
## Getting Started
|
||||
|
||||
::: warning
|
||||
This guide is for making mods for the **Quest Standalone** version of Beat Saber!
|
||||
|
||||
If you use Oculus Link or similar, you want to visit the [PC Mod Development Guide](../pc/index.md) as that uses
|
||||
the PC version of the game.
|
||||
:::
|
||||
|
||||
This guide assumes you have a basic to intermediate understanding of the following:
|
||||
|
||||
- [C++](https://www.w3schools.com/CPP/default.asp)
|
||||
- [CMake](https://cmake.org/cmake/help/latest/guide/tutorial/index.html)
|
||||
- [ADB](https://developer.android.com/studio/command-line/adb)
|
||||
- [Powershell](https://docs.microsoft.com/en-us/learn/modules/introduction-to-powershell/)
|
||||
|
||||
You may have difficulty understanding what is covered here if you do not have this foundation.
|
||||
|
||||
While this guide is for development on Windows, it is not dependent on an IDE. Instead you should configure your preferred
|
||||
IDE accordingly by referring to the documentation. For example, you would need to install C++ tools for VSCode or configure
|
||||
CMake for CLion.
|
||||
|
||||
## Environment Setup
|
||||
|
||||
The following pieces of software are needed to follow this guide.
|
||||
|
||||
- [Powershell](#powershell-core) - Cross Platform utility scripts
|
||||
- [CMake](#cmake) - Build Automation
|
||||
- [QPM](#qpm) - Dependency Management
|
||||
- [Ninja](#ninja) - Build Tool
|
||||
- [Android NDK](#android-ndk) - Native Development Kit for Android Devices
|
||||
|
||||
### Powershell Core
|
||||
|
||||
::: warning
|
||||
You must download Powershell Core, the default windows Powershell will _not_ work.
|
||||
:::
|
||||
|
||||
[Download the latest Powershell binary for your system](https://github.com/PowerShell/PowerShell/releases/latest) and add
|
||||
it to your PATH variable, or
|
||||
alternatively download and run the windows installer.
|
||||
|
||||
### CMake
|
||||
|
||||
[Download the latest CMake binary for your system](https://cmake.org/download/) and add it to your PATH variable, or
|
||||
alternatively download and run the windows installer.
|
||||
|
||||
### QPM
|
||||
|
||||
[Download the latest QPM binary for your system](https://github.com/QuestPackageManager/QPM.CLI) from the
|
||||
Actions tab, name it qpm.exe, and add it to your PATH variable, or alternatively download and run the Windows installer
|
||||
from the appropriate workflow.
|
||||
|
||||
### Ninja
|
||||
|
||||
Download ninja via qpm using `qpm download ninja`.
|
||||
|
||||
Alternatively you can [Download the latest Ninja binary for your system](https://github.com/ninja-build/ninja/releases)
|
||||
from the Releases tab
|
||||
and add it to your PATH variable.
|
||||
|
||||
### Android NDK
|
||||
|
||||
Download the Andoid NDK via qpm using `qpm ndk download 27`, and add the extracted directory to a new environment variable
|
||||
called ANDROID_NDK_HOME.
|
||||
|
||||
Alternatively you can run `qpm ndk pin 27` in a project directory to only apply the NDK in the current project.
|
||||
|
||||
If you wish you can instead download the NDK manually from the [Android NDK Downloads page](https://developer.android.com/ndk/downloads).
|
||||
|
||||
## Create a Project
|
||||
|
||||
Once you have setup your environment you can now generate a mod template. The template this guide uses is one by
|
||||
[Lauriethefish](https://github.com/Lauriethefish/quest-mod-template). To start run the following command in Powershell.
|
||||
|
||||
```powershell
|
||||
qpm templatr --git https://github.com/Lauriethefish/quest-mod-template.git <destination>
|
||||
```
|
||||
|
||||
Templatr will then ask a series of questions to create a mod project.
|
||||
|
||||

|
||||
|
||||
### Add and Update Dependencies
|
||||
|
||||
Once the project has been generated, you should now update the following two dependencies, [beatsaber-hook](https://github.com/QuestPackageManager/beatsaber-hook/)
|
||||
and [bs-cordl](https://github.com/QuestPackageManager/bs-cordl), to the version best suited for the game version you are
|
||||
developing for.
|
||||
|
||||
`beatsaber-hook` is a library that allows for modding il2cpp games. `bs-cordl` is a library that allows modders to
|
||||
interface with the game's code.
|
||||
|
||||
To update these, open a Powershell terminal in the project directory then run the following commands to add the latest versions:
|
||||
|
||||
```powershell
|
||||
qpm dependency add beatsaber-hook
|
||||
qpm dependency add bs-cordl
|
||||
```
|
||||
|
||||
If the latest versions do match those for the version you are developing for, add `-v ^x.x.x` after the command with the
|
||||
correct version instead of running those commands. For example, for Beat Saber version 1.35.0, the correct codegen
|
||||
version is 3500.0.0:
|
||||
|
||||
```powershell
|
||||
qpm dependency add bs-cordl -v ^3500.0.0
|
||||
```
|
||||
|
||||
### Restore Dependencies
|
||||
|
||||
Before you can open the project in an IDE, you must restore all of the dependencies. Consider this step similar to
|
||||
fully initializing the project.
|
||||
|
||||
In a Powershell terminal in the project directory run:
|
||||
|
||||
```powershell
|
||||
qpm restore
|
||||
```
|
||||
|
||||
## Project Contents
|
||||
|
||||
Your project should contain the following structure:
|
||||
|
||||
```properties
|
||||
// Files in .gitignore have been excluded
|
||||
cmake/
|
||||
└── ... project cmake files
|
||||
extern/
|
||||
└── ... dependencies should be here
|
||||
include/
|
||||
└── main.hpp
|
||||
scripts/
|
||||
└── ... utility scripts
|
||||
shared
|
||||
src/
|
||||
└── main.cpp
|
||||
.gitignore
|
||||
CMakeLists.txt
|
||||
mod.template.json
|
||||
qpm.json
|
||||
README.md
|
||||
```
|
||||
|
||||
### Code Breakdown
|
||||
|
||||
#### src/main.cpp
|
||||
|
||||
`main.cpp` contains the `setup()` and `late_load()` methods. These methods can exist in any source file as long as they are
|
||||
accessible by the modloader. Take a look inside of `main.cpp` for more information as Laurie has thankfully commented
|
||||
most of the code.
|
||||
|
||||
#### shared
|
||||
|
||||
The shared folder can be exposed by QPM to other mods and published to the QPM dependency registry. Useful if you want
|
||||
to make an API to let other mods control your mod in certain ways (for example Qosmetics has a model loading API).
|
||||
Speak to @Sc2ad if you want to publish something.
|
||||
|
||||
#### extern
|
||||
|
||||
The extern folder should be ignored (and/or in some cases excluded). It contains dependencies, similarly to
|
||||
`node_modules` (nodejs) or `packages` (.net core).
|
||||
|
||||
### Script Breakdown
|
||||
|
||||
It is recommended to run these scripts using Powershell Core (v7) - however, it is not required. All scripts can be run
|
||||
with the `--help` argument for a description of arguments and functionality. Scripts can be manually invoked from the
|
||||
`scripts` folder or via qpm scripts inside `qpm.json`
|
||||
|
||||
#### build.ps1
|
||||
|
||||
Usage: `qpm s build`
|
||||
|
||||
Builds your mod. Does not produce a QMOD file.
|
||||
|
||||
#### copy.ps1
|
||||
|
||||
Usage: `qpm s copy`
|
||||
|
||||
Builds your mod, then copies it to your quest and launches Beat Saber if your quest is connected with ADB.
|
||||
|
||||
#### createqmod.ps1
|
||||
|
||||
Usage: `qpm s qmod`
|
||||
|
||||
Generates a QMOD file that can be parsed by BMBF and or QuestPatcher. Will use the most recently built version of your mod.
|
||||
|
||||
#### pull-tombstone.ps1
|
||||
|
||||
Usage: `qpm s tomb`
|
||||
|
||||
Finds the most recently modified Beat Saber crash tombstone and copies it to your device. If the build on your quest matches
|
||||
what you have most recently built locally, the `-analyze` argument can be provided to generate the source file locations
|
||||
of any lines mentioned in the backtrace.
|
||||
|
||||
#### restart-game.ps1
|
||||
|
||||
Usage: `qpm s restart`
|
||||
|
||||
Closes and reopens Beat Saber on your quest if it is connected. Mostly used inside of `copy.ps1`. Does not have help text.
|
||||
|
||||
#### start-logging.ps1
|
||||
|
||||
Usage: `qpm s logcat`
|
||||
|
||||
Prints logs from Beat Saber, just your mod, or also crashes. Usage of `-self` is recommended.
|
||||
|
||||
#### validate-modjson.ps1
|
||||
|
||||
Usage: `qpm s validate`
|
||||
|
||||
Generates a `mod.json` from `mod.template.json` if not present and verifies it against the QMOD schema. Mostly used
|
||||
inside of `createqmod.ps1`. Does not have help text.
|
||||
|
||||
## Hooking
|
||||
|
||||
Hooking is core to modding. `beatsaber-hook` provides a simple way of hooking methods and other miscellaneous stuff
|
||||
like constructors.
|
||||
|
||||
> In computer programming, the term hooking covers a range of techniques used to alter or augment the behavior of an
|
||||
> operating system, of applications, or of other software components by intercepting function calls or messages or events
|
||||
> passed between software components. Code that handles such intercepted function calls, events or messages is called a hook.
|
||||
> [Wikipedia](https://en.wikipedia.org/wiki/Hooking#:~:text=In%20computer%20programming%2C%20the%20term,events%20passed%20between%20software%20components.&text=Hooking%20can%20also%20be%20used%20by%20malicious%20code.)
|
||||
|
||||
To view a list of methods and classes you can hook, the most convenient option is to use a C# decompiler such as [IlSpy](https://github.com/icsharpcode/ILSpy)
|
||||
if you own the game on PC, as it provides not only the classes and member names, but also the full contents of most methods.
|
||||
If you only own the game on the Quest, then you can still view all the classes and methods in the `includes/codegen`
|
||||
directory in your `extern` folder.
|
||||
|
||||
In this example, we will hook onto the initialization of the level screen and change the text on the play button to
|
||||
something funny.
|
||||
|
||||
The level screen runs the event `DidActivate` when it is fully initialized. This is useful for us because we can hook
|
||||
this event and add our own functionality.
|
||||
|
||||
Firstly, create your hook using the `MAKE_HOOK_MATCH` macro:
|
||||
|
||||
<!-- markdownlint-disable MD013 -->
|
||||
|
||||
```cpp
|
||||
// You can think of these as C# - using HMUI, UnityEngine, etc, but with individual classes
|
||||
// Classes without a namespace are assigned to the GlobalNamespace
|
||||
// If you use a class and do not include it, you may get unclear compiler errors, so make sure to include what you use
|
||||
#include "GlobalNamespace/StandardLevelDetailView.hpp"
|
||||
#include "GlobalNamespace/StandardLevelDetailViewController.hpp"
|
||||
#include "UnityEngine/UI/Button.hpp"
|
||||
#include "UnityEngine/GameObject.hpp"
|
||||
#include "HMUI/CurvedTextMeshPro.hpp"
|
||||
|
||||
// Create a hook struct named LevelUIHook
|
||||
// targeting the method "StandardLevelDetailViewController::DidActivate", which takes the following arguments:
|
||||
// bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling
|
||||
// and returns void.
|
||||
|
||||
// General format: MAKE_HOOK_MATCH(hook name, hooked method, method return type, method class pointer, arguments...) {
|
||||
// HookName(self, arguments...);
|
||||
// your code here
|
||||
// }
|
||||
|
||||
|
||||
MAKE_HOOK_MATCH(LevelUIHook, &GlobalNamespace::StandardLevelDetailViewController::DidActivate, void,
|
||||
GlobalNamespace::StandardLevelDetailViewController* self, bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) {
|
||||
// Run the original method before our code.
|
||||
// Note that you can run the original method after our code or even in the middle
|
||||
// if you want to change arguments or do something before it runs.
|
||||
LevelUIHook(self, firstActivation, addedToHierarchy, screenSystemEnabling);
|
||||
|
||||
// Get the actionButton text object by accessing the actionButton field and some simple Unity methods.
|
||||
// Note that auto can be used instead of declaring the full type in many cases.
|
||||
GlobalNamespace::StandardLevelDetailView* standardLevelDetailView = self->_standardLevelDetailView;
|
||||
UnityEngine::UI::Button* actionButton = standardLevelDetailView->actionButton;
|
||||
UnityEngine::GameObject* gameObject = actionButton->get_gameObject();
|
||||
HMUI::CurvedTextMeshPro* actionButtonText = gameObject->GetComponentInChildren<HMUI::CurvedTextMeshPro*>();
|
||||
|
||||
// Set the text to "Skill Issue"
|
||||
actionButtonText->set_text("Skill Issue");
|
||||
}
|
||||
```
|
||||
|
||||
<!-- markdownlint-enable MD013 -->
|
||||
|
||||
Now, you have to install your hook. Usually, hooks are installed in `load()` or `late_load()` in `main.cpp`:
|
||||
|
||||
```cpp
|
||||
MOD_EXTERN_FUNC void late_load() {
|
||||
il2cpp_functions::Init();
|
||||
|
||||
PaperLogger.info("Installing hooks...");
|
||||
|
||||
INSTALL_HOOK(PaperLogger, LevelUIHook);
|
||||
|
||||
PaperLogger.info("Installed all hooks!");
|
||||
}
|
||||
```
|
||||
|
||||
You can now test to see if this was successful!
|
||||
|
||||
## Testing your Mod
|
||||
|
||||
### Without BMBF
|
||||
|
||||
You can test your mod without BMBF quickly using [`copy.ps1`](#copy-ps1). This is recommended while developing
|
||||
for convenience. You should always test using a QMOD and BMBF if you're about to release your mod.
|
||||
|
||||
What[`copy.ps1`](#copy-ps1) does specifically is copy the `libmodname.so` in the `build` folder to the correct place on your
|
||||
quest and then restart Beat Saber for you. You can also specify while launching to collect logs with the `-log` argument
|
||||
followed by any of the arguments supported by the `start-logging.ps1` script:
|
||||
|
||||
```powershell
|
||||
copy.ps1 -log -self -file latest.log
|
||||
```
|
||||
|
||||
### With BMBF
|
||||
|
||||
Testing your mod with BMBF is useful to make sure BMBF shows and handles your QMOD correctly (copying files,
|
||||
version, cover, etc.)
|
||||
|
||||
You will need to generate a QMOD file using [`createqmod.ps1`](#createqmod-ps1).
|
||||
|
||||
You can then upload the generated QMOD file to BMBF and it should install your mod - it should appear on the mods list.
|
||||
|
||||
You can still collect logs from your mod using the [`start-logging.ps1`](#start-logging-ps1) command after you launch
|
||||
the game.
|
||||
|
||||
## Utilizing `mod.template.json`
|
||||
|
||||
`mod.template.json` contains basic information on your mod. It can also allow you to define other features such as:
|
||||
|
||||
- Cover Image (the preview image shown on the BMBF Mods tab)
|
||||
- File Copies (extract files from the QMOD to a location on the quest device)
|
||||
|
||||
Some fields in it will be of the form `${x}` - those will be automatically filled by QPM based on the information in
|
||||
your `qpm.json` and written to the file `mod.json`. It's not recommended to edit the `mod.json` manually, and it can be
|
||||
updated at any time by running the command `qpm qmod build` (which only creates the `mod.json` file, not the QMOD itself.)
|
||||
|
||||
### Cover Image
|
||||
|
||||
A cover image is used by certain mods and BMBF to show a preview of your mod.
|
||||
|
||||
To add a cover image, simply name the image `cover.png`, put it in your project directory, and add the following to your
|
||||
`mod.template.json`:
|
||||
|
||||
```json
|
||||
"coverImage": "cover.png"
|
||||
```
|
||||
|
||||
:::tip Cover Image Recommendations
|
||||
|
||||
- 1024x512 (BMBF will resize/crop the image to be this size)
|
||||
- File format either png, jpg or gif
|
||||
- Under 2mb to prevent load lag (larger images will take longer to show with no advantage)
|
||||
:::
|
||||
|
||||
#### Example Cover Images
|
||||
|
||||
Click on the arrow beside the mod name to see the image.
|
||||
|
||||
<details><summary>
|
||||
Noodle Extensions
|
||||
</summary>
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
<details><summary>
|
||||
Slice Details Quest
|
||||
</summary>
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
### File Copies
|
||||
|
||||
File copies is an array that can specify extra files in your QMOD to be copied to the quest, such as sabers included by
|
||||
default in Qosmetics. You can add files by editing `createqmod.ps1` and `mod.template.json`.
|
||||
|
||||
#### Example
|
||||
|
||||
This example will add `secret-data.json` to the QMOD and copy it to `/sdcard/ModData/com.beatgames.beatsaber/Mods/Secret/secret-data.json`
|
||||
|
||||
Edit [createqmod.ps1](#createqmod-ps1) to include `secret-data.json`:
|
||||
|
||||
```powershell
|
||||
# This is after line 59 of createqmod.ps1
|
||||
$filelist += "/path/to/secret-data.json"
|
||||
```
|
||||
|
||||
Update the following in your `mod.template.json`:
|
||||
|
||||
```json
|
||||
"fileCopies": [
|
||||
{
|
||||
"name": "secret-data.json",
|
||||
"destination": "/sdcard/ModData/com.beatgames.beatsaber/Mods/Secret/secret-data.json"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Mod Configuration
|
||||
|
||||
Most mods require a configuration to allow users to change the functionality of the mod.
|
||||
|
||||
Visit the [Quest Mod Configuration](./config.md) page to learn the basics of using `config-utils` to create
|
||||
a configuration for your mod.
|
||||
|
||||
## Custom Types
|
||||
|
||||
`custom-types` is a library that allows you to create the equivalent of C# types using macros. These types can extend
|
||||
classes such as `MonoBehaviour` and much more. `custom-types` also allows you to create and use [coroutines](https://docs.unity3d.com/Manual/Coroutines.html)
|
||||
and [delegates](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/delegates/).
|
||||
|
||||
Custom Types are complex and requires knowledge of basic C#. Visit the [Quest Custom Types](./custom-types.md)
|
||||
page to learn more about integrating this into your mod.
|
||||
|
||||
## User Interface
|
||||
|
||||
A user interface (UI) is used by many mods to show configuration options. Visit the [Quest User Interface](./ui.md)
|
||||
page to see how to use `bsml` to create a settings screen for your mod.
|
||||
|
||||
## Credits
|
||||
|
||||
Initial guide content was integrated from the Beat Saber Quest Modding Guide by [Calum](https://github.com/mineblock11)
|
||||
with contributions from [Raine](https://github.com/raineio), [Pangwen](https://github.com/PangwenE), and [Metalit](https://github.com/Metalit/).
|
||||
Integration and editing was done by [Bloodcloak](/about/staff.md#bloodcloak).
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
prev: false
|
||||
next: false
|
||||
description: Learn how to create a UI for your Quest Mod!
|
||||
---
|
||||
|
||||
# Quest User Interface
|
||||
|
||||
:::warning
|
||||
This is a stub page, content is a work in progress! Ask in `#quest-mod-dev` if you want more info!
|
||||
:::
|
||||
|
||||
UI is used by many mods to show configuration options. In this section, we'll show you how to use `bsml` to create a
|
||||
settings screen for your mod using code. `bsml` also supports creating UI with xml which can be found on the [BSML docs](https://redbrumbler.github.io/Quest-BSML-Docs/).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Install `bsml` by running `qpm dependency add bsml` in your project directory.
|
||||
- You also need to install `custom-types` even if you don't use it in your mod: `qpm dependency add custom-types`
|
||||
|
||||
Make sure to restore after adding the dependencies.
|
||||
|
||||
## Creating a `DidActivate` method
|
||||
|
||||
`DidActivate` is a method you can register with `bsml` that allows you to make a simple mod settings page.
|
||||
|
||||
Take a look at this example:
|
||||
|
||||
- You should only create your components on first activation to prevent duplication.
|
||||
- You can utilize containers (such as Scrollable, HorizontalLayout and VerticalLayout) to manipulate the locations of components.
|
||||
|
||||
```cpp
|
||||
#include "bsml/shared/BSML.hpp"
|
||||
|
||||
void DidActivate(HMUI::ViewController* self, bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) {
|
||||
// Create our UI elements only when shown for the first time.
|
||||
if(firstActivation) {
|
||||
// Create a container that has a scroll bar
|
||||
UnityEngine::GameObject* container = BSML::Lite::CreateScrollableSettingsContainer(self->get_transform());
|
||||
|
||||
// Create a text that says "Hello World!" and set the parent to the container.
|
||||
BSML::Lite::CreateText(container->get_transform(), "Hello World!");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
There are too many UI components and methods to document in this guide. However, the files in the `BSML-Lite/Creation`
|
||||
folder have comments that document almost all the methods.
|
||||
|
||||
## Registering `DidActivate`
|
||||
|
||||
`bsml` contains a few locations you can register to:
|
||||
|
||||
- Main Menu Mod Tabs
|
||||

|
||||
- Mod Settings
|
||||

|
||||
- Gameplay Setup
|
||||

|
||||
|
||||
For `bsml` to use your `DidActivate` method, you will need to register it using the `BSML::Register` class in your
|
||||
`late_load()` method.
|
||||
|
||||
```cpp
|
||||
#include "bsml/shared/BSML.hpp"
|
||||
|
||||
// other code
|
||||
|
||||
extern "C" void late_load() {
|
||||
// make sure this is after il2cpp_functions::Init()
|
||||
BSML::Init();
|
||||
BSML::Register::RegisterMainMenuViewControllerMethod(title, text, hoverHint, DidActivate);
|
||||
|
||||
// other code
|
||||
}
|
||||
```
|
||||
|
||||
The gameplay setup location requires a slightly different function signature than the other two, with the arguments
|
||||
being just `UnityEngine::GameObject* self, bool firstActivation`.
|
||||
|
||||
All the register functions can be found in the `BSML.hpp` file.
|
||||
Reference in New Issue
Block a user