Skip to content

Commit

Permalink
Update Quest mod dev wiki (#582)
Browse files Browse the repository at this point in the history
* Update quest mod dev pages

* Change description and remove phaze's hook viewer

* fix lint

* Update late_load example

---------

Co-authored-by: Bloodcloak <[email protected]>
  • Loading branch information
kodenamekrak and bloodcloak authored May 26, 2024
1 parent fbde32d commit 59cd61e
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 72 deletions.
2 changes: 1 addition & 1 deletion wiki/modding/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ limited to:

- Hooking
- Configuration using `config-utils`
- User Interfaces using `questui`
- User Interfaces using `bsml`
- Custom types

Visit the [Quest Mod Development Intro](./quest-mod-dev-intro.md) page for more information on getting started!
7 changes: 4 additions & 3 deletions wiki/modding/quest-mod-dev-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,15 @@ DECLARE_CONFIG(ModConfig,
## 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()`, or even anytime later if you really want to, but it only ever needs to be run once.
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 load() {
extern "C" void late_load() {
// Initialize and load the config
getModConfig().Init(modInfo);
Expand Down Expand Up @@ -103,5 +104,5 @@ 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/Config/` on the Quest.
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`.
7 changes: 4 additions & 3 deletions wiki/modding/quest-mod-dev-custom-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,22 +220,23 @@ 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);`, or any C# method that would
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()` like so:
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 load() {
extern "C" void late_load() {
// make sure this is after il2cpp_functions::Init()
custom_types::Register::AutoRegister();

Expand Down
100 changes: 52 additions & 48 deletions wiki/modding/quest-mod-dev-intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ and add it to your PATH variable.

### Android NDK

[Download the Android NDK](https://developer.android.com/ndk), unzip it, and add it to a new environment variable called
ANDROID_NDK_HOME.
[Download the Android NDK](https://github.com/QuestPackageManager/ndk-canary-archive/releases/tag/27.0.1), unzip it, and
add it to a new environment variable called ANDROID_NDK_HOME.

## Create a Project

Expand All @@ -69,26 +69,26 @@ 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/sc2ad/beatsaber-hook)
and [codegen](https://github.com/sc2ad/BeatSaber-Quest-Codegen), to the version best suited for the game version you are
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. `codegen` is a library that allows modders to
`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 codegen
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.28.0, the correct codegen
version is 0.33.0:
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 codegen -v ^0.33.0
qpm dependency add bs-cordl -v ^3500.0.0
```

### Restore Dependencies
Expand All @@ -108,33 +108,29 @@ 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
build.ps1
copy.ps1
CMakeLists.txt
createqmod.ps1
mod.template.json
ndk-stack.ps1
pull-tombstone.ps1
qpm.json
README.md
start-logging.ps1
restart-game.ps1
validate-modjson.ps1
```

### Code Breakdown

#### src/main.cpp

`main.cpp` contains the `setup()` and `load()` methods. These methods can exist in any source file as long as they are
`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.

Expand All @@ -152,49 +148,50 @@ The extern folder should be ignored (and/or in some cases excluded). It contains
### 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.
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: `build.ps1`
Usage: `qpm s build`

Builds your mod. Does not produce a QMOD file.

#### copy.ps1

Usage: `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: `createqmod.ps1 (optionally) -qmodName {file name}`
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: `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: `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: `start-logging.ps1 -Self`
Usage: `qpm s logcat`

Prints logs from Beat Saber, just your mod, or also crashes. Usage of `-self` is recommended.

#### validate-modjson.ps1

Usage: `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.
Expand All @@ -211,28 +208,31 @@ like constructors.
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 on [Phaze's hook viewer](https://modtools.phazed.xyz/browser)
or in the `includes/codegen` directory in your `extern` folder.
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 main menu and change the text on the solo button to
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 main menu runs the event `DidActivate` when it is fully initialized. This is useful for us because we can hook
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 MainMenuViewController, using HMUI.CurvedTextMeshPro, etc.
// 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/MainMenuViewController.hpp"
#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 MainMenuUIHook
// targeting the method "MainMenuViewController::DidActivate", which takes the following arguments:
// 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.

Expand All @@ -241,35 +241,39 @@ Firstly, create your hook using the `MAKE_HOOK_MATCH` macro:
// your code here
// }

MAKE_HOOK_MATCH(MainMenuUIHook, &GlobalNamespace::MainMenuViewController::DidActivate, void,
GlobalNamespace::MainMenuViewController* self, bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) {

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.
MainMenuUIHook(self, firstActivation, addedToHierarchy, screenSystemEnabling);
LevelUIHook(self, firstActivation, addedToHierarchy, screenSystemEnabling);

// Get the _soloButton text object by accessing the soloButton field and some simple Unity methods.
// 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.
UnityEngine::UI::Button* soloMenuButton = self->soloButton;
UnityEngine::GameObject* gameObject = soloMenuButton->get_gameObject();
HMUI::CurvedTextMeshPro* soloMenuText = gameObject->GetComponentInChildren<HMUI::CurvedTextMeshPro*>();
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"
soloMenuText->SetText("Skill Issue");
actionButtonText->set_text("Skill Issue");
}
```
Now, you have to install your hook. Usually, hooks are installed in `load()` in `main.cpp`:
<!-- markdownlint-enable MD013 -->
Now, you have to install your hook. Usually, hooks are installed in `load()` or `late_load()` in `main.cpp`:
```cpp
extern "C" void load() {
extern "C" void late_load() {
il2cpp_functions::Init();
getLogger().info("Installing hooks...");
PaperLogger.info("Installing hooks...");
INSTALL_HOOK(getLogger(), MainMenuUIHook);
INSTALL_HOOK(PaperLogger, LevelUIHook);
getLogger().info("Installed all hooks!");
PaperLogger.info("Installed all hooks!");
}
```

Expand Down Expand Up @@ -396,7 +400,7 @@ 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](./quest-mod-dev-ui.md)
page to see how to use `questui` to create a settings screen for your mod.
page to see how to use `bsml` to create a settings screen for your mod.

## Credits

Expand Down
34 changes: 17 additions & 17 deletions wiki/modding/quest-mod-dev-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,46 +10,46 @@ description: Learn how to create a UI for your Quest Mod!
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 `questui` to create a
settings screen for your mod.
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 `questui` by running `qpm dependency add questui` in your project directory.
- 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 `questui` that allows you to make a simple mod settings page.
`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 "questui/shared/BeatSaberUI.hpp"
#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 = QuestUI::BeatSaberUI::CreateScrollableSettingsContainer(self->get_transform());
UnityEngine::GameObject* container = BSML::Lite::CreateScrollableSettingsContainer(self->get_transform());

// Create a text that says "Hello World!" and set the parent to the container.
QuestUI::BeatSaberUI::CreateText(container->get_transform(), "Hello World!");
BSML::Lite::CreateText(container->get_transform(), "Hello World!");
}
}
```
There are too many UI components and methods to document in this guide. However, the file `BeatSaberUI.hpp` has
comments that document almost all the methods.
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`
`questui` contains a few locations you can register to:
`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)
Expand All @@ -58,18 +58,18 @@ comments that document almost all the methods.
- Gameplay Setup
![Gameplay Setup](/.assets/images/modding/quest-gameplay-settings.jpg)
For `questui` to use your `DidActivate` method, you will need to register it using the `QuestUI::Register` class in your
`load()` method.
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 "questui/shared/QuestUI.hpp"
#include "bsml/shared/BSML.hpp"
// other code
extern "C" void load() {
extern "C" void late_load() {
// make sure this is after il2cpp_functions::Init()
QuestUI::Init();
QuestUI::Register::RegisterModSettingsViewController(modInfo, DidActivate);
BSML::Init();
BSML::Register::RegisterMainMenuViewControllerMethod(title, text, hoverHint, DidActivate);
// other code
}
Expand All @@ -78,4 +78,4 @@ extern "C" void load() {
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 `QuestUI.hpp` file.
All the register functions can be found in the `BSML.hpp` file.

0 comments on commit 59cd61e

Please sign in to comment.