mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-09-12 19:36:01 -04:00
Merge main
This commit is contained in:
@@ -17,5 +17,4 @@ add_mod(ao_mod
|
||||
SOURCES src/mod.cpp
|
||||
MOD_JSON mod.json
|
||||
RES_DIR res
|
||||
BUNDLE
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
Submodule
+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)
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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(¤t.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, ¤t.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], ¤t.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};
|
||||
@@ -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(¤t.pos, &old.pos, this, 1, &mAcchCir, &speed, ¤t.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, ¤t.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, ¤t.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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
Submodule
+1
Submodule mods/randomizer added at 641c9c9e0b
Reference in New Issue
Block a user