Skip to content

Code Documentation

Amethyst-szs edited this page Aug 25, 2023 · 23 revisions

Welcome to the LunaKit Docs

Last updated August 24th 2023
Note that the docs are for v1.1 of LunaKit, so some info my be incorrect, and will (hopefully) be updated soon!

LunaKit is a C++ add-on to Super Mario Odyssey, built off of CraftyBoss's ImGui ExLaunch Base Repo. This program is designed to be extremely modular and customizable for adding or editing its features. Every major element of LunaKit will be covered in this documentation wiki page, please refer to the table of contents below OR click the links in the code itself to jump straight to that part of the documentation

Manager (DevGuiManager)

General Information

The DevGuiManager (LunaKit Manager) is the root class that controls every other part of LunaKit. Edits to this class are required to add nearly any new features, including new windows and home bar tabs.
This class is a singleton, meaning there is only one static instance of it that exists at all times (initalized in the game's GameSystem::init function, hook found in main.cpp)
Jump to table of contents

Current Application Version

LunaKit's current version is determined at compile time by the tags of the git repo!

If working on a fork of this repo, the only way to update the version is by adding a new tag to your repo and then recompiling, this will change the LunaKit version to match the name of your tag. The format of the version is always following semantic versioning with v#.#.#
Jump to table of contents

update() vs. updateDisplay()

void update(); // Update is always called every frame (on the sequence)
void updateDisplay(); // Update display is only used when the menu is currently open

The manager runs two seperate update functions during a frame, update() and updateDisplay(). Here are the key differences between these functions:

update(): Update is called every frame off of the HakoniwaSequence hook, mainly used to execute code that affects the game. A good example of this is setting a boolean on clicking a box in updateDisplay(), and then handling that interaction in update(). In the update function, you have no control over the ImGui display and it is not recommended to try and use anything from the ImGui library in this function. This function is always called every frame, even if the interface is closed

updateDisplay(): This function is called far later during ImGui's drawing process and is responsible for rendering every window, home menu tab, and everything else visual. Most features share updateDisplay functions that are called off of the manager including Windows, Home Tabs, and Categories. You can add your own code into this through classes that inherit from these classes and override their updateDisplay functions. This function is not called if the interface is closed (hiding the windows with the left stick does not count as closed, code will still be run)
Jump to table of contents

Anchors

Anchoring is a system handled by the manager that controls how the windows are displayed.
There are four different anchor positions:

  • Top
  • Bottom
  • Left
  • Right
// All positions the windows can be placed on the screen
enum WinAnchorType {
    ANC_TOP,
    ANC_BOTTOM,
    ANC_LEFT,
    ANC_RIGHT,
    ANC_TOTAL_SIZE
};

Window positions and sizes are refreshed any time that refreshAnchor() or setAnchorType() is called, the exact positioning of these windows is determined based on the Window's setupAnchor() function. Each window has can either be anchored or non-anchored, as well as having total anchor pages. The amount of pages determines the size of the window, the larger the number the more room will be given (proportional to total open windows/pages) More detail given at Anchored vs. Non-anchored in the Window Documentation.
Jump to table of contents

Popups and On-Screen Keyboard

LunaKit contains a popups folder featuring a custom made on-screen keyboard! In order to avoid requiring you to plugin a keyboard for text input, this feature exists to make it easier to support text input. The popup system is not nearly as robust as many other systems and there is no base class for popups, so if you want to make your own, reference the keyboard and see how it works!

In order to utilize the on-screen keyboard, access the DevGuiManager and call

tryOpenKeyboard(uint16_t maxChars, const char** output, bool* isKeyboardOpen)

As you type in the keyboard, it will automatically update the output and isKeyboardOpen ptrs that you pass it, allowing anything else to read the current information and state of the keyboard!
Jump to table of contents

Windows

What is a window?

Every LunaKit window is a box inheriting from the WindowBase class and registered in the windows array of the DevGuiManger. Note this excludes the home bar despite technically being a window, and includes both anchored and non-anchored windows, essentially any window class registered in the manager's window PtrArray

Windows have two update functions, updateWin() and tryUpdateWinDisplay(), for more information on these, check the update() vs. updateDisplay() docs above!
Jump to table of contents

Creating a Window Class

Creating a window class is designed to be quite simple, only needing to override four functions for most functionality. Each window has four main functions intended to be overridden by child classes, the constructor, updateWin, tryUpdateWinDisplay, and setupAnchor

Here is a template for what a window class may look like:

#pragma once

// Do not have include directly to DevGuiManager to avoid issues
#include "devgui/windows/WindowBase.h"

#include "devgui/categories/CategoryBase.h"

class WindowCustom : public WindowBase {
public:
    WindowCustom(DevGuiManager* parent, const char* winName, bool isActiveByDefault, bool isAnchor, int windowPages);

    void updateWin() override;
    bool tryUpdateWinDisplay() override;
    void setupAnchor(int totalAnchoredWindows, int anchorIdx) override;

private:
    // Custom parameters and information here
};

Please note that you don't need to override every single one of these functions if you aren't using them! For example, if your window is in the standard anchor list, there is no reason to override setupAnchor. If you want to see how these functions are implemented in the cpp files, look at the other already created windows as a reference.

Once you have your window created, you'll need to add it to the window list in the Manager! This can be done by going to the function DevGuiManager::createElements() at the top of the cpp file. Inside here you can add your window by adding a new call to createWindow like this!

createWindow<WindowBase>("Window Name", true/false: isActiveByDefault, true/false: isInAnchorList, int: anchorPages);

The class in <> will determine what type of window is being created
The window name string will determine the name of each window in the title bar (VERY IMPORTANT EVERY WINDOW HAS A UNIQUE NAME!!)
The isActiveByDefault boolean determines if (on a blank/new LunaKit save file) if the window is already open
The isInAnchorList boolean determines if space should be made for this window when creating the list of anchored windows. More info on anchors here
The anchorPages integer sets how much space will be given for your window (IF ANCHORED). Most windows should use 1 here, but if you need more room, this can be increased.
Jump to table of contents

Anchored vs. Non-Anchored

There are two types of windows, anchored and non-anchored.
For more general information on the anchor system as a whole, read this first

Anchored Windows: These types of windows are always positioned in a straight bar on any edge of the screen and their size is calculated based on what other windows are open (and how many pages each window takes up). Creating an anchored window is as simple as setting the isAnchor bool in the createWindow<>() to true and not overriding the setupAnchor() function in your child class.

Non-Anchored Windows: Creating a non-anchored window is a good bit more complex. These windows can be manually positioned anywhere on the screen, but this takes care to avoid overlap with the anchored section regardless of where the user decides to place their anchored windows. Start by adding your window to the createWindow<>() list with the isAnchor bool set to false. After this you'll need to override the setupAnchor() function to calculate your windows position and size manually. There is any number of ways you can do this, but here is the FPS window's custom anchor setup as an example.

    WinAnchorType type = mParent->getAnchorType();

    // Setup window's position based on the anchor type
    switch(type) {
        case WinAnchorType::ANC_TOP:
            mConfig.mTrans = ImVec2(0, mConfig.mScrSize.y - mConfig.mSize.y);
            break;
        case WinAnchorType::ANC_BOTTOM:
            mConfig.mTrans = ImVec2(0, mConfig.mMinimumY);
            break;
        case WinAnchorType::ANC_LEFT:
            mConfig.mTrans = ImVec2(mConfig.mScrSize.x - mConfig.mSize.x, mConfig.mMinimumY);
            break;
        case WinAnchorType::ANC_RIGHT:
            mConfig.mTrans = ImVec2(0, mConfig.mMinimumY);
            break;
        default:
            break;
    }

    ImGui::SetWindowPos(mConfig.mTrans);
    ImGui::SetWindowSize(mConfig.mSize);

This example picks a corner not already occupied and sets the position and size based on where everything else has been positioned. If you (for some reason) wanted a small window in the dead center of the screen, you can easily do that by setting the position and scale to preset vectors and avoid calculations based on the anchor. Depends on a case by case basis.
Jump to table of contents

Window Config

Window Config is an additional struct included in every window (through the member mConfig) Modifying the parameters in this struct will change how your window is drawn.

It is not recommended to modify these parameters outside of the setupAnchor() function since it's not guaranteed the window will get updated with the changed parameters until the anchor is refreshed

To learn more about the window config's parameters, check the in-line code docs! You can find it near the top of the WindowBase.h header
Jump to table of contents

Categories

What is a category

A category is a sub-menu within windows. Windows are not required to have categories, but if they do, they will be added to a list within the window and it will automatically draw a list of tabs, each tab being your different category classes.

These are just a built in way of abstracting code for windows into separate boxes if appropriate for the window.
Jump to table of contents

Creating a category

In order to make a category, you'll want to make a new header and source file for your category. It's recommended that you sort your categories into folders for each window! Technically, any category can be added to any window, so keeping it organized for each window is strongly recommended.

Each category class looks roughly like this:

class CategoryCustom : public CategoryBase {
public:
    CategoryCustom(const char* catName, const char* catDesc);

    void updateCat() override;
    void updateCatDisplay() override;

private:
    // Parameters or private functions go here
};

For information on the two different update functions, read the update() vs. updateDisplay() docs from the manager. If your code only displays information without editing anything, it is likely you don't need to override the updateCat() func.
Jump to table of contents

Adding a category to a window

In order to implement a category, you'll want to view your window's constructor! By default, category functionality is disabled on windows and all writing to the window is handled by the window itself, however adding a category to the list will make the window a category window and automatically use the code from each category.

In order to add a category, add the following to the constructor:

createCategory<CategoryClass>("Cat Name", "Cat Description");

Within the <>s, you'll want the class of the category you are creating, and the two parameters are just a name and description for your category. It's recommended to keep both the name and description as short as possible.
Jump to table of contents

Home Bar Items

What is the home bar

The home bar is the bar of drop downs at the very top of the screen whenever the interface is active. Home Bar Items are classes that add and run each button in this menu. It's not recommended to make too many different items to avoid making the list of options too long. If something isn't needed have quick access for all users, it's likely better as a window than a home menu item.
Jump to table of contents

Creating a home bar item

Each item is a separate class in a separate header and source file. In order to create a new home bar item, make a new header and use this as a reference:

class HomeMenuCustom : public HomeMenuBase {
public:
    HomeMenuCustom(DevGuiManager* parent, const char* menuName);

    void updateMenu() override;
    void updateMenuDisplay() override;

private:
    // Members and functions only accessible here
};

For information on the two different update functions, read the update() vs. updateDisplay() docs from the manager. If your code only displays information without editing anything, it is likely you don't need to override the updateMenu() function

Jump to table of contents

Adding a home bar item

Using a home bar item into the interface requires pushing your class into the bar list. Head to the Manager class and go to the first function in the source, DevGuiManager::createElements(). You'll need to add a new call to createHomeMenuItem<>(), which looks like this:

createHomeMenuItem<HomeMenuClass>("Header Name");

Within the <>s, you'll want the class of the item you are creating, and the parameter determines the name of the item in the home bar.
Jump to table of contents

Recursion (Dos and Donts)

When working with menu items, it's extremely common to want to include sub menus, however this process is slightly more complicated in LunaKit than standard ImGui for a few reasons.

Note 1 - addMenu(): When creating a sub menu, typically you would call ImGui::BeginMenu(), however when using this menu setup you want to call the HomeMenuBase.h protected function addMenu() IF this is the first sub menu layer. This exists to preserve font size into sub menus. By default ImGui does not preserve font size into sub-menus, however this function should only be used on the first layer otherwise the font will continuously increase size.

Note 2 - Avoid too many layers: Fitting everything you want into a home menu item can be challenging due to space. Keep in mind that the more layers you add, the further down and to the right the user is going to go, eventually going off screen and not being accessible. Try to design smartly and avoid too many layers / infinitely repeating menus.
Jump to table of contents

Settings

What are these settings

The DevGuiSettings class is designed to hold simple toggles that are persistent between game sessions and included in the "Settings" home bar tab. Any setting in this class can be accessed and modified from anywhere, and is automatically added to the menu and written to the save (unless specifically told not to save!)
Jump to table of contents

Why not just use standard variables

Each setting is stored as a DevGuiSettingsEntry inside a PtrArray which is a member of DevGuiSettings. In order to access your different settings, you need to use the functions on the main DevGuiSettings class. This may seem over complicated compared to just adding a boolean member to whatever class is using it, but this system has some key advantages

  • Instantly supports saving
  • Instantly can be added to the interface
  • Allows access from anywhere (read and write)

This means adding any new settings to the list is a breeze for everybody, making new options and cheats easier than ever!
Jump to table of contents

How to add a new setting

In order to add a new setting, access the source file for DevGuiSettings and view the constructor (located as the first function in the file). You'll need to add a new call to the registerNewSetting() function, an example of which is shown below.

registerNewSetting(isEnabledByDefault: true/false, isSave: true/false, "Setting Name");

Once this is added, the setting will automatically be added to the menu and (if requested) the save file!
Jump to table of contents

How to access your setting

There are a lot of helper functions to access your setting in different ways, below is a breakdown of each:

bool getStateByName(const char* settingName);
void setStateByName(const char* settingName, bool newState);
void toggleStateByName(const char* settingName);

These are quick ways to get info or modify your setting by it's name! For example, if you made a setting titled "Super Jump" you can then get the state of your setting by calling getStateByName("Super Jump");

bool getStateByIdx(int idx) { return mSettings.at(idx)->isTrue(); }
bool* getStatePtrByIdx(int idx) { return mSettings.at(idx)->getValuePtr(); }
void setStateByIdx(int idx, bool state) { return mSettings.at(idx)->setValue(state); }
const char* getNameByIdx(int idx) { return mSettings.at(idx)->getName(); }

These allow getting info or modifying all settings by their position in the list. This is not the recommended way to handle individual settings due to the fact that the order of the settings is likely to change over time, but is instead recommended for iterating over every setting in the list. Speaking of iterating over every setting:

DevGuiSettingsEntry* getSettingEntry(int idx) { return mSettings.at(idx); }
int getTotalSettings() { return mSettings.size(); }

These functions are more general tools intended for iterating over the entire list, including the ability to get a SettingsEntry in it's entirety by index and the total size of the list.
Jump to table of contents

Primitives

What is the Primitive Renderer

The primitive renderer is a built in feature of al (Action Library) that can be used in Mario Odyssey to draw basic debug shapes on top of the game world. Using this system is rather complicated however, requiring setting up a lot of functions that can clutter and de-organize projects.

This feature is expanded upon using LunaKit with the PrimitiveQueue system. It can be accessed from anywhere off of the Manager's getPrimitiveQueue() function. From here, you can push objects into the list from anywhere with a bunch of easy to use classes and they'll be rendered whenever the LunaKit interface is open! How nifty is that!
Jump to table of contents

Pushing an object

In order to push a new item into the Primitive Queue, you'll need need access to the queue class which can accessed from DevGuiManager::getPrimitiveQueue()

From here, call one of the push functions with all the requested parameters it will get rendered in game. Here's an example of what the flow of pushing a point looks like

queue->pushPoint(al::getTrans(player), 25.f, {1.f, 0.3f, 0.3f, 1.f});

There are a few important notes about this system however:

  • The max amount of pushes you can do in a single frame is determined by the private const member mMaxQueueSize, going over this can cause issues!
  • Not every push type is equal in performance. For example, pushing 100 area types is far far less performant than 100 lines.
  • Points drawn when there is no scene are automatically cleared, so there's nothing to worry about if you accidentally push outside of a scene, although it's recommended to check anyways for performance.
  • The points are drawn at the very end of the manager's update() function, so it's recommended to do your pushes in your window or menu's update function, not their updateDisplay() to make sure they show up on the same frame they are pushed
    Jump to table of contents

Adding new types

There is no super fast way to add new types, but you'll want to look at the PrimitiveTypes.h header. Follow these steps to ensure functionality of your new type:

  • Add new entry to enum at top of types header
  • Make a new type class, using this as a reference:
class PrimitiveTypeCustom : public PrimitiveTypeBase {
public:
    PrimitiveTypeCustom(int parameter)
        : PrimitiveTypeBase(PrimitiveTypes::PRIM_CUSTOM)
    {
        mParameter = parameter;
    }
    ~PrimitiveTypeCustom() override {}

    int mParameter;

    void render() override;
};
  • Implement the render function in PrimitiveTypes.cpp
  • Add new function to queue header implementing push function
    Jump to table of contents

Themes

What are themes

Themes are different visual configurations of LunaKit with different properties and colors made using ImGui's style system, however in this case these are presets loaded off the SD card and made easy to edit! Different themes can be accessed in the Windows section of the home bar.
Default
Jump to table of contents

How does the theme loader work

The theme loader works quite similar to the save data system's read function if you've already seen that, but with some extra steps.

On game startup, the system will navigate to sd:/LunaKit/LKData/Themes/ inside the setupDirectoryInfo() function. This creates a dynamically sized array full of the name of each and every file in this folder. Note that there should be nothing placed in this folder other than .byml files!

After collecting this list of files, the helper function FsHelper::loadFileFromPath(); is used to read all the bytes from each byml file and construct al::ByamlIter classes, each of which contains the root of each theme. This loading process also sets the default theme based on if the theme has a flags sub-iter marking it as default. Only one theme should have this, and it is necessary due to the order of themes rarely being consistent between users.

If you're looking to add new theme parameters, look in the tryUpdateTheme() section, it should be quite easy to expand the color and parameter options provided, plus including completely new features outside those if you are so inclined! Just remember that if you do add new features, don't assume every theme file will have them! Include safety checks and make sure the process can still run smoothly without them.
Jump to table of contents

Save Data

What is the LunaKit save data

Unlike nearly every other mod (at least of the current times) this mod does not write it's save information into the user's save data but instead to the SD card! This has a lot of advantages including added storage space, easier access, and it doesn't get erased if you boot the game without the mod! This file is erased and your save is reset if you update/downgrade your LunaKit version because it cannot be guaranteed that compatibility will be preserved between versions.
Jump to table of contents

Where is the LunaKit save data

The LunaKit save data is stored at sd:/LunaKit/LKData/data.byml
If you want to edit this file externally through a byml editor, you're welcome to, just be aware that it is possible to break things if you mess with it too much, so proceed with caution!
Jump to table of contents

Understanding file writing

Writing files is one of the most complicated parts of LunaKit, however it's been abstracted to the best of my ability to keep it accessible. This section will cover how it works on a technical level and the next will cover how to edit for changing/adding features.

There are three key pieces to file writing, a DevGuiWriteStream (sead::WriteStream), a sead::RamWriteStream, and a al::ByamlWriter. Each one of these has a very different function, detailed below:

al::ByamlWriter: The simplest of the three classes, this is responsible for creating the byml that's going to be written to the SD card, and takes up the majority of the write function. Works by just adding iters and values to the list and the class handles all the information storage itself. However, this class is unusable unless you can write to a stream, speaking of which:

DevGuiWriteStream (aka sead::WriteStream): This class is a custom implementation of sead::WriteStream. This class is responsible for taking the information from the ByamlWriter and pushing it into a buffer that can written to the SD card. That being said, a WriteStream cannot directly access a buffer which leads me tooo:

sead::RamWriteStream: Despite sounding a lot like the above class, they have totally different uses. This class is included by the write stream and this is what is responsible for writing the information to the buffer. This is the least important part to know about if you're just working with the completed save system, but it's important to remember that this is a very useful class to use when working with WriteStream classes.

Once this all is complete, you can just write the buffer to the SD card using the handy FsHelper function! This process is the slowest and does cause lag due to being on the same thread as the rest of the interface, but assuming you have a half-decent SD card it should freeze for less than a frame or two!
Jump to table of contents

Editing save read/write

When editing any part of the save file, you'll need to edit both the read AND write, only editing one won't get anything done. The DevGuiSaveData class has both a read and write function which you can modify.

Write: When writing information to the file, you usually don't have to directly work with the buffer or any other complex element of the saving, just adding more calls to the ByamlWriter should work! Here's an example of adding a category (iter) called "Example" and adding two strings and an integer.

    file.pushHash("Example");

    file.addString("IPAddress", "192.168.69.420");
    file.addString("AnotherThings", "Wowie");
    file.addInt("Value", 22);

    file.pop();

The function must always start with a pushHash() and end with a pop() to ensure the root iter is made and then completed.

Read: When reading from the save file, you'll need safety checks and to use the al::ByamlIter functions to read through the iter (and sub-iters). Here's an example of reading an int from a test key:

    int test;
    if(root.tryGetIntByKey(&test, "ExampleKey")) {
        // Use value grabbed from save here
    }        

Jump to table of contents

Important disclaimer

If you're writing a LOT of data to the SD card, you might run into trouble! By default the work buffer is sized at 0x1000 (4096) bytes, which is plenty of space for most people but if you're modifying this system to write a lot of data and cross over the 4KB line, you're gonna run into problems! Make sure to increase the work buffer size in the save data header if you're going past this point.
Jump to table of contents

Initalization

These docs will not go in depth about the inner workings of ImGui or the switch implementation of ImGui, look at Crafty's ImGui base repo for more information there. There are two main points of interest when working with the init process.

exl_main: This is located at the bottom of the main.cpp file and creates all the ExLaunch hooks. The main relevant sections for this include the UpdateLunaKit hook, the GameSystemInit hook, and the function for initing all settings hooks. The settings hooks are located in devgui/settings/HooksSettings and is where all hooks pretaining to the settings system should be located!

GameSystemInit Hook: This hook is where LunaKit gets it's singleton prepared and inited (along with the original debug text writer, which likely won't be used and might be scrapped in the future)!
Jump to table of contents