Merge main

This commit is contained in:
Pieter-Jan Briers
2026-09-06 02:30:08 +02:00
786 changed files with 10494 additions and 5138 deletions
-1
View File
@@ -17,5 +17,4 @@ add_mod(ao_mod
SOURCES src/mod.cpp
MOD_JSON mod.json
RES_DIR res
BUNDLE
)
-7
View File
@@ -1,7 +0,0 @@
{
"id": "dev.twilitrealm.basic_cosmetics",
"name": "Basic Cosmetics",
"version": "1.0.0",
"author": "Twilit Realm",
"description": "Change Basic Cosmetic Colors"
}
@@ -1,88 +0,0 @@
#include "color_utils.hpp"
uint8_t desaturate_rgb_565(uint16_t rgb565Val) {
const uint32_t r = (rgb565Val & 0xf800) >> 11;
const uint32_t g = (rgb565Val & 0x7e0) >> 5;
const uint32_t b = rgb565Val & 0x1f;
// Here we are doing a quicker (0.22 * r + 0.72 * g + 0.06 * b) which
// uses multiplies and shifts rather than division.
const uint32_t combined = 30480413 * r + 49085341 * g + 8312839 * b;
uint8_t shifted = (combined >> 24) & 0xff;
// Check if should round up shifted value.
if (shifted < 0xff && combined & 0x00800000) {
shifted += 1;
}
return shifted;
}
uint16_t blend_overlay_rgb_565(uint8_t grayVal, GXColor color) {
uint32_t rTimes255, gTimes255, bTimes255;
if (grayVal <= 0x7f) {
const uint32_t grayTimesTwo = 2 * grayVal;
rTimes255 = grayTimesTwo * color.r;
gTimes255 = grayTimesTwo * color.g;
bTimes255 = grayTimesTwo * color.b;
} else {
const uint32_t multiplier = 2 * (255 - grayVal);
rTimes255 = 255 * 255 - multiplier * (255 - color.r);
gTimes255 = 255 * 255 - multiplier * (255 - color.g);
bTimes255 = 255 * 255 - multiplier * (255 - color.b);
}
// Divide each by 255
const uint32_t r = (rTimes255 + 1 + (rTimes255 >> 8)) >> 8;
const uint32_t g = (gTimes255 + 1 + (gTimes255 >> 8)) >> 8;
const uint32_t b = (bTimes255 + 1 + (bTimes255 >> 8)) >> 8;
return ((r & 0xf8) << 8) | ((g & 0xfc) << 3) | ((b & 0xf8) >> 3);
}
// Helper function to perform overlay blending on a single 8-bit color channel
uint8_t blend_overlay_channel(uint8_t base, uint8_t blend) {
if (base < 128) {
return static_cast<uint8_t>((2 * base * blend) / 255);
}
return static_cast<uint8_t>(255 - (2 * (255 - base) * (255 - blend)) / 255);
}
bool is_valid_hex_color_str(std::string_view hexStr) {
return hexStr.find_first_not_of("0123456789ABCDEFabcdef") == std::string_view::npos &&
hexStr.length() == 6;
}
GXColor hex_color_str_to_gx_color(const std::string& hexColorStr) {
u8 r = std::stoi(hexColorStr.substr(0, 2), nullptr, 16);
u8 g = std::stoi(hexColorStr.substr(2, 2), nullptr, 16);
u8 b = std::stoi(hexColorStr.substr(4, 2), nullptr, 16);
return GXColor{r, g, b};
}
static f32 rainbowPhaseAngle = 0.f;
GXColor get_rainbow_rgb(f32 amplitude) {
f32 phase_rad = rainbowPhaseAngle * M_PI / 180.0f;
u8 r_val = (u8)(amplitude * (sinf(phase_rad) + 1.0f) + 0.5f);
u8 g_val = (u8)(amplitude * (sinf(phase_rad + 2.0f * M_PI / 3.0f) + 1.0f) + 0.5f);
u8 b_val = (u8)(amplitude * (sinf(phase_rad + 4.0f * M_PI / 3.0f) + 1.0f));
GXColor rgbColor;
rgbColor.r = r_val;
rgbColor.g = g_val;
rgbColor.b = b_val;
rgbColor.a = 0xff;
return rgbColor;
}
void update_rainbow_rgb(f32 increment) {
rainbowPhaseAngle += increment;
if (rainbowPhaseAngle >= 360.0f) {
rainbowPhaseAngle -= 360.0f;
}
}
@@ -1,30 +0,0 @@
#pragma once
/**
* File originally copied from console TPR with permission from isaac
* https://github.com/zsrtp/libtp_rel/blob/master/include/util/color_utils.h
*/
#include <gx.h>
#include <cstdint>
#include <string>
// Desaturates an RGB565 color to a u8 gray value (0xFF being white and 0x00
// being black).
uint8_t desaturate_rgb_565(uint16_t rgb565Val);
// Performs an "Overlay" blend of a u8 gray value and a pointer to a u8
// array of {r,g,b}. Returns the result as an RGB565.
uint16_t blend_overlay_rgb_565(uint8_t grayVal, GXColor color);
// Perform overlay blending on a single 8-bit color channel
uint8_t blend_overlay_channel(uint8_t base, uint8_t blend);
bool is_valid_hex_color_str(std::string_view hexStr);
GXColor hex_color_str_to_gx_color(const std::string& hexColorStr);
GXColor get_rainbow_rgb(f32 amplitude);
void update_rainbow_rgb(f32 increment);
-731
View File
@@ -1,731 +0,0 @@
#include "hooks.hpp"
#include "color_utils.hpp"
#include "midna_hair_color.hpp"
#include "mod.hpp"
#include "types.h"
#include "mods/svc/hook.hpp"
#include "mods/svc/log.hpp"
#include "SSystem/SComponent/c_phase.h"
#include "d/actor/d_a_alink.h"
#include "d/actor/d_a_midna.h"
#include "d/d_a_item_static.h"
#include "d/d_bright_check.h"
#include "d/d_file_sel_info.h"
#include "d/d_file_select.h"
#include "d/d_kankyo.h"
#include "d/d_kantera_icon_meter.h"
#include "d/d_menu_collect.h"
#include "d/d_menu_fishing.h"
#include "d/d_menu_fmap2D.h"
#include "d/d_menu_insect.h"
#include "d/d_menu_letter.h"
#include "d/d_menu_option.h"
#include "d/d_menu_ring.h"
#include "d/d_menu_save.h"
#include "d/d_menu_skill.h"
#include "d/d_meter2.h"
#include "d/d_meter2_draw.h"
#include "d/d_meter2_info.h"
#include "d/d_meter_button.h"
#include "d/d_meter_hakusha.h"
#include "d/d_msg_object.h"
#include "d/d_msg_out_font.h"
#include "d/d_msg_scrn_base.h"
#include "d/d_pane_class.h"
// Lantern ambience color
DEFINE_HOOK(&dKy_WolfEyeLight_set, WolfEyeLightSet);
void wolf_eye_light_set_post(ModContext*, void*, void*, void*) {
auto maybeLanternColor = get_config_var_color(get_cvars().lanternGlowColor, true);
if (maybeLanternColor.has_value()) {
auto lanternColor = maybeLanternColor.value();
dScnKy_env_light_c* kankyo = dKy_getEnvlight();
kankyo->field_0x0c18[0].mColor.r = lanternColor.r;
kankyo->field_0x0c18[0].mColor.g = lanternColor.g;
kankyo->field_0x0c18[0].mColor.b = lanternColor.b;
}
}
// Lantern Sphere color
DEFINE_HOOK(&daAlink_c::preKandelaarDraw, PreKandelaarDraw);
void pre_kandelaar_draw_post(ModContext*, void*, void*, void*) {
auto maybeLanternColor = get_config_var_color(get_cvars().lanternGlowColor, true);
if (maybeLanternColor.has_value()) {
auto lanternColor = maybeLanternColor.value();
J3DMaterial* mat_p = daAlink_getAlinkActorClass()
->mpKanteraGlowModel->getModelData()
->getMaterialNodePointer(0);
J3DGXColorS10 color;
color.r = lanternColor.r;
color.g = lanternColor.g;
color.b = lanternColor.b;
color.a = 255;
mat_p->setTevColor(1, &color);
color.r = lanternColor.r;
color.g = lanternColor.g;
color.b = lanternColor.b;
mat_p->setTevColor(2, &color);
}
}
// Main Lantern Meter Color
DEFINE_HOOK(&CPaneMgr::setBlackWhite, CPaneMgrSetBlackWhite);
HookAction cpane_mgr_set_black_white_pre(ModContext*, void* args, void*, void*) {
// Check for magic meter
auto pane = mods::arg<CPaneMgr*>(args, 0);
if (dMeter2Info_getMeterClass() == NULL ||
pane != dMeter2Info_getMeterClass()->getMeterDrawPtr()->mpMagicMeter)
{
return HOOK_CONTINUE;
}
// If magic meter, check to see we're setting lantern colors
auto& black = mods::arg_ref<JUtility::TColor>(args, 1);
auto& white = mods::arg_ref<JUtility::TColor>(args, 2);
if (black != JUtility::TColor(255, 255, 140, 255) &&
white != JUtility::TColor(230, 170, 0, 255))
{
return HOOK_CONTINUE;
}
auto maybeLanternColor = get_config_var_color(get_cvars().lanternGlowColor, true);
if (maybeLanternColor.has_value()) {
auto lanternColor = maybeLanternColor.value();
black = JUtility::TColor(lanternColor.r, lanternColor.g, lanternColor.b, 255);
white = JUtility::TColor(lanternColor.r, lanternColor.g, lanternColor.b, 255);
}
return HOOK_CONTINUE;
}
// Lantern Icon Meter Color
DEFINE_HOOK(&dKantera_icon_c::setNowGauge, KanteraIconSetNowGauge);
void kantera_icon_set_now_gauge_post(ModContext*, void* args, void*, void*) {
auto maybeLanternColor = get_config_var_color(get_cvars().lanternGlowColor, true);
if (maybeLanternColor.has_value()) {
auto lanternColor = maybeLanternColor.value();
auto kanteraIcon = mods::arg<dKantera_icon_c*>(args, 0);
kanteraIcon->mpGauge->setBlackWhite(
JUtility::TColor(lanternColor.r, lanternColor.g, lanternColor.b, 255),
JUtility::TColor(lanternColor.r, lanternColor.g, lanternColor.b, 255));
}
}
// Light Sword Effect Color
DEFINE_HOOK(&daAlink_c::setLightningSwordEffect, SetLightningSwordEffect);
void set_lightning_sword_effect_post(ModContext*, void* args, void*, void*) {
auto maybeGlowColor = get_config_var_color(get_cvars().lightSwordGlowColor, true);
if (maybeGlowColor.has_value()) {
auto glowColor = maybeGlowColor.value();
auto link = mods::arg<daAlink_c*>(args, 0);
// Check copied from inside the hooked function
if (link->mEquipItem == 0x103 && link->checkNoResetFlg3(daPy_py_c::FLG3_UNK_100000)) {
for (size_t i = 0; i < 3; i++) {
auto emitter = dComIfGp_particle_getEmitter(link->field_0x327c[i]);
if (emitter != NULL) {
emitter->setGlobalEnvColor(glowColor.r, glowColor.g, glowColor.b);
emitter->setGlobalPrmColor(glowColor.r, glowColor.g, glowColor.b);
}
}
}
}
}
void recolor_ui_button(ConfigVarHandle option, u64 tag, J2DScreen* screen) {
auto buttonColorStr = get_str_option(option, "");
if (is_valid_hex_color_str(buttonColorStr)) {
auto color = hex_color_str_to_gx_color(buttonColorStr);
auto element = static_cast<J2DPicture*>(screen->search(tag));
if (element != nullptr) {
element->setBlackWhite(
JUtility::TColor(0, 0, 0, 0), JUtility::TColor(color.r, color.g, color.b, 0xFF));
}
}
}
// Main gameplay UI. Top left hearts and top right buttons
DEFINE_HOOK(&dMeter2Draw_c::init, dMeter2Init);
void d_meter_2_init_post(ModContext*, void* args, void*, void*) {
auto dMeter2Draw = mods::arg<dMeter2Draw_c*>(args, 0);
auto screen = dMeter2Draw->getMainScreenPtr();
// Heart tags on main UI
static constexpr std::array kHeartTags = {MULTI_CHAR('hear_00'), MULTI_CHAR('hear_01'),
MULTI_CHAR('hear_02'), MULTI_CHAR('hear_03'), MULTI_CHAR('hear_04'), MULTI_CHAR('hear_05'),
MULTI_CHAR('hear_06'), MULTI_CHAR('hear_07'), MULTI_CHAR('hear_08'), MULTI_CHAR('hear_09'),
MULTI_CHAR('hear_10'), MULTI_CHAR('hear_11'), MULTI_CHAR('hear_12'), MULTI_CHAR('hear_13'),
MULTI_CHAR('hear_14'), MULTI_CHAR('hear_15'), MULTI_CHAR('hear_16'), MULTI_CHAR('hear_17'),
MULTI_CHAR('hear_18'), MULTI_CHAR('hear_19'), MULTI_CHAR('bigh_00'), MULTI_CHAR('bigh_01'),
MULTI_CHAR('bigh_02'), MULTI_CHAR('bigh_03')};
auto maybeHeartColor = get_config_var_color(get_cvars().heartColor);
if (maybeHeartColor.has_value()) {
auto heartColor = maybeHeartColor.value();
for (auto tag : kHeartTags) {
auto element = static_cast<J2DPicture*>(screen->search(tag));
if (element != nullptr) {
element->setBlackWhite(heartColor, JUtility::TColor(200, 200, 200, 255));
}
}
}
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen);
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen);
recolor_ui_button(get_cvars().xButtonColor, MULTI_CHAR('x_btn'), screen);
recolor_ui_button(get_cvars().yButtonColor, MULTI_CHAR('y_btn'), screen);
recolor_ui_button(get_cvars().zButtonColor, MULTI_CHAR('zbtn'), screen);
}
// Hearts on the file select screen
DEFINE_HOOK(&dFile_info_c::setHeartCnt, FileInfoSetHeartCount);
void file_info_set_heart_count_post(ModContext*, void* args, void*, void*) {
auto fileInfo = mods::arg<dFile_info_c*>(args, 0);
// Heart tags on file select
static constexpr std::array kFileSelectHeartTags{
MULTI_CHAR('hear_20'),
MULTI_CHAR('hear_21'),
MULTI_CHAR('hear_22'),
MULTI_CHAR('hear_23'),
MULTI_CHAR('hear_24'),
MULTI_CHAR('hear_25'),
MULTI_CHAR('hear_26'),
MULTI_CHAR('hear_27'),
MULTI_CHAR('hear_28'),
MULTI_CHAR('hear_29'),
MULTI_CHAR('hear_30'),
MULTI_CHAR('hear_31'),
MULTI_CHAR('hear_32'),
MULTI_CHAR('hear_33'),
MULTI_CHAR('hear_34'),
MULTI_CHAR('hear_35'),
MULTI_CHAR('hear_36'),
MULTI_CHAR('hear_37'),
MULTI_CHAR('hear_38'),
MULTI_CHAR('hear_39'),
};
auto maybeHeartColor = get_config_var_color(get_cvars().heartColor);
if (maybeHeartColor.has_value()) {
auto heartColor = maybeHeartColor.value();
for (auto tag : kFileSelectHeartTags) {
auto element = static_cast<J2DPicture*>(fileInfo->mFileInfo.Scr->search(tag));
if (element != nullptr) {
element->setBlackWhite(heartColor, JUtility::TColor(200, 200, 200, 255));
}
}
}
}
// Buttons that appear contextually at the bottom of the screen during gameplay
// (Are X, Y ever used there?)
DEFINE_HOOK(&dMeterButton_c::screenInitButton, ScreenInitButton);
void screen_init_button_post(ModContext*, void* args, void*, void*) {
auto meterButton = mods::arg<dMeterButton_c*>(args, 0);
auto screen = meterButton->mpButtonScreen;
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn1'), screen);
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen);
recolor_ui_button(get_cvars().xButtonColor, MULTI_CHAR('x_btn'), screen);
recolor_ui_button(get_cvars().yButtonColor, MULTI_CHAR('y_btn'), screen);
recolor_ui_button(get_cvars().zButtonColor, MULTI_CHAR('zbtn'), screen);
}
// A and B buttons when saving a file
DEFINE_HOOK(&dMenu_save_c::screenSet, MenuSaveScreenSet);
void menu_save_screen_set_post(ModContext*, void* args, void*, void*) {
auto menuSave = mods::arg<dMenu_save_c*>(args, 0);
auto screen = menuSave->mSaveSel.Scr;
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('wabtn'), screen);
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('wbbtn'), screen);
}
// A and B buttons when selecting a file
DEFINE_HOOK(&dFile_select_c::screenSet, FileSelectScreenSet);
void file_select_screen_set_post(ModContext*, void* args, void*, void*) {
auto fileSelect = mods::arg<dFile_select_c*>(args, 0);
auto screen = fileSelect->fileSel.Scr;
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('wabtn'), screen);
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('wbbtn'), screen);
}
// A button on brightness check screen
DEFINE_HOOK(&dBrightCheck_c::screenSet, BrightCheckScreenSet);
void bright_check_screen_set_post(ModContext*, void* args, void*, void*) {
auto brightCheck = mods::arg<dBrightCheck_c*>(args, 0);
auto screen = brightCheck->mBrightCheck.Scr;
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn1'), screen);
}
// X and Y buttons on the item wheel screen
DEFINE_HOOK(&dMenu_Ring_c::_create, MenuRingCreate);
void menu_ring_create_post(ModContext*, void* args, void*, void*) {
auto menuRing = mods::arg<dMenu_Ring_c*>(args, 0);
auto screen = menuRing->mpScreen;
recolor_ui_button(get_cvars().xButtonColor, MULTI_CHAR('xbtn'), screen);
recolor_ui_button(get_cvars().yButtonColor, MULTI_CHAR('ybtn'), screen);
}
// A and B buttons on the main pause screen
DEFINE_HOOK(&dMenu_Collect2D_c::_create, MenuCollect2DCreate);
void menu_collect_2D_create_post(ModContext*, void* args, void*, void*) {
auto menuCollect2D = mods::arg<dMenu_Collect2D_c*>(args, 0);
auto screen = menuCollect2D->mpScreenIcon;
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen);
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen);
}
// A and B buttons on the fishing journal screen
DEFINE_HOOK(&dMenu_Fishing_c::screenSetDoIcon, MenuFishingScreenSetDoIcon);
void menu_fishing_screen_set_do_icon_post(ModContext*, void* args, void*, void*) {
auto menuFishing = mods::arg<dMenu_Fishing_c*>(args, 0);
auto screen = menuFishing->mpIconScreen;
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen);
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen);
}
// A and B buttons on the insect collection screen
DEFINE_HOOK(&dMenu_Insect_c::screenSetDoIcon, MenuInsectScreenSetDoIcon);
void menu_insect_screen_set_do_icon_post(ModContext*, void* args, void*, void*) {
auto menuInsect = mods::arg<dMenu_Insect_c*>(args, 0);
auto screen = menuInsect->mpIconScreen;
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen);
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen);
}
// A and B buttons on the letters screen
DEFINE_HOOK(&dMenu_Letter_c::screenSetDoIcon, MenuLetterScreenSetDoIcon);
void menu_letter_screen_set_do_icon_post(ModContext*, void* args, void*, void*) {
auto menuLetter = mods::arg<dMenu_Letter_c*>(args, 0);
auto screen = menuLetter->mpIconScreen;
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen);
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen);
}
// A, B, and Z buttons on option screen
DEFINE_HOOK(&dMenu_Option_c::_create, MenuOptionCreate);
void menu_option_create_post(ModContext*, void* args, void*, void*) {
auto menuOption = mods::arg<dMenu_Option_c*>(args, 0);
auto screen = menuOption->mpScreenIcon;
auto backScreen = menuOption->mpBackScreen;
auto tvScreen = menuOption->mpTVScreen;
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen);
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen);
recolor_ui_button(get_cvars().zButtonColor, MULTI_CHAR('g_zbtn'), backScreen);
// A Button on option's brightness check screen
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn1'), tvScreen);
}
// A and B buttons on the hidden skills screen
DEFINE_HOOK(&dMenu_Skill_c::screenSetDoIcon, MenuSkillScreenSetDoIcon);
void menu_skill_screen_set_do_icon_post(ModContext*, void* args, void*, void*) {
auto menuSkill = mods::arg<dMenu_Skill_c*>(args, 0);
auto screen = menuSkill->mpIconScreen;
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen);
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen);
}
// A, B, and Z buttons on the map screen
DEFINE_HOOK(&dMenu_Fmap_c::_create, MenuFMapCreate);
void menu_fmap_create_post(ModContext*, void* args, void*, void*) {
auto menuFMap = mods::arg<dMenu_Fmap_c*>(args, 0);
auto screen = menuFMap->mpDraw2DTop->mpTitleScreen;
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen);
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn1'), screen);
recolor_ui_button(get_cvars().zButtonColor, MULTI_CHAR('zbtn'), screen);
}
// A button on the howling screen
DEFINE_HOOK(&dMsgObject_c::talkStartInit, MsgObjectTalkStartInit);
void msg_object_talk_start_init_post(ModContext*, void* args, void*, void*) {
auto msgObject = mods::arg<dMsgObject_c*>(args, 0);
// If textbox kind is howling
if (msgObject->mFukiKind == 17) {
auto screen = msgObject->mpScrnDraw->mpScreen;
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('abtn'), screen);
}
}
// A button when on Epona
DEFINE_HOOK(&dMeterHakusha_c::_create, MeterHakushaCreate);
void meter_hakusha_create_post(ModContext*, void* args, void*, void*) {
auto meterHakusha = mods::arg<dMeterHakusha_c*>(args, 0);
auto screen = meterHakusha->mpButtonScreen;
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn1'), screen);
}
// A, B, X, and Y button icons in text
DEFINE_HOOK(&COutFont_c::createPane, OutFontCreatePane);
void out_font_create_pane_post(ModContext*, void* args, void*, void*) {
auto outFont = mods::arg<COutFont_c*>(args, 0);
auto paneArr = outFont->mpPane;
auto maybeAButtonColor = get_config_var_color(get_cvars().aButtonColor);
if (maybeAButtonColor.has_value()) {
auto aButtonColor = maybeAButtonColor.value();
paneArr[0]->setBlackWhite(JUtility::TColor(255, 255, 255, 0),
JUtility::TColor(aButtonColor.r, aButtonColor.g, aButtonColor.b, 255));
}
auto maybeBButtonColor = get_config_var_color(get_cvars().bButtonColor);
if (maybeBButtonColor.has_value()) {
auto bButtonColor = maybeBButtonColor.value();
paneArr[1]->setBlackWhite(JUtility::TColor(255, 255, 255, 0),
JUtility::TColor(bButtonColor.r, bButtonColor.g, bButtonColor.b, 255));
}
// Condition copied from hooked function
auto xyBlack = JUtility::TColor(255, 255, 255, 0);
if (outFont->field_0x242 == 1) {
xyBlack = JUtility::TColor(0, 0, 0, 0);
}
auto maybeXButtonColor = get_config_var_color(get_cvars().xButtonColor);
if (maybeXButtonColor.has_value()) {
auto xButtonColor = maybeXButtonColor.value();
paneArr[5]->setBlackWhite(
xyBlack, JUtility::TColor(xButtonColor.r, xButtonColor.g, xButtonColor.b, 255));
}
auto maybeYButtonColor = get_config_var_color(get_cvars().yButtonColor);
if (maybeYButtonColor.has_value()) {
auto yButtonColor = maybeYButtonColor.value();
paneArr[6]->setBlackWhite(
xyBlack, JUtility::TColor(yButtonColor.r, yButtonColor.g, yButtonColor.b, 255));
}
}
// Heart icon in text
DEFINE_HOOK(&COutFontSet_c::drawFont, OutFontSetDrawFont);
void out_font_set_draw_font_post(ModContext*, void* args, void*, void*) {
auto outFontSet = mods::arg<COutFontSet_c*>(args, 0);
auto maybeHeartColor = get_config_var_color(get_cvars().heartColor);
if (maybeHeartColor.has_value()) {
auto heartColor = maybeHeartColor.value();
u32 heartColor_u32 = heartColor.r << 24 | heartColor.g << 16 | heartColor.b << 8 | 0xFF;
if (outFontSet->getType() == 0x1B) { // Heart icon type
outFontSet->mColor = heartColor_u32;
}
}
}
// Heart Drop/Piece of Heart/Heart Container model color
DEFINE_HOOK(&daItemBase_c::CreateItemHeap, ItemBaseCreateItemHeap);
void item_base_create_item_heap_post(ModContext*, void* args, void*, void*) {
auto itemBase = mods::arg<daItemBase_c*>(args, 0);
if (itemBase == NULL) {
return;
}
auto itemNo = itemBase->m_itemNo;
if (itemNo == dItemNo_HEART_e || itemNo == dItemNo_UTAWA_HEART_e ||
itemNo == dItemNo_KAKERA_HEART_e)
{
auto maybeHeartColor = get_config_var_color(get_cvars().heartColor);
if (maybeHeartColor.has_value()) {
auto heartColor = maybeHeartColor.value();
auto heartColorS10 = GXColorS10(heartColor.r, heartColor.g, heartColor.b, heartColor.a);
// Edit inner heart material color for Piece of Heart / Heart Container
if (itemNo == dItemNo_UTAWA_HEART_e || itemNo == dItemNo_KAKERA_HEART_e) {
*itemBase->mpModel->getModelData()->getMaterialNodePointer(3)->getTevKColor(1) =
heartColor;
*itemBase->mpModel->getModelData()->getMaterialNodePointer(3)->getTevColor(1) =
heartColorS10;
}
const u8 heartColorRGB[3] = {heartColor.r, heartColor.g, heartColor.b};
u8** cRegTable =
reinterpret_cast<u8**>(&itemBase->mpBrkAnm->getBrkAnm()->mAnmCRegDataR);
u8** kRegTable =
reinterpret_cast<u8**>(&itemBase->mpBrkAnm->getBrkAnm()->mAnmKRegDataR);
for (int i = 0; i < 3; i++) {
u8* cReg = cRegTable[i];
u8* kReg = kRegTable[i];
auto curColor = heartColorRGB[i];
// Set heart drop inner heart color
cReg[0x3] = curColor;
cReg[0xB] = curColor;
// Set heart drop outer heart color
kReg[0x3] = curColor;
kReg[0xB] = curColor;
if (itemNo == dItemNo_KAKERA_HEART_e) {
cReg[0x13] = curColor;
cReg[0x1B] = curColor;
kReg[0x13] = curColor;
kReg[0x1B] = curColor;
}
if (itemNo == dItemNo_UTAWA_HEART_e) {
cReg[0x13] = curColor;
kReg[0x13] = curColor;
kReg[0x1B] = curColor;
kReg[0x23] = curColor;
kReg[0x2B] = curColor;
}
}
}
}
}
// Midna Hair Color
DEFINE_HOOK(&daMidna_c::create, MidnaCreate);
void midna_create_post(ModContext*, void* args, void* retval, void*) {
auto step = reinterpret_cast<int*>(retval);
// Don't set colors if midna isn't done loading
if (*step != cPhs_COMPLEATE_e) {
return;
}
auto midna = mods::arg<daMidna_c*>(args, 0);
midna->field_0x6e0 = g_currentMidnaHairColors.normalColor;
if (dKy_darkworld_check()) {
midna->field_0x6e8 = g_currentMidnaHairColors.normalKColor;
midna->field_0x6ec = g_currentMidnaHairColors.normalKColor2;
} else {
midna->field_0x6e8 = g_currentMidnaHairColors.lNormalKColor;
midna->field_0x6ec = g_currentMidnaHairColors.lNormalKColor2;
}
}
// Override Midna Hair Color Part 2
DEFINE_HOOK(&daMidna_c::setBodyPartMatrix, MidnaSetBodyPartMatrix);
static GXColorS10 midnaField0x6e0{};
static GXColor midnaField0x6e8{};
static GXColor midnaField0x6ec{};
// Copy the original values before setBodyPartMatrix runs
HookAction midna_set_body_part_matrix_pre(ModContext*, void* args, void* retval, void* userdata) {
auto midna = mods::arg<daMidna_c*>(args, 0);
midnaField0x6e0 = midna->field_0x6e0;
midnaField0x6e8 = midna->field_0x6e8;
midnaField0x6ec = midna->field_0x6ec;
return HOOK_CONTINUE;
}
void midna_set_body_part_matrix_post(ModContext*, void* args, void* retval, void* userdata) {
auto midna = mods::arg<daMidna_c*>(args, 0);
if (midna->mpHairhandBmd != NULL) {
// Restore the original values from before setBodyPartMatrix ran (this undoes the chase)
midna->field_0x6e0 = midnaField0x6e0;
midna->field_0x6e8 = midnaField0x6e8;
midna->field_0x6ec = midnaField0x6ec;
// Statement copied from inside function to determine colors
bool bigColors =
midna->checkStateFlg0(daMidna_c::FLG0_UNK_10000000) ||
midna->mBckHeap[2].getIdx() == daMidna_c::m_anmDataTable[daMidna_c::ANM_HAIR].mResID ||
midna->mBckHeap[2].getIdx() ==
daMidna_c::m_anmDataTable[daMidna_c::ANM_S_TAKES].mResID ||
midna->mBckHeap[2].getIdx() ==
daMidna_c::m_anmDataTable[daMidna_c::ANM_S_WAITS].mResID ||
midna->mBckHeap[2].getIdx() ==
daMidna_c::m_anmDataTable[daMidna_c::ANM_S_PACKAWAY].mResID ||
midna->mBckHeap[2].getIdx() ==
daMidna_c::m_anmDataTable[daMidna_c::ANM_GRABST].mResID ||
midna->checkEndResetStateFlg0(daMidna_c::ERFLG0_UNK_40) ||
dComIfGp_checkPlayerStatus1(0, 0x800000);
GXColorS10 color{};
GXColor kcolor1{};
GXColor kcolor2{};
// Set our own colors
if (bigColors) {
kcolor1 = g_currentMidnaHairColors.bigKColor;
if (dKy_darkworld_check()) {
color = g_currentMidnaHairColors.bigColor;
kcolor2 = g_currentMidnaHairColors.normalKColor2;
} else {
color = g_currentMidnaHairColors.lBigColor;
kcolor2 = g_currentMidnaHairColors.lBigKColor2;
}
} else {
color = g_currentMidnaHairColors.normalColor;
if (dKy_darkworld_check()) {
kcolor1 = g_currentMidnaHairColors.normalKColor;
kcolor2 = g_currentMidnaHairColors.normalKColor2;
} else {
kcolor1 = g_currentMidnaHairColors.lNormalKColor;
kcolor2 = g_currentMidnaHairColors.lNormalKColor2;
}
}
// Reapply the chase that happens in the function
cLib_chaseS(&midna->field_0x6e0.r, color.r, 10);
cLib_chaseS(&midna->field_0x6e0.g, color.g, 10);
cLib_chaseS(&midna->field_0x6e0.b, color.b, 10);
cLib_chaseUC(&midna->field_0x6e8.r, kcolor1.r, 10);
cLib_chaseUC(&midna->field_0x6e8.g, kcolor1.g, 10);
cLib_chaseUC(&midna->field_0x6e8.b, kcolor1.b, 10);
cLib_chaseUC(&midna->field_0x6ec.r, kcolor2.r, 10);
cLib_chaseUC(&midna->field_0x6ec.g, kcolor2.g, 10);
cLib_chaseUC(&midna->field_0x6ec.b, kcolor2.b, 10);
}
}
// Midna Charge Ring Color
DEFINE_HOOK(&daAlink_c::setWolfLockDomeModel, SetWolfLockDomeModel);
void wolf_lock_dome_model_post(ModContext*, void*, void*, void*) {
auto domeRingColorStr = get_str_option(get_cvars().midnaChargeRingColor, "");
if (is_valid_hex_color_str(domeRingColorStr)) {
auto domeRingColor = hex_color_str_to_gx_color(domeRingColorStr);
const u8 domeWave1RGBA[3] = {domeRingColor.r, domeRingColor.g, domeRingColor.b};
const u8 domeWave2RGBA[3] = {domeRingColor.r, domeRingColor.g, domeRingColor.b};
u8** chromaRegisterTable =
reinterpret_cast<u8**>(&daAlink_getAlinkActorClass()->field_0x0724->mAnmCRegDataR);
for (int i = 0; i < 3; i++) {
u8* currentTable = chromaRegisterTable[i];
const u8 currentWave1Color = domeWave1RGBA[i];
const u8 currentWave2Color = domeWave2RGBA[i];
const u8 currentBaseColor = (currentWave1Color + currentWave2Color) / 2;
currentTable[0x3] = currentBaseColor; // Set Alpha for the ring base
currentTable[0x13] = currentWave1Color; // Set Alpha for ring wave 1
currentTable[0x23] = currentWave2Color; // Set Alpha for ring wave 2
currentTable[0xB] = currentBaseColor; // Set Alpha for darkworld ring base
currentTable[0x1B] = currentWave1Color; // Set Alpha for darkworld ring wave 1
currentTable[0x2B] = currentWave2Color; // Set Alpha for darkworld ring wave 2
}
}
}
#define ADD_POST_HOOK(defined_hook, function, original) \
result = mods::hook::add_post<defined_hook>(function); \
if (result != MOD_OK) { \
mods::log::debug( \
"failed to add post hook to" #original ", Result {}", static_cast<int>(result)); \
return result; \
}
#define ADD_PRE_HOOK(defined_hook, function, original) \
result = mods::hook::add_pre<defined_hook>(function); \
if (result != MOD_OK) { \
mods::log::debug( \
"failed to add pre hook to" #original ", Result {}", static_cast<int>(result)); \
return result; \
}
ModResult add_all_hooks() {
ModResult result{};
// Hooks for lantern glow
ADD_POST_HOOK(WolfEyeLightSet, wolf_eye_light_set_post, dKy_WolfEyeLight_set)
ADD_POST_HOOK(PreKandelaarDraw, pre_kandelaar_draw_post, daAlink_c::preKandelaarDraw)
// Hooks for lantern meter color
ADD_PRE_HOOK(CPaneMgrSetBlackWhite, cpane_mgr_set_black_white_pre, CPaneMgr::setBlackWhite)
ADD_POST_HOOK(
KanteraIconSetNowGauge, kantera_icon_set_now_gauge_post, dKantera_icon_c::setNowGauge)
// Hook for midna charge ring
ADD_POST_HOOK(SetWolfLockDomeModel, wolf_lock_dome_model_post, daAlink_c::setWolfLockDomeModel)
// Hook for light sword glow
ADD_POST_HOOK(SetLightningSwordEffect, set_lightning_sword_effect_post,
daAlink_c::setLightningSwordEffect)
// Hooks for Midna Hair Color
ADD_POST_HOOK(MidnaCreate, midna_create_post, daMidna_c::create)
ADD_PRE_HOOK(
MidnaSetBodyPartMatrix, midna_set_body_part_matrix_pre, daMidna_c::setBodyPartMatrix)
ADD_POST_HOOK(
MidnaSetBodyPartMatrix, midna_set_body_part_matrix_post, daMidna_c::setBodyPartMatrix)
// Hooks for UI colors
ADD_POST_HOOK(dMeter2Init, d_meter_2_init_post, dMeter2Draw_c::init)
ADD_POST_HOOK(FileInfoSetHeartCount, file_info_set_heart_count_post, dFile_info_c::setHeartCnt)
ADD_POST_HOOK(ScreenInitButton, screen_init_button_post, dMeterButton_c::screenInitButton)
ADD_POST_HOOK(MenuSaveScreenSet, menu_save_screen_set_post, dMenu_save_c::screenSet)
ADD_POST_HOOK(FileSelectScreenSet, file_select_screen_set_post, dFile_select_c::screenSet)
ADD_POST_HOOK(BrightCheckScreenSet, bright_check_screen_set_post, dBrightCheck_c::screenSet)
ADD_POST_HOOK(MenuRingCreate, menu_ring_create_post, dMenu_Ring_c::_create)
ADD_POST_HOOK(MenuCollect2DCreate, menu_collect_2D_create_post, dMenu_Collect2D_c::_create)
ADD_POST_HOOK(MenuFishingScreenSetDoIcon, menu_fishing_screen_set_do_icon_post,
dMenu_Fishing_c::screenSetDoIcon)
ADD_POST_HOOK(MenuInsectScreenSetDoIcon, menu_insect_screen_set_do_icon_post,
dMenu_Insect_c::screenSetDoIcon)
ADD_POST_HOOK(MenuLetterScreenSetDoIcon, menu_letter_screen_set_do_icon_post,
dMenu_Letter_c::screenSetDoIcon)
ADD_POST_HOOK(MenuOptionCreate, menu_option_create_post, dMenu_Option_c::_create)
ADD_POST_HOOK(MenuSkillScreenSetDoIcon, menu_skill_screen_set_do_icon_post,
dMenu_Skill_c::screenSetDoIcon)
ADD_POST_HOOK(OutFontCreatePane, out_font_create_pane_post, COutFont_c::createPane)
ADD_POST_HOOK(OutFontSetDrawFont, out_font_set_draw_font_post, COutFontSet_c::drawFont)
ADD_POST_HOOK(MenuFMapCreate, menu_fmap_create_post, dMenu_Fmap_c::_create)
ADD_POST_HOOK(
MsgObjectTalkStartInit, msg_object_talk_start_init_post, dMsgObject_c::talkStartInit)
ADD_POST_HOOK(MeterHakushaCreate, meter_hakusha_create_post, dMeterHakusha_c::_create)
// Heart Model Color
ADD_POST_HOOK(
ItemBaseCreateItemHeap, item_base_create_item_heap_post, daItemBase_c::CreateItemHeap)
return MOD_OK;
}
#define UNINSTALL_HOOK(defined_hook) mods::hook::uninstall<defined_hook>(svc_hook);
ModResult remove_all_hooks() {
UNINSTALL_HOOK(WolfEyeLightSet)
UNINSTALL_HOOK(PreKandelaarDraw)
UNINSTALL_HOOK(CPaneMgrSetBlackWhite)
UNINSTALL_HOOK(KanteraIconSetNowGauge)
UNINSTALL_HOOK(SetWolfLockDomeModel)
UNINSTALL_HOOK(SetLightningSwordEffect)
UNINSTALL_HOOK(MidnaCreate)
UNINSTALL_HOOK(MidnaSetBodyPartMatrix)
UNINSTALL_HOOK(MidnaSetBodyPartMatrix)
UNINSTALL_HOOK(dMeter2Init)
UNINSTALL_HOOK(FileInfoSetHeartCount)
UNINSTALL_HOOK(ScreenInitButton)
UNINSTALL_HOOK(MenuSaveScreenSet)
UNINSTALL_HOOK(FileSelectScreenSet)
UNINSTALL_HOOK(BrightCheckScreenSet)
UNINSTALL_HOOK(MenuRingCreate)
UNINSTALL_HOOK(MenuCollect2DCreate)
UNINSTALL_HOOK(MenuFishingScreenSetDoIcon)
UNINSTALL_HOOK(MenuInsectScreenSetDoIcon)
UNINSTALL_HOOK(MenuLetterScreenSetDoIcon)
UNINSTALL_HOOK(MenuOptionCreate)
UNINSTALL_HOOK(MenuSkillScreenSetDoIcon)
UNINSTALL_HOOK(OutFontCreatePane)
UNINSTALL_HOOK(OutFontSetDrawFont)
UNINSTALL_HOOK(MenuFMapCreate)
UNINSTALL_HOOK(MsgObjectTalkStartInit)
UNINSTALL_HOOK(MeterHakushaCreate)
UNINSTALL_HOOK(ItemBaseCreateItemHeap)
return MOD_OK;
}
-7
View File
@@ -1,7 +0,0 @@
#pragma once
#include "mods/service.hpp"
ModResult add_all_hooks();
ModResult remove_all_hooks();
@@ -1,115 +0,0 @@
#include "midna_hair_color.hpp"
#include "mod.hpp"
#include "mods/svc/log.hpp"
#include <array>
namespace {
constexpr std::array<std::array<GXColor, 9>, 10> kMidnaHairColors = {{
// Default
{{
/*l_lNormalKColor*/ /*l_normalKColor*/ /*l_normalColor*/
{0xFF, 0xDC, 0x00}, {0xB4, 0x87, 0x00}, {0x50, 0x00, 0x00},
/*l_bigKColor*/ /*l_lBigColor*/ /*l_bigColor*/
{0x1E, 0x00, 0x00}, {0xFF, 0x78, 0x00}, {0xFF, 0x64, 0x78},
/*l_lNormalKColor2*//*l_normalKColor2*/ /*l_lBigKColor2*/
{0x00, 0xC3, 0xEB}, {0x00, 0xC3, 0xC3}, {0xAA, 0xFF, 0xC3}
}},
// Pink
{{
{0xF5, 0xCF, 0xF3}, {0xAD, 0x7F, 0x7F}, {0x1B, 0x00, 0x20},
{0x3C, 0x02, 0x58}, {0xE3, 0x72, 0xF2}, {0xE3, 0x5F, 0xF8},
{0xDD, 0x00, 0xEB}, {0xDD, 0x00, 0xC3}, {0xF6, 0x4C, 0xFF}
}},
// Red
{{
{0xE4, 0x65, 0x41}, {0xA1, 0x3E, 0x22}, {0x21, 0x00, 0x00},
{0x4F, 0x02, 0x01}, {0xF0, 0x3A, 0x25}, {0xF0, 0x30, 0x8C},
{0xEB, 0x00, 0x00}, {0xEB, 0x00, 0x00}, {0xFF, 0x4F, 0x3A}
}},
// Yellow
{{
{0x91, 0x83, 0x0E}, {0x66, 0x50, 0x07}, {0x0E, 0x0B, 0x00},
{0x24, 0x25, 0x02}, {0xCB, 0xB7, 0x00}, {0xCB, 0x99, 0x78},
{0xEB, 0xDE, 0x00}, {0xEB, 0xDE, 0x00}, {0xFF, 0xF8, 0xBF}
}},
// Green
{{
{0x35, 0x79, 0x53}, {0x25, 0x4A, 0x2B}, {0x00, 0x0E, 0x05},
{0x0A, 0x29, 0x10}, {0x00, 0xB6, 0x6F}, {0x00, 0x98, 0xB3},
{0x1F, 0xEB, 0x00}, {0x1F, 0xEB, 0x00}, {0x9A, 0xFF, 0x81}
}},
// Blue
{{
{0x00, 0x72, 0xFF}, {0x00, 0x46, 0x85}, {0x00, 0x08, 0x28},
{0x16, 0x1B, 0x5D}, {0x00, 0x60, 0xFF}, {0x00, 0x50, 0xFF},
{0x00, 0x48, 0xEB}, {0x00, 0x48, 0xC3}, {0x3A, 0x66, 0xFF}
}},
// Purple
{{
{0x6F, 0x34, 0xFF}, {0x4E, 0x20, 0x85}, {0x0D, 0x00, 0x34},
{0x15, 0x08, 0x79}, {0x62, 0x00, 0xFF}, {0x62, 0x00, 0xFF},
{0x7B, 0x00, 0xEB}, {0x7B, 0x00, 0xC3}, {0x94, 0x3E, 0xFF}
}},
// Brown
{{
{0x00, 0x00, 0x00}, {0x00, 0x00, 0x00}, {0x1A, 0x05, 0x00},
{0x3C, 0x19, 0x0E}, {0x3F, 0x1D, 0x0B}, {0x3F, 0x18, 0x7E},
{0x3F, 0x1D, 0x0B}, {0x3F, 0x1D, 0x09}, {0x59, 0x32, 0x1E}
}},
// White
{{
{0xF0, 0xF1, 0xF1}, {0xA9, 0x94, 0x7E}, {0x09, 0x0B, 0x0C},
{0x22, 0x24, 0x24}, {0xFF, 0xFF, 0xFF}, {0xFF, 0xD5, 0xFF},
{0xEA, 0xEA, 0xEA}, {0xEA, 0xEA, 0xC2}, {0xF3, 0xF3, 0xF3}
}},
// Black
{{
{0x00, 0x00, 0x00}, {0x00, 0x00, 0x00}, {0x0B, 0x0B, 0x0B},
{0x23, 0x23, 0x23}, {0x00, 0x00, 0x00}, {0x00, 0x00, 0x78},
{0x00, 0x00, 0x00}, {0x00, 0x00, 0x00}, {0x00, 0x00, 0x00}
}}
}};
size_t get_midna_hair_color_index(ConfigVarHandle handle) {
const auto optionIndex = get_int_option(handle, 0);
if (optionIndex < 0 || optionIndex >= static_cast<int64_t>(kMidnaHairColors.size())) {
return 0;
}
return static_cast<size_t>(optionIndex);
}
}
MidnaHairColors g_currentMidnaHairColors = {
.normalColor = {0x50, 0x00, 0x00, 0x00},
.normalKColor = {0xB4, 0x87, 0x00, 0x00},
.normalKColor2 = {0x00, 0xC3, 0xC3, 0x00},
.bigColor = {0xFF, 0x64, 0x78, 0x00},
.bigKColor = {0x1E, 0x00, 0x00, 0x00},
.lNormalKColor = {0xFF, 0xDC, 0x00, 0x00},
.lNormalKColor2 = {0x00, 0xC3, 0xEB, 0x00},
.lBigColor = {0xFF, 0x78, 0x00, 0x00},
.lBigKColor2 = {0xAA, 0xFF, 0xC3, 0x00},
};
void set_all_midna_hair_colors() {
auto& g_cvars = get_cvars();
auto hairBaseColor = get_midna_hair_color_index(g_cvars.midnaHairBaseColor);
auto hairTipsColor = get_midna_hair_color_index(g_cvars.midnaHairTipsColor);
// Colors we have to convert to GXColorS10
auto& normalColor = kMidnaHairColors.at(hairBaseColor)[2];
auto& bigColor = kMidnaHairColors.at(hairBaseColor)[5];
auto& lBigColor = kMidnaHairColors.at(hairBaseColor)[4];
g_currentMidnaHairColors.normalColor = GXColorS10{normalColor.r, normalColor.g, normalColor.b};
g_currentMidnaHairColors.normalKColor = kMidnaHairColors.at(hairBaseColor)[1];
g_currentMidnaHairColors.normalKColor2 = kMidnaHairColors.at(hairTipsColor)[7];
g_currentMidnaHairColors.bigColor = GXColorS10{bigColor.r, bigColor.g, bigColor.b};
g_currentMidnaHairColors.bigKColor = kMidnaHairColors.at(hairBaseColor)[3];
g_currentMidnaHairColors.lNormalKColor = kMidnaHairColors.at(hairBaseColor)[0];
g_currentMidnaHairColors.lNormalKColor2 = kMidnaHairColors.at(hairTipsColor)[6];
g_currentMidnaHairColors.lBigColor = GXColorS10{lBigColor.r, lBigColor.g, lBigColor.b};
g_currentMidnaHairColors.lBigKColor2 = kMidnaHairColors.at(hairTipsColor)[8];
}
@@ -1,19 +0,0 @@
#pragma once
#include <gx.h>
struct MidnaHairColors {
GXColorS10 normalColor;
GXColor normalKColor;
GXColor normalKColor2;
GXColorS10 bigColor;
GXColor bigKColor;
GXColor lNormalKColor;
GXColor lNormalKColor2;
GXColorS10 lBigColor;
GXColor lBigKColor2;
};
extern MidnaHairColors g_currentMidnaHairColors;
void set_all_midna_hair_colors();
-653
View File
@@ -1,653 +0,0 @@
#include "mod.hpp"
#include "color_utils.hpp"
#include "hooks.hpp"
#include "midna_hair_color.hpp"
#include "texture_utils.hpp"
#include "mods/service.hpp"
#include "mods/svc/config.h"
#include "mods/svc/hook.hpp"
#include "mods/svc/log.h"
#include "mods/svc/log.hpp"
#include "mods/svc/ui.h"
#include "d/d_com_inf_game.h"
#include <xxhash.h>
#include <optional>
#include <ranges>
#include <string>
DEFINE_MOD();
IMPORT_SERVICE(LogService, svc_log);
IMPORT_SERVICE(ConfigService, svc_config);
IMPORT_SERVICE(HookService, svc_hook);
IMPORT_SERVICE(TextureService, svc_texture);
IMPORT_SERVICE(UiService, svc_ui);
static cvars g_cvars;
cvars& get_cvars() {
return g_cvars;
}
std::string get_str_option(ConfigVarHandle handle, const std::string& fallback) {
std::string value{};
size_t outLength{};
svc_config->get_string(mod_ctx, handle, NULL, 0, &outLength);
value.resize(outLength);
if (handle == 0 || svc_config->get_string(
mod_ctx, handle, value.data(), value.size() + 1, &outLength) != MOD_OK)
{
return fallback;
}
return value;
}
int64_t get_int_option(ConfigVarHandle handle, int64_t fallback) {
int64_t value = fallback;
if (handle == 0 || svc_config->get_int(mod_ctx, handle, &value) != MOD_OK) {
return fallback;
}
return value;
}
// Helper for getting configVar color
std::optional<GXColor> get_config_var_color(ConfigVarHandle handle, bool allowRainbow) {
auto colorStr = get_str_option(handle, "");
// Convert to lowercase
for (auto& c : colorStr) {
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
if (colorStr == "rainbow" && allowRainbow) {
auto color = get_rainbow_rgb(127.5f);
color.r /= 2;
color.g /= 2;
color.b /= 2;
return color;
}
if (is_valid_hex_color_str(colorStr)) {
return hex_color_str_to_gx_color(colorStr);
}
return std::nullopt;
}
namespace {
UiWindowHandle g_cosmeticsWindow = 0;
bool g_loadedAllBaseTextures = false;
constexpr uint32_t kTextureLoadRetryFrames = 30;
uint32_t g_textureLoadRetryCountdown = 0;
constexpr const char* kOverlayPresets[] = {
"ab706e",
"6382a0",
"94749a",
"ec8644",
"b9ab00",
"ec9fc8",
"505154",
"f8f7f4",
"91723e",
};
constexpr const char* kLightPresets[] = {
"ff0000",
"f68821",
"f6f321",
"00ff00",
"0000ff",
"8000ff",
"a0a0a0",
"30d0d0",
};
constexpr const char* kRainbowLightPresets[] = {
"rainbow",
"ff0000",
"f68821",
"f6f321",
"00ff00",
"0000ff",
"8000ff",
"a0a0a0",
};
constexpr const char* kAButtonPresets[] = {
"ff0000",
"ff5000",
"ffaf00",
"0080ff",
"0000ff",
"8000ff",
"5555ff",
"ff20ff",
};
constexpr const char* kBButtonPresets[] = {
"ffff40",
"ffa0ff",
"00e87b",
"00aaff",
"6078ff",
"000000",
"00f3ff",
};
constexpr const char* kXyButtonPresets[] = {
"ff0000",
"ff8200",
"f7df00",
"70ff00",
"00bd11",
"0000ff",
"800088",
"000000",
"ff00aa",
"00ffff",
};
constexpr const char* kZButtonPresets[] = {
"ff0000",
"ff8200",
"f7df00",
"70ff00",
"00bd11",
"800088",
"000000",
"00ffff",
};
constexpr const char* kChargeRingPresets[] = {
"ff9f9f",
"ff0000",
"ffff00",
"00ff00",
"0000ff",
"ff00ff",
"331900",
"feffff",
"000000",
};
ModResult register_str_option(
const char* name, const char* defaultValue, ConfigVarHandle& outHandle, ModError* error) {
ConfigVarDesc cvarDesc = CONFIG_VAR_DESC_INIT;
cvarDesc.name = name;
cvarDesc.type = CONFIG_VAR_STRING;
cvarDesc.default_string = defaultValue;
if (svc_config->register_var(mod_ctx, &cvarDesc, &outHandle) != MOD_OK) {
return mods::set_error(error, MOD_ERROR, "failed to register cosmetics option");
}
return MOD_OK;
}
ModResult register_int_option(
const char* name, int64_t defaultValue, ConfigVarHandle& outHandle, ModError* error) {
ConfigVarDesc cvarDesc = CONFIG_VAR_DESC_INIT;
cvarDesc.name = name;
cvarDesc.type = CONFIG_VAR_INT;
cvarDesc.default_int = defaultValue;
if (svc_config->register_var(mod_ctx, &cvarDesc, &outHandle) != MOD_OK) {
return mods::set_error(error, MOD_ERROR, "failed to register cosmetics option");
}
return MOD_OK;
}
void add_control(UiElementHandle pane, const UiControlDesc& desc) {
auto result = svc_ui->pane_add_control(mod_ctx, pane, &desc, nullptr);
if (result != MOD_OK) {
mods::log::debug("pane_add_control failed {}", static_cast<int>(result));
}
}
template <size_t N>
void add_cosmetic_option(
UiElementHandle pane, ConfigVarHandle cvar, const char* name, const char* const (&presets)[N]) {
UiControlDesc control = UI_CONTROL_DESC_INIT;
control.kind = UI_CONTROL_COLOR;
control.label = name;
control.binding = UI_BINDING_CONFIG_VAR;
control.config_var = cvar;
control.color_presets = presets;
control.color_preset_count = N;
add_control(pane, control);
}
void add_midna_hair_option(UiElementHandle left, ConfigVarHandle cvar, const std::string& name) {
static const char* kMidnaHairOptions[] = {
"Default", "Pink", "Red", "Yellow", "Green", "Blue", "Purple", "Brown", "White", "Black"};
UiControlDesc control = UI_CONTROL_DESC_INIT;
control.kind = UI_CONTROL_SELECT;
control.label = name.c_str();
control.help_rml = "Choose Midna's hair color.";
control.binding = UI_BINDING_CONFIG_VAR;
control.config_var = cvar;
control.options = kMidnaHairOptions;
control.option_count = 10;
add_control(left, control);
}
void add_group(
UiElementHandle groups, UiElementHandle colors, const char* label, UiGroupBuildFn build) {
UiGroupDesc group = UI_GROUP_DESC_INIT;
group.label = label;
group.build = build;
const auto result = svc_ui->pane_add_group(mod_ctx, groups, colors, &group, nullptr);
if (result != MOD_OK) {
mods::log::debug("pane_add_group failed {}", static_cast<int>(result));
}
}
ModResult build_hero_tunic_colors(ModContext*, UiElementHandle pane, void*, ModError*) {
svc_ui->pane_add_section(mod_ctx, pane, "Hero's Tunic");
add_cosmetic_option(pane, g_cvars.herosTunicCapColor, "Cap", kOverlayPresets);
add_cosmetic_option(pane, g_cvars.herosTunicTorsoColor, "Body", kOverlayPresets);
add_cosmetic_option(pane, g_cvars.herosTunicSkirtColor, "Skirt", kOverlayPresets);
return MOD_OK;
}
ModResult build_zora_armor_colors(ModContext*, UiElementHandle pane, void*, ModError*) {
svc_ui->pane_add_section(mod_ctx, pane, "Zora Armor");
add_cosmetic_option(pane, g_cvars.zoraArmorCapColor, "Cap", kOverlayPresets);
add_cosmetic_option(pane, g_cvars.zoraArmorHelmetColor, "Helmet", kOverlayPresets);
add_cosmetic_option(pane, g_cvars.zoraArmorTorsoColor, "Torso", kOverlayPresets);
add_cosmetic_option(pane, g_cvars.zoraArmorScalesColor, "Scales", kOverlayPresets);
add_cosmetic_option(pane, g_cvars.zoraArmorFlippersColor, "Flippers", kOverlayPresets);
return MOD_OK;
}
ModResult build_sword_colors(ModContext*, UiElementHandle pane, void*, ModError*) {
svc_ui->pane_add_section(mod_ctx, pane, "Swords");
add_cosmetic_option(pane, g_cvars.woodenSwordColor, "Wooden Sword", kOverlayPresets);
add_cosmetic_option(pane, g_cvars.ordonSwordBladeColor, "Ordon Blade", kLightPresets);
add_cosmetic_option(pane, g_cvars.ordonSwordHandleColor, "Ordon Handle", kLightPresets);
add_cosmetic_option(pane, g_cvars.msBladeColor, "Master Sword Blade", kLightPresets);
add_cosmetic_option(pane, g_cvars.msHandleColor, "Master Sword Handle", kLightPresets);
add_cosmetic_option(
pane, g_cvars.lightSwordGlowColor, "Light Sword Glow", kRainbowLightPresets);
return MOD_OK;
}
ModResult build_equipment_colors(ModContext*, UiElementHandle pane, void*, ModError*) {
svc_ui->pane_add_section(mod_ctx, pane, "Equipment");
add_cosmetic_option(pane, g_cvars.lanternGlowColor, "Lantern Glow", kRainbowLightPresets);
add_cosmetic_option(pane, g_cvars.boomerangColor, "Gale Boomerang", kOverlayPresets);
add_cosmetic_option(pane, g_cvars.ironBootsColor, "Iron Boots", kOverlayPresets);
add_cosmetic_option(pane, g_cvars.spinnerColor, "Spinner", kOverlayPresets);
return MOD_OK;
}
ModResult build_button_colors(ModContext*, UiElementHandle pane, void*, ModError*) {
svc_ui->pane_add_section(mod_ctx, pane, "Buttons");
add_cosmetic_option(pane, g_cvars.aButtonColor, "A Button", kAButtonPresets);
add_cosmetic_option(pane, g_cvars.bButtonColor, "B Button", kBButtonPresets);
add_cosmetic_option(pane, g_cvars.xButtonColor, "X Button", kXyButtonPresets);
add_cosmetic_option(pane, g_cvars.yButtonColor, "Y Button", kXyButtonPresets);
add_cosmetic_option(pane, g_cvars.zButtonColor, "Z Button", kZButtonPresets);
return MOD_OK;
}
ModResult build_hud_colors(ModContext*, UiElementHandle pane, void*, ModError*) {
svc_ui->pane_add_section(mod_ctx, pane, "HUD");
add_cosmetic_option(pane, g_cvars.heartColor, "Hearts", kBButtonPresets);
return MOD_OK;
}
ModResult build_link_colors(ModContext*, UiElementHandle pane, void*, ModError*) {
svc_ui->pane_add_section(mod_ctx, pane, "Link");
add_cosmetic_option(pane, g_cvars.linkHairColor, "Hair", kOverlayPresets);
return MOD_OK;
}
ModResult build_midna_colors(ModContext*, UiElementHandle pane, void*, ModError*) {
svc_ui->pane_add_section(mod_ctx, pane, "Midna");
add_cosmetic_option(pane, g_cvars.midnaChargeRingColor, "Charge Ring", kChargeRingPresets);
return MOD_OK;
}
ModResult build_companion_colors(ModContext*, UiElementHandle pane, void*, ModError*) {
svc_ui->pane_add_section(mod_ctx, pane, "Companions");
add_cosmetic_option(pane, g_cvars.wolfLinkColor, "Wolf Link", kOverlayPresets);
add_cosmetic_option(pane, g_cvars.eponaColor, "Epona", kOverlayPresets);
return MOD_OK;
}
ModResult build_equipment_colors_tab(
ModContext*, UiWindowHandle, UiElementHandle left, UiElementHandle right, void*, ModError*) {
svc_ui->pane_add_section(mod_ctx, left, "Color Groups");
add_group(left, right, "Hero's Tunic", build_hero_tunic_colors);
add_group(left, right, "Zora Armor", build_zora_armor_colors);
add_group(left, right, "Swords", build_sword_colors);
add_group(left, right, "Equipment", build_equipment_colors);
return MOD_OK;
}
ModResult build_ui_colors_tab(
ModContext*, UiWindowHandle, UiElementHandle left, UiElementHandle right, void*, ModError*) {
svc_ui->pane_add_section(mod_ctx, left, "Color Groups");
add_group(left, right, "Buttons", build_button_colors);
add_group(left, right, "HUD", build_hud_colors);
return MOD_OK;
}
ModResult build_misc_colors_tab(
ModContext*, UiWindowHandle, UiElementHandle left, UiElementHandle right, void*, ModError*) {
svc_ui->pane_add_section(mod_ctx, left, "Hair Presets");
add_midna_hair_option(left, g_cvars.midnaHairBaseColor, "Midna's Hair Base Color");
add_midna_hair_option(left, g_cvars.midnaHairTipsColor, "Midna's Hair Tips Color");
svc_ui->pane_add_section(mod_ctx, left, "Color Groups");
add_group(left, right, "Link", build_link_colors);
add_group(left, right, "Midna", build_midna_colors);
add_group(left, right, "Companions", build_companion_colors);
return MOD_OK;
}
void on_cosmetics_menu_window_closed(ModContext*, UiWindowHandle, void*) {
g_cosmeticsWindow = 0;
}
void on_open_cosmetics_menu(ModContext*, void*) {
if (g_cosmeticsWindow != 0) {
return;
}
UiTabDesc tabs[] = {UI_TAB_DESC_INIT, UI_TAB_DESC_INIT, UI_TAB_DESC_INIT};
tabs[0].title = "Equipment";
tabs[0].build = build_equipment_colors_tab;
tabs[1].title = "Interface";
tabs[1].build = build_ui_colors_tab;
tabs[2].title = "Characters";
tabs[2].build = build_misc_colors_tab;
UiWindowDesc desc = UI_WINDOW_DESC_INIT;
desc.tabs = tabs;
desc.tab_count = ARRAY_SIZE(tabs);
desc.on_closed = on_cosmetics_menu_window_closed;
if (svc_ui->window_push(mod_ctx, &desc, &g_cosmeticsWindow) != MOD_OK) {
svc_log->error(mod_ctx, "failed to open basic cosmetics window");
}
}
ModResult build_panel(ModContext*, UiElementHandle panel, void*, ModError*) {
UiControlDesc control = UI_CONTROL_DESC_INIT;
control.kind = UI_CONTROL_GROUP;
control.label = "Open Cosmetics Menu";
control.on_pressed = on_open_cosmetics_menu;
add_control(panel, control);
return MOD_OK;
}
} // namespace
void load_base_texture_data() {
// Go through each texture we can recolor and attempt to load and store
// the texture data for future recoloring
for (auto& replacements : get_texture_replacements() | std::views::values) {
for (auto& replacement : replacements) {
// If we've already loaded the texture data, don't load it again
if (replacement.loadedTextureData) {
continue;
}
// Avoid noisy resource lookups until the archive has finished loading.
auto* resInfo = dComIfG_getObjectResInfo(replacement.arc);
if (resInfo == nullptr || resInfo->getArchive() == nullptr) {
continue;
}
// Try to get the model this texture is part of. If we can't get it, try again later
auto model = static_cast<J3DModelData*>(
dComIfG_getObjectRes(replacement.arc, replacement.modelFileName));
if (model == nullptr) {
continue;
}
J3DTexture* tex = model->getTexture();
JUTNameTab* nametable = model->getTextureName();
if (tex != nullptr && nametable != nullptr) {
for (u16 i = 0; i < tex->getNum(); i++) {
const char* texName = nametable->getName(i);
if (texName != nullptr && std::strcmp(texName, replacement.textureName) == 0) {
// Once we've found the texture, set all our TextureKey and TextureData
// fields that we can set right now.
auto imageHeader = tex->getResTIMG(i);
auto& key = replacement.key;
auto& data = replacement.data;
key.kind = TEXTURE_KEY_SOURCE;
key.has_tlut = imageHeader->numColors > 0;
key.width = imageHeader->width;
key.height = imageHeader->height;
key.gx_format = imageHeader->format;
// Currently, no replaced textures have a tlut
key.tlut_hash = replacement.tlutHash;
// Calculate the size of the image data
const uint32_t mipCount =
imageHeader->mipmapEnabled ?
std::max<uint32_t>(imageHeader->mipmapCount, 1) :
1;
auto size = get_image_data_size(
imageHeader->format, imageHeader->width, imageHeader->height, mipCount);
replacement.baseTextureData.resize(size);
std::memcpy(
replacement.baseTextureData.data(), tex->getImgDataPtr(i), size);
// Source texture keys hash only the base mip level.
const auto baseMipSize = get_image_data_size(
imageHeader->format, imageHeader->width, imageHeader->height, 1);
auto textureHash =
XXH64(replacement.baseTextureData.data(), baseMipSize, 0);
replacement.key.texture_hash = textureHash;
mods::log::debug("Loaded base texture data for {}. size: {:X} hash: {:X}",
replacement.textureName, size, textureHash);
replacement.loadedTextureData = true;
data.width = imageHeader->width;
data.height = imageHeader->height;
data.mip_count = mipCount;
data.size = size;
data.gx_format = imageHeader->format;
break;
}
}
}
}
}
// If we've loaded all base textures for recoloring, then we don't need to call this function
// again
g_loadedAllBaseTextures = std::ranges::all_of(
get_texture_replacements() | std::views::values, [](auto& replacementList) {
return std::ranges::all_of(
replacementList, [](const TextureReplacementData& replacement) {
return replacement.loadedTextureData;
});
});
}
ModResult check_and_set_recolored_textures() {
for (auto& [configVar, replacements] : get_texture_replacements()) {
// If the configvar hasn't been set, don't continue
if (configVar == 0) {
continue;
}
auto maybeColor = get_config_var_color(configVar);
if (!maybeColor.has_value()) {
// An empty or invalid option means the original texture should be restored.
for (auto& replacement : replacements) {
if (replacement.handle != 0) {
auto result = svc_texture->unregister(mod_ctx, replacement.handle);
if (result != MOD_OK) {
mods::log::debug("Could not unregister replacement for {}. Result: {}",
replacement.textureName, static_cast<int>(result));
}
replacement.handle = 0;
}
replacement.curColor.reset();
}
continue;
}
auto color = maybeColor.value();
for (auto& replacement : replacements) {
// If we haven't loaded the base texture yet, don't try to recolor it
if (!replacement.loadedTextureData) {
continue;
}
// If our color hasn't changed, don't try to recolor
auto& curColor = replacement.curColor;
if (curColor == std::nullopt || curColor.value().r != color.r ||
curColor.value().g != color.g || curColor.value().b != color.b)
{
// Make a copy of the base texture data to recolor
auto newTexture = replacement.baseTextureData;
recolor_texture(replacement, color, newTexture);
TextureData newTextureData = replacement.data;
newTextureData.data = newTexture.data();
newTextureData.size = newTexture.size();
// Keep the current replacement active if registering the new one fails.
TextureReplacementHandle newHandle{};
auto result = svc_texture->register_data(
mod_ctx, &replacement.key, &newTextureData, &newHandle);
if (result != MOD_OK) {
mods::log::debug("Could not register_data for {}. Result: {}",
replacement.textureName, static_cast<int>(result));
} else {
mods::log::debug("Registered replacement for {}.", replacement.textureName,
static_cast<int>(result));
auto oldHandle = replacement.handle;
replacement.handle = newHandle;
curColor = color;
if (oldHandle != 0) {
result = svc_texture->unregister(mod_ctx, oldHandle);
if (result != MOD_OK) {
mods::log::debug(
"Could not unregister previous replacement for {}. Result: {}",
replacement.textureName, static_cast<int>(result));
}
}
}
}
}
}
return MOD_OK;
}
void unregister_all_texture_handles() {
for (auto& replacements : get_texture_replacements() | std::views::values) {
for (auto& replacement : replacements) {
if (replacement.handle != 0) {
svc_texture->unregister(mod_ctx, replacement.handle);
replacement.handle = 0;
}
replacement.curColor.reset();
}
}
}
#define REGISTER_COSMETIC_OPTION(option) \
result = register_str_option(#option, NULL, g_cvars.option, error); \
if (result != MOD_OK) { \
return result; \
}
extern "C" {
MOD_EXPORT ModResult mod_initialize(ModError* error) {
svc_log->info(mod_ctx, "basic_cosmetics_mod initialized");
ModResult result{};
REGISTER_COSMETIC_OPTION(herosTunicCapColor)
REGISTER_COSMETIC_OPTION(herosTunicTorsoColor)
REGISTER_COSMETIC_OPTION(herosTunicSkirtColor)
REGISTER_COSMETIC_OPTION(zoraArmorCapColor)
REGISTER_COSMETIC_OPTION(zoraArmorHelmetColor)
REGISTER_COSMETIC_OPTION(zoraArmorTorsoColor)
REGISTER_COSMETIC_OPTION(zoraArmorScalesColor)
REGISTER_COSMETIC_OPTION(zoraArmorFlippersColor)
REGISTER_COSMETIC_OPTION(lanternGlowColor)
REGISTER_COSMETIC_OPTION(woodenSwordColor)
REGISTER_COSMETIC_OPTION(ordonSwordBladeColor)
REGISTER_COSMETIC_OPTION(ordonSwordHandleColor)
REGISTER_COSMETIC_OPTION(msBladeColor)
REGISTER_COSMETIC_OPTION(msHandleColor)
REGISTER_COSMETIC_OPTION(lightSwordGlowColor)
REGISTER_COSMETIC_OPTION(boomerangColor)
REGISTER_COSMETIC_OPTION(ironBootsColor)
REGISTER_COSMETIC_OPTION(spinnerColor)
REGISTER_COSMETIC_OPTION(aButtonColor)
REGISTER_COSMETIC_OPTION(bButtonColor)
REGISTER_COSMETIC_OPTION(xButtonColor)
REGISTER_COSMETIC_OPTION(yButtonColor)
REGISTER_COSMETIC_OPTION(zButtonColor)
REGISTER_COSMETIC_OPTION(heartColor)
result = register_int_option("midnaHairBaseColor", 0, g_cvars.midnaHairBaseColor, error);
if (result != MOD_OK) {
return result;
}
result = register_int_option("midnaHairTipsColor", 0, g_cvars.midnaHairTipsColor, error);
if (result != MOD_OK) {
return result;
}
REGISTER_COSMETIC_OPTION(midnaChargeRingColor)
REGISTER_COSMETIC_OPTION(linkHairColor)
REGISTER_COSMETIC_OPTION(wolfLinkColor)
REGISTER_COSMETIC_OPTION(eponaColor)
UiModsPanelDesc panelDesc = UI_MODS_PANEL_DESC_INIT;
panelDesc.build = build_panel;
svc_ui->register_mods_panel(mod_ctx, &panelDesc);
// Add all our hooks
result = add_all_hooks();
if (result != MOD_OK) {
return result;
}
g_loadedAllBaseTextures = false;
g_textureLoadRetryCountdown = 0;
return MOD_OK;
}
MOD_EXPORT ModResult mod_update(ModError*) {
update_rainbow_rgb(1.0f);
set_all_midna_hair_colors();
if (!g_loadedAllBaseTextures) {
if (g_textureLoadRetryCountdown == 0) {
load_base_texture_data();
g_textureLoadRetryCountdown = kTextureLoadRetryFrames;
} else {
--g_textureLoadRetryCountdown;
}
}
check_and_set_recolored_textures();
return MOD_OK;
}
MOD_EXPORT ModResult mod_shutdown(ModError*) {
svc_log->info(mod_ctx, "basic_cosmetics_mod unloaded");
g_cosmeticsWindow = 0;
remove_all_hooks();
unregister_all_texture_handles();
get_texture_replacements().clear();
g_loadedAllBaseTextures = false;
g_textureLoadRetryCountdown = 0;
return MOD_OK;
}
}
-65
View File
@@ -1,65 +0,0 @@
#pragma once
#include "mods/svc/config.h"
#include "mods/svc/texture.h"
#include <gx.h>
#include <optional>
#include <string>
#include <vector>
struct cvars {
ConfigVarHandle herosTunicCapColor = 0;
ConfigVarHandle herosTunicTorsoColor = 0;
ConfigVarHandle herosTunicSkirtColor = 0;
ConfigVarHandle zoraArmorCapColor = 0;
ConfigVarHandle zoraArmorHelmetColor = 0;
ConfigVarHandle zoraArmorTorsoColor = 0;
ConfigVarHandle zoraArmorScalesColor = 0;
ConfigVarHandle zoraArmorFlippersColor = 0;
ConfigVarHandle lanternGlowColor = 0;
ConfigVarHandle woodenSwordColor = 0;
ConfigVarHandle ordonSwordBladeColor = 0;
ConfigVarHandle ordonSwordHandleColor = 0;
ConfigVarHandle msBladeColor = 0;
ConfigVarHandle msHandleColor = 0;
ConfigVarHandle lightSwordGlowColor = 0;
ConfigVarHandle boomerangColor = 0;
ConfigVarHandle ironBootsColor = 0;
ConfigVarHandle spinnerColor = 0;
ConfigVarHandle heartColor = 0;
ConfigVarHandle aButtonColor = 0;
ConfigVarHandle bButtonColor = 0;
ConfigVarHandle xButtonColor = 0;
ConfigVarHandle yButtonColor = 0;
ConfigVarHandle zButtonColor = 0;
ConfigVarHandle midnaHairBaseColor = 0;
ConfigVarHandle midnaHairTipsColor = 0;
ConfigVarHandle midnaChargeRingColor = 0;
ConfigVarHandle linkHairColor = 0;
ConfigVarHandle wolfLinkColor = 0;
ConfigVarHandle eponaColor = 0;
};
struct TextureReplacementData {
const char* arc{};
const char* modelFileName{};
const char* textureName{};
uint64_t textureHash{};
uint64_t tlutHash{};
TextureKey key = TEXTURE_KEY_INIT;
TextureData data = TEXTURE_DATA_INIT;
TextureReplacementHandle handle{};
std::vector<u8> baseTextureData{};
bool loadedTextureData = false;
std::optional<GXColor> curColor{};
};
cvars& get_cvars();
std::string get_str_option(ConfigVarHandle handle, const std::string& fallback);
int64_t get_int_option(ConfigVarHandle handle, int64_t fallback);
std::optional<GXColor> get_config_var_color(ConfigVarHandle handle, bool allowRainbow = false);
@@ -1,682 +0,0 @@
#include "texture_utils.hpp"
#include "color_utils.hpp"
#include "mod.hpp"
#include "mods/svc/config.h"
#include "mods/svc/log.hpp"
#include "JSystem/J3DGraphLoader/J3DModelLoader.h"
#include "JSystem/JSupport/JSupport.h"
#include "JSystem/JUtility/JUTNameTab.h"
#include "d/actor/d_a_alink.h"
static void get_gx_tile_info(uint8_t format, uint32_t& tileWidth, uint32_t& tileHeight, uint32_t& tileSize) {
switch (format) {
case GX_TF_I8:
case GX_TF_IA4:
case GX_TF_C8:
tileWidth = 8; tileHeight = 4; tileSize = 32;
break;
case GX_TF_IA8:
case GX_TF_RGB565:
case GX_TF_RGB5A3:
case GX_TF_C14X2:
tileWidth = 4; tileHeight = 4; tileSize = 32;
break;
case GX_TF_RGBA8:
tileWidth = 4; tileHeight = 4; tileSize = 64;
break;
case GX_TF_I4:
case GX_TF_C4:
case GX_TF_CMPR:
default:
tileWidth = 8; tileHeight = 8; tileSize = 32;
break;
}
}
uint32_t get_image_data_size(uint32_t format, uint32_t width, uint32_t height, uint32_t mipmapCount) {
uint32_t tileWidth, tileHeight, tileSize;
get_gx_tile_info(format, tileWidth, tileHeight, tileSize);
uint32_t totalSize = 0;
for (uint8_t i = 0; i < mipmapCount; ++i) {
// Round dimensions up to nearest tile boundary
uint32_t paddedWidth = (width + tileWidth - 1) & ~(tileWidth - 1);
uint32_t paddedHeight = (height + tileHeight - 1) & ~(tileHeight - 1);
uint32_t tilesX = paddedWidth / tileWidth;
uint32_t tilesY = paddedHeight / tileHeight;
totalSize += tilesX * tilesY * tileSize;
// Downscale dimensions for next mipmap level
width = std::max(1u, width >> 1);
height = std::max(1u, height >> 1);
}
return totalSize;
}
// When left is greater than right
// 0b00 points to the left color
// 0b01 points to the right color
// 0b10 is closer to left color
// 0b11 is closer to right color
// When left is not greater than right
// 0b00 points to the left color
// 0b01 points to the right color
// 0b10 is midway between the colors
// 0b11 is transparent
// That means when maintaining the relative order, if we have to swap the colors:
// in the case of left being greater than right:
// 0b00 will swap to 0b01
// 0b01 will swap to 0b00
// 0b10 will swap to 0b11
// 0b11 will swap to 0b10
// So the left bit stays the same, and the right bit changes
// Can do xor (^) like 0b01010101 or 0x55 for each u16
// in the case of left not being greater than right:
// 0b00 will swap to 0b01
// 0b01 will swap to 0b00
// 0b10 will stay the same
// 0b11 will stay the same
// so if the left bit is a 0, the right bit will change
uint32_t swap_index_bits(bool leftIsGreater, uint32_t bits) {
if (leftIsGreater) {
return bits ^ 0x55555555;
}
const uint32_t mask = ((bits >> 1) & 0x55555555) ^ 0x55555555;
return bits ^ mask;
}
void recolor_cmpr_texture(const TextureReplacementData& replacementData, const GXColor color, std::vector<u8>& newTextureDataOut)
{
uint16_t recolors[0x100];
for (int32_t i = 0; i < 0x100; i++) {
recolors[i] = blend_overlay_rgb_565(i, color);
}
const uint8_t mipCount = (replacementData.data.mip_count > 0) ? replacementData.data.mip_count : 1;
uint32_t mipWidth = replacementData.key.width;
uint32_t mipHeight = replacementData.key.height;
uint8_t* currentAddr = newTextureDataOut.data();
for (uint8_t mip = 0; mip < mipCount; ++mip) {
// Round dimensions up to the nearest 8x8 tile boundary
const uint32_t roundedWidth = (mipWidth + 7) & ~7;
const uint32_t roundedHeight = (mipHeight + 7) & ~7;
const uint32_t numBlocks = (roundedWidth / 8) * (roundedHeight / 8);
const uint32_t iterations = numBlocks * 4; // 4 CMPR sub-blocks per 8x8 tile
for (uint32_t i = 0; i < iterations; i++) {
auto* rgb565Ptr = reinterpret_cast<BE<uint16_t>*>(currentAddr);
auto leftRgb565 = rgb565Ptr[0];
auto rightRgb565 = rgb565Ptr[1];
const bool leftIsGreater = leftRgb565 > rightRgb565;
const uint32_t leftGrayVal = desaturate_rgb_565(leftRgb565);
const uint32_t rightGrayVal = desaturate_rgb_565(rightRgb565);
uint16_t leftNewRgb565 = recolors[leftGrayVal];
uint16_t rightNewRgb565 = recolors[rightGrayVal];
bool needsBitSwap = false;
if (leftIsGreater) {
if (leftNewRgb565 == rightNewRgb565) {
// Need to make sure that subtracting 1 does not mess
// everything up. For example, 0x1000 - 1 => 0x0fff which is
// a completely different color.
if ((leftNewRgb565 & 0x1f) == 0) {
// If left value has 0 blue, we change its blue to 1.
leftNewRgb565 += 1;
}
rightNewRgb565 = leftNewRgb565 - 1;
}
else if (leftNewRgb565 < rightNewRgb565) {
needsBitSwap = true;
}
}
else if (leftNewRgb565 > rightNewRgb565) {
needsBitSwap = true;
}
if (needsBitSwap) {
// The left and right colors are swapping so that their values
// are relative in the same way. We need to update the bits
// referencing the palette entries to handle the swap.
const uint16_t temp = leftNewRgb565;
leftNewRgb565 = rightNewRgb565;
rightNewRgb565 = temp;
auto wordPtr = reinterpret_cast<BE<uint32_t>*>(currentAddr);
const uint32_t bits = wordPtr[1];
const uint32_t newBits = swap_index_bits(leftIsGreater, bits);
wordPtr[1] = newBits;
}
rgb565Ptr[0] = leftNewRgb565;
rgb565Ptr[1] = rightNewRgb565;
currentAddr += 8;
}
// Halve dimensions for the next mipmap level
mipWidth = std::max(1u, mipWidth >> 1);
mipHeight = std::max(1u, mipHeight >> 1);
}
}
void recolor_rgb5a3_texture(const TextureReplacementData& replacementData, const GXColor color, std::vector<u8>& newTextureDataOut)
{
// Precompute lookup tables for both RGB555 (opaque) and RGB444 (translucent) modes
uint16_t recolors_rgb555[0x100];
uint16_t recolors_rgb444[0x100];
for (int32_t i = 0; i < 0x100; i++) {
const uint8_t r = blend_overlay_channel(i, color.r);
const uint8_t g = blend_overlay_channel(i, color.g);
const uint8_t b = blend_overlay_channel(i, color.b);
// Pack as RGB555: Bit 15 set to 1 + 5 bits R, G, B
recolors_rgb555[i] = 0x8000 | ((r >> 3) << 10) | ((g >> 3) << 5) | (b >> 3);
// Pack as RGB444: 4 bits R, G, B (Bit 15 remains 0)
recolors_rgb444[i] = ((r >> 4) << 8) | ((g >> 4) << 4) | (b >> 4);
}
const uint8_t mipCount =
replacementData.data.mip_count > 0 ? replacementData.data.mip_count : 1;
uint32_t mipWidth = replacementData.key.width;
uint32_t mipHeight = replacementData.key.height;
auto* pixelPtr = reinterpret_cast<BE<uint16_t>*>(newTextureDataOut.data());
for (uint8_t mip = 0; mip < mipCount; ++mip) {
const uint32_t roundedWidth = (mipWidth + 3) & ~3;
const uint32_t roundedHeight = (mipHeight + 3) & ~3;
const uint32_t totalPixels = roundedWidth * roundedHeight;
for (uint32_t i = 0; i < totalPixels; i++) {
const uint16_t rawPixel = pixelPtr[i];
// MSB determines if pixel is opaque or translucent
if (rawPixel & 0x8000) {
// Pixel is opaque
const uint8_t r5 = (rawPixel >> 10) & 0x1F;
const uint8_t g5 = (rawPixel >> 5) & 0x1F;
const uint8_t b5 = rawPixel & 0x1F;
// Expand 5-bit to 8-bit
const uint8_t r8 = (r5 << 3) | (r5 >> 2);
const uint8_t g8 = (g5 << 3) | (g5 >> 2);
const uint8_t b8 = (b5 << 3) | (b5 >> 2);
const uint8_t grayVal = static_cast<uint8_t>((r8 * 77 + g8 * 150 + b8 * 29) >> 8);
pixelPtr[i] = recolors_rgb555[grayVal];
} else {
// Pixel is translucent
const uint16_t alpha3 = rawPixel & 0x7000;
const uint8_t r4 = (rawPixel >> 8) & 0x0F;
const uint8_t g4 = (rawPixel >> 4) & 0x0F;
const uint8_t b4 = rawPixel & 0x0F;
// Expand 4-bit to 8-bit
const uint8_t r8 = (r4 << 4) | r4;
const uint8_t g8 = (g4 << 4) | g4;
const uint8_t b8 = (b4 << 4) | b4;
const uint8_t grayVal = static_cast<uint8_t>((r8 * 77 + g8 * 150 + b8 * 29) >> 8);
// Combine original alpha with recolored RGB444
pixelPtr[i] = alpha3 | recolors_rgb444[grayVal];
}
}
pixelPtr += totalPixels;
mipWidth = std::max(1u, mipWidth >> 1);
mipHeight = std::max(1u, mipHeight >> 1);
}
}
// Function to encode a single 4x4 sub-block (16 pixels) into an 8-byte CMPR block
static void encode_cmpr_sub_block(uint8_t* dst, const uint8_t pixels[16]) {
uint8_t min_val = 255;
uint8_t max_val = 0;
for (int i = 0; i < 16; ++i) {
if (pixels[i] < min_val) min_val = pixels[i];
if (pixels[i] > max_val) max_val = pixels[i];
}
auto intensity_to_rgb565 = [](uint8_t val) -> uint16_t {
uint16_t r5 = val >> 3;
uint16_t g6 = val >> 2;
uint16_t b5 = val >> 3;
return static_cast<uint16_t>((r5 << 11) | (g6 << 5) | b5);
};
uint16_t c0_565 = intensity_to_rgb565(max_val);
uint16_t c1_565 = intensity_to_rgb565(min_val);
uint32_t indices = 0;
if (max_val > min_val) {
// Enforce c0_565 > c1_565 in unsigned 16-bit representation to use 4-color mode
if (c0_565 == c1_565) {
if ((c0_565 & 0x001F) < 0x001F) {
c0_565 += 1;
} else {
c1_565 -= 1;
}
}
// Interpolated 8-bit intensity values for quantization
const int c0 = max_val;
const int c1 = min_val;
const int c2 = (2 * max_val + min_val) / 3;
const int c3 = (max_val + 2 * min_val) / 3;
// Map each pixel to the nearest palette entry
for (int i = 0; i < 16; ++i) {
const int p = pixels[i];
const int d0 = std::abs(p - c0);
const int d1 = std::abs(p - c1);
const int d2 = std::abs(p - c2);
const int d3 = std::abs(p - c3);
uint32_t best_idx = 0;
int min_d = d0;
if (d1 < min_d) { min_d = d1; best_idx = 1; }
if (d2 < min_d) { min_d = d2; best_idx = 2; }
if (d3 < min_d) { min_d = d3; best_idx = 3; }
indices |= (best_idx << (30 - (2 * i)));
}
}
// Account for big endian data expectation
dst[0] = static_cast<uint8_t>(c0_565 >> 8);
dst[1] = static_cast<uint8_t>(c0_565 & 0xFF);
dst[2] = static_cast<uint8_t>(c1_565 >> 8);
dst[3] = static_cast<uint8_t>(c1_565 & 0xFF);
dst[4] = static_cast<uint8_t>(indices >> 24);
dst[5] = static_cast<uint8_t>((indices >> 16) & 0xFF);
dst[6] = static_cast<uint8_t>((indices >> 8) & 0xFF);
dst[7] = static_cast<uint8_t>(indices & 0xFF);
}
bool convert_i8_to_cmpr(TextureReplacementData& replacementData, std::vector<u8>& cmprOut) {
if (cmprOut.empty()) {
return false;
}
const uint8_t mipCount = (replacementData.data.mip_count > 0) ? replacementData.data.mip_count : 1;
const auto expectedInputSize = get_image_data_size(
GX_TF_I8, replacementData.key.width, replacementData.key.height, mipCount);
if (cmprOut.size() < expectedInputSize) {
return false;
}
const auto sourceData = cmprOut;
std::vector<u8> convertedData(get_image_data_size(
GX_TF_CMPR, replacementData.key.width, replacementData.key.height, mipCount));
uint32_t mipWidth = replacementData.key.width;
uint32_t mipHeight = replacementData.key.height;
const uint8_t* readPtr = sourceData.data();
uint8_t* writePtr = convertedData.data();
for (uint8_t mip = 0; mip < mipCount; ++mip) {
const uint32_t paddedWidthI8 = (mipWidth + 7) & ~7;
const uint32_t paddedHeightI8 = (mipHeight + 3) & ~3;
const uint32_t tilesX_I8 = paddedWidthI8 / 8;
const uint32_t paddedWidthCMPR = (mipWidth + 7) & ~7;
const uint32_t paddedHeightCMPR = (mipHeight + 7) & ~7;
const uint32_t blocksX_CMPR = paddedWidthCMPR / 8;
const uint32_t blocksY_CMPR = paddedHeightCMPR / 8;
for (uint32_t by = 0; by < blocksY_CMPR; ++by) {
for (uint32_t bx = 0; bx < blocksX_CMPR; ++bx) {
uint8_t subBlockPixels[4][16]{};
for (uint32_t subBlockY = 0; subBlockY < 2; ++subBlockY) {
for (uint32_t subBlockX = 0; subBlockX < 2; ++subBlockX) {
const uint32_t subBlock = subBlockY * 2 + subBlockX;
for (uint32_t row = 0; row < 4; ++row) {
for (uint32_t col = 0; col < 4; ++col) {
const uint32_t x = bx * 8 + subBlockX * 4 + col;
const uint32_t y = by * 8 + subBlockY * 4 + row;
if (x >= mipWidth || y >= mipHeight) {
continue;
}
const uint32_t tile = (y / 4) * tilesX_I8 + (x / 8);
const uint32_t tileOffset = (y % 4) * 8 + (x % 8);
subBlockPixels[subBlock][row * 4 + col] =
readPtr[tile * 32 + tileOffset];
}
}
}
}
for (const auto& subBlockPixel : subBlockPixels) {
encode_cmpr_sub_block(writePtr, subBlockPixel);
writePtr += 8;
}
}
}
const uint32_t tilesY_I8 = paddedHeightI8 / 4;
readPtr += tilesX_I8 * tilesY_I8 * 32;
mipWidth = std::max(1u, mipWidth >> 1);
mipHeight = std::max(1u, mipHeight >> 1);
}
cmprOut.swap(convertedData);
// Update format
replacementData.data.gx_format = GX_TF_CMPR;
return true;
}
void recolor_texture(TextureReplacementData& replacementData, GXColor color, std::vector<u8>& newTextureDataOut) {
switch (replacementData.key.gx_format) {
case GX_TF_CMPR:
recolor_cmpr_texture(replacementData, color, newTextureDataOut);
break;
case GX_TF_RGB5A3:
recolor_rgb5a3_texture(replacementData, color, newTextureDataOut);
break;
case GX_TF_I8:
if (convert_i8_to_cmpr(replacementData, newTextureDataOut)) {
recolor_cmpr_texture(replacementData, color, newTextureDataOut);
} else {
mods::log::debug("Could not convert {} from i8 to cmpr", replacementData.textureName);
}
break;
default:
break;
}
}
std::unordered_map<ConfigVarHandle, std::list<TextureReplacementData>>& get_texture_replacements() {
static std::unordered_map<ConfigVarHandle, std::list<TextureReplacementData>> replacements{};
if (replacements.empty()) {
replacements = {
{get_cvars().herosTunicCapColor, {
{
.arc = "Kmdl",
.modelFileName = "al_head.bmd",
.textureName = "al_cap",
}
}},
{get_cvars().herosTunicTorsoColor, {
{
.arc = "Kmdl",
.modelFileName = "al.bmd",
.textureName = "al_upbody",
}
}},
{get_cvars().herosTunicSkirtColor, {
{
.arc = "Kmdl",
.modelFileName = "al.bmd",
.textureName = "al_lowbody",
}
}},
{get_cvars().zoraArmorCapColor, {
{
.arc = "Zmdl",
.modelFileName = "zl_head.bmd",
.textureName = "zl_cap",
}
}},
{get_cvars().zoraArmorHelmetColor, {
{
.arc = "Zmdl",
.modelFileName = "zl_head.bmd",
.textureName = "zl_helmet",
}
}},
{get_cvars().zoraArmorTorsoColor, {
{
.arc = "Zmdl",
.modelFileName = "zl.bmd",
.textureName = "zl_armor",
},
{
.arc = "Zmdl",
.modelFileName = "zl.bmd",
.textureName = "zl_armL",
}
}},
{get_cvars().zoraArmorScalesColor, {
{
.arc = "Zmdl",
.modelFileName = "zl.bmd",
.textureName = "zl_body",
}
}},
{get_cvars().zoraArmorFlippersColor, {
{
.arc = "Zmdl",
.modelFileName = "zl.bmd",
.textureName = "zl_boots",
}
}},
{get_cvars().woodenSwordColor, {
{
.arc = "Bmdl", // Ordon Clothes Model
.modelFileName = "al_swb.bmd",
.textureName = "al_SWB",
},
{
.arc = "Kmdl", // Hero's Tunic Model
.modelFileName = "al_swb.bmd",
.textureName = "al_SWB",
},
{
.arc = "Zmdl", // Zora Armor Model
.modelFileName = "al_swb.bmd",
.textureName = "al_SWB",
},
{
.arc = "Mmdl", // Magic Armor Model
.modelFileName = "al_swb.bmd",
.textureName = "al_SWB",
},
{
.arc = "O_gD_SWB", // Get Item Model
.modelFileName = "o_gd_al_swb.bmd",
.textureName = "al_SWB",
}
}},
{get_cvars().ordonSwordHandleColor, {
{
.arc = "Alink",
.modelFileName = "al_swa.bmd",
.textureName = "al_SWgripA",
},
{
.arc = "O_gD_SWA", // Get Item Model
.modelFileName = "o_gd_al_swa.bmd",
.textureName = "al_SWgripA",
}
}},
{get_cvars().ordonSwordBladeColor, {
{
.arc = "Alink",
.modelFileName = "al_swa.bmd",
.textureName = "al_SWA",
}
}},
{get_cvars().msHandleColor, {
{
.arc = "Alink",
.modelFileName = "al_swm.bmd",
.textureName = "al_SWgripM",
}
}},
{get_cvars().msBladeColor, {
{
.arc = "Alink",
.modelFileName = "al_swm.bmd",
.textureName = "al_SWM",
}
}},
{get_cvars().boomerangColor, {
{
.arc = "Alink", // Boomerang in Link's hand
.modelFileName = "al_boom.bmd",
.textureName = "L_al_boom00",
},
{
.arc = "E_mk", // Boomerang in Ook's hand
.modelFileName = "bm.bmd",
.textureName = "L_al_boom00",
},
{
.arc = "E_mk", // Boomerang in Ook's hand
.modelFileName = "bm.bmd",
.textureName = "bm_boom",
},
{
.arc = "O_gD_boom", // Get Item Model
.modelFileName = "o_gd_boom.bmd",
.textureName = "L_al_boom00",
}
}},
{get_cvars().ironBootsColor, {
{
.arc = "Bmdl", // Ordon Clothes Model
.modelFileName = "al_bootsh.bmd",
.textureName = "al_bootsH",
},
{
.arc = "Kmdl", // Hero's Tunic Model
.modelFileName = "al_bootsh.bmd",
.textureName = "al_bootsH",
},
{
.arc = "Zmdl", // Zora Armor Model
.modelFileName = "al_bootsh.bmd",
.textureName = "al_bootsH",
},
{
.arc = "Mmdl", // Magic Armor Model
.modelFileName = "al_bootsh.bmd",
.textureName = "al_bootsH",
},
{
.arc = "O_gD_boot", // Get Item Model
.modelFileName = "o_gd_al_bootsh.bmd",
.textureName = "al_bootsH",
}
}},
{get_cvars().spinnerColor, {
{
.arc = "Alink", // Spinner used by Link
.modelFileName = "al_sp.bmd",
.textureName = "al_SP",
},
{
.arc = "O_gD_SP", // Get Item Model
.modelFileName = "o_gd_al_sp.bmd",
.textureName = "al_SP",
}
}},
{get_cvars().linkHairColor, {
{
.arc = "Bmdl", // Ordon Clothes Model
.modelFileName = "bl_head.bmd",
.textureName = "bl_hair",
},
{
.arc = "Kmdl", // Hero's Tunic Model
.modelFileName = "al_head.bmd",
.textureName = "al_hair",
},
{
.arc = "Mmdl", // Magic Armor Model
.modelFileName = "ml_head.bmd",
.textureName = "al_hair",
}
}},
{get_cvars().wolfLinkColor, {
{
.arc = "Wmdl",
.modelFileName = "wl.bmd",
.textureName = "wl_body",
},
{
.arc = "Wmdl",
.modelFileName = "wl.bmd",
.textureName = "wl_eye.1",
},
{
.arc = "Wmdl",
.modelFileName = "wl.bmd",
.textureName = "wl_eye.2",
},
{
.arc = "Wmdl",
.modelFileName = "wl.bmd",
.textureName = "wl_eye.3",
},
{
.arc = "Wmdl",
.modelFileName = "wl.bmd",
.textureName = "wl_eye.4",
},
{
.arc = "Wmdl",
.modelFileName = "wl.bmd",
.textureName = "wl_eye.5",
}
}},
{get_cvars().eponaColor, {
{
.arc = "Horse",
.modelFileName = "hs.bmd",
.textureName = "hs_body",
},
{
.arc = "Horse",
.modelFileName = "hs.bmd",
.textureName = "hs_eye.1",
},
{
.arc = "Horse",
.modelFileName = "hs.bmd",
.textureName = "hs_eye.2",
},
{
.arc = "Horse",
.modelFileName = "hs.bmd",
.textureName = "hs_eye.3",
},
}},
};
}
return replacements;
}
@@ -1,21 +0,0 @@
#pragma once
/**
* File originally copied from console TPR with permission from isaac
* https://github.com/zsrtp/libtp_rel/blob/master/include/util/texture_utils.h
*/
#include "mod.hpp"
#include <gx.h>
#include <list>
#include <unordered_map>
uint32_t get_image_data_size(
uint32_t format, uint32_t width, uint32_t height, uint32_t mipmapCount);
void recolor_texture(
TextureReplacementData& replacementData, GXColor color, std::vector<u8>& newTextureDataOut);
std::unordered_map<ConfigVarHandle, std::list<TextureReplacementData>>& get_texture_replacements();
+1
Submodule mods/cosmetics added at 93971c38aa
@@ -1,5 +1,6 @@
cmake_minimum_required(VERSION 3.25)
project(basic_cosmetics_mod CXX)
project(custom_actor_demo CXX)
set(CMAKE_CXX_STANDARD 20)
if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
set(DUSK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." CACHE PATH "Path to dusk source root")
@@ -12,24 +13,9 @@ if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
endif ()
endif ()
include(FetchContent)
message(STATUS "basic_cosmetics_mod: Fetching xxhash")
FetchContent_Declare(
xxhash
GIT_REPOSITORY https://github.com/Cyan4973/xxHash.git
GIT_TAG v0.8.3
)
FetchContent_MakeAvailable(xxhash)
set(COSMETIC_SOURCES src/color_utils.cpp src/midna_hair_color.cpp src/texture_utils.cpp src/hooks.cpp)
add_mod(basic_cosmetics_mod
add_mod(custom_actor_demo
FEATURES game fmt
SOURCES src/mod.cpp ${COSMETIC_SOURCES}
SOURCES src/mod.cpp src/m_a_obj_wrock.cpp src/m_a_mine.cpp
MOD_JSON mod.json
RES_DIR res
BUNDLE
)
target_link_libraries(basic_cosmetics_mod PRIVATE xxHash::xxhash)
+7
View File
@@ -0,0 +1,7 @@
{
"id": "dev.twilitrealm.custom_actor_demo",
"name": "[Demo] Custom Actor",
"version": "1.0.0",
"author": "Twilit Realm",
"description": "A mod to demonstrate how to create and use custom actors. Spawns a \"wrock\" in South Faron Woods."
}
+227
View File
@@ -0,0 +1,227 @@
#include "m_a_mine.hpp"
#include "d/d_com_inf_game.h"
#include "res/Object/O_mD_jira.h"
// The name of the archive in /res/Object/
static const char* l_resName = "O_mD_jira";
// The actor's heap (for resources) should be enough to hold the data for the model and collision
static constexpr u32 heap_size = ALIGN_NEXT(16832, 0x20);
ma_Mine_c::~ma_Mine_c() {
// Called every time the actor is deleted
// Delete the sound object
mSound.deleteObject();
// Request to unload the archive (the data acts as a shared pointer, and only gets deleted when
// the reference counter goes to zero)
dComIfG_resDelete(&mPhase, l_resName);
}
cPhs_Step ma_Mine_c::create() {
// Because of how the actor system works, an actor's constructor doesn't get called when an
// actor is created. We need to manually do it here with the following ma:
fopAcM_ct(this, ma_Mine_c);
// The create function gets called until we return cPhs_COMPLEATE_e while an actor is loading.
// We request to load the archive here, and wait until the dvd completes loading it
cPhs_Step step = dComIfG_resLoad(&mPhase, l_resName);
if (step == cPhs_COMPLEATE_e) {
// Initialize the solid heap for the actor's resources, if needed
if (!fopAcM_entrySolidHeap(this, createHeapCallBack, heap_size)) {
return cPhs_ERROR_e;
}
// Setup our collision sphere and register it to the world
// Set a circle "wall" of 30 units around the actor
mAcchCir.SetWall(30.0f, 30.0f);
mAcch.Set(this, 1, &mAcchCir);
mAcch.ClrWaterNone();
mAcch.SetRoofCrrHeight(60.0f);
mAcch.SetWaterCheckOffset(10000.0f);
mAcch.SetWtrChkMode(2);
mAcch.OnLineCheck();
mCcStts.Init(30, 0xFF, this);
// Collision Sphere for the actor (copied from Bomb Actor)
static const dCcD_SrcSph
l_sphSrc = {.mObjInf =
{
.mObj = {.mFlags = 0x0,
.mSrcObjHitInf = {.mObjAt = {.mType = AT_TYPE_BOMB,
.mAtp = 0x4,
.mBase = {.mSPrm = 0x1e}},
.mObjTg = {.mType = 0xd8fbffef, .mBase = {.mSPrm = 0x11}},
.mObjCo = {.mBase = {.mSPrm = 0x79}}}},
.mGObjAt{.mSe = dCcD_SE_NONE,
.mHitMark = 0x0,
.mSpl = 0x1,
.mMtrl = 0x0,
.mBase = {.mGFlag = 0x0}},
.mGObjTg{.mSe = dCcD_SE_NONE,
.mHitMark = 0x0,
.mSpl = 0x0,
.mMtrl = 0x0,
.mBase = {.mGFlag = 0x4}},
.mGObjCo{.mBase = {.mGFlag = 0x0}},
},
.mSphAttr = {.mSph = {.mCenter = {0.0f, 0.0f, 0.0f}, .mRadius = 80.0f}}};
mCollisionSphere.Set(l_sphSrc);
mCollisionSphere.SetStts(&mCcStts);
// We register a callback anytime the actor is hit (both attacks and is hit)
mCollisionSphere.SetAtHitCallback(atHitCallback);
mCollisionSphere.SetTgHitCallback(atHitCallback);
mCollisionSphere.OffTgSetBit();
mCollisionSphere.OffCoSetBit();
mCollisionSphere.OnAtSetBit(); // Enable the attack sphere
// Set the initial matrix and cull box for the actor
fopAcM_SetMtx(this, mpModel->getBaseTRMtx());
fopAcM_SetMin(this, -36.0f, 0.0f, -36.0f);
fopAcM_SetMax(this, 36.0f, 66.0f, 36.0f);
// Call execute so the actor's information in the world can be updated
Execute();
}
return step;
}
// Initializes the heap that all instances of this actor will use for resources
// Gets called from the callback in fopAcM_entrySolidHeap
int ma_Mine_c::CreateHeap() {
// Get the bmd data from the archive and initialize it
J3DModelData* model_data =
(J3DModelData*)dComIfG_getObjectRes(l_resName, dRes_INDEX_O_MD_JIRA_BMD_O_MD_JIRAI_e);
if (model_data == NULL) {
return 0;
}
mpModel = mDoExt_J3DModel__create(model_data, 0x80000, 0x11000084);
if (mpModel == NULL) {
return 0;
}
// Create the sound object the actor will use
mSound.init(&current.pos, 1);
return 1;
}
int ma_Mine_c::createHeapCallBack(fopAc_ac_c* i_this) {
return static_cast<ma_Mine_c*>(i_this)->CreateHeap();
}
int ma_Mine_c::Delete() {
// Call the destructor anytime we delete the actor
this->~ma_Mine_c();
return 1;
}
int ma_Mine_c::Execute() {
// Update collision with the world
mAcch.CrrPos(dComIfG_Bgsp());
mCollisionSphere.SetC(attention_info.position);
dComIfG_Ccsp()->Set(&mCollisionSphere);
// Get the collision below the actor
cBgS_GndChk groundChunk = mAcch.m_gnd;
f32 groundH = mAcch.GetGroundH();
if (groundH != -G_CM3D_F_INF) {
// Set the actor's environment colors to match the room's current ones
int roomNo = dComIfG_Bgsp().GetRoomId(groundChunk);
tevStr.YukaCol = dComIfG_Bgsp().GetPolyColor(groundChunk);
tevStr.room_no = roomNo;
// Set the actor's room to be where it is sitting
mCcStts.SetRoomId(roomNo);
fopAcM_SetRoomNo(this, roomNo);
// Get the reverb info here that the actor can use when exploding
mReverb = dComIfGp_getReverb(roomNo);
}
// Update the model's transformation matrix to match the actor's transform
mDoMtx_stack_c::transS(current.pos.x, current.pos.y, current.pos.z);
mDoMtx_stack_c::ZXYrotM(shape_angle);
mDoMtx_stack_c::scaleM(scale);
mpModel->setBaseTRMtx(mDoMtx_stack_c::get());
// Set any attention flags (if needed)
eyePos = attention_info.position = current.pos;
attention_info.flags = 0;
return 1;
}
int ma_Mine_c::Draw() {
// Update the model's lighting with the scene
g_env_light.settingTevStruct(0x20, &current.pos, &tevStr);
g_env_light.setLightTevColorType_MAJI(mpModel, &tevStr);
// Set the bmd model to be drawn when the display list is executed
mDoExt_modelUpdateDL(mpModel);
return 1;
}
void ma_Mine_c::atHit(dCcD_GObjInf* i_atObjInf) {
// Create particles with these IDs at the actor's position
static const u16 normalNameID[] = {
0x161, 0x162, 0x163, 0x164, 0x165, 0x166, 0x167, 0x168, 0x1EC};
for (int i = 0; i < ARRAY_SIZE(normalNameID); i++) {
dComIfGp_particle_setColor(normalNameID[i], &current.pos, &tevStr, NULL, NULL, 0.0f, 0xFF,
&shape_angle, &scale, NULL, -1, NULL);
}
// Create an explosion sound
mSound.startSound(Z2SE_OBJ_BOMB_EXPLODE, 0, mReverb);
// Vibrate the controller
dComIfGp_getVibration().StartShock(4, 31, cXyz(0.0f, 1.0f, 0.0f));
// Request to delete the actor so it disappears
fopAcM_delete(this);
}
void ma_Mine_c::atHitCallback(fopAc_ac_c* i_tgActor, dCcD_GObjInf* i_tgObjInf,
fopAc_ac_c* i_atActor, dCcD_GObjInf* i_atObjInf) {
// This callback gets triggered anytime an intersection happens with the object's collision sphere
((ma_Mine_c*)i_tgActor)->atHit(i_atObjInf);
}
static cPhs_Step ma_Mine_create(void* i_this) {
return static_cast<ma_Mine_c*>(i_this)->create();
}
static int maMine_Delete(void* i_this) {
return static_cast<ma_Mine_c*>(i_this)->Delete();
}
static int maMine_Execute(void* i_this) {
return static_cast<ma_Mine_c*>(i_this)->Execute();
}
static int maMine_Draw(void* i_this) {
return static_cast<ma_Mine_c*>(i_this)->Draw();
}
static int maMine_IsDelete(void*) {
return 1;
}
s16 ma_Mine_c::sProcName = -1;
ActorHandle ma_Mine_c::sActorHandle = -1;
const ActorProfileDesc ma_Mine_c::sProfile = {.name = MA_MINE_NAME,
.priority_group = 7,
.process_size = sizeof(ma_Mine_c),
.draw_priority = fpcDwPi_OBJ_LBOX_e, // An unused draw priority
.status = fopAcStts_UNK_0x40000_e | fopAcStts_UNK_0x4000_e | fopAcStts_CULL_e,
.group = fopAc_ACTOR_e,
.cull_type = fopAc_CULLBOX_CUSTOM_e,
.create_function = ma_Mine_create,
.delete_function = maMine_Delete,
.execute_function = maMine_Execute,
.is_delete_function = maMine_IsDelete,
.draw_function = maMine_Draw};
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include "mods/svc/actor.h"
// Base actor class definitions
#include "f_op/f_op_actor.h"
// Definitions for request_of_phase_process_class and cPhs_Step
#include "SSystem/SComponent/c_phase.h"
// Definitions for collision
#include "d/d_bg_s_acch.h"
#include "d/d_bg_w.h"
#include "d/d_cc_d.h"
#define MA_MINE_NAME "m_mine"
class ma_Mine_c : public fopAc_ac_c {
public:
request_of_phase_process_class mPhase;
J3DModel* mpModel;
dBgS_ObjAcch mAcch;
dBgS_AcchCir mAcchCir;
Mtx mColliderMtx;
dCcD_Stts mCcStts;
dCcD_Sph mCollisionSphere;
Z2SoundObjSimple mSound;
s8 mReverb;
virtual ~ma_Mine_c();
cPhs_Step create();
int CreateHeap();
int Delete();
int Execute();
int Draw();
void atHit(dCcD_GObjInf* i_atObjInf);
static int createHeapCallBack(fopAc_ac_c*);
static void atHitCallback(fopAc_ac_c* i_tgActor, dCcD_GObjInf* i_tgObjInf,
fopAc_ac_c* i_atActor, dCcD_GObjInf* i_atObjInf);
static s16 sProcName;
static ActorHandle sActorHandle;
static const ActorProfileDesc sProfile;
};
@@ -0,0 +1,195 @@
/*
* m_a_obj_wrock.cpp
* An example actor for a rock that can be placed in the world.
*/
#include "m_a_obj_wrock.hpp"
#include "d/d_com_inf_game.h"
#include "res/Object/WRock.h"
// The name of the archive in /res/Object/
static const char* l_resName = "Wrock";
// The actor's heap (for resources) should be enough to hold the data for the model and collision
static constexpr u32 heap_size = ALIGN_NEXT(13952, 0x20) + ALIGN_NEXT(1920, 0x20);
maObj_Wrock_c::~maObj_Wrock_c() {
// Called every time the actor is deleted
// Remove the collider from the world's collision
if (mpCollider != NULL) {
dComIfG_Bgsp().Release(mpCollider);
}
// Request to unload the archive (the data acts as a shared pointer, and only gets deleted when
// the reference counter goes to zero)
dComIfG_resDelete(&mPhase, l_resName);
}
cPhs_Step maObj_Wrock_c::create() {
// Because of how the actor system works, an actor's constructor doesn't get called when an
// actor is created. We need to manually do it here with the following macro:
fopAcM_ct(this, maObj_Wrock_c);
// The create function gets called until we return cPhs_COMPLEATE_e while an actor is loading.
// We request to load the archive here, and wait until the dvd completes loading it
cPhs_Step step = dComIfG_resLoad(&mPhase, l_resName);
if (step == cPhs_COMPLEATE_e) {
// Initialize the solid heap for the actor's resources, if needed
if (!fopAcM_entrySolidHeap(this, createHeapCallBack, heap_size)) {
return cPhs_ERROR_e;
}
// Register the actor's collider to the current world's collision
if (mpCollider != NULL) {
if (dComIfG_Bgsp().Regist(mpCollider, this) == true) {
return cPhs_ERROR_e;
}
}
// Set the initial matrix and cull box for the actor
fopAcM_SetMtx(this, mpModel->getBaseTRMtx());
fopAcM_setCullSizeBox(this, -400.0f, -400.0f, -400.0f, 400.0f, 400.0f, 400.0f);
// Setup collision info (will be used in execute)
mAcch.Set(&current.pos, &old.pos, this, 1, &mAcchCir, &speed, &current.angle, &shape_angle);
// Call execute so the actor's information in the world can be updated
Execute();
}
return step;
}
// Initializes the heap that all instances of this actor will use for resources
// Gets called from the callback in fopAcM_entrySolidHeap
int maObj_Wrock_c::CreateHeap() {
// Get the bmd data from the archive and initialize it
J3DModelData* model_data =
(J3DModelData*)dComIfG_getObjectRes(l_resName, dRes_INDEX_WROCK_BMD_WROCK_e);
if (model_data == NULL) {
return 0;
}
mpModel = mDoExt_J3DModel__create(model_data, 0x80000, 0x11000084);
if (mpModel == NULL) {
return 0;
}
// Get the dzb collision data from the archive and initialize it
mpCollider = JKR_NEW dBgW();
if (mpCollider == NULL) {
return 0;
}
cBgD_t* dzb = (cBgD_t*)dComIfG_getObjectRes(l_resName, dRes_INDEX_WROCK_DZB_WROCK_e);
if (mpCollider->Set(dzb, 1, &mColliderMtx) == true) {
return 0;
}
mpCollider->SetCrrFunc(dBgS_MoveBGProc_Typical);
return 1;
}
int maObj_Wrock_c::createHeapCallBack(fopAc_ac_c* i_this) {
return static_cast<maObj_Wrock_c*>(i_this)->CreateHeap();
}
int maObj_Wrock_c::Delete() {
// Call the destructor anytime we delete the actor
this->~maObj_Wrock_c();
return 1;
}
int maObj_Wrock_c::Execute() {
// Update collision with the world
mAcch.CrrPos(dComIfG_Bgsp());
// Get the collision below the actor
mGndChk = mAcch.m_gnd;
mGroundH = mAcch.GetGroundH();
if (mGroundH != -G_CM3D_F_INF) {
// Set the actor's environment colors to match the room's current ones
tevStr.YukaCol = dComIfG_Bgsp().GetPolyColor(mGndChk);
tevStr.room_no = dComIfG_Bgsp().GetRoomId(mGndChk);
// Set the actor's room to be where it is sitting
fopAcM_SetRoomNo(this, dComIfG_Bgsp().GetRoomId(mGndChk));
}
// Update the model's transformation matrix to match the actor's transform
mDoMtx_stack_c::transS(current.pos.x, current.pos.y, current.pos.z);
mDoMtx_stack_c::ZXYrotM(shape_angle);
mDoMtx_stack_c::scaleM(scale);
mpModel->setBaseTRMtx(mDoMtx_stack_c::get());
// Copy the model's transformation matrix to the collider's transformation matrix and update the
// collider
if (mpCollider != NULL) {
PSMTXCopy(mpModel->getBaseTRMtx(), mColliderMtx);
mpCollider->Move();
}
// Set any attention flags (if needed)
eyePos = attention_info.position = current.pos;
attention_info.flags = 0;
return 1;
}
int maObj_Wrock_c::Draw() {
// Update the model's lighting with the scene
g_env_light.settingTevStruct(0x20, &current.pos, &tevStr);
g_env_light.setLightTevColorType_MAJI(mpModel, &tevStr);
// Set the current dlist to BG Which means that shadows can be cast on it
// Because of the messy collider, the shadows don't look great, but this is here as an example
dComIfGd_setListBG();
// Set the bmd model to be drawn when the display list is executed
mDoExt_modelUpdateDL(mpModel);
// Cast a shadow for the actor onto the ground.
// We can only do this if we are drawing to the normal dlist, not the BG dlist
// if (mGroundH != -G_CM3D_F_INF) {
// mShadow = dComIfGd_setShadow(mShadow, 1, mpModel, &current.pos,
// 2000.0f, 0.0f,
// current.pos.y, mGroundH, mGndChk, &tevStr, 0,
// 1.0f, &dDlst_shadowControl_c::mSimpleTexObj);
// }
// Reset the active dlist
dComIfGd_setList();
return 1;
}
static cPhs_Step maObj_Wrock_Create(void* i_this) {
return static_cast<maObj_Wrock_c*>(i_this)->create();
}
static int maObj_Wrock_Delete(void* i_this) {
return static_cast<maObj_Wrock_c*>(i_this)->Delete();
}
static int maObj_Wrock_Execute(void* i_this) {
return static_cast<maObj_Wrock_c*>(i_this)->Execute();
}
static int maObj_Wrock_Draw(void* i_this) {
return static_cast<maObj_Wrock_c*>(i_this)->Draw();
}
static int maObj_Wrock_IsDelete(void*) {
return 1;
}
s16 maObj_Wrock_c::sProcName = -1;
ActorHandle maObj_Wrock_c::sActorHandle = -1;
const ActorProfileDesc maObj_Wrock_c::sProfile = {.name = MAOBJ_WROCK_NAME,
.priority_group = 7,
.process_size = sizeof(maObj_Wrock_c),
.draw_priority = fpcDwPi_OBJ_LBOX_e, // An unused draw priority
.status = fopAcStts_UNK_0x40000_e | fopAcStts_UNK_0x4000_e | fopAcStts_CULL_e,
.group = fopAc_ACTOR_e,
.cull_type = fopAc_CULLBOX_CUSTOM_e,
.create_function = maObj_Wrock_Create,
.delete_function = maObj_Wrock_Delete,
.execute_function = maObj_Wrock_Execute,
.is_delete_function = maObj_Wrock_IsDelete,
.draw_function = maObj_Wrock_Draw};
@@ -0,0 +1,39 @@
#pragma once
#include "mods/svc/actor.h"
// Base actor class definitions
#include "f_op/f_op_actor.h"
// Definitions for request_of_phase_process_class and cPhs_Step
#include "SSystem/SComponent/c_phase.h"
// Definitions for collision
#include "d/d_bg_s_acch.h"
#include "d/d_bg_w.h"
#define MAOBJ_WROCK_NAME "wrock"
class maObj_Wrock_c : public fopAc_ac_c {
public:
request_of_phase_process_class mPhase;
J3DModel* mpModel;
dBgS_ObjAcch mAcch;
cBgS_GndChk mGndChk;
dBgS_AcchCir mAcchCir;
Mtx mColliderMtx;
dBgW* mpCollider;
f32 mGroundH;
int mShadow;
virtual ~maObj_Wrock_c();
cPhs_Step create();
int CreateHeap();
int Delete();
int Execute();
int Draw();
static int createHeapCallBack(fopAc_ac_c*);
static s16 sProcName;
static ActorHandle sActorHandle;
static const ActorProfileDesc sProfile;
};
+85
View File
@@ -0,0 +1,85 @@
#include "mods/service.hpp"
#include "mods/svc/actor.h"
#include "mods/svc/log.hpp"
#include "mods/svc/stage.h"
#include "d/d_com_inf_game.h"
#include "m_a_mine.hpp"
#include "m_a_obj_wrock.hpp"
#include <array>
DEFINE_MOD();
IMPORT_SERVICE(LogService, svc_log);
IMPORT_SERVICE(ActorService, svc_actor);
IMPORT_SERVICE(StageService, svc_stage);
extern "C" {
MOD_EXPORT ModResult mod_initialize(ModError*) {
if (svc_actor->register_actor(mod_ctx, &maObj_Wrock_c::sProfile, &maObj_Wrock_c::sProcName,
&maObj_Wrock_c::sActorHandle) != MOD_OK)
{
mods::log::error("Failed to register actor wrock!");
return MOD_ERROR;
}
const stage_actor_data_class wrockParams{
.name = MAOBJ_WROCK_NAME,
.base =
{
.parameters = 0,
.position = {-14324.0f, 0.0f, 341.0f},
.angle = {0, -16595, 0},
.setID = 0xFFFF,
},
};
if (svc_stage->add_actor(
mod_ctx, "F_SP108", 0, -1, &wrockParams, sizeof(wrockParams), nullptr) != MOD_OK)
{
mods::log::error("Adding wrock to F_SP108 Failed!");
return MOD_ERROR;
}
if (svc_actor->register_actor(mod_ctx, &ma_Mine_c::sProfile, &ma_Mine_c::sProcName,
&ma_Mine_c::sActorHandle) != MOD_OK)
{
mods::log::error("Failed to register actor " MA_MINE_NAME);
return MOD_ERROR;
}
static const std::array<cXyz, 6> minePositions = {
{{-14445.0f, 11.0f, 1304.0f}, {-14557.0f, 7.0f, 990.0f}, {-14790.0f, 7.0f, 634.0f},
{-14829.0f, 0.0f, 144.0f}, {-14615.0f, 0.0f, -170.0f}, {-14283.0f, 10.0f, -420.0f}}};
for (const auto& pos : minePositions) {
const stage_actor_data_class mineParams{
.name = MA_MINE_NAME,
.base =
{
.parameters = 0,
.position = {pos.x, pos.y, pos.z},
.angle = {0x2000, (s16)(pos.x*100000), 0}, // Adjusted and seemingly random angle
.setID = 0xFFFF,
},
};
if (svc_stage->add_actor(
mod_ctx, "F_SP108", 0, -1, &mineParams, sizeof(mineParams), nullptr) != MOD_OK)
{
mods::log::error("Adding mine to F_SP108 Failed!");
return MOD_ERROR;
}
}
mods::log::info("custom_actor_demo initialized");
return MOD_OK;
}
MOD_EXPORT ModResult mod_update(ModError*) {
return MOD_OK;
}
MOD_EXPORT ModResult mod_shutdown(ModError*) {
mods::log::info("custom_actor_demo shutdown");
return MOD_OK;
}
}
+40
View File
@@ -0,0 +1,40 @@
cmake_minimum_required(VERSION 3.25)
project(luau_runtime CXX)
if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
set(DUSK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." CACHE PATH "Path to dusk source root")
option(DUSK_MOD_USE_FULL_TREE "Use full build instead of the minimal mod SDK" OFF)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
if (DUSK_MOD_USE_FULL_TREE)
add_subdirectory("${DUSK_DIR}" dusk EXCLUDE_FROM_ALL)
else ()
add_subdirectory("${DUSK_DIR}/sdk" dusk-sdk EXCLUDE_FROM_ALL)
endif ()
endif ()
include(FetchContent)
set(LUAU_BUILD_CLI OFF CACHE BOOL "" FORCE)
set(LUAU_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(LUAU_BUILD_WEB OFF CACHE BOOL "" FORCE)
set(LUAU_WERROR OFF CACHE BOOL "" FORCE)
FetchContent_Declare(luau
URL https://github.com/luau-lang/luau/archive/refs/tags/0.734.tar.gz
URL_HASH SHA256=cb55a891226d8c70284e22eb9281cc2b4496c709a4050f52aaa18a355fe7b1a3
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(luau)
add_mod(luau_runtime
SOURCES
src/bindings.cpp
src/config.cpp
src/runtime.cpp
src/ui.cpp
MOD_JSON mod.json
RES_DIR res
BUNDLE
)
target_link_libraries(luau_runtime PRIVATE Luau.VM Luau.Compiler)
+7
View File
@@ -0,0 +1,7 @@
{
"id": "dev.twilitrealm.luau",
"name": "Luau Support",
"version": "1.0.0",
"author": "Twilit Realm",
"description": "Luau script runtime for mods"
}
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2019-2025 Roblox Corporation
Copyright (c) 1994-2019 Lua.org, PUC-Rio.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+340
View File
@@ -0,0 +1,340 @@
#include "runtime.hpp"
#include <cstring>
#include <string>
#include <vector>
namespace luau_runtime {
namespace {
constexpr char kOverlayMetatable[] = "dusklight.overlay_handle";
constexpr char kTextureMetatable[] = "dusklight.texture_handle";
uint64_t get_hash_field(lua_State* state, int table, const char* field, bool required) {
lua_getfield(state, table, field);
if (lua_isnil(state, -1)) {
lua_pop(state, 1);
if (required) {
luaL_error(state, "field '%s' is required", field);
}
return 0;
}
if (!lua_isinteger64(state, -1)) {
luaL_error(state, "field '%s' must be an integer", field);
}
const uint64_t value = static_cast<uint64_t>(lua_tointeger64(state, -1, nullptr));
lua_pop(state, 1);
return value;
}
int log_write(lua_State* state, LogLevel level, int messageIndex) {
Vm& vm = vm_from_upvalue(state);
if (svc_log == nullptr) {
service_unavailable(state, "LogService");
}
size_t length = 0;
const char* message = luaL_checklstring(state, messageIndex, &length);
const std::string copy{message, length};
svc_log->write(vm.subject, level, copy.c_str());
return 0;
}
int log_trace(lua_State* state) {
return log_write(state, LOG_LEVEL_TRACE, 1);
}
int log_debug(lua_State* state) {
return log_write(state, LOG_LEVEL_DEBUG, 1);
}
int log_info(lua_State* state) {
return log_write(state, LOG_LEVEL_INFO, 1);
}
int log_warn(lua_State* state) {
return log_write(state, LOG_LEVEL_WARN, 1);
}
int log_error(lua_State* state) {
return log_write(state, LOG_LEVEL_ERROR, 1);
}
int log_write_level(lua_State* state) {
static constexpr const char* kLevels[] = {"trace", "debug", "info", "warn", "error", nullptr};
const int level = luaL_checkoption(state, 1, nullptr, kLevels);
return log_write(state, static_cast<LogLevel>(level), 2);
}
int host_mod_id(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
if (svc_host == nullptr) {
service_unavailable(state, "HostService");
}
lua_pushstring(state, svc_host->mod_id(vm.subject));
return 1;
}
int host_mod_name(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
if (svc_host == nullptr) {
service_unavailable(state, "HostService");
}
lua_pushstring(state, svc_host->mod_name(vm.subject));
return 1;
}
int host_mod_version(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
if (svc_host == nullptr) {
service_unavailable(state, "HostService");
}
lua_pushstring(state, svc_host->mod_version(vm.subject));
return 1;
}
int host_mod_dir(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
if (svc_host == nullptr) {
service_unavailable(state, "HostService");
}
lua_pushstring(state, svc_host->mod_dir(vm.subject));
return 1;
}
int host_data_dir(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
if (svc_host == nullptr || !SERVICE_HAS(svc_host, HostService, data_dir) ||
svc_host->data_dir == nullptr)
{
service_unavailable(state, "HostService::data_dir");
}
const char* path = nullptr;
check_result(state, svc_host->data_dir(vm.subject, &path), "host.data_dir");
lua_pushstring(state, path != nullptr ? path : "");
return 1;
}
int host_fail(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
if (svc_host == nullptr) {
service_unavailable(state, "HostService");
}
const char* message = luaL_checkstring(state, 1);
svc_host->fail(vm.subject, MOD_ERROR, message);
luaL_error(state, "%s", message);
}
int register_host_callback(lua_State* state, std::vector<int>& callbacks) {
luaL_checktype(state, 1, LUA_TFUNCTION);
lua_pushvalue(state, 1);
callbacks.push_back(lua_ref(state, -1));
lua_pop(state, 1);
return 0;
}
int host_on_update(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
return register_host_callback(state, vm.updateRefs);
}
int host_on_shutdown(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
return register_host_callback(state, vm.shutdownRefs);
}
int resource_load(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
if (svc_resource == nullptr) {
service_unavailable(state, "ResourceService");
}
const char* path = luaL_checkstring(state, 1);
ResourceBuffer buffer = RESOURCE_BUFFER_INIT;
check_result(state, svc_resource->load(vm.subject, path, &buffer), "resource.load");
const auto* data = buffer.data != nullptr ? static_cast<const char*>(buffer.data) : "";
const std::string copy{data, buffer.size};
svc_resource->free(vm.subject, &buffer);
lua_pushlstring(state, copy.data(), copy.size());
return 1;
}
int overlay_remove(lua_State* state) {
auto& handle = check_handle(state, 1, kOverlayMetatable, HandleKind::Overlay);
if (svc_overlay == nullptr) {
service_unavailable(state, "OverlayService");
}
check_result(state, svc_overlay->remove(handle.vm->subject, handle.value), "overlay.remove");
handle.value = 0;
return 0;
}
int overlay_add_file(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
if (svc_overlay == nullptr) {
service_unavailable(state, "OverlayService");
}
const char* discPath = luaL_checkstring(state, 1);
const char* bundlePath = luaL_checkstring(state, 2);
OverlayHandle handle = 0;
check_result(state, svc_overlay->add_file(vm.subject, discPath, bundlePath, &handle),
"overlay.add_file");
push_handle(state, vm, handle, HandleKind::Overlay, kOverlayMetatable);
return 1;
}
int overlay_add_buffer(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
if (svc_overlay == nullptr) {
service_unavailable(state, "OverlayService");
}
const char* discPath = luaL_checkstring(state, 1);
size_t size = 0;
const char* data = luaL_checklstring(state, 2, &size);
OverlayHandle handle = 0;
check_result(state, svc_overlay->add_buffer(vm.subject, discPath, data, size, &handle),
"overlay.add_buffer");
push_handle(state, vm, handle, HandleKind::Overlay, kOverlayMetatable);
return 1;
}
int texture_unregister(lua_State* state) {
auto& handle = check_handle(state, 1, kTextureMetatable, HandleKind::Texture);
if (svc_texture == nullptr) {
service_unavailable(state, "TextureService");
}
check_result(
state, svc_texture->unregister(handle.vm->subject, handle.value), "texture.unregister");
handle.value = 0;
return 0;
}
int texture_register_file(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
if (svc_texture == nullptr) {
service_unavailable(state, "TextureService");
}
const char* path = luaL_checkstring(state, 1);
TextureReplacementHandle handle = 0;
check_result(
state, svc_texture->register_file(vm.subject, path, &handle), "texture.register_file");
push_handle(state, vm, handle, HandleKind::Texture, kTextureMetatable);
return 1;
}
int texture_register_data(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
if (svc_texture == nullptr) {
service_unavailable(state, "TextureService");
}
luaL_checktype(state, 1, LUA_TTABLE);
luaL_checktype(state, 2, LUA_TTABLE);
TextureKey key = TEXTURE_KEY_INIT;
key.kind = TEXTURE_KEY_SOURCE;
key.texture_hash = get_hash_field(state, 1, "texture_hash", true);
key.tlut_hash = get_hash_field(state, 1, "tlut_hash", false);
key.width = static_cast<uint32_t>(get_optional_int(state, 1, "width", 0));
key.height = static_cast<uint32_t>(get_optional_int(state, 1, "height", 0));
key.gx_format = static_cast<uint32_t>(get_optional_int(state, 1, "gx_format", 0));
key.has_tlut = get_optional_bool(state, 1, "has_tlut", false);
lua_getfield(state, 2, "data");
size_t size = 0;
const char* bytes = luaL_checklstring(state, -1, &size);
TextureData data = TEXTURE_DATA_INIT;
data.data = bytes;
data.size = size;
data.width = static_cast<uint32_t>(get_optional_int(state, 2, "width", key.width));
data.height = static_cast<uint32_t>(get_optional_int(state, 2, "height", key.height));
data.mip_count = static_cast<uint32_t>(get_optional_int(state, 2, "mip_count", 1));
data.gx_format = static_cast<uint32_t>(get_optional_int(state, 2, "gx_format", key.gx_format));
TextureReplacementHandle handle = 0;
const ModResult result = svc_texture->register_data(vm.subject, &key, &data, &handle);
lua_pop(state, 1);
check_result(state, result, "texture.register_data");
push_handle(state, vm, handle, HandleKind::Texture, kTextureMetatable);
return 1;
}
} // namespace
int open_log(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
if (svc_log == nullptr) {
service_unavailable(state, "LogService");
}
lua_newtable(state);
set_function(state, vm, "write", log_write_level);
set_function(state, vm, "trace", log_trace);
set_function(state, vm, "debug", log_debug);
set_function(state, vm, "info", log_info);
set_function(state, vm, "warn", log_warn);
set_function(state, vm, "error", log_error);
lua_setreadonly(state, -1, true);
return 1;
}
int open_host(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
if (svc_host == nullptr) {
service_unavailable(state, "HostService");
}
lua_newtable(state);
lua_pushstring(state, svc_host->version != nullptr ? svc_host->version : "");
lua_setfield(state, -2, "version");
set_function(state, vm, "mod_id", host_mod_id);
set_function(state, vm, "mod_name", host_mod_name);
set_function(state, vm, "mod_version", host_mod_version);
set_function(state, vm, "mod_dir", host_mod_dir);
set_function(state, vm, "data_dir", host_data_dir);
set_function(state, vm, "on_update", host_on_update);
set_function(state, vm, "on_shutdown", host_on_shutdown);
set_function(state, vm, "fail", host_fail);
lua_setreadonly(state, -1, true);
return 1;
}
int open_resource(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
if (svc_resource == nullptr) {
service_unavailable(state, "ResourceService");
}
lua_newtable(state);
set_function(state, vm, "load", resource_load);
lua_setreadonly(state, -1, true);
return 1;
}
int open_overlay(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
if (svc_overlay == nullptr) {
service_unavailable(state, "OverlayService");
}
static const luaL_Reg kMethods[] = {{"remove", overlay_remove}, {nullptr, nullptr}};
create_handle_metatable(state, kOverlayMetatable, kMethods, "OverlayHandle");
lua_newtable(state);
set_function(state, vm, "add_file", overlay_add_file);
set_function(state, vm, "add_buffer", overlay_add_buffer);
lua_setreadonly(state, -1, true);
return 1;
}
int open_texture(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
if (svc_texture == nullptr) {
service_unavailable(state, "TextureService");
}
static const luaL_Reg kMethods[] = {{"unregister", texture_unregister}, {nullptr, nullptr}};
create_handle_metatable(state, kTextureMetatable, kMethods, "TextureHandle");
lua_newtable(state);
set_function(state, vm, "register_file", texture_register_file);
set_function(state, vm, "register_data", texture_register_data);
lua_pushinteger64(state, static_cast<int64_t>(TEXTURE_HASH_WILDCARD));
lua_setfield(state, -2, "hash_wildcard");
lua_pushinteger64(state, static_cast<int64_t>(TEXTURE_TLUT_WILDCARD));
lua_setfield(state, -2, "tlut_wildcard");
lua_setreadonly(state, -1, true);
return 1;
}
} // namespace luau_runtime
+243
View File
@@ -0,0 +1,243 @@
#include "runtime.hpp"
#include <string>
#include <vector>
namespace luau_runtime {
namespace {
constexpr char kConfigVarMetatable[] = "dusklight.config_var";
constexpr char kConfigSubscriptionMetatable[] = "dusklight.config_subscription";
void config_changed(ModContext*, ConfigVarHandle, const ConfigVarValue* value,
const ConfigVarValue* previous, void* userData) {
auto& callback = *static_cast<Callback*>(userData);
if (value == nullptr || previous == nullptr) {
return;
}
push_config_value(callback.vm->state, *value);
push_config_value(callback.vm->state, *previous);
std::string error;
if (!call_ref(*callback.vm, callback.refs[0], 2, 0, kCallbackBudget, error)) {
fail_callback(*callback.vm, "config change callback", error);
}
}
int config_get(lua_State* state) {
auto& handle = check_handle(state, 1, kConfigVarMetatable, HandleKind::ConfigVar);
if (svc_config == nullptr) {
service_unavailable(state, "ConfigService");
}
switch (handle.configType) {
case CONFIG_VAR_BOOL: {
bool value = false;
check_result(
state, svc_config->get_bool(handle.vm->subject, handle.value, &value), "config get");
lua_pushboolean(state, value);
break;
}
case CONFIG_VAR_INT: {
int64_t value = 0;
check_result(
state, svc_config->get_int(handle.vm->subject, handle.value, &value), "config get");
lua_pushinteger64(state, value);
break;
}
case CONFIG_VAR_FLOAT: {
double value = 0;
check_result(
state, svc_config->get_float(handle.vm->subject, handle.value, &value), "config get");
lua_pushnumber(state, value);
break;
}
case CONFIG_VAR_STRING: {
size_t size = 0;
check_result(state,
svc_config->get_string(handle.vm->subject, handle.value, nullptr, 0, &size),
"config get");
std::vector<char> value(size + 1);
check_result(state,
svc_config->get_string(
handle.vm->subject, handle.value, value.data(), value.size(), nullptr),
"config get");
lua_pushlstring(state, value.data(), size);
break;
}
default:
luaL_error(state, "unknown config value type");
}
return 1;
}
int config_set(lua_State* state) {
auto& handle = check_handle(state, 1, kConfigVarMetatable, HandleKind::ConfigVar);
if (svc_config == nullptr) {
service_unavailable(state, "ConfigService");
}
ModResult result = MOD_INVALID_ARGUMENT;
switch (handle.configType) {
case CONFIG_VAR_BOOL:
result = svc_config->set_bool(
handle.vm->subject, handle.value, luaL_checkboolean(state, 2) != 0);
break;
case CONFIG_VAR_INT:
result = svc_config->set_int(handle.vm->subject, handle.value, check_int64(state, 2));
break;
case CONFIG_VAR_FLOAT:
result =
svc_config->set_float(handle.vm->subject, handle.value, luaL_checknumber(state, 2));
break;
case CONFIG_VAR_STRING:
result =
svc_config->set_string(handle.vm->subject, handle.value, luaL_checkstring(state, 2));
break;
default:
break;
}
check_result(state, result, "config set");
return 0;
}
int config_unregister(lua_State* state) {
auto& handle = check_handle(state, 1, kConfigVarMetatable, HandleKind::ConfigVar);
if (svc_config == nullptr) {
service_unavailable(state, "ConfigService");
}
check_result(
state, svc_config->unregister_var(handle.vm->subject, handle.value), "config unregister");
handle.value = 0;
return 0;
}
int config_unsubscribe(lua_State* state) {
auto& handle =
check_handle(state, 1, kConfigSubscriptionMetatable, HandleKind::ConfigSubscription);
if (svc_config == nullptr) {
service_unavailable(state, "ConfigService");
}
check_result(
state, svc_config->unsubscribe(handle.vm->subject, handle.value), "config unsubscribe");
handle.value = 0;
return 0;
}
int subscribe(lua_State* state, ScriptHandle& variable, int functionIndex) {
luaL_checktype(state, functionIndex, LUA_TFUNCTION);
Callback& callback = retain_callback(*variable.vm);
callback.refs[0] = lua_ref(state, functionIndex);
ConfigSubscriptionHandle subscription = 0;
check_result(state,
svc_config->subscribe(
variable.vm->subject, variable.value, config_changed, &callback, &subscription),
"config.subscribe");
push_handle(state, *variable.vm, subscription, HandleKind::ConfigSubscription,
kConfigSubscriptionMetatable);
return 1;
}
int config_subscribe(lua_State* state) {
if (svc_config == nullptr) {
service_unavailable(state, "ConfigService");
}
auto& variable = check_handle(state, 1, kConfigVarMetatable, HandleKind::ConfigVar);
return subscribe(state, variable, 2);
}
int config_var_subscribe(lua_State* state) {
if (svc_config == nullptr) {
service_unavailable(state, "ConfigService");
}
auto& variable = check_handle(state, 1, kConfigVarMetatable, HandleKind::ConfigVar);
return subscribe(state, variable, 2);
}
int config_register(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
if (svc_config == nullptr) {
service_unavailable(state, "ConfigService");
}
luaL_checktype(state, 1, LUA_TTABLE);
const std::string name = get_optional_string(state, 1, "name");
const std::string type = get_optional_string(state, 1, "type");
ConfigVarDesc desc = CONFIG_VAR_DESC_INIT;
desc.name = name.c_str();
if (type == "bool") {
desc.type = CONFIG_VAR_BOOL;
desc.default_bool = get_optional_bool(state, 1, "default", false);
} else if (type == "int") {
desc.type = CONFIG_VAR_INT;
desc.default_int = get_optional_int(state, 1, "default", 0);
} else if (type == "float") {
desc.type = CONFIG_VAR_FLOAT;
desc.default_float = get_optional_number(state, 1, "default", 0);
} else if (type == "string") {
desc.type = CONFIG_VAR_STRING;
} else {
luaL_error(state, "config type must be 'bool', 'int', 'float', or 'string'");
}
std::string defaultString;
if (desc.type == CONFIG_VAR_STRING) {
defaultString = get_optional_string(state, 1, "default");
desc.default_string = defaultString.c_str();
}
ConfigVarHandle handle = 0;
check_result(state, svc_config->register_var(vm.subject, &desc, &handle), "config.register");
push_handle(state, vm, handle, HandleKind::ConfigVar, kConfigVarMetatable, desc.type);
return 1;
}
} // namespace
void push_config_value(lua_State* state, const ConfigVarValue& value) {
switch (value.type) {
case CONFIG_VAR_BOOL:
lua_pushboolean(state, value.bool_value);
break;
case CONFIG_VAR_INT:
lua_pushinteger64(state, value.int_value);
break;
case CONFIG_VAR_FLOAT:
lua_pushnumber(state, value.float_value);
break;
case CONFIG_VAR_STRING:
lua_pushlstring(state, value.string_value != nullptr ? value.string_value : "",
value.string_value != nullptr ? value.string_length : 0);
break;
default:
lua_pushnil(state);
break;
}
}
int open_config(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
if (svc_config == nullptr) {
service_unavailable(state, "ConfigService");
}
static const luaL_Reg kVarMethods[] = {
{"get", config_get},
{"set", config_set},
{"subscribe", config_var_subscribe},
{"unregister", config_unregister},
{nullptr, nullptr},
};
static const luaL_Reg kSubscriptionMethods[] = {
{"unsubscribe", config_unsubscribe},
{nullptr, nullptr},
};
create_handle_metatable(state, kConfigVarMetatable, kVarMethods, "ConfigVar");
create_handle_metatable(
state, kConfigSubscriptionMetatable, kSubscriptionMethods, "ConfigSubscription");
lua_newtable(state);
set_function(state, vm, "register", config_register);
set_function(state, vm, "subscribe", config_subscribe);
lua_setreadonly(state, -1, true);
return 1;
}
} // namespace luau_runtime
+613
View File
@@ -0,0 +1,613 @@
#include "runtime.hpp"
#include "Luau/Common.h"
#include "luacode.h"
#include "mods/runtime.h"
#include "mods/service.hpp"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <new>
#include <optional>
#include <string_view>
#include <utility>
LUAU_FASTFLAG(LuauIntegerLibrary)
LUAU_FASTFLAG(LuauIntegerType2)
DEFINE_MOD();
IMPORT_OPTIONAL_SERVICE(LogService, svc_log);
IMPORT_OPTIONAL_SERVICE(HostService, svc_host);
IMPORT_OPTIONAL_SERVICE(ConfigService, svc_config);
IMPORT_OPTIONAL_SERVICE(ResourceService, svc_resource);
IMPORT_OPTIONAL_SERVICE(OverlayService, svc_overlay);
IMPORT_OPTIONAL_SERVICE(TextureService, svc_texture);
IMPORT_OPTIONAL_SERVICE(UiService, svc_ui);
namespace luau_runtime {
namespace {
std::unordered_map<ModContext*, std::unique_ptr<Vm>> s_vms;
void* limited_realloc(void* userData, void* pointer, size_t oldSize, size_t newSize) {
auto& budget = *static_cast<MemoryBudget*>(userData);
if (newSize == 0) {
std::free(pointer);
budget.used -= std::min(budget.used, oldSize);
return nullptr;
}
const size_t growth = newSize > oldSize ? newSize - oldSize : 0;
if (growth > budget.limit - std::min(budget.used, budget.limit)) {
return nullptr;
}
void* result = std::realloc(pointer, newSize);
if (result != nullptr) {
budget.used -= std::min(budget.used, oldSize);
budget.used += newSize;
}
return result;
}
int traceback_handler(lua_State* state) {
const char* message = lua_tostring(state, 1);
luaL_traceback(state, state, message != nullptr ? message : "Luau error", 1);
return 1;
}
class DeadlineScope {
public:
DeadlineScope(Vm& vm, std::chrono::steady_clock::duration budget)
: m_vm{vm}, m_previousDeadline{vm.deadline}, m_previousActive{vm.deadlineActive} {
const auto requested = std::chrono::steady_clock::now() + budget;
if (!vm.deadlineActive || requested < vm.deadline) {
vm.deadline = requested;
}
vm.deadlineActive = true;
++vm.callDepth;
}
~DeadlineScope() {
--m_vm.callDepth;
m_vm.deadline = m_previousDeadline;
m_vm.deadlineActive = m_previousActive;
}
private:
Vm& m_vm;
std::chrono::steady_clock::time_point m_previousDeadline;
bool m_previousActive;
};
bool protected_call(lua_State* state, Vm& vm, int argumentCount, int resultCount,
std::chrono::steady_clock::duration budget, std::string& outError) {
const int functionIndex = lua_gettop(state) - argumentCount;
lua_pushcfunction(state, traceback_handler, "dusklight traceback");
lua_insert(state, functionIndex);
DeadlineScope deadline{vm, budget};
const int status = lua_pcall(state, argumentCount, resultCount, functionIndex);
if (status != LUA_OK) {
const char* message = lua_tostring(state, -1);
outError = message != nullptr ? message : "unknown Luau error";
lua_pop(state, 1);
lua_remove(state, functionIndex);
return false;
}
lua_remove(state, functionIndex);
return true;
}
std::optional<std::string> normalize_module_path(
std::string_view currentPath, std::string_view requested) {
if ((!requested.starts_with("./") && !requested.starts_with("../")) ||
requested.find('\\') != std::string_view::npos)
{
return std::nullopt;
}
std::vector<std::string_view> parts;
const auto parentEnd = currentPath.rfind('/');
std::string combined;
if (parentEnd != std::string_view::npos) {
combined.assign(currentPath.substr(0, parentEnd + 1));
}
combined.append(requested);
size_t begin = 0;
while (begin <= combined.size()) {
const size_t end = combined.find('/', begin);
const auto part = std::string_view{combined}.substr(
begin, end == std::string::npos ? combined.size() - begin : end - begin);
if (part.empty() || part == ".") {
} else if (part == "..") {
if (parts.empty()) {
return std::nullopt;
}
parts.pop_back();
} else {
parts.push_back(part);
}
if (end == std::string::npos) {
break;
}
begin = end + 1;
}
if (parts.empty()) {
return std::nullopt;
}
std::string normalized;
for (const auto part : parts) {
if (!normalized.empty()) {
normalized.push_back('/');
}
normalized.append(part);
}
if (!normalized.ends_with(".luau")) {
normalized += ".luau";
}
return normalized;
}
ModuleOpenFn module_factory(std::string_view name) {
if (name == "dusklight.log") {
return open_log;
}
if (name == "dusklight.host") {
return open_host;
}
if (name == "dusklight.resource") {
return open_resource;
}
if (name == "dusklight.overlay") {
return open_overlay;
}
if (name == "dusklight.texture") {
return open_texture;
}
if (name == "dusklight.config") {
return open_config;
}
if (name == "dusklight.ui") {
return open_ui;
}
return nullptr;
}
int module_require(lua_State* state);
void install_module_require(lua_State* state, Vm& vm, std::string_view currentPath) {
lua_pushlightuserdata(state, &vm);
lua_pushlstring(state, currentPath.data(), currentPath.size());
lua_pushcclosure(state, module_require, "require", 2);
lua_setglobal(state, "require");
}
bool load_source_module(
lua_State* caller, Vm& vm, const std::string& path, bool keepResult, std::string& outError) {
if (svc_resource == nullptr) {
outError = "ResourceService is not available in this Dusklight build";
return false;
}
ResourceBuffer buffer = RESOURCE_BUFFER_INIT;
const ModResult loadResult = svc_resource->load(vm.subject, path.c_str(), &buffer);
if (loadResult != MOD_OK) {
outError = loadResult == MOD_UNAVAILABLE ? "module not found: res/" + path :
"failed to load module: res/" + path;
return false;
}
std::string source;
if (buffer.data != nullptr) {
source.assign(static_cast<const char*>(buffer.data), buffer.size);
}
svc_resource->free(vm.subject, &buffer);
size_t bytecodeSize = 0;
lua_CompileOptions options{};
options.optimizationLevel = 1;
options.debugLevel = 1;
char* bytecode = luau_compile(source.data(), source.size(), &options, &bytecodeSize);
if (bytecode == nullptr) {
outError = "Luau compiler ran out of memory";
return false;
}
lua_State* moduleState = lua_newthread(caller);
if (moduleState == nullptr) {
std::free(bytecode);
outError = "Luau VM ran out of memory";
return false;
}
luaL_sandboxthread(moduleState);
install_module_require(moduleState, vm, path);
const std::string chunkName = "@res/" + path;
const int loadStatus = luau_load(moduleState, chunkName.c_str(), bytecode, bytecodeSize, 0);
std::free(bytecode);
if (loadStatus != LUA_OK) {
const char* message = lua_tostring(moduleState, -1);
outError = message != nullptr ? message : "failed to load Luau bytecode";
lua_pop(caller, 1);
return false;
}
if (!protected_call(moduleState, vm, 0, keepResult ? 1 : 0, kLifecycleBudget, outError)) {
lua_pop(caller, 1);
return false;
}
if (!keepResult) {
lua_pop(caller, 1);
return true;
}
if (lua_isnil(moduleState, -1)) {
outError = "module res/" + path + " must return a value";
lua_pop(caller, 1);
return false;
}
lua_xmove(moduleState, caller, 1);
lua_remove(caller, -2);
return true;
}
int module_require(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
const std::string requested = luaL_checkstring(state, 1);
std::string moduleName;
ModuleOpenFn factory = nullptr;
if (requested.starts_with("dusklight.")) {
moduleName = requested;
factory = module_factory(moduleName);
if (factory == nullptr) {
luaL_error(state, "unknown module '%s'", moduleName.c_str());
}
} else {
const char* currentPath = lua_tostring(state, lua_upvalueindex(2));
const auto normalized =
normalize_module_path(currentPath != nullptr ? currentPath : "main.luau", requested);
if (!normalized.has_value()) {
luaL_error(
state, "module paths must be relative and remain inside the mod's res directory");
}
moduleName = *normalized;
}
if (const auto found = vm.moduleRefs.find(moduleName); found != vm.moduleRefs.end()) {
lua_getref(state, found->second);
return 1;
}
if (!vm.loadingModules.insert(moduleName).second) {
luaL_error(state, "cyclic require of '%s'", moduleName.c_str());
}
std::string error;
if (factory != nullptr) {
factory(state);
} else if (!load_source_module(state, vm, moduleName, true, error)) {
vm.loadingModules.erase(moduleName);
luaL_error(state, "%s", error.c_str());
}
vm.loadingModules.erase(moduleName);
const int ref = lua_ref(state, -1);
vm.moduleRefs.emplace(moduleName, ref);
return 1;
}
ModResult runtime_activate(ModContext*, ModContext* subject, ModError* outError) {
if (subject == nullptr) {
return set_error(outError, MOD_INVALID_ARGUMENT, "Delegated mod context is null");
}
if (s_vms.contains(subject)) {
return set_error(outError, MOD_CONFLICT, "Delegated mod is already active");
}
auto vm = std::make_unique<Vm>();
vm->subject = subject;
vm->state = lua_newstate(limited_realloc, &vm->memory);
if (vm->state == nullptr) {
return set_error(outError, MOD_ERROR, "Failed to create Luau VM");
}
lua_callbacks(vm->state)->userdata = vm.get();
lua_callbacks(vm->state)->interrupt = [](lua_State* state, int gc) {
auto* current = static_cast<Vm*>(lua_callbacks(state)->userdata);
if (gc < 0 && current != nullptr && current->deadlineActive &&
std::chrono::steady_clock::now() > current->deadline)
{
luaL_error(state, "script execution exceeded its time budget");
}
};
luaL_openlibs(vm->state);
luaL_sandbox(vm->state);
std::string error;
vm->loadingModules.insert("main.luau");
const bool loadedMain = load_source_module(vm->state, *vm, "main.luau", false, error);
vm->loadingModules.erase("main.luau");
if (!loadedMain) {
return set_error(outError, MOD_ERROR, error);
}
s_vms.emplace(subject, std::move(vm));
return MOD_OK;
}
ModResult runtime_update(ModContext*, ModContext* subject, ModError* outError) {
const auto found = s_vms.find(subject);
if (found == s_vms.end()) {
return set_error(outError, MOD_INVALID_ARGUMENT, "Delegated mod is not active");
}
Vm& vm = *found->second;
const size_t callbackCount = vm.updateRefs.size();
if (callbackCount == 0) {
return MOD_OK;
}
DeadlineScope deadline{vm, kUpdateBudget};
std::string error;
for (size_t i = 0; i < callbackCount; ++i) {
const int ref = vm.updateRefs[i];
if (!call_ref(vm, ref, 0, 0, kUpdateBudget, error)) {
return set_error(outError, MOD_ERROR, error);
}
}
return MOD_OK;
}
ModResult runtime_deactivate(ModContext*, ModContext* subject, ModError*) {
const auto found = s_vms.find(subject);
if (found == s_vms.end()) {
return MOD_OK;
}
{
Vm& vm = *found->second;
DeadlineScope deadline{vm, kLifecycleBudget};
const size_t callbackCount = vm.shutdownRefs.size();
for (size_t i = callbackCount; i > 0; --i) {
std::string error;
const int ref = vm.shutdownRefs[i - 1];
if (!call_ref(vm, ref, 0, 0, kLifecycleBudget, error) && svc_log != nullptr) {
svc_log->write(subject, LOG_LEVEL_ERROR, error.c_str());
}
}
}
s_vms.erase(found);
return MOD_OK;
}
constexpr ModRuntimeService s_runtimeService{
.header = SERVICE_HEADER(ModRuntimeService, 1, 0),
.activate = runtime_activate,
.update = runtime_update,
.deactivate = runtime_deactivate,
};
EXPORT_SERVICE_AS(s_runtimeService, "dev.twilitrealm.luau");
} // namespace
Vm::~Vm() {
if (state != nullptr) {
lua_close(state);
}
}
Vm& vm_from_upvalue(lua_State* state) {
auto* vm = static_cast<Vm*>(lua_tolightuserdata(state, lua_upvalueindex(1)));
if (vm == nullptr) {
luaL_error(state, "missing Luau runtime context");
}
return *vm;
}
void push_vm_closure(lua_State* state, Vm& vm, lua_CFunction function, const char* name) {
lua_pushlightuserdata(state, &vm);
lua_pushcclosure(state, function, name, 1);
}
void set_function(lua_State* state, Vm& vm, const char* name, lua_CFunction function) {
push_vm_closure(state, vm, function, name);
lua_setfield(state, -2, name);
}
[[noreturn]] void service_unavailable(lua_State* state, const char* name) {
luaL_error(state, "%s is not available in this Dusklight build", name);
}
void check_result(lua_State* state, ModResult result, const char* operation) {
if (result == MOD_OK) {
return;
}
const char* resultName = "error";
switch (result) {
case MOD_UNAVAILABLE:
resultName = "unavailable";
break;
case MOD_UNSUPPORTED:
resultName = "unsupported";
break;
case MOD_CONFLICT:
resultName = "conflict";
break;
case MOD_INVALID_ARGUMENT:
resultName = "invalid argument";
break;
default:
break;
}
luaL_error(state, "%s failed: %s", operation, resultName);
}
bool get_optional_bool(lua_State* state, int table, const char* field, bool fallback) {
lua_getfield(state, table, field);
const bool value = lua_isnil(state, -1) ? fallback : luaL_checkboolean(state, -1) != 0;
lua_pop(state, 1);
return value;
}
bool to_int64(lua_State* state, int index, int64_t& outValue) {
if (lua_isinteger64(state, index)) {
outValue = lua_tointeger64(state, index, nullptr);
return true;
}
if (!lua_isnumber(state, index)) {
return false;
}
const double value = lua_tonumber(state, index);
constexpr double kMaxSafeInteger = 9007199254740991.0;
if (!std::isfinite(value) || value < -kMaxSafeInteger || value > kMaxSafeInteger ||
std::trunc(value) != value)
{
return false;
}
outValue = static_cast<int64_t>(value);
return true;
}
int64_t check_int64(lua_State* state, int index) {
int64_t value = 0;
if (!to_int64(state, index, value)) {
luaL_argerror(state, index, "integer value expected");
}
return value;
}
int64_t get_optional_int(lua_State* state, int table, const char* field, int64_t fallback) {
lua_getfield(state, table, field);
const int64_t value = lua_isnil(state, -1) ? fallback : check_int64(state, -1);
lua_pop(state, 1);
return value;
}
double get_optional_number(lua_State* state, int table, const char* field, double fallback) {
lua_getfield(state, table, field);
const double value = lua_isnil(state, -1) ? fallback : luaL_checknumber(state, -1);
lua_pop(state, 1);
return value;
}
std::string get_optional_string(
lua_State* state, int table, const char* field, std::string fallback) {
lua_getfield(state, table, field);
if (!lua_isnil(state, -1)) {
size_t length = 0;
const char* value = luaL_checklstring(state, -1, &length);
fallback.assign(value, length);
}
lua_pop(state, 1);
return fallback;
}
int ref_optional_function(lua_State* state, int table, const char* field) {
lua_getfield(state, table, field);
if (lua_isnil(state, -1)) {
lua_pop(state, 1);
return LUA_NOREF;
}
luaL_argexpected(state, lua_isfunction(state, -1), table, "function field");
const int ref = lua_ref(state, -1);
lua_pop(state, 1);
return ref;
}
int ref_required_function(lua_State* state, int table, const char* field) {
const int ref = ref_optional_function(state, table, field);
if (ref == LUA_NOREF) {
luaL_error(state, "field '%s' is required", field);
}
return ref;
}
Callback& retain_callback(Vm& vm) {
auto callback = std::make_unique<Callback>();
callback->vm = &vm;
callback->refs.fill(LUA_NOREF);
vm.callbacks.push_back(std::move(callback));
return *vm.callbacks.back();
}
bool call_ref(Vm& vm, int ref, int argumentCount, int resultCount,
std::chrono::steady_clock::duration budget, std::string& outError) {
lua_State* state = vm.state;
lua_getref(state, ref);
if (!lua_isfunction(state, -1)) {
lua_pop(state, 1);
outError = "callback is no longer a function";
return false;
}
if (argumentCount != 0) {
lua_insert(state, -argumentCount - 1);
}
return protected_call(state, vm, argumentCount, resultCount, budget, outError);
}
void fail_callback(Vm& vm, std::string_view callbackName, std::string_view error) {
const std::string message = std::string{callbackName} + ": " + std::string{error};
if (svc_host != nullptr) {
svc_host->fail(vm.subject, MOD_ERROR, message.c_str());
} else if (svc_log != nullptr) {
svc_log->write(vm.subject, LOG_LEVEL_ERROR, message.c_str());
}
}
ModResult set_error(ModError* outError, ModResult result, std::string_view message) {
if (outError != nullptr && outError->struct_size >= sizeof(ModError)) {
outError->code = result;
const size_t size = std::min(message.size(), sizeof(outError->message) - 1);
std::memcpy(outError->message, message.data(), size);
outError->message[size] = '\0';
}
return result;
}
void create_handle_metatable(
lua_State* state, const char* name, const luaL_Reg* methods, const char* typeName) {
luaL_newmetatable(state, name);
luaL_register(state, nullptr, methods);
lua_pushvalue(state, -1);
lua_setfield(state, -2, "__index");
lua_pushstring(state, typeName);
lua_setfield(state, -2, "__type");
lua_setreadonly(state, -1, true);
lua_pop(state, 1);
}
ScriptHandle& check_handle(lua_State* state, int index, const char* metatable, HandleKind kind) {
auto* handle = static_cast<ScriptHandle*>(luaL_checkudata(state, index, metatable));
if (handle == nullptr || handle->kind != kind || handle->vm == nullptr || handle->value == 0) {
luaL_argerror(state, index, "stale handle");
}
return *handle;
}
void push_handle(lua_State* state, Vm& vm, uint64_t value, HandleKind kind, const char* metatable,
ConfigVarType configType) {
auto* handle = static_cast<ScriptHandle*>(lua_newuserdata(state, sizeof(ScriptHandle)));
*handle = ScriptHandle{.vm = &vm, .value = value, .kind = kind, .configType = configType};
luaL_getmetatable(state, metatable);
lua_setmetatable(state, -2);
}
} // namespace luau_runtime
extern "C" {
MOD_EXPORT ModResult mod_initialize(ModError*) {
FFlag::LuauIntegerType2.value = true;
FFlag::LuauIntegerLibrary.value = true;
return MOD_OK;
}
MOD_EXPORT ModResult mod_update(ModError*) {
return MOD_OK;
}
MOD_EXPORT ModResult mod_shutdown(ModError*) {
luau_runtime::s_vms.clear();
return MOD_OK;
}
}
+124
View File
@@ -0,0 +1,124 @@
#pragma once
#include "lua.h"
#include "lualib.h"
#include "mods/api.h"
#include "mods/svc/config.h"
#include "mods/svc/host.h"
#include "mods/svc/log.h"
#include "mods/svc/overlay.h"
#include "mods/svc/resource.h"
#include "mods/svc/texture.h"
#include "mods/svc/ui.h"
#include <array>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace luau_runtime {
constexpr size_t kMemoryLimit = 64u * 1024u * 1024u;
constexpr auto kUpdateBudget = std::chrono::milliseconds{250};
constexpr auto kLifecycleBudget = std::chrono::seconds{5};
constexpr auto kCallbackBudget = std::chrono::milliseconds{250};
struct MemoryBudget {
size_t used = 0;
size_t limit = kMemoryLimit;
};
struct Vm;
struct Callback {
Vm* vm = nullptr;
std::array<int, 8> refs{};
std::string returnedString;
int tag = 0;
};
struct Vm {
MemoryBudget memory;
lua_State* state = nullptr;
ModContext* subject = nullptr;
std::vector<int> updateRefs;
std::vector<int> shutdownRefs;
std::unordered_map<std::string, int> moduleRefs;
std::unordered_set<std::string> loadingModules;
std::vector<std::unique_ptr<Callback>> callbacks;
std::chrono::steady_clock::time_point deadline{};
unsigned callDepth = 0;
bool deadlineActive = false;
~Vm();
};
enum class HandleKind : uint8_t {
ConfigVar,
ConfigSubscription,
Overlay,
Texture,
UiWindow,
UiDialog,
UiElement,
UiStyle,
UiMenuTab,
UiList,
};
struct ScriptHandle {
Vm* vm = nullptr;
uint64_t value = 0;
HandleKind kind = HandleKind::ConfigVar;
ConfigVarType configType = CONFIG_VAR_BOOL;
};
using ModuleOpenFn = int (*)(lua_State* state);
Vm& vm_from_upvalue(lua_State* state);
void push_vm_closure(lua_State* state, Vm& vm, lua_CFunction function, const char* name);
void set_function(lua_State* state, Vm& vm, const char* name, lua_CFunction function);
[[noreturn]] void service_unavailable(lua_State* state, const char* name);
void check_result(lua_State* state, ModResult result, const char* operation);
bool get_optional_bool(lua_State* state, int table, const char* field, bool fallback);
bool to_int64(lua_State* state, int index, int64_t& outValue);
int64_t check_int64(lua_State* state, int index);
int64_t get_optional_int(lua_State* state, int table, const char* field, int64_t fallback);
double get_optional_number(lua_State* state, int table, const char* field, double fallback);
std::string get_optional_string(
lua_State* state, int table, const char* field, std::string fallback = {});
int ref_optional_function(lua_State* state, int table, const char* field);
int ref_required_function(lua_State* state, int table, const char* field);
Callback& retain_callback(Vm& vm);
bool call_ref(Vm& vm, int ref, int argumentCount, int resultCount,
std::chrono::steady_clock::duration budget, std::string& outError);
void fail_callback(Vm& vm, std::string_view callbackName, std::string_view error);
ModResult set_error(ModError* outError, ModResult result, std::string_view message);
void create_handle_metatable(
lua_State* state, const char* name, const luaL_Reg* methods, const char* typeName);
ScriptHandle& check_handle(lua_State* state, int index, const char* metatable, HandleKind kind);
void push_handle(lua_State* state, Vm& vm, uint64_t value, HandleKind kind, const char* metatable,
ConfigVarType configType = CONFIG_VAR_BOOL);
void push_config_value(lua_State* state, const ConfigVarValue& value);
void push_ui_handle(lua_State* state, Vm& vm, uint64_t value, HandleKind kind);
int open_log(lua_State* state);
int open_host(lua_State* state);
int open_resource(lua_State* state);
int open_overlay(lua_State* state);
int open_texture(lua_State* state);
int open_config(lua_State* state);
int open_ui(lua_State* state);
} // namespace luau_runtime
+856
View File
@@ -0,0 +1,856 @@
#include "runtime.hpp"
#include <algorithm>
#include <string>
#include <utility>
#include <vector>
namespace luau_runtime {
namespace {
constexpr char kUiWindowMetatable[] = "dusklight.ui_window";
constexpr char kUiDialogMetatable[] = "dusklight.ui_dialog";
constexpr char kUiElementMetatable[] = "dusklight.ui_element";
constexpr char kUiStyleMetatable[] = "dusklight.ui_style";
constexpr char kUiMenuTabMetatable[] = "dusklight.ui_menu_tab";
constexpr char kUiListMetatable[] = "dusklight.ui_list";
const char* ui_metatable(HandleKind kind) {
switch (kind) {
case HandleKind::UiWindow:
return kUiWindowMetatable;
case HandleKind::UiDialog:
return kUiDialogMetatable;
case HandleKind::UiStyle:
return kUiStyleMetatable;
case HandleKind::UiMenuTab:
return kUiMenuTabMetatable;
case HandleKind::UiList:
return kUiListMetatable;
default:
return kUiElementMetatable;
}
}
ScriptHandle& check_ui_handle(lua_State* state, int index, HandleKind kind) {
return check_handle(state, index, ui_metatable(kind), kind);
}
ScriptHandle& check_config_var(lua_State* state, int index) {
luaL_checktype(state, index, LUA_TUSERDATA);
auto* handle = static_cast<ScriptHandle*>(lua_touserdata(state, index));
if (handle == nullptr || handle->kind != HandleKind::ConfigVar || handle->value == 0 ||
handle->vm == nullptr)
{
luaL_argerror(state, index, "expected a live ConfigVar");
}
return *handle;
}
bool call_callback(Callback& callback, int ref, int argumentCount, int resultCount,
const char* name, std::string* outError = nullptr) {
std::string error;
if (call_ref(*callback.vm, ref, argumentCount, resultCount, kCallbackBudget, error)) {
return true;
}
if (outError != nullptr) {
*outError = std::move(error);
} else {
fail_callback(*callback.vm, name, error);
}
return false;
}
ModResult call_build(
Callback& callback, int ref, int argumentCount, const char* name, ModError* outError) {
std::string error;
if (call_callback(callback, ref, argumentCount, 0, name, &error)) {
return MOD_OK;
}
return set_error(outError, MOD_ERROR, error);
}
bool call_predicate(Callback& callback, int ref, int argumentCount, const char* name) {
if (!call_callback(callback, ref, argumentCount, 1, name)) {
return false;
}
lua_State* state = callback.vm->state;
const bool result = lua_toboolean(state, -1) != 0;
lua_pop(state, 1);
return result;
}
void control_get(ModContext*, void* userData, UiControlValue* outValue) {
auto& callback = *static_cast<Callback*>(userData);
if (outValue == nullptr || !call_callback(callback, callback.refs[0], 0, 1, "control get")) {
return;
}
lua_State* state = callback.vm->state;
switch (static_cast<UiControlKind>(callback.tag)) {
case UI_CONTROL_TOGGLE:
outValue->bool_value = lua_toboolean(state, -1) != 0;
break;
case UI_CONTROL_NUMBER:
case UI_CONTROL_SELECT: {
int64_t value = 0;
if (!to_int64(state, -1, value)) {
fail_callback(*callback.vm, "control get", "callback must return an integer");
} else {
outValue->int_value = value;
}
break;
}
case UI_CONTROL_STRING:
case UI_CONTROL_COLOR:
case UI_CONTROL_FILE_PICKER: {
size_t length = 0;
const char* value = lua_tolstring(state, -1, &length);
if (value == nullptr) {
fail_callback(*callback.vm, "control get", "callback must return a string");
} else {
callback.returnedString.assign(value, length);
outValue->string_value = callback.returnedString.c_str();
}
break;
}
default:
break;
}
lua_pop(state, 1);
}
void control_set(ModContext*, void* userData, const UiControlValue* value) {
auto& callback = *static_cast<Callback*>(userData);
if (value == nullptr) {
return;
}
lua_State* state = callback.vm->state;
switch (static_cast<UiControlKind>(callback.tag)) {
case UI_CONTROL_TOGGLE:
lua_pushboolean(state, value->bool_value);
break;
case UI_CONTROL_NUMBER:
case UI_CONTROL_SELECT:
lua_pushinteger64(state, value->int_value);
break;
case UI_CONTROL_STRING:
case UI_CONTROL_COLOR:
case UI_CONTROL_FILE_PICKER:
lua_pushstring(state, value->string_value != nullptr ? value->string_value : "");
break;
default:
return;
}
call_callback(callback, callback.refs[1], 1, 0, "control set");
}
bool control_disabled(ModContext*, void* userData) {
auto& callback = *static_cast<Callback*>(userData);
return call_predicate(callback, callback.refs[3], 0, "control is_disabled");
}
bool control_modified(ModContext*, void* userData) {
auto& callback = *static_cast<Callback*>(userData);
return call_predicate(callback, callback.refs[4], 0, "control is_modified");
}
bool control_selected(ModContext*, void* userData) {
auto& callback = *static_cast<Callback*>(userData);
return call_predicate(callback, callback.refs[5], 0, "control is_selected");
}
void control_pressed(ModContext*, void* userData) {
auto& callback = *static_cast<Callback*>(userData);
call_callback(callback, callback.refs[2], 0, 0, "control on_pressed");
}
ModResult panel_build(ModContext*, UiElementHandle pane, void* userData, ModError* outError) {
auto& callback = *static_cast<Callback*>(userData);
push_ui_handle(callback.vm->state, *callback.vm, pane, HandleKind::UiElement);
return call_build(callback, callback.refs[0], 1, "panel build", outError);
}
ModResult panel_update(ModContext*, void* userData, ModError* outError) {
auto& callback = *static_cast<Callback*>(userData);
return call_build(callback, callback.refs[1], 0, "panel update", outError);
}
ModResult group_build(ModContext*, UiElementHandle pane, void* userData, ModError* outError) {
auto& callback = *static_cast<Callback*>(userData);
push_ui_handle(callback.vm->state, *callback.vm, pane, HandleKind::UiElement);
return call_build(callback, callback.refs[0], 1, "group build", outError);
}
ModResult tab_build(ModContext*, UiWindowHandle window, UiElementHandle left, UiElementHandle right,
void* userData, ModError* outError) {
auto& callback = *static_cast<Callback*>(userData);
push_ui_handle(callback.vm->state, *callback.vm, window, HandleKind::UiWindow);
push_ui_handle(callback.vm->state, *callback.vm, left, HandleKind::UiElement);
push_ui_handle(callback.vm->state, *callback.vm, right, HandleKind::UiElement);
return call_build(callback, callback.refs[0], 3, "window tab build", outError);
}
void window_closed(ModContext*, UiWindowHandle window, void* userData) {
auto& callback = *static_cast<Callback*>(userData);
push_ui_handle(callback.vm->state, *callback.vm, window, HandleKind::UiWindow);
call_callback(callback, callback.refs[0], 1, 0, "window on_closed");
}
void dialog_action(ModContext*, UiDialogHandle dialog, void* userData) {
auto& callback = *static_cast<Callback*>(userData);
push_ui_handle(callback.vm->state, *callback.vm, dialog, HandleKind::UiDialog);
call_callback(callback, callback.refs[0], 1, 0, "dialog action");
}
bool dialog_action_disabled(ModContext*, void* userData) {
auto& callback = *static_cast<Callback*>(userData);
return call_predicate(callback, callback.refs[1], 0, "dialog action is_disabled");
}
void dialog_dismissed(ModContext*, UiDialogHandle dialog, void* userData) {
auto& callback = *static_cast<Callback*>(userData);
push_ui_handle(callback.vm->state, *callback.vm, dialog, HandleKind::UiDialog);
call_callback(callback, callback.refs[0], 1, 0, "dialog on_dismiss");
}
ModResult dialog_build(ModContext*, UiElementHandle pane, void* userData, ModError* outError) {
auto& callback = *static_cast<Callback*>(userData);
push_ui_handle(callback.vm->state, *callback.vm, pane, HandleKind::UiElement);
return call_build(callback, callback.refs[1], 1, "dialog build", outError);
}
void menu_selected(ModContext*, void* userData) {
auto& callback = *static_cast<Callback*>(userData);
call_callback(callback, callback.refs[0], 0, 0, "menu tab on_selected");
}
void list_pressed(ModContext*, UiListHandle list, uint64_t key, void* userData) {
auto& callback = *static_cast<Callback*>(userData);
push_ui_handle(callback.vm->state, *callback.vm, list, HandleKind::UiList);
lua_pushinteger64(callback.vm->state, static_cast<int64_t>(key));
call_callback(callback, callback.refs[0], 2, 0, "list on_pressed");
}
bool list_selected(ModContext*, UiListHandle list, uint64_t key, void* userData) {
auto& callback = *static_cast<Callback*>(userData);
push_ui_handle(callback.vm->state, *callback.vm, list, HandleKind::UiList);
lua_pushinteger64(callback.vm->state, static_cast<int64_t>(key));
return call_predicate(callback, callback.refs[1], 2, "list is_selected");
}
bool list_disabled(ModContext*, UiListHandle list, uint64_t key, void* userData) {
auto& callback = *static_cast<Callback*>(userData);
push_ui_handle(callback.vm->state, *callback.vm, list, HandleKind::UiList);
lua_pushinteger64(callback.vm->state, static_cast<int64_t>(key));
return call_predicate(callback, callback.refs[2], 2, "list is_disabled");
}
std::vector<std::string> string_array(lua_State* state, int table, const char* field) {
std::vector<std::string> result;
lua_getfield(state, table, field);
if (lua_isnil(state, -1)) {
lua_pop(state, 1);
return result;
}
luaL_checktype(state, -1, LUA_TTABLE);
const int count = lua_objlen(state, -1);
result.reserve(count);
for (int i = 1; i <= count; ++i) {
lua_rawgeti(state, -1, i);
result.emplace_back(luaL_checkstring(state, -1));
lua_pop(state, 1);
}
lua_pop(state, 1);
return result;
}
std::vector<UiListItem> list_items(lua_State* state, int table, std::vector<std::string>& labels) {
luaL_checktype(state, table, LUA_TTABLE);
const int count = lua_objlen(state, table);
labels.reserve(count);
std::vector<UiListItem> items;
items.reserve(count);
for (int i = 1; i <= count; ++i) {
lua_rawgeti(state, table, i);
luaL_checktype(state, -1, LUA_TTABLE);
labels.push_back(get_optional_string(state, -1, "label"));
UiListItem item = UI_LIST_ITEM_INIT;
item.key = static_cast<uint64_t>(get_optional_int(state, -1, "key", 0));
items.push_back(item);
lua_pop(state, 1);
}
for (size_t i = 0; i < items.size(); ++i) {
items[i].label = labels[i].c_str();
}
return items;
}
UiControlKind control_kind(lua_State* state, const std::string& kind) {
if (kind == "button")
return UI_CONTROL_BUTTON;
if (kind == "toggle")
return UI_CONTROL_TOGGLE;
if (kind == "number")
return UI_CONTROL_NUMBER;
if (kind == "string")
return UI_CONTROL_STRING;
if (kind == "select")
return UI_CONTROL_SELECT;
if (kind == "color")
return UI_CONTROL_COLOR;
if (kind == "group")
return UI_CONTROL_GROUP;
if (kind == "file_picker")
return UI_CONTROL_FILE_PICKER;
luaL_error(state, "unknown UI control kind '%s'", kind.c_str());
}
UiStyleScope style_scope(lua_State* state, const std::string& scope) {
if (scope == "prelaunch")
return UI_SCOPE_PRELAUNCH;
if (scope == "window")
return UI_SCOPE_WINDOW;
if (scope == "menu_bar")
return UI_SCOPE_MENU_BAR;
if (scope == "overlay")
return UI_SCOPE_OVERLAY;
if (scope == "touch_controls")
return UI_SCOPE_TOUCH_CONTROLS;
if (scope == "graphics_tuner")
return UI_SCOPE_GRAPHICS_TUNER;
luaL_error(state, "unknown UI style scope '%s'", scope.c_str());
}
int pane_add_section(lua_State* state) {
auto& pane = check_ui_handle(state, 1, HandleKind::UiElement);
check_result(state,
svc_ui->pane_add_section(pane.vm->subject, pane.value, luaL_checkstring(state, 2)),
"ui pane_add_section");
return 0;
}
int pane_add_text(lua_State* state) {
auto& pane = check_ui_handle(state, 1, HandleKind::UiElement);
UiElementHandle element = 0;
check_result(state,
svc_ui->pane_add_text(pane.vm->subject, pane.value, luaL_checkstring(state, 2), &element),
"ui pane_add_text");
push_ui_handle(state, *pane.vm, element, HandleKind::UiElement);
return 1;
}
int pane_add_rml(lua_State* state) {
auto& pane = check_ui_handle(state, 1, HandleKind::UiElement);
UiElementHandle element = 0;
check_result(state,
svc_ui->pane_add_rml(pane.vm->subject, pane.value, luaL_checkstring(state, 2), &element),
"ui pane_add_rml");
push_ui_handle(state, *pane.vm, element, HandleKind::UiElement);
return 1;
}
int pane_add_progress(lua_State* state) {
auto& pane = check_ui_handle(state, 1, HandleKind::UiElement);
UiElementHandle element = 0;
check_result(state,
svc_ui->pane_add_progress(
pane.vm->subject, pane.value, static_cast<float>(luaL_checknumber(state, 2)), &element),
"ui pane_add_progress");
push_ui_handle(state, *pane.vm, element, HandleKind::UiElement);
return 1;
}
int pane_add_control(lua_State* state) {
auto& pane = check_ui_handle(state, 1, HandleKind::UiElement);
luaL_checktype(state, 2, LUA_TTABLE);
UiControlDesc desc = UI_CONTROL_DESC_INIT;
const std::string kind = get_optional_string(state, 2, "kind");
const std::string label = get_optional_string(state, 2, "label");
const std::string help = get_optional_string(state, 2, "help_rml");
const std::string prefix = get_optional_string(state, 2, "prefix");
const std::string suffix = get_optional_string(state, 2, "suffix");
desc.kind = control_kind(state, kind);
desc.label = label.c_str();
desc.help_rml = help.empty() ? nullptr : help.c_str();
desc.min = get_optional_int(state, 2, "min", 0);
desc.max = get_optional_int(state, 2, "max", 0);
desc.step = get_optional_int(state, 2, "step", 1);
desc.prefix = prefix.empty() ? nullptr : prefix.c_str();
desc.suffix = suffix.empty() ? nullptr : suffix.c_str();
desc.max_length = static_cast<int32_t>(get_optional_int(state, 2, "max_length", 0));
desc.color_alpha = get_optional_bool(state, 2, "color_alpha", false);
desc.directory_mode = get_optional_bool(state, 2, "directory_mode", false);
desc.string_set_mode = get_optional_string(state, 2, "string_set_mode") == "change" ?
UI_STRING_SET_ON_CHANGE :
UI_STRING_SET_ON_COMMIT;
std::vector<std::string> options = string_array(state, 2, "options");
std::vector<const char*> optionPointers;
optionPointers.reserve(options.size());
for (const auto& option : options)
optionPointers.push_back(option.c_str());
desc.options = optionPointers.data();
desc.option_count = optionPointers.size();
std::vector<std::string> presets = string_array(state, 2, "color_presets");
std::vector<const char*> presetPointers;
presetPointers.reserve(presets.size());
for (const auto& preset : presets)
presetPointers.push_back(preset.c_str());
desc.color_presets = presetPointers.data();
desc.color_preset_count = presetPointers.size();
std::vector<std::string> filterNames;
std::vector<std::string> filterPatterns;
std::vector<FileFilter> filters;
lua_getfield(state, 2, "file_filters");
if (!lua_isnil(state, -1)) {
luaL_checktype(state, -1, LUA_TTABLE);
const int count = lua_objlen(state, -1);
filterNames.reserve(count);
filterPatterns.reserve(count);
filters.resize(count);
for (int i = 1; i <= count; ++i) {
lua_rawgeti(state, -1, i);
filterNames.push_back(get_optional_string(state, -1, "name"));
filterPatterns.push_back(get_optional_string(state, -1, "pattern"));
lua_pop(state, 1);
}
for (int i = 0; i < count; ++i) {
filters[i] = {filterNames[i].c_str(), filterPatterns[i].c_str()};
}
}
lua_pop(state, 1);
desc.file_filters = filters.data();
desc.file_filter_count = filters.size();
Callback& callback = retain_callback(*pane.vm);
callback.tag = desc.kind;
callback.refs[2] = ref_optional_function(state, 2, "on_pressed");
callback.refs[3] = ref_optional_function(state, 2, "is_disabled");
callback.refs[4] = ref_optional_function(state, 2, "is_modified");
callback.refs[5] = ref_optional_function(state, 2, "is_selected");
desc.user_data = &callback;
desc.on_pressed = callback.refs[2] != LUA_NOREF ? control_pressed : nullptr;
desc.is_disabled = callback.refs[3] != LUA_NOREF ? control_disabled : nullptr;
desc.is_modified = callback.refs[4] != LUA_NOREF ? control_modified : nullptr;
desc.is_selected = callback.refs[5] != LUA_NOREF ? control_selected : nullptr;
lua_getfield(state, 2, "config_var");
if (!lua_isnil(state, -1)) {
auto& variable = check_config_var(state, -1);
desc.binding = UI_BINDING_CONFIG_VAR;
desc.config_var = variable.value;
} else if (desc.kind != UI_CONTROL_BUTTON && desc.kind != UI_CONTROL_GROUP) {
desc.binding = UI_BINDING_CALLBACKS;
callback.refs[0] = ref_required_function(state, 2, "get");
callback.refs[1] = ref_required_function(state, 2, "set");
desc.get = control_get;
desc.set = control_set;
}
lua_pop(state, 1);
UiElementHandle element = 0;
check_result(state, svc_ui->pane_add_control(pane.vm->subject, pane.value, &desc, &element),
"ui pane_add_control");
push_ui_handle(state, *pane.vm, element, HandleKind::UiElement);
return 1;
}
int pane_add_group(lua_State* state) {
auto& pane = check_ui_handle(state, 1, HandleKind::UiElement);
auto& target = check_ui_handle(state, 2, HandleKind::UiElement);
luaL_checktype(state, 3, LUA_TTABLE);
const std::string label = get_optional_string(state, 3, "label");
Callback& callback = retain_callback(*pane.vm);
callback.refs[0] = ref_required_function(state, 3, "build");
UiGroupDesc desc = UI_GROUP_DESC_INIT;
desc.label = label.c_str();
desc.build = group_build;
desc.user_data = &callback;
UiElementHandle element = 0;
check_result(state,
svc_ui->pane_add_group(pane.vm->subject, pane.value, target.value, &desc, &element),
"ui pane_add_group");
push_ui_handle(state, *pane.vm, element, HandleKind::UiElement);
return 1;
}
int pane_add_list(lua_State* state) {
auto& pane = check_ui_handle(state, 1, HandleKind::UiElement);
luaL_checktype(state, 2, LUA_TTABLE);
Callback& callback = retain_callback(*pane.vm);
callback.refs[0] = ref_required_function(state, 2, "on_pressed");
callback.refs[1] = ref_optional_function(state, 2, "is_selected");
callback.refs[2] = ref_optional_function(state, 2, "is_disabled");
std::vector<std::string> labels;
std::vector<UiListItem> items;
lua_getfield(state, 2, "items");
if (!lua_isnil(state, -1))
items = list_items(state, -1, labels);
lua_pop(state, 1);
UiListDesc desc = UI_LIST_DESC_INIT;
desc.items = items.data();
desc.item_count = items.size();
desc.on_pressed = list_pressed;
desc.is_selected = callback.refs[1] != LUA_NOREF ? list_selected : nullptr;
desc.is_disabled = callback.refs[2] != LUA_NOREF ? list_disabled : nullptr;
desc.user_data = &callback;
UiListHandle list = 0;
check_result(state, svc_ui->pane_add_list(pane.vm->subject, pane.value, &desc, &list),
"ui pane_add_list");
push_ui_handle(state, *pane.vm, list, HandleKind::UiList);
return 1;
}
int element_set_text(lua_State* state) {
auto& element = check_ui_handle(state, 1, HandleKind::UiElement);
check_result(state,
svc_ui->elem_set_text(element.vm->subject, element.value, luaL_checkstring(state, 2)),
"ui elem_set_text");
return 0;
}
int element_set_rml(lua_State* state) {
auto& element = check_ui_handle(state, 1, HandleKind::UiElement);
check_result(state,
svc_ui->elem_set_rml(element.vm->subject, element.value, luaL_checkstring(state, 2)),
"ui elem_set_rml");
return 0;
}
int element_set_progress(lua_State* state) {
auto& element = check_ui_handle(state, 1, HandleKind::UiElement);
check_result(state,
svc_ui->elem_set_progress(
element.vm->subject, element.value, static_cast<float>(luaL_checknumber(state, 2))),
"ui elem_set_progress");
return 0;
}
int element_set_class(lua_State* state) {
auto& element = check_ui_handle(state, 1, HandleKind::UiElement);
check_result(state,
svc_ui->elem_set_class(element.vm->subject, element.value, luaL_checkstring(state, 2),
luaL_checkboolean(state, 3) != 0),
"ui elem_set_class");
return 0;
}
int list_set_items(lua_State* state) {
auto& list = check_ui_handle(state, 1, HandleKind::UiList);
std::vector<std::string> labels;
auto items = list_items(state, 2, labels);
check_result(state,
svc_ui->list_set_items(list.vm->subject, list.value, items.data(), items.size()),
"ui list_set_items");
return 0;
}
int window_close(lua_State* state) {
auto& window = check_ui_handle(state, 1, HandleKind::UiWindow);
check_result(state, svc_ui->window_close(window.vm->subject, window.value), "ui window_close");
window.value = 0;
return 0;
}
int dialog_close(lua_State* state) {
auto& dialog = check_ui_handle(state, 1, HandleKind::UiDialog);
check_result(state, svc_ui->dialog_close(dialog.vm->subject, dialog.value), "ui dialog_close");
dialog.value = 0;
return 0;
}
int dialog_set_body(lua_State* state) {
auto& dialog = check_ui_handle(state, 1, HandleKind::UiDialog);
check_result(state,
svc_ui->dialog_set_body(dialog.vm->subject, dialog.value, luaL_checkstring(state, 2)),
"ui dialog_set_body");
return 0;
}
int dialog_set_icon(lua_State* state) {
auto& dialog = check_ui_handle(state, 1, HandleKind::UiDialog);
check_result(state,
svc_ui->dialog_set_icon(dialog.vm->subject, dialog.value, luaL_checkstring(state, 2)),
"ui dialog_set_icon");
return 0;
}
int style_unregister(lua_State* state) {
auto& style = check_ui_handle(state, 1, HandleKind::UiStyle);
check_result(
state, svc_ui->unregister_styles(style.vm->subject, style.value), "ui unregister_styles");
style.value = 0;
return 0;
}
int menu_tab_unregister(lua_State* state) {
auto& tab = check_ui_handle(state, 1, HandleKind::UiMenuTab);
check_result(
state, svc_ui->unregister_menu_tab(tab.vm->subject, tab.value), "ui unregister_menu_tab");
tab.value = 0;
return 0;
}
int register_mods_panel(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
luaL_checktype(state, 1, LUA_TTABLE);
Callback& callback = retain_callback(vm);
callback.refs[0] = ref_required_function(state, 1, "build");
callback.refs[1] = ref_optional_function(state, 1, "update");
UiModsPanelDesc desc = UI_MODS_PANEL_DESC_INIT;
desc.build = panel_build;
desc.update = callback.refs[1] != LUA_NOREF ? panel_update : nullptr;
desc.user_data = &callback;
check_result(state, svc_ui->register_mods_panel(vm.subject, &desc), "ui register_mods_panel");
return 0;
}
int window_push(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
luaL_checktype(state, 1, LUA_TTABLE);
std::vector<UiTabDesc> tabs;
std::vector<std::string> titles;
lua_getfield(state, 1, "tabs");
luaL_checktype(state, -1, LUA_TTABLE);
const int tabCount = lua_objlen(state, -1);
tabs.reserve(tabCount);
titles.reserve(tabCount);
for (int i = 1; i <= tabCount; ++i) {
lua_rawgeti(state, -1, i);
titles.push_back(get_optional_string(state, -1, "title"));
Callback& callback = retain_callback(vm);
callback.refs[0] = ref_required_function(state, -1, "build");
callback.refs[1] = ref_optional_function(state, -1, "update");
UiTabDesc tab = UI_TAB_DESC_INIT;
tab.build = tab_build;
tab.update = callback.refs[1] != LUA_NOREF ? panel_update : nullptr;
tab.user_data = &callback;
tabs.push_back(tab);
lua_pop(state, 1);
}
lua_pop(state, 1);
for (size_t i = 0; i < tabs.size(); ++i)
tabs[i].title = titles[i].c_str();
const std::string rcss = get_optional_string(state, 1, "rcss");
Callback& closed = retain_callback(vm);
closed.refs[0] = ref_optional_function(state, 1, "on_closed");
UiWindowDesc desc = UI_WINDOW_DESC_INIT;
desc.tabs = tabs.data();
desc.tab_count = tabs.size();
desc.rcss = rcss.empty() ? nullptr : rcss.c_str();
desc.on_closed = closed.refs[0] != LUA_NOREF ? window_closed : nullptr;
desc.user_data = &closed;
UiWindowHandle window = 0;
check_result(state, svc_ui->window_push(vm.subject, &desc, &window), "ui window_push");
push_ui_handle(state, vm, window, HandleKind::UiWindow);
return 1;
}
UiDialogVariant dialog_variant(lua_State* state, const std::string& variant) {
if (variant.empty() || variant == "normal")
return UI_DIALOG_NORMAL;
if (variant == "warning")
return UI_DIALOG_WARNING;
if (variant == "danger")
return UI_DIALOG_DANGER;
luaL_error(state, "unknown dialog variant '%s'", variant.c_str());
}
int dialog_push(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
luaL_checktype(state, 1, LUA_TTABLE);
const std::string title = get_optional_string(state, 1, "title");
const std::string body = get_optional_string(state, 1, "body_rml");
const std::string icon = get_optional_string(state, 1, "icon");
const std::string variant = get_optional_string(state, 1, "variant");
std::vector<UiDialogAction> actions;
std::vector<std::string> labels;
lua_getfield(state, 1, "actions");
luaL_checktype(state, -1, LUA_TTABLE);
const int actionCount = lua_objlen(state, -1);
actions.reserve(actionCount);
labels.reserve(actionCount);
for (int i = 1; i <= actionCount; ++i) {
lua_rawgeti(state, -1, i);
labels.push_back(get_optional_string(state, -1, "label"));
Callback& callback = retain_callback(vm);
callback.refs[0] = ref_optional_function(state, -1, "on_pressed");
callback.refs[1] = ref_optional_function(state, -1, "is_disabled");
UiDialogAction action = UI_DIALOG_ACTION_INIT;
action.on_pressed = callback.refs[0] != LUA_NOREF ? dialog_action : nullptr;
action.is_disabled = callback.refs[1] != LUA_NOREF ? dialog_action_disabled : nullptr;
action.user_data = &callback;
action.keep_open = get_optional_bool(state, -1, "keep_open", false);
actions.push_back(action);
lua_pop(state, 1);
}
lua_pop(state, 1);
for (size_t i = 0; i < actions.size(); ++i)
actions[i].label = labels[i].c_str();
Callback& callback = retain_callback(vm);
callback.refs[0] = ref_optional_function(state, 1, "on_dismiss");
callback.refs[1] = ref_optional_function(state, 1, "build");
UiDialogDesc desc = UI_DIALOG_DESC_INIT;
desc.title = title.c_str();
desc.body_rml = body.c_str();
desc.icon = icon.empty() ? nullptr : icon.c_str();
desc.variant = dialog_variant(state, variant);
desc.actions = actions.data();
desc.action_count = actions.size();
desc.on_dismiss = callback.refs[0] != LUA_NOREF ? dialog_dismissed : nullptr;
desc.build = callback.refs[1] != LUA_NOREF ? dialog_build : nullptr;
desc.user_data = &callback;
UiDialogHandle dialog = 0;
check_result(state, svc_ui->dialog_push(vm.subject, &desc, &dialog), "ui dialog_push");
push_ui_handle(state, vm, dialog, HandleKind::UiDialog);
return 1;
}
int is_any_document_visible(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
bool visible = false;
check_result(
state, svc_ui->is_any_document_visible(vm.subject, &visible), "ui is_any_document_visible");
lua_pushboolean(state, visible);
return 1;
}
int register_styles(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
const auto scope = style_scope(state, luaL_checkstring(state, 1));
UiStyleHandle style = 0;
check_result(state,
svc_ui->register_styles(vm.subject, scope, luaL_checkstring(state, 2), &style),
"ui register_styles");
push_ui_handle(state, vm, style, HandleKind::UiStyle);
return 1;
}
int register_styles_file(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
const auto scope = style_scope(state, luaL_checkstring(state, 1));
UiStyleHandle style = 0;
check_result(state,
svc_ui->register_styles_file(vm.subject, scope, luaL_checkstring(state, 2), &style),
"ui register_styles_file");
push_ui_handle(state, vm, style, HandleKind::UiStyle);
return 1;
}
int register_menu_tab(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
luaL_checktype(state, 1, LUA_TTABLE);
const std::string label = get_optional_string(state, 1, "label");
Callback& callback = retain_callback(vm);
callback.refs[0] = ref_required_function(state, 1, "on_selected");
UiMenuTabDesc desc = UI_MENU_TAB_DESC_INIT;
desc.label = label.c_str();
desc.on_selected = menu_selected;
desc.user_data = &callback;
UiMenuTabHandle tab = 0;
check_result(state, svc_ui->register_menu_tab(vm.subject, &desc, &tab), "ui register_menu_tab");
push_ui_handle(state, vm, tab, HandleKind::UiMenuTab);
return 1;
}
int push_toast(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
luaL_checktype(state, 1, LUA_TTABLE);
const std::string type = get_optional_string(state, 1, "type");
const std::string title = get_optional_string(state, 1, "title_rml");
const std::string body = get_optional_string(state, 1, "body_rml");
UiToastDesc desc = UI_TOAST_DESC_INIT;
desc.type = type.empty() ? nullptr : type.c_str();
desc.title_rml = title.empty() ? nullptr : title.c_str();
desc.body_rml = body.empty() ? nullptr : body.c_str();
desc.duration_ms = static_cast<uint32_t>(get_optional_int(state, 1, "duration_ms", 0));
check_result(state, svc_ui->push_toast(vm.subject, &desc), "ui push_toast");
return 0;
}
int get_clipboard_text(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
size_t size = 0;
check_result(
state, svc_ui->get_clipboard_text(vm.subject, nullptr, 0, &size), "ui get_clipboard_text");
std::vector<char> text(size + 1);
check_result(state, svc_ui->get_clipboard_text(vm.subject, text.data(), text.size(), nullptr),
"ui get_clipboard_text");
lua_pushlstring(state, text.data(), size);
return 1;
}
int set_clipboard_text(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
check_result(state, svc_ui->set_clipboard_text(vm.subject, luaL_checkstring(state, 1)),
"ui set_clipboard_text");
return 0;
}
} // namespace
void push_ui_handle(lua_State* state, Vm& vm, uint64_t value, HandleKind kind) {
push_handle(state, vm, value, kind, ui_metatable(kind));
}
int open_ui(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
if (svc_ui == nullptr) {
service_unavailable(state, "UiService");
}
static const luaL_Reg kElementMethods[] = {
{"add_section", pane_add_section},
{"add_text", pane_add_text},
{"add_rml", pane_add_rml},
{"add_progress", pane_add_progress},
{"add_control", pane_add_control},
{"add_group", pane_add_group},
{"add_list", pane_add_list},
{"set_text", element_set_text},
{"set_rml", element_set_rml},
{"set_progress", element_set_progress},
{"set_class", element_set_class},
{nullptr, nullptr},
};
static const luaL_Reg kWindowMethods[] = {{"close", window_close}, {nullptr, nullptr}};
static const luaL_Reg kDialogMethods[] = {
{"close", dialog_close},
{"set_body", dialog_set_body},
{"set_icon", dialog_set_icon},
{nullptr, nullptr},
};
static const luaL_Reg kStyleMethods[] = {{"unregister", style_unregister}, {nullptr, nullptr}};
static const luaL_Reg kMenuMethods[] = {
{"unregister", menu_tab_unregister}, {nullptr, nullptr}};
static const luaL_Reg kListMethods[] = {{"set_items", list_set_items}, {nullptr, nullptr}};
create_handle_metatable(state, kUiElementMetatable, kElementMethods, "UiElement");
create_handle_metatable(state, kUiWindowMetatable, kWindowMethods, "UiWindow");
create_handle_metatable(state, kUiDialogMetatable, kDialogMethods, "UiDialog");
create_handle_metatable(state, kUiStyleMetatable, kStyleMethods, "UiStyle");
create_handle_metatable(state, kUiMenuTabMetatable, kMenuMethods, "UiMenuTab");
create_handle_metatable(state, kUiListMetatable, kListMethods, "UiList");
lua_newtable(state);
set_function(state, vm, "register_mods_panel", register_mods_panel);
set_function(state, vm, "window_push", window_push);
set_function(state, vm, "dialog_push", dialog_push);
set_function(state, vm, "is_any_document_visible", is_any_document_visible);
set_function(state, vm, "register_styles", register_styles);
set_function(state, vm, "register_styles_file", register_styles_file);
set_function(state, vm, "register_menu_tab", register_menu_tab);
set_function(state, vm, "push_toast", push_toast);
set_function(state, vm, "get_clipboard_text", get_clipboard_text);
set_function(state, vm, "set_clipboard_text", set_clipboard_text);
lua_setreadonly(state, -1, true);
return 1;
}
} // namespace luau_runtime
+1
Submodule mods/randomizer added at 641c9c9e0b