Bootstrap completed, hello world in BSIPA logs confirmed

This commit is contained in:
pleb
2026-04-18 15:09:01 -07:00
parent 96b0e143ea
commit 752419121f
23 changed files with 3561 additions and 7 deletions
+108
View File
@@ -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`.
+338
View File
@@ -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).
+433
View File
@@ -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.
![Templatr Example](/.assets/images/modding/quest-mod-template-example.png)
### 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>
![Noodle Extensions](/.assets/images/modding/quest-ne-cover.jpg)
</details>
<details><summary>
Slice Details Quest
</summary>
![Slice Details Quest](/.assets/images/modding/quest-slice-details.jpg)
</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).
+81
View File
@@ -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
![Main Menu Mod Tabs](/.assets/images/modding/quest-menu-mod-tab.png)
- Mod Settings
![Mod Settings](/.assets/images/modding/quest-mod-settings.jpg)
- Gameplay Setup
![Gameplay Setup](/.assets/images/modding/quest-gameplay-settings.jpg)
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.