Update codebase with changes for Zelda 64: Recompiled 1.2

This commit is contained in:
Mr-Wiseguy
2025-06-03 01:14:09 -04:00
parent 6a10095c88
commit 1e19dad587
71 changed files with 4913 additions and 674 deletions
+82 -18
View File
@@ -1,5 +1,10 @@
cmake_minimum_required(VERSION 3.20)
project(BanjoRecompiled)
if (APPLE)
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "Minimum OS X deployment version")
endif()
project(BanjoRecompiled LANGUAGES C CXX)
set(CMAKE_C_STANDARD 17)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -16,6 +21,14 @@ if (WIN32)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR")
endif()
if (APPLE)
enable_language(OBJC OBJCXX)
endif()
if (CMAKE_SYSTEM_NAME MATCHES "Linux")
option(RECOMP_FLATPAK "Configure the build for Flatpak compatibility." OFF)
endif()
# Avoid warning about DOWNLOAD_EXTRACT_TIMESTAMP in CMake 3.24:
if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0")
cmake_policy(SET CMP0135 NEW)
@@ -32,6 +45,10 @@ set(RT64_STATIC TRUE)
set(RT64_SDL_WINDOW_VULKAN TRUE)
add_compile_definitions(HLSL_CPU)
if (RECOMP_FLATPAK)
add_compile_definitions(RECOMP_FLATPAK)
endif()
add_subdirectory(${CMAKE_SOURCE_DIR}/lib/rt64 ${CMAKE_BINARY_DIR}/rt64)
# set(BUILD_SHARED_LIBS_SAVED "${BUILD_SHARED_LIBS}")
@@ -42,6 +59,7 @@ add_subdirectory(${CMAKE_SOURCE_DIR}/lib/lunasvg)
SET(RMLUI_SVG_PLUGIN ON CACHE BOOL "" FORCE)
SET(RMLUI_TESTS_ENABLED OFF CACHE BOOL "" FORCE)
add_subdirectory(${CMAKE_SOURCE_DIR}/lib/RmlUi)
target_compile_definitions(rmlui_core PRIVATE LUNASVG_BUILD_STATIC)
add_subdirectory(${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime)
@@ -110,7 +128,7 @@ add_custom_target(PatchesBin
# Generate patches_bin.c from patches.bin
add_custom_command(OUTPUT ${CMAKE_SOURCE_DIR}/RecompiledPatches/patches_bin.c
COMMAND file_to_c ${CMAKE_SOURCE_DIR}/patches/patches.bin bk_patches_bin ${CMAKE_SOURCE_DIR}/RecompiledPatches/patches_bin.c ${CMAKE_SOURCE_DIR}/RecompiledPatches/patches_bin.h
COMMAND file_to_c ${CMAKE_SOURCE_DIR}/patches/patches.bin bk_patches_bin ${CMAKE_SOURCE_DIR}/RecompiledPatches/patches_bin.c ${CMAKE_SOURCE_DIR}/RecompiledPatches/patches_bin.h
DEPENDS ${CMAKE_SOURCE_DIR}/patches/patches.bin
)
@@ -126,22 +144,12 @@ add_custom_command(OUTPUT
DEPENDS ${CMAKE_SOURCE_DIR}/patches/patches.elf
)
# Download controller db file for controller support via SDL2
set(GAMECONTROLLERDB_COMMIT "b1e4090b3d4266e55feb0793efa35792e05faf66")
set(GAMECONTROLLERDB_URL "https://raw.githubusercontent.com/gabomdq/SDL_GameControllerDB/${GAMECONTROLLERDB_COMMIT}/gamecontrollerdb.txt")
file(DOWNLOAD ${GAMECONTROLLERDB_URL} ${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt
TLS_VERIFY ON)
add_custom_target(DownloadGameControllerDB
DEPENDS ${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt)
# Main executable
add_executable(BanjoRecompiled)
add_dependencies(BanjoRecompiled DownloadGameControllerDB)
set (SOURCES
${CMAKE_SOURCE_DIR}/src/main/main.cpp
${CMAKE_SOURCE_DIR}/src/main/support.cpp
${CMAKE_SOURCE_DIR}/src/main/register_overlays.cpp
${CMAKE_SOURCE_DIR}/src/main/register_patches.cpp
${CMAKE_SOURCE_DIR}/src/main/rt64_render_context.cpp
@@ -151,20 +159,26 @@ set (SOURCES
${CMAKE_SOURCE_DIR}/src/game/config.cpp
${CMAKE_SOURCE_DIR}/src/game/debug.cpp
${CMAKE_SOURCE_DIR}/src/game/recomp_api.cpp
${CMAKE_SOURCE_DIR}/src/game/recomp_mem_api.cpp
${CMAKE_SOURCE_DIR}/src/game/recomp_actor_api.cpp
${CMAKE_SOURCE_DIR}/src/game/recomp_data_api.cpp
${CMAKE_SOURCE_DIR}/src/game/rom_decompression.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_renderer.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_state.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_launcher.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_config.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_prompt.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_config_sub_menu.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_color_hack.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_rml_hacks.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_elements.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_mod_details_panel.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_mod_installer.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_mod_menu.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_api.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_api_events.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_api_images.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_utils.cpp
${CMAKE_SOURCE_DIR}/src/ui/util/hsv.cpp
${CMAKE_SOURCE_DIR}/src/ui/core/ui_context.cpp
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_button.cpp
@@ -176,6 +190,7 @@ set (SOURCES
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_radio.cpp
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_scroll_container.cpp
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_slider.cpp
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_span.cpp
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_style.cpp
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_text_input.cpp
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_toggle.cpp
@@ -185,6 +200,10 @@ set (SOURCES
${CMAKE_SOURCE_DIR}/lib/RmlUi/Backends/RmlUi_Platform_SDL.cpp
)
if (APPLE)
list(APPEND SOURCES ${CMAKE_SOURCE_DIR}/src/main/support_apple.mm)
endif()
target_include_directories(BanjoRecompiled PRIVATE
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/N64Recomp/include
@@ -221,6 +240,12 @@ endif()
if (MSVC)
# Disable identical code folding, since this breaks mod function patching as multiple functions can get merged into one.
target_link_options(BanjoRecompiled PRIVATE /OPT:NOICF)
elseif (APPLE)
# Use a wrapper around ld64 that respects segprot's `max_prot` value in order
# to make our executable memory writable (required for mod function patching)
target_link_options(BanjoRecompiled PRIVATE
"-fuse-ld=${CMAKE_SOURCE_DIR}/.github/macos/ld64"
)
endif()
if (WIN32)
@@ -263,6 +288,20 @@ if (WIN32)
)
# target_sources(BanjoRecompiled PRIVATE ${CMAKE_SOURCE_DIR}/icons/app.rc)
target_link_libraries(BanjoRecompiled PRIVATE SDL2)
endif()
if (APPLE)
find_package(SDL2 REQUIRED)
target_include_directories(BanjoRecompiled PRIVATE ${SDL2_INCLUDE_DIRS})
set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
find_package(Threads REQUIRED)
target_link_libraries(BanjoRecompiled PRIVATE ${CMAKE_DL_LIBS} Threads::Threads SDL2::SDL2)
include(${CMAKE_SOURCE_DIR}/.github/macos/apple_bundle.cmake)
endif()
if (CMAKE_SYSTEM_NAME MATCHES "Linux")
@@ -273,7 +312,7 @@ if (CMAKE_SYSTEM_NAME MATCHES "Linux")
# Generate icon_bytes.c from the app icon PNG.
add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.c ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.h
COMMAND file_to_c ${CMAKE_SOURCE_DIR}/icons/512.png icon_bytes ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.c ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.h
COMMAND file_to_c ${CMAKE_SOURCE_DIR}/icons/512.png icon_bytes ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.c ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.h
DEPENDS ${CMAKE_SOURCE_DIR}/icons/512.png
)
target_sources(BanjoRecompiled PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.c)
@@ -298,7 +337,7 @@ if (CMAKE_SYSTEM_NAME MATCHES "Linux")
message(STATUS "FREETYPE_LIBRARIES = ${FREETYPE_LIBRARIES}")
include_directories(${FREETYPE_LIBRARIES})
target_link_libraries(BanjoRecompiled PRIVATE ${FREETYPE_LIBRARIES})
target_link_libraries(BanjoRecompiled PRIVATE ${FREETYPE_LIBRARIES} SDL2::SDL2)
set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
@@ -310,7 +349,6 @@ endif()
target_link_libraries(BanjoRecompiled PRIVATE
PatchesLib
RecompiledFuncs
SDL2
librecomp
ultramodern
rt64
@@ -338,9 +376,14 @@ else()
if (APPLE)
# Apple's binary is universal, so it'll work on both x86_64 and arm64
set (DXC "DYLD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/lib/arm64" "${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/arm64/dxc-macos")
if(CMAKE_SIZEOF_VOID_P EQUAL 8 AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|amd64|AMD64")
set(SPIRVCROSS "DYLD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/spirv-cross/lib/x64" "${PROJECT_SOURCE_DIR}/lib/rt64//src/contrib/spirv-cross/bin/x64/spirv-cross")
else()
set(SPIRVCROSS "DYLD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/spirv-cross/lib/arm64" "${PROJECT_SOURCE_DIR}/lib/rt64//src/contrib/spirv-cross/bin/x64/spirv-cross")
endif()
else()
if(CMAKE_SIZEOF_VOID_P EQUAL 8 AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|amd64|AMD64")
set (DXC "LD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/lib/x64" "${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/x64/dxc")
set (DXC "LD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/lib/x64" "${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/x64/dxc-linux")
else()
set (DXC "LD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/lib/arm64" "${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/arm64/dxc-linux")
endif()
@@ -350,6 +393,27 @@ endif()
build_vertex_shader(BanjoRecompiled "shaders/InterfaceVS.hlsl" "shaders/InterfaceVS.hlsl")
build_pixel_shader(BanjoRecompiled "shaders/InterfacePS.hlsl" "shaders/InterfacePS.hlsl")
# Embed all .nrm files in the "mods" directory
file(GLOB NRM_FILES "${CMAKE_SOURCE_DIR}/mods/*.nrm")
set(GENERATED_NRM_SOURCES "")
foreach(NRM_FILE ${NRM_FILES})
get_filename_component(NRM_NAME ${NRM_FILE} NAME_WE)
set(OUT_C "${CMAKE_CURRENT_BINARY_DIR}/mods/${NRM_NAME}.c")
set(OUT_H "${CMAKE_CURRENT_BINARY_DIR}/mods/${NRM_NAME}.h")
add_custom_command(
OUTPUT ${OUT_C} ${OUT_H}
COMMAND file_to_c ${NRM_FILE} ${NRM_NAME} ${OUT_C} ${OUT_H}
DEPENDS ${NRM_FILE}
)
list(APPEND GENERATED_NRM_SOURCES ${OUT_C})
endforeach()
target_sources(BanjoRecompiled PRIVATE ${GENERATED_NRM_SOURCES})
target_sources(BanjoRecompiled PRIVATE ${SOURCES})
set_property(TARGET BanjoRecompiled PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
+15 -3
View File
@@ -14,6 +14,8 @@ namespace RT64 {
namespace banjo {
namespace renderer {
inline const std::string special_option_texture_pack_enabled = "_recomp_texture_pack_enabled";
class RT64Context final : public ultramodern::renderer::RendererContext {
public:
~RT64Context() override;
@@ -30,9 +32,12 @@ namespace banjo {
uint32_t get_display_framerate() const override;
float get_resolution_scale() const override;
protected:
private:
std::unique_ptr<RT64::Application> app;
std::unordered_set<std::filesystem::path> enabled_texture_packs;
std::unordered_set<std::string> enabled_texture_packs;
std::unordered_set<std::string> secondary_disabled_texture_packs;
void check_texture_pack_actions();
};
std::unique_ptr<ultramodern::renderer::RendererContext> create_render_context(uint8_t *rdram, ultramodern::renderer::WindowHandle window_handle, bool developer_mode);
@@ -41,8 +46,15 @@ namespace banjo {
bool RT64SamplePositionsSupported();
bool RT64HighPrecisionFBEnabled();
void enable_texture_pack(const recomp::mods::ModHandle& mod);
void trigger_texture_pack_update();
void enable_texture_pack(const recomp::mods::ModContext& context, const recomp::mods::ModHandle& mod);
void disable_texture_pack(const recomp::mods::ModHandle& mod);
void secondary_enable_texture_pack(const std::string& mod_id);
void secondary_disable_texture_pack(const std::string& mod_id);
// Texture pack enable option. Must be an enum with two options.
// The first option is treated as disabled and the second option is treated as enabled.
bool is_texture_pack_enable_config_option(const recomp::mods::ConfigOption& option, bool show_errors);
}
}
+26
View File
@@ -0,0 +1,26 @@
#ifndef __BANJO_SUPPORT_H__
#define __BANJO_SUPPORT_H__
#include <functional>
#include <filesystem>
#include <vector>
#include <optional>
#include <list>
namespace banjo {
std::filesystem::path get_program_path();
std::filesystem::path get_asset_path(const char* asset);
void open_file_dialog(std::function<void(bool success, const std::filesystem::path& path)> callback);
void open_file_dialog_multiple(std::function<void(bool success, const std::list<std::filesystem::path>& paths)> callback);
void show_error_message_box(const char *title, const char *message);
// Apple specific methods that usually require Objective-C. Implemented in support_apple.mm.
#ifdef __APPLE__
void dispatch_on_ui_thread(std::function<void()> func);
std::optional<std::filesystem::path> get_application_support_directory();
std::filesystem::path get_bundle_resource_directory();
std::filesystem::path get_bundle_directory();
#endif
}
#endif
+10
View File
@@ -0,0 +1,10 @@
#ifndef __OVERLOADED_H__
#define __OVERLOADED_H__
// Helper for std::visit
template<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
#endif
+3 -1
View File
@@ -1,9 +1,11 @@
#ifndef __RECOMP_DATA_H__
#define __RECOMP_DATA_H__
namespace recomp {
namespace recomputil {
void init_extended_actor_data();
void reset_actor_data();
void register_data_api_exports();
}
#endif
+45 -15
View File
@@ -4,6 +4,7 @@
#include <memory>
#include <string>
#include <string_view>
#include <list>
// TODO move this file into src/ui
@@ -29,7 +30,7 @@ namespace recompui {
class MenuController {
public:
virtual ~MenuController() {}
virtual Rml::ElementDocument* load_document(Rml::Context* context) = 0;
virtual void load_document() = 0;
virtual void register_events(UiEventListenerInstancer& listener) = 0;
virtual void make_bindings(Rml::Context* context) = 0;
};
@@ -49,13 +50,14 @@ namespace recompui {
void hide_context(ContextId context);
void hide_all_contexts();
bool is_context_shown(ContextId context);
bool is_context_taking_input();
bool is_context_capturing_input();
bool is_context_capturing_mouse();
bool is_any_context_shown();
ContextId try_close_current_context();
ContextId get_launcher_context_id();
ContextId get_config_context_id();
ContextId get_config_sub_menu_context_id();
ContextId get_close_prompt_context_id();
enum class ConfigTab {
General,
@@ -67,6 +69,11 @@ namespace recompui {
};
void set_config_tab(ConfigTab tab);
int config_tab_to_index(ConfigTab tab);
Rml::ElementTabSet* get_config_tabset();
Rml::Element* get_mod_tab();
void set_config_tabset_mod_nav();
void focus_mod_configure_button();
enum class ButtonVariant {
Primary,
@@ -78,19 +85,37 @@ namespace recompui {
NumVariants,
};
void open_prompt(
const std::string& headerText,
const std::string& contentText,
const std::string& confirmLabelText,
const std::string& cancelLabelText,
std::function<void()> confirmCb,
std::function<void()> cancelCb,
ButtonVariant _confirmVariant = ButtonVariant::Success,
ButtonVariant _cancelVariant = ButtonVariant::Error,
bool _focusOnCancel = true,
const std::string& _returnElementId = ""
void init_styling(const std::filesystem::path& rcss_file);
void init_prompt_context();
void open_choice_prompt(
const std::string& header_text,
const std::string& content_text,
const std::string& confirm_label_text,
const std::string& cancel_label_text,
std::function<void()> confirm_action,
std::function<void()> cancel_action,
ButtonVariant confirm_variant = ButtonVariant::Success,
ButtonVariant cancel_variant = ButtonVariant::Error,
bool focus_on_cancel = true,
const std::string& return_element_id = ""
);
void open_info_prompt(
const std::string& header_text,
const std::string& content_text,
const std::string& okay_label_text,
std::function<void()> okay_action,
ButtonVariant okay_variant = ButtonVariant::Error,
const std::string& return_element_id = ""
);
void open_notification(
const std::string& header_text,
const std::string& content_text,
const std::string& return_element_id = ""
);
void close_prompt();
bool is_prompt_open();
void update_mod_list(bool scan_mods = true);
void process_game_started();
void apply_color_hack();
void get_window_size(int& width, int& height);
@@ -109,8 +134,13 @@ namespace recompui {
Rml::ElementPtr create_custom_element(Rml::Element* parent, std::string tag);
Rml::ElementDocument* load_document(const std::filesystem::path& path);
Rml::ElementDocument* create_empty_document();
void queue_image_from_bytes(const std::string &src, const std::vector<char> &bytes);
Rml::Element* get_child_by_tag(Rml::Element* parent, const std::string& tag);
void queue_image_from_bytes_rgba32(const std::string &src, const std::vector<char> &bytes, uint32_t width, uint32_t height);
void queue_image_from_bytes_file(const std::string &src, const std::vector<char> &bytes);
void release_image(const std::string &src);
void drop_files(const std::list<std::filesystem::path> &file_list);
}
#endif
+14
View File
@@ -0,0 +1,14 @@
#ifndef __MEM_FUNCS_H__
#define __MEM_FUNCS_H__
#include "patch_helpers.h"
DECLARE_FUNC(u32, recomp_register_actor_extension, u32 actor_type, u32 size);
DECLARE_FUNC(u32, recomp_register_actor_extension_generic, u32 size);
DECLARE_FUNC(void, recomp_clear_all_actor_data);
DECLARE_FUNC(u32, recomp_create_actor_data, u32 actor_type);
DECLARE_FUNC(void, recomp_destroy_actor_data, u32 actor_handle);
DECLARE_FUNC(void*, recomp_get_actor_data, u32 actor_handle, u32 extension_handle, u32 actor_type);
DECLARE_FUNC(u32, recomp_get_actor_spawn_index, u32 actor_handle);
#endif
-55
View File
@@ -1,60 +1,5 @@
#include "patches.h"
typedef struct Cube_s Cube;
RECOMP_PATCH bool viewport_cube_isInFrustum(Cube *cube) {
// f32 sp24[3];
// f32 sp18[3];
// sp24[0] = (f32) ((cube->x * 1000) - 150);
// sp24[1] = (f32) ((cube->y * 1000) - 150);
// sp24[2] = (f32) ((cube->z * 1000) - 150);
// sp18[0] = sp24[0] + 1300.0f;
// sp18[1] = sp24[1] + 1300.0f;
// sp18[2] = sp24[2] + 1300.0f;
// return func_8024D374(sp24, sp18);
return TRUE;
}
RECOMP_PATCH bool viewport_cube_isInFrustum2(Cube *cube) {
// f32 sp34[3];
// f32 sp28[3];
// f32 sp1C[3];
// if (cube->x == -0x10) {
// return TRUE;
// }
// sp1C[0] = (f32) ((cube->x * 1000) + 500) - viewportPosition[0];
// sp1C[1] = (f32) ((cube->y * 1000) + 500) - viewportPosition[1];
// sp1C[2] = (f32) ((cube->z * 1000) + 500) - viewportPosition[2];
// if (LENGTH_SQ_VEC3F(sp1C) > 1.6e7f) {
// return FALSE;
// }
// sp34[0] = (f32) ((cube->x * 1000) - 150);
// sp34[1] = (f32) ((cube->y * 1000) - 150);
// sp34[2] = (f32) ((cube->z * 1000) - 150);
// sp28[0] = sp34[0] + 1300.0f;
// sp28[1] = sp34[1] + 1300.0f;
// sp28[2] = sp34[2] + 1300.0f;
// return func_8024D374(sp34, sp28);
return TRUE;
}
RECOMP_PATCH bool viewport_func_8024DB50(f32 arg0[3], f32 arg1) {
// f32 sp3C[3];
// s32 i;
// sp3C[0] = arg0[0] - viewportPosition[0];
// sp3C[1] = arg0[1] - viewportPosition[1];
// sp3C[2] = arg0[2] - viewportPosition[2];
// for(i = 0; i < 4; i++){
// if(arg1 <= ml_dotProduct_vec3f(sp3C, D_80280ED0[i])){
// return FALSE;
// }
// }
return TRUE;
}
RECOMP_PATCH bool viewport_isBoundingBoxInFrustum(f32 arg0[3], f32 arg1[3]) {
return TRUE;
}
-1
View File
@@ -19,7 +19,6 @@ extern RecompAimingOverideMode recomp_aiming_override_mode;
DECLARE_FUNC(void, recomp_get_gyro_deltas, float* x, float* y);
DECLARE_FUNC(void, recomp_get_mouse_deltas, float* x, float* y);
DECLARE_FUNC(s32, recomp_get_targeting_mode);
DECLARE_FUNC(void, recomp_get_inverted_axes, s32* x, s32* y);
DECLARE_FUNC(s32, recomp_get_analog_cam_enabled);
DECLARE_FUNC(void, recomp_get_analog_inverted_axes, s32* x, s32* y);
+52
View File
@@ -0,0 +1,52 @@
#ifndef __UI_FUNCS_H__
#define __UI_FUNCS_H__
// These two enums must be kept in sync with src/ui/elements/ui_types.h!
typedef enum {
UI_EVENT_NONE,
UI_EVENT_CLICK,
UI_EVENT_FOCUS,
UI_EVENT_HOVER,
UI_EVENT_ENABLE,
UI_EVENT_DRAG,
UI_EVENT_RESERVED1, // Would be UI_EVENT_TEXT but text events aren't usable in mods currently
UI_EVENT_UPDATE,
UI_EVENT_COUNT
} RecompuiEventType;
typedef enum {
UI_DRAG_NONE,
UI_DRAG_START,
UI_DRAG_MOVE,
UI_DRAG_END
} RecompuiDragPhase;
typedef struct {
RecompuiEventType type;
union {
struct {
float x;
float y;
} click;
struct {
bool active;
} focus;
struct {
bool active;
} hover;
struct {
bool active;
} enable;
struct {
float x;
float y;
RecompuiDragPhase phase;
} drag;
} data;
} RecompuiEventData;
#endif
+6
View File
@@ -0,0 +1,6 @@
#ifndef __TRANSFORM_IDS_H__
#define __TRANSFORM_IDS_H__
#endif
+3
View File
@@ -0,0 +1,3 @@
#include "patches.h"
#include "transform_ids.h"
+9
View File
@@ -0,0 +1,9 @@
#ifndef __UI_FUNCS_INTERNAL_H__
#define __UI_FUNCS_INTERNAL_H__
#include "patch_helpers.h"
#include "recompui_event_structs.h"
DECLARE_FUNC(void, recomp_run_ui_callbacks);
#endif
+30 -8
View File
@@ -2,6 +2,7 @@
#include "recomp_input.h"
#include "banjo_sound.h"
#include "banjo_render.h"
#include "banjo_support.h"
#include "ultramodern/config.hpp"
#include "librecomp/files.hpp"
#include <filesystem>
@@ -13,6 +14,8 @@
#elif defined(__linux__)
#include <unistd.h>
#include <pwd.h>
#elif defined(__APPLE__)
#include "apple/rt64_apple.h"
#endif
constexpr std::u8string_view general_filename = u8"general.json";
@@ -71,7 +74,7 @@ T from_or_default(const json& j, const std::string& key, T default_value) {
else {
ret = default_value;
}
return ret;
}
@@ -129,11 +132,19 @@ namespace recomp {
}
std::filesystem::path banjo::get_app_folder_path() {
// directly check for portable.txt (windows and native linux binary)
// directly check for portable.txt (windows and native linux binary)
if (std::filesystem::exists("portable.txt")) {
return std::filesystem::current_path();
}
#if defined(__APPLE__)
// Check for portable file in the directory containing the app bundle.
const auto app_bundle_path = banjo::get_bundle_directory().parent_path();
if (std::filesystem::exists(app_bundle_path / "portable.txt")) {
return app_bundle_path;
}
#endif
std::filesystem::path recomp_dir{};
#if defined(_WIN32)
@@ -145,16 +156,27 @@ std::filesystem::path banjo::get_app_folder_path() {
}
CoTaskMemFree(known_path);
#elif defined(__linux__)
// check for APP_FOLDER_PATH env var used by AppImage
#elif defined(__linux__) || defined(__APPLE__)
// check for APP_FOLDER_PATH env var
if (getenv("APP_FOLDER_PATH") != nullptr) {
return std::filesystem::path{getenv("APP_FOLDER_PATH")};
}
#if defined(__APPLE__)
const auto supportdir = banjo::get_application_support_directory();
if (supportdir) {
return *supportdir / banjo::program_id;
}
#endif
const char *homedir;
if ((homedir = getenv("HOME")) == nullptr) {
#if defined(__linux__)
homedir = getpwuid(getuid())->pw_dir;
#elif defined(__APPLE__)
homedir = GetHomeDirectory();
#endif
}
if (homedir != nullptr) {
@@ -206,7 +228,7 @@ bool save_json_with_backups(const std::filesystem::path& path, const nlohmann::j
return recomp::finalize_output_file_with_backup(path);
}
bool save_general_config(const std::filesystem::path& path) {
bool save_general_config(const std::filesystem::path& path) {
nlohmann::json config_json{};
recomp::to_json(config_json["background_input_mode"], recomp::get_background_input_mode());
@@ -218,7 +240,7 @@ bool save_general_config(const std::filesystem::path& path) {
config_json["analog_cam_mode"] = banjo::get_analog_cam_mode();
config_json["analog_camera_invert_mode"] = banjo::get_analog_camera_invert_mode();
config_json["debug_mode"] = banjo::get_debug_mode_enabled();
return save_json_with_backups(path, config_json);
}
@@ -433,7 +455,7 @@ bool save_sound_config(const std::filesystem::path& path) {
config_json["main_volume"] = banjo::get_main_volume();
config_json["bgm_volume"] = banjo::get_bgm_volume();
return save_json_with_backups(path, config_json);
}
@@ -494,7 +516,7 @@ void banjo::save_config() {
}
std::filesystem::create_directories(recomp_dir);
// TODO error handling for failing to save config files.
save_general_config(recomp_dir / general_filename);
+129 -102
View File
@@ -31,7 +31,7 @@ static struct {
std::mutex cur_controllers_mutex;
std::vector<SDL_GameController*> cur_controllers{};
std::unordered_map<SDL_JoystickID, ControllerState> controller_states;
std::array<float, 2> rotation_delta{};
std::array<float, 2> mouse_delta{};
std::mutex pending_input_mutex;
@@ -42,6 +42,10 @@ static struct {
bool rumble_active;
} InputState;
static struct {
std::list<std::filesystem::path> files_dropped;
} DropState;
std::atomic<recomp::InputDevice> scanning_device = recomp::InputDevice::COUNT;
std::atomic<recomp::InputField> scanned_input;
@@ -93,85 +97,82 @@ bool should_override_keystate(SDL_Scancode key, SDL_Keymod mod) {
}
}
return false;
return false;
}
bool sdl_event_filter(void* userdata, SDL_Event* event) {
switch (event->type) {
case SDL_EventType::SDL_KEYDOWN:
{
SDL_KeyboardEvent* keyevent = &event->key;
{
SDL_KeyboardEvent* keyevent = &event->key;
// Skip repeated events when not in the menu
if (!recompui::is_context_taking_input() &&
event->key.repeat) {
break;
}
// Skip repeated events when not in the menu
if (!recompui::is_context_capturing_input() &&
event->key.repeat) {
break;
}
if ((keyevent->keysym.scancode == SDL_Scancode::SDL_SCANCODE_RETURN && (keyevent->keysym.mod & SDL_Keymod::KMOD_ALT)) ||
keyevent->keysym.scancode == SDL_Scancode::SDL_SCANCODE_F11
if ((keyevent->keysym.scancode == SDL_Scancode::SDL_SCANCODE_RETURN && (keyevent->keysym.mod & SDL_Keymod::KMOD_ALT)) ||
keyevent->keysym.scancode == SDL_Scancode::SDL_SCANCODE_F11
) {
recompui::toggle_fullscreen();
recompui::toggle_fullscreen();
}
if (scanning_device != recomp::InputDevice::COUNT) {
if (keyevent->keysym.scancode == SDL_Scancode::SDL_SCANCODE_ESCAPE) {
recomp::cancel_scanning_input();
}
if (scanning_device != recomp::InputDevice::COUNT) {
if (keyevent->keysym.scancode == SDL_Scancode::SDL_SCANCODE_ESCAPE) {
recomp::cancel_scanning_input();
} else if (scanning_device == recomp::InputDevice::Keyboard) {
set_scanned_input({(uint32_t)InputType::Keyboard, keyevent->keysym.scancode});
}
} else {
if (!should_override_keystate(keyevent->keysym.scancode, static_cast<SDL_Keymod>(keyevent->keysym.mod))) {
queue_if_enabled(event);
}
else if (scanning_device == recomp::InputDevice::Keyboard) {
set_scanned_input({ (uint32_t)InputType::Keyboard, keyevent->keysym.scancode });
}
}
break;
else {
if (!should_override_keystate(keyevent->keysym.scancode, static_cast<SDL_Keymod>(keyevent->keysym.mod))) {
queue_if_enabled(event);
}
}
}
break;
case SDL_EventType::SDL_CONTROLLERDEVICEADDED:
{
SDL_ControllerDeviceEvent* controller_event = &event->cdevice;
SDL_GameController* controller = SDL_GameControllerOpen(controller_event->which);
printf("Controller added: %d\n", controller_event->which);
if (controller != nullptr) {
printf(" Instance ID: %d\n", SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(controller)));
ControllerState& state = InputState.controller_states[SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(controller))];
state.controller = controller;
{
SDL_ControllerDeviceEvent* controller_event = &event->cdevice;
SDL_GameController* controller = SDL_GameControllerOpen(controller_event->which);
printf("Controller added: %d\n", controller_event->which);
if (controller != nullptr) {
printf(" Instance ID: %d\n", SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(controller)));
ControllerState& state = InputState.controller_states[SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(controller))];
state.controller = controller;
if (SDL_GameControllerHasSensor(controller, SDL_SensorType::SDL_SENSOR_GYRO) && SDL_GameControllerHasSensor(controller, SDL_SensorType::SDL_SENSOR_ACCEL)) {
SDL_GameControllerSetSensorEnabled(controller, SDL_SensorType::SDL_SENSOR_GYRO, SDL_TRUE);
SDL_GameControllerSetSensorEnabled(controller, SDL_SensorType::SDL_SENSOR_ACCEL, SDL_TRUE);
}
if (SDL_GameControllerHasSensor(controller, SDL_SensorType::SDL_SENSOR_GYRO) && SDL_GameControllerHasSensor(controller, SDL_SensorType::SDL_SENSOR_ACCEL)) {
SDL_GameControllerSetSensorEnabled(controller, SDL_SensorType::SDL_SENSOR_GYRO, SDL_TRUE);
SDL_GameControllerSetSensorEnabled(controller, SDL_SensorType::SDL_SENSOR_ACCEL, SDL_TRUE);
}
}
break;
}
break;
case SDL_EventType::SDL_CONTROLLERDEVICEREMOVED:
{
SDL_ControllerDeviceEvent* controller_event = &event->cdevice;
printf("Controller removed: %d\n", controller_event->which);
InputState.controller_states.erase(controller_event->which);
}
break;
{
SDL_ControllerDeviceEvent* controller_event = &event->cdevice;
printf("Controller removed: %d\n", controller_event->which);
InputState.controller_states.erase(controller_event->which);
}
break;
case SDL_EventType::SDL_QUIT: {
if (!ultramodern::is_game_started()) {
ultramodern::quit();
return true;
}
recompui::ContextId config_context_id = recompui::get_config_context_id();
if (!recompui::is_context_shown(config_context_id)) {
recompui::show_context(config_context_id, "");
}
banjo::open_quit_game_prompt();
recompui::activate_mouse();
break;
}
case SDL_EventType::SDL_MOUSEWHEEL:
{
SDL_MouseWheelEvent* wheel_event = &event->wheel;
InputState.mouse_wheel_pos.fetch_add(wheel_event->y * (wheel_event->direction == SDL_MOUSEWHEEL_FLIPPED ? -1 : 1));
}
queue_if_enabled(event);
break;
{
SDL_MouseWheelEvent* wheel_event = &event->wheel;
InputState.mouse_wheel_pos.fetch_add(wheel_event->y * (wheel_event->direction == SDL_MOUSEWHEEL_FLIPPED ? -1 : 1));
}
queue_if_enabled(event);
break;
case SDL_EventType::SDL_CONTROLLERBUTTONDOWN:
if (scanning_device != recomp::InputDevice::COUNT) {
auto menuToggleBinding0 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 0, recomp::InputDevice::Controller);
@@ -180,22 +181,24 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
if ((menuToggleBinding0.input_type != 0 && event->cbutton.button == menuToggleBinding0.input_id) ||
(menuToggleBinding1.input_type != 0 && event->cbutton.button == menuToggleBinding1.input_id)) {
recomp::cancel_scanning_input();
} else if (scanning_device == recomp::InputDevice::Controller) {
}
else if (scanning_device == recomp::InputDevice::Controller) {
SDL_ControllerButtonEvent* button_event = &event->cbutton;
auto scanned_input_index = recomp::get_scanned_input_index();
if ((scanned_input_index == static_cast<int>(recomp::GameInput::TOGGLE_MENU) ||
scanned_input_index == static_cast<int>(recomp::GameInput::ACCEPT_MENU) ||
scanned_input_index == static_cast<int>(recomp::GameInput::APPLY_MENU)) && (
button_event->button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_UP ||
button_event->button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_DOWN ||
button_event->button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_LEFT ||
button_event->button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) {
scanned_input_index == static_cast<int>(recomp::GameInput::ACCEPT_MENU) ||
scanned_input_index == static_cast<int>(recomp::GameInput::APPLY_MENU)) && (
button_event->button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_UP ||
button_event->button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_DOWN ||
button_event->button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_LEFT ||
button_event->button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) {
break;
}
set_scanned_input({(uint32_t)InputType::ControllerDigital, button_event->button});
set_scanned_input({ (uint32_t)InputType::ControllerDigital, button_event->button });
}
} else {
}
else {
queue_if_enabled(event);
}
break;
@@ -217,8 +220,8 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
set_stick_return_event.user.data1 = nullptr;
set_stick_return_event.user.data2 = nullptr;
recompui::queue_event(set_stick_return_event);
set_scanned_input({(uint32_t)InputType::ControllerAnalog, axis_event->axis + 1});
set_scanned_input({ (uint32_t)InputType::ControllerAnalog, axis_event->axis + 1 });
}
else if (axis_value < -axis_threshold) {
SDL_Event set_stick_return_event;
@@ -228,9 +231,10 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
set_stick_return_event.user.data2 = nullptr;
recompui::queue_event(set_stick_return_event);
set_scanned_input({(uint32_t)InputType::ControllerAnalog, -axis_event->axis - 1});
set_scanned_input({ (uint32_t)InputType::ControllerAnalog, -axis_event->axis - 1 });
}
} else {
}
else {
queue_if_enabled(event);
}
break;
@@ -276,6 +280,22 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
InputState.pending_mouse_delta[0] += motion_event->xrel;
InputState.pending_mouse_delta[1] += motion_event->yrel;
}
queue_if_enabled(event);
break;
case SDL_EventType::SDL_DROPBEGIN:
DropState.files_dropped.clear();
break;
case SDL_EventType::SDL_DROPFILE:
DropState.files_dropped.emplace_back(std::filesystem::path(std::u8string_view((const char8_t*)(event->drop.file))));
SDL_free(event->drop.file);
break;
case SDL_EventType::SDL_DROPCOMPLETE:
recompui::drop_files(DropState.files_dropped);
break;
case SDL_EventType::SDL_CONTROLLERBUTTONUP:
// Always queue button up events to avoid missing them during binding.
recompui::queue_event(*event);
break;
default:
queue_if_enabled(event);
break;
@@ -285,6 +305,7 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
void recomp::handle_events() {
SDL_Event cur_event;
static bool started = false;
static bool exited = false;
while (SDL_PollEvent(&cur_event) && !exited) {
exited = sdl_event_filter(nullptr, &cur_event);
@@ -301,6 +322,11 @@ void recomp::handle_events() {
SDL_ShowCursor(cursor_visible ? SDL_ENABLE : SDL_DISABLE);
SDL_SetRelativeMouseMode(cursor_locked ? SDL_TRUE : SDL_FALSE);
}
if (!started && ultramodern::is_game_started()) {
started = true;
recompui::process_game_started();
}
}
constexpr SDL_GameControllerButton SDL_CONTROLLER_BUTTON_SOUTH = SDL_CONTROLLER_BUTTON_A;
@@ -465,7 +491,7 @@ void recomp::poll_inputs() {
// Read the deltas while resetting them to zero.
{
std::lock_guard lock{ InputState.pending_input_mutex };
InputState.rotation_delta = InputState.pending_rotation_delta;
InputState.pending_rotation_delta = { 0.0f, 0.0f };
@@ -482,14 +508,14 @@ void recomp::set_rumble(int controller_num, bool on) {
ultramodern::input::connected_device_info_t recomp::get_connected_device_info(int controller_num) {
switch (controller_num) {
case 0:
return ultramodern::input::connected_device_info_t {
.connected_device = ultramodern::input::Device::Controller,
.connected_pak = ultramodern::input::Pak::RumblePak,
};
case 0:
return ultramodern::input::connected_device_info_t{
.connected_device = ultramodern::input::Device::Controller,
.connected_pak = ultramodern::input::Pak::RumblePak,
};
}
return ultramodern::input::connected_device_info_t {
return ultramodern::input::connected_device_info_t{
.connected_device = ultramodern::input::Device::None,
.connected_pak = ultramodern::input::Pak::None,
};
@@ -506,7 +532,8 @@ void recomp::update_rumble() {
if (InputState.rumble_active) {
InputState.cur_rumble += 0.17f;
if (InputState.cur_rumble > 1) InputState.cur_rumble = 1;
} else {
}
else {
InputState.cur_rumble *= 0.92f;
InputState.cur_rumble -= 0.01f;
if (InputState.cur_rumble < 0) InputState.cur_rumble = 0;
@@ -647,13 +674,13 @@ void recomp::get_mouse_deltas(float* x, float* y) {
void recomp::apply_joystick_deadzone(float x_in, float y_in, float* x_out, float* y_out) {
float joystick_deadzone = (float)recomp::get_joystick_deadzone() / 100.0f;
if(fabsf(x_in) < joystick_deadzone) {
if (fabsf(x_in) < joystick_deadzone) {
x_in = 0.0f;
}
else {
if(x_in > 0.0f) {
if (x_in > 0.0f) {
x_in -= joystick_deadzone;
}
}
else {
x_in += joystick_deadzone;
}
@@ -661,13 +688,13 @@ void recomp::apply_joystick_deadzone(float x_in, float y_in, float* x_out, float
x_in /= (1.0f - joystick_deadzone);
}
if(fabsf(y_in) < joystick_deadzone) {
if (fabsf(y_in) < joystick_deadzone) {
y_in = 0.0f;
}
else {
if(y_in > 0.0f) {
if (y_in > 0.0f) {
y_in -= joystick_deadzone;
}
}
else {
y_in += joystick_deadzone;
}
@@ -695,7 +722,7 @@ void recomp::set_right_analog_suppressed(bool suppressed) {
bool recomp::game_input_disabled() {
// Disable input if any menu that blocks input is open.
return recompui::is_context_taking_input();
return recompui::is_context_capturing_input();
}
bool recomp::all_input_disabled() {
@@ -735,16 +762,16 @@ std::string controller_button_to_string(SDL_GameControllerButton button) {
return PF_DPAD_LEFT;
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
return PF_DPAD_RIGHT;
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_MISC1:
// return "";
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE1:
// return "";
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE2:
// return "";
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE3:
// return "";
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE4:
// return "";
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_MISC1:
// return "";
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE1:
// return "";
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE2:
// return "";
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE3:
// return "";
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE4:
// return "";
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_TOUCHPAD:
return PF_SONY_TOUCHPAD;
default:
@@ -752,7 +779,7 @@ std::string controller_button_to_string(SDL_GameControllerButton button) {
}
}
std::unordered_map<SDL_Scancode, std::string> scancode_codepoints {
std::unordered_map<SDL_Scancode, std::string> scancode_codepoints{
{SDL_SCANCODE_LEFT, PF_KEYBOARD_LEFT},
// NOTE: UP and RIGHT are swapped with promptfont.
{SDL_SCANCODE_UP, PF_KEYBOARD_RIGHT},
@@ -856,15 +883,15 @@ std::string controller_axis_to_string(int axis) {
std::string recomp::InputField::to_string() const {
switch ((InputType)input_type) {
case InputType::None:
return "";
case InputType::ControllerDigital:
return controller_button_to_string((SDL_GameControllerButton)input_id);
case InputType::ControllerAnalog:
return controller_axis_to_string(input_id);
case InputType::Keyboard:
return keyboard_input_to_string((SDL_Scancode)input_id);
default:
return std::to_string(input_type) + "," + std::to_string(input_id);
case InputType::None:
return "";
case InputType::ControllerDigital:
return controller_button_to_string((SDL_GameControllerButton)input_id);
case InputType::ControllerAnalog:
return controller_axis_to_string(input_id);
case InputType::Keyboard:
return keyboard_input_to_string((SDL_Scancode)input_id);
default:
return std::to_string(input_type) + "," + std::to_string(input_id);
}
}
@@ -8,7 +8,7 @@
#include "ultramodern/error_handling.hpp"
#include "recomp_ui.h"
#include "recomp_data.h"
#include "../patches/mem_funcs.h"
#include "../patches/actor_funcs.h"
struct ExtensionInfo {
// Either the actor's type ID, or 0xFFFFFFFF if this is for generic data.
@@ -41,7 +41,7 @@ bool can_register = false;
size_t alloc_count = 0;
size_t free_count = 0;
void recomp::init_extended_actor_data() {
void recomputil::init_extended_actor_data() {
std::lock_guard lock{ actor_data_mutex };
actor_data_sizes.clear();
@@ -54,7 +54,7 @@ void recomp::init_extended_actor_data() {
actor_extensions.push_back({});
}
void recomp::reset_actor_data() {
void recomputil::reset_actor_data() {
std::lock_guard lock{ actor_data_mutex };
actor_data.reset();
actor_spawn_count = 0;
@@ -113,7 +113,7 @@ extern "C" void recomp_register_actor_extension_generic(uint8_t* rdram, recomp_c
extern "C" void recomp_clear_all_actor_data(uint8_t* rdram, recomp_context* ctx) {
(void)rdram;
(void)ctx;
recomp::reset_actor_data();
recomputil::reset_actor_data();
}
extern "C" void recomp_create_actor_data(uint8_t* rdram, recomp_context* ctx) {
+727
View File
@@ -0,0 +1,727 @@
#include <vector>
#include <mutex>
#include <unordered_map>
#include <unordered_set>
#include "slot_map.h"
#include "recomp_data.h"
#include "recomp_ui.h"
#include "librecomp/helpers.hpp"
#include "librecomp/overlays.hpp"
#include "librecomp/addresses.hpp"
#include "ultramodern/error_handling.hpp"
template <typename KeyType, typename ValueType>
class LockedMap {
private:
std::mutex mutex{};
std::unordered_map<KeyType, ValueType> map{};
public:
bool get(const KeyType& key, ValueType& out) {
std::lock_guard lock{mutex};
auto find_it = map.find(key);
if (find_it == map.end()) {
return false;
}
out = find_it->second;
return true;
}
bool insert(const KeyType& key, ValueType val) {
std::lock_guard lock{mutex};
auto ret = map.insert_or_assign(key, val);
return ret.second;
}
bool erase(const KeyType& key) {
std::lock_guard lock{mutex};
size_t num_erased = map.erase(key);
return num_erased != 0;
}
void clear() {
std::lock_guard lock{mutex};
map.clear();
}
bool erase_first(ValueType& out) {
std::lock_guard lock{mutex};
auto it = map.begin();
if (it == map.end()) {
return false;
}
out = it->second;
map.erase(it);
return true;
}
bool contains(const KeyType& key) {
std::lock_guard lock{mutex};
return map.contains(key);
}
size_t size() {
std::lock_guard lock{mutex};
return map.size();
}
};
template <typename KeyType>
class LockedSet {
private:
std::mutex mutex{};
std::unordered_set<KeyType> set{};
public:
bool contains(const KeyType& key) {
std::lock_guard lock{mutex};
return set.contains(key);
}
bool insert(const KeyType& key) {
std::lock_guard lock{mutex};
auto it = set.insert(key);
return it.second;
}
bool erase(const KeyType& key) {
std::lock_guard lock{mutex};
size_t num_erased = set.erase(key);
return num_erased != 0;
}
void clear() {
std::lock_guard lock{mutex};
set.clear();
}
size_t size() {
std::lock_guard lock{mutex};
return set.size();
}
};
template <typename ValueType>
class LockedSlotmap {
private:
std::mutex mutex{};
dod::slot_map32<ValueType> map{};
using key_t = typename dod::slot_map32<ValueType>::key;
public:
bool get(uint32_t key, ValueType** out) {
std::lock_guard lock{mutex};
ValueType* ret = map.get(key_t{key});
*out = ret;
return ret != nullptr;
}
uint32_t create() {
std::lock_guard lock{mutex};
return map.emplace().raw;
}
bool erase(uint32_t key) {
std::lock_guard lock{mutex};
if (!map.has_key(key_t{ key })) {
return false;
}
map.erase(key_t{ key });
return true;
}
void clear() {
std::lock_guard lock{mutex};
map.clear();
}
bool erase_first(ValueType& out) {
std::lock_guard lock{mutex};
auto it = map.items().begin();
if (it == map.items().end()) {
return false;
}
out = it->second;
map.erase(it->first);
return true;
}
size_t size() {
std::lock_guard lock{mutex};
return map.size();
}
};
using U32ValueMap = LockedMap<uint32_t, uint32_t>;
using U32MemoryMap = std::pair<LockedMap<uint32_t, PTR(void)>, u32>;
using U32HashSet = LockedSet<uint32_t>;
using U32Slotmap = LockedSlotmap<uint32_t>;
using MemorySlotmap = std::pair<LockedSlotmap<PTR(void)>, u32>;
LockedSlotmap<U32ValueMap> u32_value_hashmaps{};
LockedSlotmap<U32MemoryMap> u32_memory_hashmaps{};
LockedSlotmap<U32HashSet> u32_hashsets{};
LockedSlotmap<U32Slotmap> u32_slotmaps{};
LockedSlotmap<MemorySlotmap> memory_slotmaps{};
#define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name)
static void show_fatal_error_message_box(const char* funcname, const char* errstr) {
std::string message = std::string{"Fatal error in mod - "} + funcname + " : " + errstr;
recompui::message_box(message.c_str());
}
#define HANDLE_INVALID_ERROR() \
show_fatal_error_message_box(__FUNCTION__, "handle is invalid"); \
assert(false); \
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
#define SLOTMAP_KEY_INVALID_ERROR() \
show_fatal_error_message_box(__FUNCTION__, "slotmap key is invalid"); \
assert(false); \
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
// u32 -> 32-bit value hashmap.
void recomputil_create_u32_value_hashmap(uint8_t* rdram, recomp_context* ctx) {
(void)rdram;
_return(ctx, u32_value_hashmaps.create());
}
void recomputil_destroy_u32_value_hashmap(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
if (!u32_value_hashmaps.erase(mapkey)) {
HANDLE_INVALID_ERROR();
}
}
void recomputil_u32_value_hashmap_contains(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32ValueMap* map;
if (!u32_value_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, map->contains(key));
}
void recomputil_u32_value_hashmap_insert(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
uint32_t value = _arg<2, uint32_t>(rdram, ctx);
U32ValueMap* map;
if (!u32_value_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, map->insert(key, value));
}
void recomputil_u32_value_hashmap_get(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
PTR(uint32_t) val_out = _arg<2, PTR(uint32_t)>(rdram, ctx);
U32ValueMap* map;
if (!u32_value_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
uint32_t ret;
if (map->get(key, ret)) {
MEM_W(0, val_out) = ret;
_return(ctx, 1);
return;
}
else {
_return(ctx, 0);
return;
}
}
void recomputil_u32_value_hashmap_erase(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32ValueMap* map;
if (!u32_value_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, map->erase(key));
}
void recomputil_u32_value_hashmap_size(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
U32ValueMap* map;
if (!u32_value_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, static_cast<uint32_t>(map->size()));
}
// u32 -> memory hashmap.
void recomputil_create_u32_memory_hashmap(uint8_t* rdram, recomp_context* ctx) {
uint32_t element_size = _arg<0, uint32_t>(rdram, ctx);
// Create the map.
uint32_t map_key = u32_memory_hashmaps.create();
// Retrieve the map and set its element size to the provided value.
U32MemoryMap* map;
u32_memory_hashmaps.get(map_key, &map);
map->second = element_size;
// Return the created map's key.
_return(ctx, map_key);
}
void recomputil_destroy_u32_memory_hashmap(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
// Retrieve the map.
U32MemoryMap* map;
if (!u32_memory_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
// Free all of the entries in the map.
PTR(void) cur_mem;
while (map->first.erase_first(cur_mem)) {
recomp::free(rdram, TO_PTR(void, cur_mem));
}
// Destroy the map itself.
u32_memory_hashmaps.erase(mapkey);
}
void recomputil_u32_memory_hashmap_contains(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32MemoryMap* map;
if (!u32_memory_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, map->first.contains(key));
}
void recomputil_u32_memory_hashmap_create(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32MemoryMap* map;
if (!u32_memory_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
// Check if the map contains the key already to prevent inserting it twice.
PTR(void) dummy;
if (map->first.get(key, dummy)) {
_return(ctx, 0);
return;
}
// Allocate the map's size and return the pointer.
void* mem = recomp::alloc(rdram, map->second);
gpr addr = reinterpret_cast<uint8_t*>(mem) - rdram + 0xFFFFFFFF80000000ULL;
// Zero the memory.
for (size_t i = 0; i < map->second; i++) {
MEM_B(i, addr) = 0;
}
PTR(void) ret = static_cast<PTR(void)>(addr);
map->first.insert(key, ret);
_return(ctx, 1);
}
void recomputil_u32_memory_hashmap_get(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32MemoryMap* map;
if (!u32_memory_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
PTR(void) ret;
if (map->first.get(key, ret)) {
_return(ctx, ret);
return;
}
else {
_return(ctx, NULLPTR);
return;
}
}
void recomputil_u32_memory_hashmap_erase(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32MemoryMap* map;
if (!u32_memory_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
// Free the memory for this key if the key exists.
PTR(void) addr;
bool has_value = map->first.get(key, addr);
if (has_value) {
void* mem = TO_PTR(void, addr);
recomp::free(rdram, mem);
}
_return(ctx, map->first.erase(key));
}
void recomputil_u32_memory_hashmap_size(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
U32MemoryMap* map;
if (!u32_memory_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, static_cast<uint32_t>(map->first.size()));
}
// u32 hashset.
void recomputil_create_u32_hashset(uint8_t* rdram, recomp_context* ctx) {
(void)rdram;
_return(ctx, u32_hashsets.create());
}
void recomputil_destroy_u32_hashset(uint8_t* rdram, recomp_context* ctx) {
uint32_t setkey = _arg<0, uint32_t>(rdram, ctx);
if (!u32_hashsets.erase(setkey)) {
HANDLE_INVALID_ERROR();
}
}
void recomputil_u32_hashset_contains(uint8_t* rdram, recomp_context* ctx) {
uint32_t setkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32HashSet* set;
if (!u32_hashsets.get(setkey, &set)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, set->contains(key));
}
void recomputil_u32_hashset_insert(uint8_t* rdram, recomp_context* ctx) {
uint32_t setkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32HashSet* set;
if (!u32_hashsets.get(setkey, &set)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, set->insert(key));
}
void recomputil_u32_hashset_erase(uint8_t* rdram, recomp_context* ctx) {
uint32_t setkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32HashSet* set;
if (!u32_hashsets.get(setkey, &set)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, set->erase(key));
}
void recomputil_u32_hashset_size(uint8_t* rdram, recomp_context* ctx) {
uint32_t setkey = _arg<0, uint32_t>(rdram, ctx);
U32HashSet* set;
if (!u32_hashsets.get(setkey, &set)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, static_cast<uint32_t>(set->size()));
}
// u32 value slotmap.
void recomputil_create_u32_slotmap(uint8_t* rdram, recomp_context* ctx) {
(void)rdram;
_return(ctx, u32_slotmaps.create());
}
void recomputil_destroy_u32_slotmap(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
if (!u32_slotmaps.erase(mapkey)) {
HANDLE_INVALID_ERROR();
}
}
void recomputil_u32_slotmap_contains(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32Slotmap* map;
if (!u32_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
uint32_t* dummy_ptr;
_return(ctx, map->get(key, &dummy_ptr));
}
void recomputil_u32_slotmap_create(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
U32Slotmap* map;
if (!u32_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, map->create());
}
void recomputil_u32_slotmap_get(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
PTR(uint32_t) val_out = _arg<2, PTR(uint32_t)>(rdram, ctx);
U32Slotmap* map;
if (!u32_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
uint32_t* ret;
if (!map->get(key, &ret)) {
_return(ctx, 0);
}
MEM_W(0, val_out) = *ret;
_return(ctx, 1);
}
void recomputil_u32_slotmap_set(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
uint32_t value = _arg<2, uint32_t>(rdram, ctx);
U32Slotmap* map;
if (!u32_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
uint32_t* value_ptr;
if (!map->get(key, &value_ptr)) {
_return(ctx, 0);
}
*value_ptr = value;
_return(ctx, 1);
}
void recomputil_u32_slotmap_erase(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32Slotmap* map;
if (!u32_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
if (!map->erase(key)) {
_return(ctx, 0);
}
_return(ctx, 1);
}
void recomputil_u32_slotmap_size(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
U32Slotmap* map;
if (!u32_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, static_cast<uint32_t>(map->size()));
}
// memory slotmap.
void recomputil_create_memory_slotmap(uint8_t* rdram, recomp_context* ctx) {
(void)rdram;
_return(ctx, memory_slotmaps.create());
}
void recomputil_destroy_memory_slotmap(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
// Retrieve the map.
MemorySlotmap* map;
if (!memory_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
// Free all of the entries in the map.
PTR(void) cur_mem;
while (map->first.erase_first(cur_mem)) {
recomp::free(rdram, TO_PTR(void, cur_mem));
}
// Destroy the map itself.
memory_slotmaps.erase(mapkey);
}
void recomputil_memory_slotmap_contains(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
MemorySlotmap* map;
if (!memory_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
PTR(void)* dummy_ptr;
_return(ctx, map->first.get(key, &dummy_ptr));
}
void recomputil_memory_slotmap_create(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
MemorySlotmap* map;
if (!memory_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
// Create the slotmap element.
u32 key = map->first.create();
// Allocate the map's element size.
void* mem = recomp::alloc(rdram, map->second);
gpr addr = reinterpret_cast<uint8_t*>(mem) - rdram + 0xFFFFFFFF80000000ULL;
// Zero the memory.
for (size_t i = 0; i < map->second; i++) {
MEM_B(i, addr) = 0;
}
// Store the allocated pointer.
PTR(void)* value_ptr;
map->first.get(key, &value_ptr);
MEM_W(0, *value_ptr) = addr;
// Return the key.
_return(ctx, key);
}
void recomputil_memory_slotmap_get(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
PTR(uint32_t) val_out = _arg<2, PTR(uint32_t)>(rdram, ctx);
MemorySlotmap* map;
if (!memory_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
PTR(void)* ret;
if (!map->first.get(key, &ret)) {
SLOTMAP_KEY_INVALID_ERROR();
}
MEM_W(0, val_out) = *ret;
}
void recomputil_memory_slotmap_erase(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
MemorySlotmap* map;
if (!memory_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
// Free the memory for this key if the key exists.
PTR(void)* addr;
bool has_value = map->first.get(key, &addr);
if (has_value) {
void* mem = TO_PTR(void, addr);
recomp::free(rdram, mem);
}
_return(ctx, map->first.erase(key));
}
void recomputil_memory_slotmap_size(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
MemorySlotmap* map;
if (!memory_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, static_cast<uint32_t>(map->first.size()));
}
// Exports.
void recomputil::register_data_api_exports() {
REGISTER_FUNC(recomputil_create_u32_value_hashmap);
REGISTER_FUNC(recomputil_destroy_u32_value_hashmap);
REGISTER_FUNC(recomputil_u32_value_hashmap_contains);
REGISTER_FUNC(recomputil_u32_value_hashmap_insert);
REGISTER_FUNC(recomputil_u32_value_hashmap_get);
REGISTER_FUNC(recomputil_u32_value_hashmap_erase);
REGISTER_FUNC(recomputil_u32_value_hashmap_size);
REGISTER_FUNC(recomputil_create_u32_memory_hashmap);
REGISTER_FUNC(recomputil_destroy_u32_memory_hashmap);
REGISTER_FUNC(recomputil_u32_memory_hashmap_contains);
REGISTER_FUNC(recomputil_u32_memory_hashmap_create);
REGISTER_FUNC(recomputil_u32_memory_hashmap_get);
REGISTER_FUNC(recomputil_u32_memory_hashmap_erase);
REGISTER_FUNC(recomputil_u32_memory_hashmap_size);
REGISTER_FUNC(recomputil_create_u32_hashset);
REGISTER_FUNC(recomputil_destroy_u32_hashset);
REGISTER_FUNC(recomputil_u32_hashset_contains);
REGISTER_FUNC(recomputil_u32_hashset_insert);
REGISTER_FUNC(recomputil_u32_hashset_erase);
REGISTER_FUNC(recomputil_u32_hashset_size);
REGISTER_FUNC(recomputil_create_u32_slotmap);
REGISTER_FUNC(recomputil_destroy_u32_slotmap);
REGISTER_FUNC(recomputil_u32_slotmap_contains);
REGISTER_FUNC(recomputil_u32_slotmap_create);
REGISTER_FUNC(recomputil_u32_slotmap_get);
REGISTER_FUNC(recomputil_u32_slotmap_set);
REGISTER_FUNC(recomputil_u32_slotmap_erase);
REGISTER_FUNC(recomputil_u32_slotmap_size);
REGISTER_FUNC(recomputil_create_memory_slotmap);
REGISTER_FUNC(recomputil_destroy_memory_slotmap);
REGISTER_FUNC(recomputil_memory_slotmap_contains);
REGISTER_FUNC(recomputil_memory_slotmap_create);
REGISTER_FUNC(recomputil_memory_slotmap_get);
REGISTER_FUNC(recomputil_memory_slotmap_erase);
REGISTER_FUNC(recomputil_memory_slotmap_size);
}
+30 -11
View File
@@ -25,6 +25,7 @@
#include "banjo_config.h"
#include "banjo_sound.h"
#include "banjo_render.h"
#include "banjo_support.h"
#include "banjo_game.h"
#include "recomp_data.h"
#include "ovl_patches.hpp"
@@ -45,14 +46,15 @@
#include "../../lib/rt64/src/contrib/stb/stb_image.h"
const std::string version_string = "0.0.1";
const std::string version_string = "0.1.0";
template<typename... Ts>
void exit_error(const char* str, Ts ...args) {
// TODO pop up an error
((void)fprintf(stderr, str, args), ...);
assert(false);
std::quick_exit(EXIT_FAILURE);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
ultramodern::gfx_callbacks_t::gfx_data_t create_gfx() {
@@ -126,11 +128,13 @@ SDL_Window* window;
ultramodern::renderer::WindowHandle create_window(ultramodern::gfx_callbacks_t::gfx_data_t) {
uint32_t flags = SDL_WINDOW_RESIZABLE;
#if defined(RT64_SDL_WINDOW_VULKAN)
#if defined(__APPLE__)
flags |= SDL_WINDOW_METAL;
#elif defined(RT64_SDL_WINDOW_VULKAN)
flags |= SDL_WINDOW_VULKAN;
#endif
window = SDL_CreateWindow("Banjo: Recompiled", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1600, 960, flags);
window = SDL_CreateWindow("Zelda 64: Recompiled", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1600, 960, flags);
#if defined(__linux__)
SetImageAsIcon("icons/512.png",window);
if (ultramodern::renderer::get_graphics_config().wm_option == ultramodern::renderer::WindowMode::Fullscreen) { // TODO: Remove once RT64 gets native fullscreen support on Linux
@@ -152,6 +156,9 @@ ultramodern::renderer::WindowHandle create_window(ultramodern::gfx_callbacks_t::
return ultramodern::renderer::WindowHandle{ wmInfo.info.win.window, GetCurrentThreadId() };
#elif defined(__linux__) || defined(__ANDROID__)
return ultramodern::renderer::WindowHandle{ window };
#elif defined(__APPLE__)
SDL_MetalView view = SDL_Metal_CreateView(window);
return ultramodern::renderer::WindowHandle{ wmInfo.info.cocoa.window, SDL_Metal_GetLayer(view) };
#else
static_assert(false && "Unimplemented");
#endif
@@ -503,15 +510,17 @@ void release_preload(PreloadContext& context) {
#endif
void enable_texture_pack(recomp::mods::ModContext& context, const recomp::mods::ModHandle& mod) {
(void)context;
banjo::renderer::enable_texture_pack(mod);
banjo::renderer::enable_texture_pack(context, mod);
}
void disable_texture_pack(recomp::mods::ModContext& context, const recomp::mods::ModHandle& mod) {
(void)context;
void disable_texture_pack(recomp::mods::ModContext&, const recomp::mods::ModHandle& mod) {
banjo::renderer::disable_texture_pack(mod);
}
void reorder_texture_pack(recomp::mods::ModContext&) {
banjo::renderer::trigger_texture_pack_update();
}
#define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name)
int main(int argc, char** argv) {
@@ -555,14 +564,22 @@ int main(int argc, char** argv) {
// Force wasapi on Windows, as there seems to be some issue with sample queueing with directsound currently.
SDL_setenv("SDL_AUDIODRIVER", "wasapi", true);
#endif
//printf("Current dir: %ls\n", std::filesystem::current_path().c_str());
#if defined(__linux__) && defined(RECOMP_FLATPAK)
// When using Flatpak, applications tend to launch from the home directory by default.
// Mods might use the current working directory to store the data, so we switch it to a directory
// with persistent data storage and write permissions under Flatpak to ensure it works.
std::error_code ec;
std::filesystem::current_path("/var/data", ec);
#endif
// Initialize SDL audio and set the output frequency.
SDL_InitSubSystem(SDL_INIT_AUDIO);
reset_audio(48000);
// Source controller mappings file
if (SDL_GameControllerAddMappingsFromFile("gamecontrollerdb.txt") < 0) {
std::u8string controller_db_path = (banjo::get_program_path() / "recompcontrollerdb.txt").u8string();
if (SDL_GameControllerAddMappingsFromFile(reinterpret_cast<const char *>(controller_db_path.c_str())) < 0) {
fprintf(stderr, "Failed to load controller mappings: %s\n", SDL_GetError());
}
@@ -584,10 +601,11 @@ int main(int argc, char** argv) {
REGISTER_FUNC(recomp_get_inverted_axes);
REGISTER_FUNC(recomp_get_analog_inverted_axes);
recompui::register_ui_exports();
recomputil::register_data_api_exports();
banjo::register_bk_overlays();
banjo::register_bk_patches();
recomp::init_extended_actor_data();
recomputil::init_extended_actor_data();
banjo::load_config();
recomp::rsp::callbacks_t rsp_callbacks{
@@ -636,6 +654,7 @@ int main(int argc, char** argv) {
.allow_runtime_toggle = true,
.on_enabled = enable_texture_pack,
.on_disabled = disable_texture_pack,
.on_reordered = reorder_texture_pack,
};
auto texture_pack_content_type_id = recomp::mods::register_mod_content_type(texture_pack_content_type);
+166 -40
View File
@@ -1,10 +1,12 @@
#include <memory>
#include <cstring>
#include <variant>
#include <algorithm>
#define HLSL_CPU
#include "hle/rt64_application.h"
#include "rt64_render_hooks.h"
#include "overloaded.h"
#include "ultramodern/ultramodern.hpp"
#include "ultramodern/config.hpp"
@@ -13,12 +15,6 @@
#include "recomp_ui.h"
#include "concurrentqueue.h"
// Helper class for variant visiting.
template<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
static RT64::UserConfiguration::Antialiasing device_max_msaa = RT64::UserConfiguration::Antialiasing::None;
static bool sample_positions_supported = false;
static bool high_precision_fb_enabled = false;
@@ -27,14 +23,25 @@ static uint8_t DMEM[0x1000];
static uint8_t IMEM[0x1000];
struct TexturePackEnableAction {
std::filesystem::path path;
std::string mod_id;
};
struct TexturePackDisableAction {
std::filesystem::path path;
std::string mod_id;
};
using TexturePackAction = std::variant<TexturePackEnableAction, TexturePackDisableAction>;
struct TexturePackSecondaryEnableAction {
std::string mod_id;
};
struct TexturePackSecondaryDisableAction {
std::string mod_id;
};
struct TexturePackUpdateAction {
};
using TexturePackAction = std::variant<TexturePackEnableAction, TexturePackDisableAction, TexturePackSecondaryEnableAction, TexturePackSecondaryDisableAction, TexturePackUpdateAction>;
static moodycamel::ConcurrentQueue<TexturePackAction> texture_pack_action_queue;
@@ -171,6 +178,7 @@ void set_application_user_config(RT64::Application* application, const ultramode
application->userConfig.refreshRate = to_rt64(config.rr_option);
application->userConfig.refreshRateTarget = config.rr_manual_value;
application->userConfig.internalColorFormat = to_rt64(config.hpfb_option);
application->userConfig.displayBuffering = RT64::UserConfiguration::DisplayBuffering::Triple;
}
ultramodern::renderer::SetupResult map_setup_result(RT64::Application::SetupResult rt64_result) {
@@ -192,6 +200,23 @@ ultramodern::renderer::SetupResult map_setup_result(RT64::Application::SetupResu
std::exit(EXIT_FAILURE);
}
ultramodern::renderer::GraphicsApi map_graphics_api(RT64::UserConfiguration::GraphicsAPI api) {
switch (api) {
case RT64::UserConfiguration::GraphicsAPI::D3D12:
return ultramodern::renderer::GraphicsApi::D3D12;
case RT64::UserConfiguration::GraphicsAPI::Vulkan:
return ultramodern::renderer::GraphicsApi::Vulkan;
case RT64::UserConfiguration::GraphicsAPI::Metal:
return ultramodern::renderer::GraphicsApi::Metal;
case RT64::UserConfiguration::GraphicsAPI::Automatic:
return ultramodern::renderer::GraphicsApi::Auto;
}
fprintf(stderr, "Unhandled `RT64::UserConfiguration::GraphicsAPI` ?\n");
assert(false);
std::exit(EXIT_FAILURE);
}
banjo::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::renderer::WindowHandle window_handle, bool debug) {
static unsigned char dummy_rom_header[0x40];
recompui::set_render_hooks();
@@ -263,9 +288,11 @@ banjo::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::renderer:
case ultramodern::renderer::GraphicsApi::Vulkan:
app->userConfig.graphicsAPI = RT64::UserConfiguration::GraphicsAPI::Vulkan;
break;
default:
case ultramodern::renderer::GraphicsApi::Metal:
app->userConfig.graphicsAPI = RT64::UserConfiguration::GraphicsAPI::Metal;
break;
case ultramodern::renderer::GraphicsApi::Auto:
// Don't override if auto is selected.
app->userConfig.graphicsAPI = RT64::UserConfiguration::GraphicsAPI::Automatic;
break;
}
@@ -275,6 +302,8 @@ banjo::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::renderer:
thread_id = window_handle.thread_id;
#endif
setup_result = map_setup_result(app->setup(thread_id));
// Get the API that RT64 chose.
chosen_api = map_graphics_api(app->chosenGraphicsAPI);
if (setup_result != ultramodern::renderer::SetupResult::Success) {
app = nullptr;
return;
@@ -303,32 +332,7 @@ banjo::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::renderer:
banjo::renderer::RT64Context::~RT64Context() = default;
void banjo::renderer::RT64Context::send_dl(const OSTask* task) {
bool packs_disabled = false;
TexturePackAction cur_action;
while (texture_pack_action_queue.try_dequeue(cur_action)) {
std::visit(overloaded{
[&](TexturePackDisableAction& to_disable) {
enabled_texture_packs.erase(to_disable.path);
packs_disabled = true;
},
[&](TexturePackEnableAction& to_enable) {
enabled_texture_packs.insert(to_enable.path);
// Load the pack now if no packs have been disabled.
if (!packs_disabled) {
app->textureCache->loadReplacementDirectory(to_enable.path);
}
}
}, cur_action);
}
// If any packs were disabled, unload all packs and load all the active ones.
if (packs_disabled) {
app->textureCache->clearReplacementDirectories();
for (const std::filesystem::path& cur_pack_path : enabled_texture_packs) {
app->textureCache->loadReplacementDirectory(cur_pack_path);
}
}
check_texture_pack_actions();
app->state->rsp->reset();
app->interpreter->loadUCodeGBI(task->t.ucode & 0x3FFFFFF, task->t.ucode_data & 0x3FFFFFF, true);
app->processDisplayLists(app->core.RDRAM, task->t.data_ptr & 0x3FFFFFF, 0, true);
@@ -394,6 +398,66 @@ float banjo::renderer::RT64Context::get_resolution_scale() const {
}
}
void banjo::renderer::RT64Context::check_texture_pack_actions() {
bool packs_changed = false;
TexturePackAction cur_action;
while (texture_pack_action_queue.try_dequeue(cur_action)) {
std::visit(overloaded{
[&](TexturePackDisableAction &to_disable) {
enabled_texture_packs.erase(to_disable.mod_id);
packs_changed = true;
},
[&](TexturePackEnableAction &to_enable) {
enabled_texture_packs.insert(to_enable.mod_id);
packs_changed = true;
},
[&](TexturePackSecondaryDisableAction &to_override_disable) {
secondary_disabled_texture_packs.insert(to_override_disable.mod_id);
packs_changed = true;
},
[&](TexturePackSecondaryEnableAction &to_override_enable) {
secondary_disabled_texture_packs.erase(to_override_enable.mod_id);
packs_changed = true;
},
[&](TexturePackUpdateAction &) {
packs_changed = true;
}
}, cur_action);
}
// If any packs were disabled, unload all packs and load all the active ones.
if (packs_changed) {
// Sort the enabled texture packs in reverse order so that earlier ones override later ones.
std::vector<std::string> sorted_texture_packs{};
sorted_texture_packs.reserve(enabled_texture_packs.size());
for (const std::string& mod : enabled_texture_packs) {
if (!secondary_disabled_texture_packs.contains(mod)) {
sorted_texture_packs.emplace_back(mod);
}
}
std::sort(sorted_texture_packs.begin(), sorted_texture_packs.end(),
[](const std::string& lhs, const std::string& rhs) {
return recomp::mods::get_mod_order_index(lhs) > recomp::mods::get_mod_order_index(rhs);
}
);
// Build the path list from the sorted mod list.
std::vector<RT64::ReplacementDirectory> replacement_directories;
replacement_directories.reserve(enabled_texture_packs.size());
for (const std::string &mod_id : sorted_texture_packs) {
replacement_directories.emplace_back(RT64::ReplacementDirectory(recomp::mods::get_mod_filename(mod_id)));
}
if (!replacement_directories.empty()) {
app->textureCache->loadReplacementDirectories(replacement_directories);
}
else {
app->textureCache->clearReplacementDirectories();
}
}
}
RT64::UserConfiguration::Antialiasing banjo::renderer::RT64MaxMSAA() {
return device_max_msaa;
}
@@ -410,10 +474,72 @@ bool banjo::renderer::RT64HighPrecisionFBEnabled() {
return high_precision_fb_enabled;
}
void banjo::renderer::enable_texture_pack(const recomp::mods::ModHandle& mod) {
texture_pack_action_queue.enqueue(TexturePackEnableAction{mod.manifest.mod_root_path});
void banjo::renderer::trigger_texture_pack_update() {
texture_pack_action_queue.enqueue(TexturePackUpdateAction{});
}
void banjo::renderer::enable_texture_pack(const recomp::mods::ModContext& context, const recomp::mods::ModHandle& mod) {
texture_pack_action_queue.enqueue(TexturePackEnableAction{mod.manifest.mod_id});
// Check for the texture pack enabled config option.
const recomp::mods::ConfigSchema& config_schema = context.get_mod_config_schema(mod.manifest.mod_id);
auto find_it = config_schema.options_by_id.find(banjo::renderer::special_option_texture_pack_enabled);
if (find_it != config_schema.options_by_id.end()) {
const recomp::mods::ConfigOption& config_option = config_schema.options[find_it->second];
if (is_texture_pack_enable_config_option(config_option, false)) {
recomp::mods::ConfigValueVariant value_variant = context.get_mod_config_value(mod.manifest.mod_id, config_option.id);
uint32_t value;
if (uint32_t* value_ptr = std::get_if<uint32_t>(&value_variant)) {
value = *value_ptr;
}
else {
value = 0;
}
if (value) {
banjo::renderer::secondary_enable_texture_pack(mod.manifest.mod_id);
}
else {
banjo::renderer::secondary_disable_texture_pack(mod.manifest.mod_id);
}
}
}
}
void banjo::renderer::disable_texture_pack(const recomp::mods::ModHandle& mod) {
texture_pack_action_queue.enqueue(TexturePackDisableAction{mod.manifest.mod_root_path});
texture_pack_action_queue.enqueue(TexturePackDisableAction{mod.manifest.mod_id});
}
void banjo::renderer::secondary_enable_texture_pack(const std::string& mod_id) {
texture_pack_action_queue.enqueue(TexturePackSecondaryEnableAction{mod_id});
}
void banjo::renderer::secondary_disable_texture_pack(const std::string& mod_id) {
texture_pack_action_queue.enqueue(TexturePackSecondaryDisableAction{mod_id});
}
// HD texture enable option. Must be an enum with two options.
// The first option is treated as disabled and the second option is treated as enabled.
bool banjo::renderer::is_texture_pack_enable_config_option(const recomp::mods::ConfigOption& option, bool show_errors) {
if (option.id == banjo::renderer::special_option_texture_pack_enabled) {
if (option.type != recomp::mods::ConfigOptionType::Enum) {
if (show_errors) {
recompui::message_box(("Mod has the special config option id for enabling an HD texture pack (\"" + banjo::renderer::special_option_texture_pack_enabled + "\"), but the config option is not an enum.").c_str());
}
return false;
}
const recomp::mods::ConfigOptionEnum &option_enum = std::get<recomp::mods::ConfigOptionEnum>(option.variant);
if (option_enum.options.size() != 2) {
if (show_errors) {
recompui::message_box(("Mod has the special config option id for enabling an HD texture pack (\"" + banjo::renderer::special_option_texture_pack_enabled + "\"), but the config option doesn't have exactly 2 values.").c_str());
}
return false;
}
return true;
}
return false;
}
+94
View File
@@ -0,0 +1,94 @@
#include "banjo_support.h"
#include <SDL.h>
#include "nfd.h"
#include "RmlUi/Core.h"
namespace banjo {
// MARK: - Internal Helpers
void perform_file_dialog_operation(const std::function<void(bool, const std::filesystem::path&)>& callback) {
nfdnchar_t* native_path = nullptr;
nfdresult_t result = NFD_OpenDialogN(&native_path, nullptr, 0, nullptr);
bool success = (result == NFD_OKAY);
std::filesystem::path path;
if (success) {
path = std::filesystem::path{native_path};
NFD_FreePathN(native_path);
}
callback(success, path);
}
void perform_file_dialog_operation_multiple(const std::function<void(bool, const std::list<std::filesystem::path>&)>& callback) {
const nfdpathset_t* native_paths = nullptr;
nfdresult_t result = NFD_OpenDialogMultipleN(&native_paths, nullptr, 0, nullptr);
bool success = (result == NFD_OKAY);
std::list<std::filesystem::path> paths;
nfdpathsetsize_t count = 0;
if (success) {
NFD_PathSet_GetCount(native_paths, &count);
for (nfdpathsetsize_t i = 0; i < count; i++) {
nfdnchar_t* cur_path = nullptr;
nfdresult_t cur_result = NFD_PathSet_GetPathN(native_paths, i, &cur_path);
if (cur_result == NFD_OKAY) {
paths.emplace_back(std::filesystem::path{cur_path});
}
}
NFD_PathSet_Free(native_paths);
}
callback(success, paths);
}
// MARK: - Public API
std::filesystem::path get_program_path() {
#if defined(__APPLE__)
return get_bundle_resource_directory();
#elif defined(__linux__) && defined(RECOMP_FLATPAK)
return "/app/bin";
#else
return "";
#endif
}
std::filesystem::path get_asset_path(const char* asset) {
return get_program_path() / "assets" / asset;
}
void open_file_dialog(std::function<void(bool success, const std::filesystem::path& path)> callback) {
#ifdef __APPLE__
dispatch_on_ui_thread([callback]() {
perform_file_dialog_operation(callback);
});
#else
perform_file_dialog_operation(callback);
#endif
}
void open_file_dialog_multiple(std::function<void(bool success, const std::list<std::filesystem::path>& paths)> callback) {
#ifdef __APPLE__
dispatch_on_ui_thread([callback]() {
perform_file_dialog_operation_multiple(callback);
});
#else
perform_file_dialog_operation_multiple(callback);
#endif
}
void show_error_message_box(const char *title, const char *message) {
#ifdef __APPLE__
std::string title_copy(title);
std::string message_copy(message);
dispatch_on_ui_thread([title_copy, message_copy] {
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, title_copy.c_str(), message_copy.c_str(), nullptr);
});
#else
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, title, message, nullptr);
#endif
}
}
+174 -14
View File
@@ -1,8 +1,10 @@
#include <mutex>
#include <string>
#include <unordered_map>
#include <fstream>
#include "slot_map.h"
#include "RmlUi/Core/StreamMemory.h"
#include "ultramodern/error_handling.hpp"
#include "recomp_ui.h"
@@ -32,8 +34,12 @@ namespace recompui {
resource_slotmap resources;
Rml::ElementDocument* document;
Element root_element;
Element* autofocus_element = nullptr;
std::vector<Element*> loose_elements;
std::unordered_set<ResourceId> to_update;
std::vector<std::tuple<Element*, ResourceId, std::string>> to_set_text;
bool captures_input = true;
bool captures_mouse = true;
Context(Rml::ElementDocument* document) : document(document), root_element(document) {}
};
} // namespace recompui
@@ -41,10 +47,11 @@ namespace recompui {
using context_slotmap = dod::slot_map32<recompui::Context>;
static struct {
std::mutex all_contexts_lock;
std::recursive_mutex all_contexts_lock;
context_slotmap all_contexts;
std::unordered_set<recompui::ContextId> opened_contexts;
std::unordered_map<Rml::ElementDocument*, recompui::ContextId> documents_to_contexts;
Rml::SharedPtr<Rml::StyleSheetContainer> style_sheet;
} context_state;
thread_local recompui::Context* opened_context = nullptr;
@@ -61,12 +68,16 @@ enum class ContextErrorType {
AddResourceToWrongContext,
UpdateElementWithoutContext,
UpdateElementInWrongContext,
SetTextElementWithoutContext,
SetTextElementInWrongContext,
GetResourceWithoutOpen,
GetResourceFailed,
DestroyResourceWithoutOpen,
DestroyResourceInWrongContext,
DestroyResourceNotFound,
GetDocumentInvalidContext,
GetAutofocusInvalidContext,
SetAutofocusInvalidContext,
InternalError,
};
@@ -111,6 +122,12 @@ void context_error(recompui::ContextId id, ContextErrorType type) {
case ContextErrorType::UpdateElementInWrongContext:
error_message = "Attempted to update a UI element in a different UI context than the one that's open";
break;
case ContextErrorType::SetTextElementWithoutContext:
error_message = "Attempted to set the text of a UI element with no open UI context";
break;
case ContextErrorType::SetTextElementInWrongContext:
error_message = "Attempted to set the text of a UI element in a different UI context than the one that's open";
break;
case ContextErrorType::GetResourceWithoutOpen:
error_message = "Attempted to get a UI resource with no open UI context";
break;
@@ -129,6 +146,12 @@ void context_error(recompui::ContextId id, ContextErrorType type) {
case ContextErrorType::GetDocumentInvalidContext:
error_message = "Attempted to get the document of an invalid UI context";
break;
case ContextErrorType::GetAutofocusInvalidContext:
error_message = "Attempted to get the autofocus element of an invalid UI context";
break;
case ContextErrorType::SetAutofocusInvalidContext:
error_message = "Attempted to set the autofocus element of an invalid UI context";
break;
case ContextErrorType::InternalError:
error_message = "Internal error in UI context";
break;
@@ -166,6 +189,21 @@ recompui::ContextId create_context_impl(Rml::ElementDocument* document) {
return ret;
}
void recompui::init_styling(const std::filesystem::path& rcss_file) {
std::string style{};
{
std::ifstream style_stream{rcss_file};
style_stream.seekg(0, std::ios::end);
style.resize(style_stream.tellg());
style_stream.seekg(0, std::ios::beg);
style_stream.read(style.data(), style.size());
}
std::unique_ptr<Rml::StreamMemory> rml_stream = std::make_unique<Rml::StreamMemory>(reinterpret_cast<Rml::byte*>(style.data()), style.size());
rml_stream->SetSourceURL(rcss_file.filename().string());
context_state.style_sheet = Rml::Factory::InstanceStyleSheetStream(rml_stream.get());
}
recompui::ContextId recompui::create_context(const std::filesystem::path& path) {
ContextId new_context = create_context_impl(nullptr);
@@ -193,29 +231,20 @@ recompui::ContextId recompui::create_context(Rml::ElementDocument* document) {
recompui::ContextId recompui::create_context() {
Rml::ElementDocument* doc = create_empty_document();
doc->SetStyleSheetContainer(context_state.style_sheet);
ContextId ret = create_context_impl(doc);
Element* root = ret.get_root_element();
// Mark the root element as not being a shim, as that's only needed for elements that were parented to Rml ones manually.
root->shim = false;
// TODO move these defaults elsewhere. Copied from the existing rcss.
ret.open();
root->set_width(100.0f, Unit::Percent);
root->set_height(100.0f, Unit::Percent);
root->set_display(Display::Flex);
root->set_opacity(1.0f);
root->set_font_family("LatoLatin");
root->set_font_style(FontStyle::Normal);
root->set_font_weight(400);
float sz = 16.0f;
float spacing = 0.0f;
float sz_add = sz + 4;
root->set_font_size(sz_add, Unit::Dp);
root->set_letter_spacing(sz_add * spacing, Unit::Dp);
root->set_line_height(sz_add, Unit::Dp);
ret.close();
doc->Hide();
return ret;
}
@@ -307,6 +336,15 @@ void recompui::ContextId::open() {
opened_context_id = *this;
}
bool recompui::ContextId::open_if_not_already() {
if (opened_context_id == *this) {
return false;
}
open();
return true;
}
void recompui::ContextId::close() {
// Ensure a context is currently opened by this thread.
if (opened_context_id == ContextId::null()) {
@@ -330,6 +368,15 @@ void recompui::ContextId::close() {
}
}
recompui::ContextId recompui::try_close_current_context() {
if (opened_context_id != ContextId::null()) {
ContextId prev_context = opened_context_id;
opened_context_id.close();
return prev_context;
}
return ContextId::null();
}
void recompui::ContextId::process_updates() {
// Ensure a context is currently opened by this thread.
if (opened_context_id == ContextId::null()) {
@@ -367,8 +414,83 @@ void recompui::ContextId::process_updates() {
continue;
}
static_cast<Element*>(cur_resource->get())->process_event(update_event);
static_cast<Element*>(cur_resource->get())->handle_event(update_event);
}
std::vector<std::tuple<Element*, ResourceId, std::string>> to_set_text = std::move(opened_context->to_set_text);
// Delete the Rml elements that are pending deletion.
for (auto cur_text_update : to_set_text) {
Element* element_ptr = std::get<0>(cur_text_update);
ResourceId resource = std::get<1>(cur_text_update);
std::string& text = std::get<2>(cur_text_update);
// If the resource ID is valid, prefer that as we can quickly validate if the resource still exists.
if (resource != ResourceId::null()) {
resource_slotmap::key cur_key{ resource.slot_id };
std::unique_ptr<Style>* cur_resource = opened_context->resources.get(cur_key);
// Make sure the resource exists before setting its text, as it may have been deleted.
if (cur_resource == nullptr) {
continue;
}
// Perform the text update.
static_cast<Element*>(cur_resource->get())->base->SetInnerRML(text);
}
// Otherwise we use the element pointer, but we need to validate that it still exists before doing so.
else {
// Scan the current resources to find the target element.
for (const std::unique_ptr<Style>& cur_e : opened_context->resources) {
if (cur_e.get() == element_ptr) {
element_ptr->base->SetInnerRML(text);
// We can stop after finding the element.
break;
}
}
}
}
}
bool recompui::ContextId::captures_input() {
std::lock_guard lock{ context_state.all_contexts_lock };
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
if (ctx == nullptr) {
return false;
}
return ctx->captures_input;
}
bool recompui::ContextId::captures_mouse() {
std::lock_guard lock{ context_state.all_contexts_lock };
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
if (ctx == nullptr) {
return false;
}
return ctx->captures_mouse;
}
void recompui::ContextId::set_captures_input(bool captures_input) {
std::lock_guard lock{ context_state.all_contexts_lock };
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
if (ctx == nullptr) {
return;
}
ctx->captures_input = captures_input;
}
void recompui::ContextId::set_captures_mouse(bool captures_mouse) {
std::lock_guard lock{ context_state.all_contexts_lock };
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
if (ctx == nullptr) {
return;
}
ctx->captures_mouse = captures_mouse;
}
recompui::Style* recompui::ContextId::add_resource_impl(std::unique_ptr<Style>&& resource) {
@@ -387,6 +509,8 @@ recompui::Style* recompui::ContextId::add_resource_impl(std::unique_ptr<Style>&&
auto key = opened_context->resources.emplace(std::move(resource));
if (is_element) {
Element* element_ptr = static_cast<Element*>(resource_ptr);
element_ptr->set_id(std::string{element_ptr->get_type_name()} + "-" + std::to_string(key.raw));
key.set_tag(static_cast<uint8_t>(SlotTag::Element));
// Send one update to the element.
opened_context->to_update.emplace(ResourceId{ key.raw });
@@ -433,6 +557,20 @@ void recompui::ContextId::queue_element_update(ResourceId element) {
opened_context->to_update.emplace(element);
}
void recompui::ContextId::queue_set_text(Element* element, std::string&& text) {
// Ensure a context is currently opened by this thread.
if (opened_context_id == ContextId::null()) {
context_error(*this, ContextErrorType::SetTextElementWithoutContext);
}
// Check that the context that was specified is the same one that's currently open.
if (*this != opened_context_id) {
context_error(*this, ContextErrorType::SetTextElementInWrongContext);
}
opened_context->to_set_text.emplace_back(std::make_tuple(element, element->resource_id, std::move(text)));
}
recompui::Style* recompui::ContextId::create_style() {
return add_resource_impl(std::make_unique<Style>());
}
@@ -502,6 +640,28 @@ recompui::Element* recompui::ContextId::get_root_element() {
return &ctx->root_element;
}
recompui::Element* recompui::ContextId::get_autofocus_element() {
std::lock_guard lock{ context_state.all_contexts_lock };
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
if (ctx == nullptr) {
context_error(*this, ContextErrorType::GetAutofocusInvalidContext);
}
return ctx->autofocus_element;
}
void recompui::ContextId::set_autofocus_element(Element* element) {
std::lock_guard lock{ context_state.all_contexts_lock };
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
if (ctx == nullptr) {
context_error(*this, ContextErrorType::SetAutofocusInvalidContext);
}
ctx->autofocus_element = element;
}
recompui::ContextId recompui::get_current_context() {
// Ensure a context is currently opened by this thread.
if (opened_context_id == ContextId::null()) {
+9 -2
View File
@@ -31,6 +31,7 @@ namespace recompui {
void add_loose_element(Element* element);
void queue_element_update(ResourceId element);
void queue_set_text(Element* element, std::string&& text);
Style* create_style();
@@ -40,15 +41,21 @@ namespace recompui {
Rml::ElementDocument* get_document();
Element* get_root_element();
Element* get_autofocus_element();
void set_autofocus_element(Element* element);
void open();
bool open_if_not_already();
void close();
void process_updates();
static constexpr ContextId null() { return ContextId{ .slot_id = uint32_t(-1) }; }
// TODO
bool takes_input() { return true; }
bool captures_input();
bool captures_mouse();
void set_captures_input(bool captures_input);
void set_captures_mouse(bool captures_input);
};
ContextId create_context(const std::filesystem::path& path);
+25 -3
View File
@@ -4,9 +4,11 @@
namespace recompui {
Button::Button(Element *parent, const std::string &text, ButtonStyle style) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Enable), "button") {
Button::Button(Element *parent, const std::string &text, ButtonStyle style) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Enable, EventType::Focus), "button", true) {
this->style = style;
enable_focus();
set_text(text);
set_display(Display::Block);
set_padding(23.0f);
@@ -21,6 +23,7 @@ namespace recompui {
set_color(Color{ 204, 204, 204, 255 });
set_tab_index(TabIndex::Auto);
hover_style.set_color(Color{ 242, 242, 242, 255 });
focus_style.set_color(Color{ 242, 242, 242, 255 });
disabled_style.set_color(Color{ 204, 204, 204, 128 });
hover_disabled_style.set_color(Color{ 242, 242, 242, 128 });
@@ -34,6 +37,8 @@ namespace recompui {
set_background_color({ 185, 125, 242, background_opacity });
hover_style.set_border_color({ 185, 125, 242, border_hover_opacity });
hover_style.set_background_color({ 185, 125, 242, background_hover_opacity });
focus_style.set_border_color({ 185, 125, 242, border_hover_opacity });
focus_style.set_background_color({ 185, 125, 242, background_hover_opacity });
disabled_style.set_border_color({ 185, 125, 242, border_opacity / 4 });
disabled_style.set_background_color({ 185, 125, 242, background_opacity / 4 });
hover_disabled_style.set_border_color({ 185, 125, 242, border_hover_opacity / 4 });
@@ -45,6 +50,8 @@ namespace recompui {
set_background_color({ 23, 214, 232, background_opacity });
hover_style.set_border_color({ 23, 214, 232, border_hover_opacity });
hover_style.set_background_color({ 23, 214, 232, background_hover_opacity });
focus_style.set_border_color({ 23, 214, 232, border_hover_opacity });
focus_style.set_background_color({ 23, 214, 232, background_hover_opacity });
disabled_style.set_border_color({ 23, 214, 232, border_opacity / 4 });
disabled_style.set_background_color({ 23, 214, 232, background_opacity / 4 });
hover_disabled_style.set_border_color({ 23, 214, 232, border_hover_opacity / 4 });
@@ -57,6 +64,7 @@ namespace recompui {
}
add_style(&hover_style, hover_state);
add_style(&focus_style, focus_state);
add_style(&disabled_style, disabled_state);
add_style(&hover_disabled_style, { hover_state, disabled_state });
@@ -73,10 +81,24 @@ namespace recompui {
}
break;
case EventType::Hover:
set_style_enabled(hover_state, std::get<EventHover>(e.variant).active);
set_style_enabled(hover_state, std::get<EventHover>(e.variant).active && is_enabled());
break;
case EventType::Enable:
set_style_enabled(disabled_state, !std::get<EventEnable>(e.variant).active);
{
bool enable_active = std::get<EventEnable>(e.variant).active;
set_style_enabled(disabled_state, !enable_active);
if (enable_active) {
set_cursor(Cursor::Pointer);
set_focusable(true);
}
else {
set_cursor(Cursor::None);
set_focusable(false);
}
}
break;
case EventType::Focus:
set_style_enabled(focus_state, std::get<EventFocus>(e.variant).active);
break;
case EventType::Update:
break;
+6
View File
@@ -13,15 +13,21 @@ namespace recompui {
protected:
ButtonStyle style = ButtonStyle::Primary;
Style hover_style;
Style focus_style;
Style disabled_style;
Style hover_disabled_style;
std::list<std::function<void()>> pressed_callbacks;
// Element overrides.
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "Button"; }
public:
Button(Element *parent, const std::string &text, ButtonStyle style);
void add_pressed_callback(std::function<void()> callback);
Style* get_hover_style() { return &hover_style; }
Style* get_focus_style() { return &focus_style; }
Style* get_disabled_style() { return &disabled_style; }
Style* get_hover_disabled_style() { return &hover_disabled_style; }
};
} // namespace recompui
+43 -12
View File
@@ -2,7 +2,8 @@
namespace recompui {
Clickable::Clickable(Element *parent, bool draggable) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Enable, draggable ? EventType::Drag : EventType::None)) {
Clickable::Clickable(Element *parent, bool draggable) : Element(parent, Events(EventType::Click, EventType::MouseButton, EventType::Hover, EventType::Enable, draggable ? EventType::Drag : EventType::None)) {
set_cursor(Cursor::Pointer);
if (draggable) {
set_drag(Drag::Drag);
}
@@ -11,30 +12,60 @@ namespace recompui {
void Clickable::process_event(const Event &e) {
switch (e.type) {
case EventType::Click: {
const EventClick &click = std::get<EventClick>(e.variant);
for (const auto &function : pressed_callbacks) {
function(click.x, click.y);
if (is_enabled()) {
const EventClick &click = std::get<EventClick>(e.variant);
for (const auto &function : clicked_callbacks) {
function(click.x, click.y);
}
break;
}
}
case EventType::MouseButton: {
if (is_enabled()) {
const EventMouseButton &mousebutton = std::get<EventMouseButton>(e.variant);
if (mousebutton.button == MouseButton::Left && mousebutton.pressed) {
for (const auto &function : pressed_callbacks) {
function(mousebutton.x, mousebutton.y);
}
}
break;
}
break;
}
case EventType::Hover:
set_style_enabled(hover_state, std::get<EventHover>(e.variant).active);
set_style_enabled(hover_state, std::get<EventHover>(e.variant).active && is_enabled());
break;
case EventType::Enable:
set_style_enabled(disabled_state, !std::get<EventEnable>(e.variant).active);
break;
case EventType::Drag: {
const EventDrag &drag = std::get<EventDrag>(e.variant);
for (const auto &function : dragged_callbacks) {
function(drag.x, drag.y, drag.phase);
{
bool enable_active = std::get<EventEnable>(e.variant).active;
set_style_enabled(disabled_state, !enable_active);
if (enable_active) {
set_cursor(Cursor::Pointer);
set_focusable(true);
}
else {
set_cursor(Cursor::None);
set_focusable(false);
}
}
break;
case EventType::Drag: {
if (is_enabled()) {
const EventDrag &drag = std::get<EventDrag>(e.variant);
for (const auto &function : dragged_callbacks) {
function(drag.x, drag.y, drag.phase);
}
break;
}
}
default:
break;
}
}
void Clickable::add_clicked_callback(std::function<void(float, float)> callback) {
clicked_callbacks.emplace_back(callback);
}
void Clickable::add_pressed_callback(std::function<void(float, float)> callback) {
pressed_callbacks.emplace_back(callback);
}
+3
View File
@@ -6,13 +6,16 @@ namespace recompui {
class Clickable : public Element {
protected:
std::vector<std::function<void(float, float)>> clicked_callbacks;
std::vector<std::function<void(float, float)>> pressed_callbacks;
std::vector<std::function<void(float, float, DragPhase)>> dragged_callbacks;
// Element overrides.
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "Clickable"; }
public:
Clickable(Element *parent, bool draggable = false);
void add_clicked_callback(std::function<void(float, float)> callback);
void add_pressed_callback(std::function<void(float, float)> callback);
void add_dragged_callback(std::function<void(float, float, DragPhase)> callback);
};
+1 -2
View File
@@ -4,9 +4,8 @@
namespace recompui {
Container::Container(Element *parent, FlexDirection direction, JustifyContent justify_content) : Element(parent) {
Container::Container(Element *parent, FlexDirection direction, JustifyContent justify_content, uint32_t events_enabled) : Element(parent, events_enabled) {
set_display(Display::Flex);
set_flex(1.0f, 1.0f);
set_flex_direction(direction);
set_justify_content(justify_content);
}
+3 -1
View File
@@ -5,8 +5,10 @@
namespace recompui {
class Container : public Element {
protected:
std::string_view get_type_name() override { return "Container"; }
public:
Container(Element* parent, FlexDirection direction, JustifyContent justify_content);
Container(Element* parent, FlexDirection direction, JustifyContent justify_content, uint32_t events_enabled = 0);
};
} // namespace recompui
+219 -15
View File
@@ -1,3 +1,7 @@
#include "RmlUi/Core/StringUtilities.h"
#include "overloaded.h"
#include "recomp_ui.h"
#include "ui_element.h"
#include "../core/ui_context.h"
@@ -13,7 +17,7 @@ Element::Element(Rml::Element *base) {
this->shim = true;
}
Element::Element(Element* parent, uint32_t events_enabled, Rml::String base_class) {
Element::Element(Element* parent, uint32_t events_enabled, Rml::String base_class, bool can_set_text) : can_set_text(can_set_text) {
ContextId context = get_current_context();
base_owning = context.get_document()->CreateElement(base_class);
@@ -25,6 +29,9 @@ Element::Element(Element* parent, uint32_t events_enabled, Rml::String base_clas
base = base_owning.get();
}
set_display(Display::Block);
set_property(Rml::PropertyId::BoxSizing, Rml::Style::BoxSizing::BorderBox);
register_event_listeners(events_enabled);
}
@@ -39,6 +46,10 @@ Element::~Element() {
void Element::add_child(Element *child) {
assert(child != nullptr);
if (can_set_text) {
assert(false && "Elements with settable text cannot have children");
return;
}
children.emplace_back(child);
@@ -61,7 +72,12 @@ void Element::register_event_listeners(uint32_t events_enabled) {
this->events_enabled = events_enabled;
if (events_enabled & Events(EventType::Click)) {
base->AddEventListener(Rml::EventId::Click, this);
}
if (events_enabled & Events(EventType::MouseButton)) {
base->AddEventListener(Rml::EventId::Mousedown, this);
base->AddEventListener(Rml::EventId::Mouseup, this);
}
if (events_enabled & Events(EventType::Focus)) {
@@ -83,11 +99,20 @@ void Element::register_event_listeners(uint32_t events_enabled) {
if (events_enabled & Events(EventType::Text)) {
base->AddEventListener(Rml::EventId::Change, this);
}
if (events_enabled & Events(EventType::Navigate)) {
base->AddEventListener(Rml::EventId::Keydown, this);
}
}
void Element::apply_style(Style *style) {
for (auto it : style->property_map) {
base->SetProperty(it.first, it.second);
// Skip redundant SetProperty calls to prevent dirtying unnecessary state.
// This avoids expensive layout operations when a simple color-only style is applied.
const Rml::Property* cur_value = base->GetLocalProperty(it.first);
if (*cur_value != it.second) {
base->SetProperty(it.first, it.second);
}
}
}
@@ -110,7 +135,7 @@ void Element::propagate_disabled(bool disabled) {
base->SetAttribute("disabled", attribute_state);
if (events_enabled & Events(EventType::Enable)) {
process_event(Event::enable_event(!attribute_state));
handle_event(Event::enable_event(!attribute_state));
}
for (auto &child : children) {
@@ -119,25 +144,86 @@ void Element::propagate_disabled(bool disabled) {
}
}
void Element::handle_event(const Event& event) {
for (const auto& callback : callbacks) {
recompui::queue_ui_callback(resource_id, event, callback);
}
process_event(event);
}
void Element::set_id(const std::string& new_id) {
id = new_id;
base->SetId(new_id);
}
recompui::MouseButton convert_rml_mouse_button(int button) {
switch (button) {
case 0:
return recompui::MouseButton::Left;
case 1:
return recompui::MouseButton::Right;
case 2:
return recompui::MouseButton::Middle;
default:
return recompui::MouseButton::Count;
}
}
void Element::ProcessEvent(Rml::Event &event) {
ContextId prev_context = recompui::try_close_current_context();
ContextId context = ContextId::null();
Rml::ElementDocument* doc = event.GetTargetElement()->GetOwnerDocument();
if (doc != nullptr) {
context = get_context_from_document(doc);
}
bool did_open = false;
// TODO disallow null contexts once the entire UI system has been migrated.
if (context != ContextId::null()) {
context.open();
did_open = context.open_if_not_already();
}
// Events that are processed during any phase.
switch (event.GetId()) {
case Rml::EventId::Click:
handle_event(Event::click_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f)));
break;
case Rml::EventId::Mousedown:
process_event(Event::click_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f)));
{
MouseButton mouse_button = convert_rml_mouse_button(event.GetParameter("button", 3));
if (mouse_button != MouseButton::Count) {
handle_event(Event::mousebutton_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), mouse_button, true));
}
}
break;
case Rml::EventId::Mouseup:
{
MouseButton mouse_button = convert_rml_mouse_button(event.GetParameter("button", 3));
if (mouse_button != MouseButton::Count) {
handle_event(Event::mousebutton_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), mouse_button, false));
}
}
break;
case Rml::EventId::Keydown:
switch ((Rml::Input::KeyIdentifier)event.GetParameter<int>("key_identifier", 0)) {
case Rml::Input::KeyIdentifier::KI_LEFT:
handle_event(Event::navigate_event(NavDirection::Left));
break;
case Rml::Input::KeyIdentifier::KI_UP:
handle_event(Event::navigate_event(NavDirection::Up));
break;
case Rml::Input::KeyIdentifier::KI_RIGHT:
handle_event(Event::navigate_event(NavDirection::Right));
break;
case Rml::Input::KeyIdentifier::KI_DOWN:
handle_event(Event::navigate_event(NavDirection::Down));
break;
}
break;
case Rml::EventId::Drag:
process_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::Move));
handle_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::Move));
break;
default:
break;
@@ -147,28 +233,28 @@ void Element::ProcessEvent(Rml::Event &event) {
if (event.GetPhase() == Rml::EventPhase::Target) {
switch (event.GetId()) {
case Rml::EventId::Mouseover:
process_event(Event::hover_event(true));
handle_event(Event::hover_event(true));
break;
case Rml::EventId::Mouseout:
process_event(Event::hover_event(false));
handle_event(Event::hover_event(false));
break;
case Rml::EventId::Focus:
process_event(Event::focus_event(true));
handle_event(Event::focus_event(true));
break;
case Rml::EventId::Blur:
process_event(Event::focus_event(false));
handle_event(Event::focus_event(false));
break;
case Rml::EventId::Dragstart:
process_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::Start));
handle_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::Start));
break;
case Rml::EventId::Dragend:
process_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::End));
handle_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::End));
break;
case Rml::EventId::Change: {
if (events_enabled & Events(EventType::Text)) {
Rml::Variant *value_variant = base->GetAttribute("value");
if (value_variant != nullptr) {
process_event(Event::text_event(value_variant->Get<std::string>()));
handle_event(Event::text_event(value_variant->Get<std::string>()));
}
}
@@ -179,9 +265,13 @@ void Element::ProcessEvent(Rml::Event &event) {
}
}
if (context != ContextId::null()) {
if (context != ContextId::null() && did_open) {
context.close();
}
if (prev_context != ContextId::null()) {
prev_context.open();
}
}
void Element::set_attribute(const Rml::String &attribute_key, const Rml::String &attribute_value) {
@@ -192,6 +282,15 @@ void Element::process_event(const Event &) {
// Does nothing by default.
}
void Element::enable_focus() {
set_tab_index_auto();
set_focusable(true);
set_nav_auto(NavDirection::Up);
set_nav_auto(NavDirection::Down);
set_nav_auto(NavDirection::Left);
set_nav_auto(NavDirection::Right);
}
void Element::clear_children() {
if (children.empty()) {
return;
@@ -207,6 +306,24 @@ void Element::clear_children() {
children.clear();
}
bool Element::remove_child(ResourceId child) {
bool found = false;
ContextId context = get_current_context();
for (auto it = children.begin(); it != children.end(); ++it) {
Element* cur_child = *it;
if (cur_child->get_resource_id() == child) {
children.erase(it);
context.destroy_resource(cur_child);
found = true;
break;
}
}
return found;
}
void Element::add_style(Style *style, const std::string_view style_name) {
add_style(style, { style_name });
}
@@ -238,8 +355,46 @@ bool Element::is_enabled() const {
return enabled && !disabled_from_parent;
}
// Adapted from RmlUi's `EncodeRml`.
std::string escape_rml(std::string_view string)
{
std::string result;
result.reserve(string.size());
for (char c : string)
{
switch (c)
{
case '<': result += "&lt;"; break;
case '>': result += "&gt;"; break;
case '&': result += "&amp;"; break;
case '"': result += "&quot;"; break;
case '\n': result += "<br/>"; break;
default: result += c; break;
}
}
return result;
}
void Element::set_text(std::string_view text) {
base->SetInnerRML(std::string(text));
if (can_set_text) {
// Queue the text update. If it's applied immediately, it might happen
// while the document is being updated or rendered. This can cause a crash
// due to the child elements being deleted while the document is being updated.
// Queueing them defers it to the update thread, which prevents that issue.
// Escape the string into Rml to prevent element injection.
get_current_context().queue_set_text(this, escape_rml(text));
}
else {
assert(false && "Attempted to set text of an element that cannot have its text set.");
}
}
std::string Element::get_input_text() {
return base->GetAttribute("value", std::string{});
}
void Element::set_input_text(std::string_view val) {
base->SetAttribute("value", std::string{ val });
}
void Element::set_src(std::string_view src) {
@@ -274,6 +429,10 @@ void Element::set_style_enabled(std::string_view style_name, bool enable) {
apply_styles();
}
bool Element::is_style_enabled(std::string_view style_name) {
return style_active_set.contains(style_name);
}
float Element::get_absolute_left() {
return base->GetAbsoluteLeft();
}
@@ -298,6 +457,47 @@ float Element::get_client_height() {
return base->GetClientHeight();
}
uint32_t Element::get_input_value_u32() {
ElementValue value = get_element_value();
return std::visit(overloaded {
[](double d) { return (uint32_t)d; },
[](float f) { return (uint32_t)f; },
[](uint32_t u) { return u; },
[](std::monostate) { return 0U; }
}, value);
}
float Element::get_input_value_float() {
ElementValue value = get_element_value();
return std::visit(overloaded {
[](double d) { return (float)d; },
[](float f) { return f; },
[](uint32_t u) { return (float)u; },
[](std::monostate) { return 0.0f; }
}, value);
}
double Element::get_input_value_double() {
ElementValue value = get_element_value();
return std::visit(overloaded {
[](double d) { return d; },
[](float f) { return (double)f; },
[](uint32_t u) { return (double)u; },
[](std::monostate) { return 0.0; }
}, value);
}
void Element::focus() {
base->Focus();
}
void Element::blur() {
base->Blur();
}
void Element::queue_update() {
ContextId cur_context = get_current_context();
@@ -309,4 +509,8 @@ void Element::queue_update() {
cur_context.queue_element_update(resource_id);
}
void Element::register_callback(ContextId context, PTR(void) callback, PTR(void) userdata) {
callbacks.emplace_back(UICallback{.context = context, .callback = callback, .userdata = userdata});
}
}
+41 -3
View File
@@ -3,14 +3,26 @@
#include "ui_style.h"
#include "../core/ui_context.h"
#include "recomp.h"
#include <ultramodern/ultra64.h>
#include <unordered_set>
#include <variant>
namespace recompui {
struct UICallback {
ContextId context;
PTR(void) callback;
PTR(void) userdata;
};
using ElementValue = std::variant<uint32_t, float, double, std::monostate>;
class ContextId;
class Element : public Style, public Rml::EventListener {
friend ContextId create_context(const std::filesystem::path& path);
friend ContextId create_context();
friend class ContextId; // To allow ContextId to call the process_event method directly.
friend class ContextId; // To allow ContextId to call the handle_event method directly.
private:
Rml::Element *base = nullptr;
Rml::ElementPtr base_owning = {};
@@ -19,17 +31,21 @@ private:
std::vector<uint32_t> styles_counter;
std::unordered_set<std::string_view> style_active_set;
std::unordered_multimap<std::string_view, uint32_t> style_name_index_map;
std::vector<UICallback> callbacks;
std::vector<Element *> children;
std::string id;
bool shim = false;
bool enabled = true;
bool disabled_attribute = false;
bool disabled_from_parent = false;
bool can_set_text = false;
void add_child(Element *child);
void register_event_listeners(uint32_t events_enabled);
void apply_style(Style *style);
void apply_styles();
void propagate_disabled(bool disabled);
void handle_event(const Event &e);
void set_id(const std::string& new_id);
// Style overrides.
virtual void set_property(Rml::PropertyId property_id, const Rml::Property &property) override;
@@ -40,21 +56,30 @@ protected:
// Use of this method in inherited classes is discouraged unless it's necessary.
void set_attribute(const Rml::String &attribute_key, const Rml::String &attribute_value);
virtual void process_event(const Event &e);
virtual ElementValue get_element_value() { return std::monostate{}; }
virtual void set_input_value(const ElementValue&) {}
virtual std::string_view get_type_name() { return "Element"; }
public:
// Used for backwards compatibility with legacy UI elements.
Element(Rml::Element *base);
// Used to actually construct elements.
Element(Element* parent, uint32_t events_enabled = 0, Rml::String base_class = "div");
Element(Element* parent, uint32_t events_enabled = 0, Rml::String base_class = "div", bool can_set_text = false);
virtual ~Element();
void clear_children();
bool remove_child(ResourceId child);
bool remove_child(Element *child) { return remove_child(child->get_resource_id()); }
void add_style(Style *style, std::string_view style_name);
void add_style(Style *style, const std::initializer_list<std::string_view> &style_names);
void set_enabled(bool enabled);
bool is_enabled() const;
void set_text(std::string_view text);
std::string get_input_text();
void set_input_text(std::string_view text);
void set_src(std::string_view src);
void set_style_enabled(std::string_view style_name, bool enabled);
bool is_style_enabled(std::string_view style_name);
void apply_styles();
bool is_element() override { return true; }
float get_absolute_left();
float get_absolute_top();
@@ -62,7 +87,20 @@ public:
float get_client_top();
float get_client_width();
float get_client_height();
void enable_focus();
void focus();
void blur();
void queue_update();
void register_callback(ContextId context, PTR(void) callback, PTR(void) userdata);
uint32_t get_input_value_u32();
float get_input_value_float();
double get_input_value_double();
void set_input_value_u32(uint32_t val) { set_input_value(val); }
void set_input_value_float(float val) { set_input_value(val); }
void set_input_value_double(double val) { set_input_value(val); }
const std::string& get_id() { return id; }
};
void queue_ui_callback(recompui::ResourceId resource, const Event& e, const UICallback& callback);
} // namespace recompui
+2
View File
@@ -5,6 +5,8 @@
namespace recompui {
class Image : public Element {
protected:
std::string_view get_type_name() override { return "ImageView"; }
public:
Image(Element *parent, std::string_view src);
};
+1 -1
View File
@@ -4,7 +4,7 @@
namespace recompui {
Label::Label(Element *parent, LabelStyle label_style) : Element(parent) {
Label::Label(Element *parent, LabelStyle label_style) : Element(parent, 0U, "div", true) {
switch (label_style) {
case LabelStyle::Annotation:
set_color(Color{ 185, 125, 242, 255 });
+2
View File
@@ -12,6 +12,8 @@ namespace recompui {
};
class Label : public Element {
protected:
std::string_view get_type_name() override { return "Label"; }
public:
Label(Element *parent, LabelStyle label_style);
Label(Element *parent, const std::string &text, LabelStyle label_style);
+170 -8
View File
@@ -1,12 +1,15 @@
#include "overloaded.h"
#include "ui_radio.h"
#include "../ui_utils.h"
namespace recompui {
// RadioOption
RadioOption::RadioOption(Element *parent, std::string_view name, uint32_t index) : Element(parent, Events(EventType::Click, EventType::Focus, EventType::Hover, EventType::Enable), "label") {
RadioOption::RadioOption(Element *parent, std::string_view name, uint32_t index) : Element(parent, Events(EventType::MouseButton, EventType::Click, EventType::Focus, EventType::Hover, EventType::Enable, EventType::Update), "label", true) {
this->index = index;
enable_focus();
set_text(name);
set_cursor(Cursor::Pointer);
set_font_size(20.0f);
@@ -14,29 +17,44 @@ namespace recompui {
set_line_height(20.0f);
set_font_weight(400);
set_font_style(FontStyle::Normal);
set_border_color(Color{ 242, 242, 242, 255 });
set_border_bottom_width(0.0f);
set_border_color(Color{ 242, 242, 242, 0 });
set_border_bottom_width(1.0f);
set_color(Color{ 255, 255, 255, 153 });
set_padding_bottom(8.0f);
set_text_transform(TextTransform::Uppercase);
set_height_auto();
hover_style.set_color(Color{ 255, 255, 255, 204 });
checked_style.set_color(Color{ 255, 255, 255, 255 });
checked_style.set_border_bottom_width(1.0f);
checked_style.set_border_color(Color{ 242, 242, 242, 255 });
pulsing_style.set_border_color(Color{ 23, 214, 232, 244 });
add_style(&hover_style, { hover_state });
add_style(&checked_style, { checked_state });
add_style(&pulsing_style, { focus_state });
}
void RadioOption::set_pressed_callback(std::function<void(uint32_t)> callback) {
pressed_callback = callback;
}
void RadioOption::set_focus_callback(std::function<void(bool)> callback) {
focus_callback = callback;
}
void RadioOption::set_selected_state(bool enable) {
set_style_enabled(checked_state, enable);
}
void RadioOption::process_event(const Event &e) {
switch (e.type) {
case EventType::MouseButton:
{
const EventMouseButton &mousebutton = std::get<EventMouseButton>(e.variant);
if (mousebutton.button == MouseButton::Left && mousebutton.pressed) {
pressed_callback(index);
}
}
break;
case EventType::Click:
pressed_callback(index);
break;
@@ -46,6 +64,25 @@ namespace recompui {
case EventType::Enable:
set_style_enabled(disabled_state, !std::get<EventEnable>(e.variant).active);
break;
case EventType::Focus:
{
bool active = std::get<EventFocus>(e.variant).active;
set_style_enabled(focus_state, active);
if (active) {
queue_update();
}
if (focus_callback != nullptr) {
focus_callback(active);
}
}
break;
case EventType::Update:
if (is_style_enabled(focus_state)) {
pulsing_style.set_color(recompui::get_pulse_color(750));
apply_styles();
queue_update();
}
break;
default:
break;
}
@@ -70,10 +107,41 @@ namespace recompui {
void Radio::option_selected(uint32_t index) {
set_index_internal(index, false, true);
}
void Radio::set_input_value(const ElementValue& val) {
std::visit(overloaded {
[this](uint32_t u) { set_index(u); },
[this](float f) { set_index(f); },
[this](double d) { set_index(d); },
[](std::monostate) {}
}, val);
}
Radio::Radio(Element *parent) : Container(parent, FlexDirection::Row, JustifyContent::FlexStart) {
Radio::Radio(Element *parent) : Container(parent, FlexDirection::Row, JustifyContent::FlexStart, Events(EventType::Focus, EventType::Update)) {
set_gap(24.0f);
set_flex_grow(0.0f);
set_align_items(AlignItems::FlexStart);
enable_focus();
}
void Radio::process_event(const Event &e) {
switch (e.type) {
case EventType::Focus:
if (!options.empty()) {
if (std::get<EventFocus>(e.variant).active) {
blur();
queue_child_focus();
}
if (focus_callback != nullptr) {
focus_callback(std::get<EventFocus>(e.variant).active);
}
}
break;
case EventType::Update:
if (child_focus_queued) {
child_focus_queued = false;
options[index]->focus();
}
}
}
Radio::~Radio() {
@@ -82,13 +150,23 @@ namespace recompui {
void Radio::add_option(std::string_view name) {
RadioOption *option = get_current_context().create_element<RadioOption>(this, name, uint32_t(options.size()));
option->set_pressed_callback(std::bind(&Radio::option_selected, this, std::placeholders::_1));
option->set_pressed_callback([this](uint32_t index){ options[index]->focus(); option_selected(index); });
option->set_focus_callback([this](bool active) {
if (focus_callback != nullptr) {
focus_callback(active);
}
});
options.emplace_back(option);
// The first option was added, select it.
if (options.size() == 1) {
set_index_internal(0, true, false);
}
// At least one other option already existed, so set up navigation.
else {
options[options.size() - 2]->set_nav(NavDirection::Right, options[options.size() - 1]);
options[options.size() - 1]->set_nav(NavDirection::Left, options[options.size() - 2]);
}
}
void Radio::set_index(uint32_t index) {
@@ -103,4 +181,88 @@ namespace recompui {
index_changed_callbacks.emplace_back(callback);
}
};
void Radio::set_focus_callback(std::function<void(bool)> callback) {
focus_callback = callback;
}
void Radio::set_nav_auto(NavDirection dir) {
Element::set_nav_auto(dir);
if (!options.empty()) {
switch (dir) {
case NavDirection::Up:
case NavDirection::Down:
for (Element* e : options) {
e->set_nav_auto(dir);
}
break;
case NavDirection::Left:
options.front()->set_nav_auto(dir);
break;
case NavDirection::Right:
options.back()->set_nav_auto(dir);
break;
}
}
}
void Radio::set_nav_none(NavDirection dir) {
Element::set_nav_none(dir);
if (!options.empty()) {
switch (dir) {
case NavDirection::Up:
case NavDirection::Down:
for (Element* e : options) {
e->set_nav_none(dir);
}
break;
case NavDirection::Left:
options.front()->set_nav_none(dir);
break;
case NavDirection::Right:
options.back()->set_nav_none(dir);
break;
}
}
}
void Radio::set_nav(NavDirection dir, Element* element) {
Element::set_nav(dir, element);
if (!options.empty()) {
switch (dir) {
case NavDirection::Up:
case NavDirection::Down:
for (Element* e : options) {
e->set_nav(dir, element);
}
break;
case NavDirection::Left:
options.front()->set_nav(dir, element);
break;
case NavDirection::Right:
options.back()->set_nav(dir, element);
break;
}
}
}
void Radio::set_nav_manual(NavDirection dir, const std::string& target) {
Element::set_nav_manual(dir, target);
if (!options.empty()) {
switch (dir) {
case NavDirection::Up:
case NavDirection::Down:
for (Element* e : options) {
e->set_nav_manual(dir, target);
}
break;
case NavDirection::Left:
options.front()->set_nav_manual(dir, target);
break;
case NavDirection::Right:
options.back()->set_nav_manual(dir, target);
break;
}
}
}
};
+21 -1
View File
@@ -8,13 +8,17 @@ namespace recompui {
private:
Style hover_style;
Style checked_style;
Style pulsing_style;
std::function<void(uint32_t)> pressed_callback = nullptr;
std::function<void(bool)> focus_callback = nullptr;
uint32_t index = 0;
protected:
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "LabelRadioOption"; }
public:
RadioOption(Element *parent, std::string_view name, uint32_t index);
void set_pressed_callback(std::function<void(uint32_t)> callback);
void set_focus_callback(std::function<void(bool)> callback);
void set_selected_state(bool enable);
};
@@ -23,9 +27,17 @@ namespace recompui {
std::vector<RadioOption *> options;
uint32_t index = 0;
std::vector<std::function<void(uint32_t)>> index_changed_callbacks;
std::function<void(bool)> focus_callback = nullptr;
bool child_focus_queued = false;
void set_index_internal(uint32_t index, bool setup, bool trigger_callbacks);
void option_selected(uint32_t index);
void set_input_value(const ElementValue& val) override;
ElementValue get_element_value() override { return get_index(); }
protected:
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "LabelRadio"; }
void queue_child_focus() { child_focus_queued = true; queue_update(); }
public:
Radio(Element *parent);
virtual ~Radio();
@@ -33,6 +45,14 @@ namespace recompui {
void set_index(uint32_t index);
uint32_t get_index() const;
void add_index_changed_callback(std::function<void(uint32_t)> callback);
void set_focus_callback(std::function<void(bool)> callback);
size_t num_options() const { return options.size(); }
RadioOption* get_option_element(size_t option_index) { return options[option_index]; }
RadioOption* get_current_option_element() { return options.empty() ? nullptr : options[index]; }
void set_nav_auto(NavDirection dir) override;
void set_nav_none(NavDirection dir) override;
void set_nav(NavDirection dir, Element* element) override;
void set_nav_manual(NavDirection dir, const std::string& target) override;
};
} // namespace recompui
} // namespace recompui
+2
View File
@@ -10,6 +10,8 @@ namespace recompui {
};
class ScrollContainer : public Element {
protected:
std::string_view get_type_name() override { return "ScrollContainer"; }
public:
ScrollContainer(Element *parent, ScrollDirection direction);
};
+118 -20
View File
@@ -1,4 +1,6 @@
#include "overloaded.h"
#include "ui_slider.h"
#include "../ui_utils.h"
#include <cmath>
#include <charconv>
@@ -23,7 +25,7 @@ namespace recompui {
}
}
void Slider::bar_clicked(float x, float) {
void Slider::bar_pressed(float x, float) {
update_value_from_mouse(x);
}
@@ -44,29 +46,104 @@ namespace recompui {
void Slider::update_circle_position() {
double ratio = std::clamp((value - min_value) / (max_value - min_value), 0.0, 1.0);
circle_element->set_left(slider_width_dp * ratio);
circle_element->set_left(ratio * 100.0, Unit::Percent);
}
void Slider::update_label_text() {
char text_buffer[32];
int precision = type == SliderType::Double ? 1 : 0;
auto result = std::to_chars(text_buffer, text_buffer + sizeof(text_buffer) - 1, value, std::chars_format::fixed, precision);
if (result.ec == std::errc()) {
if (type == SliderType::Percent) {
*result.ptr = '%';
result.ptr++;
}
value_label->set_text(std::string(text_buffer, result.ptr));
if (type == SliderType::Double) {
std::snprintf(text_buffer, sizeof(text_buffer), "%.1f", value);
} else if (type == SliderType::Percent) {
std::snprintf(text_buffer, sizeof(text_buffer), "%d%%", static_cast<int>(value));
} else {
std::snprintf(text_buffer, sizeof(text_buffer), "%d", static_cast<int>(value));
}
value_label->set_text(text_buffer);
}
void Slider::set_input_value(const ElementValue& val) {
std::visit(overloaded {
[this](uint32_t u) { set_value(u); },
[this](float f) { set_value(f); },
[this](double d) { set_value(d); },
[](std::monostate) {}
}, val);
}
void Slider::process_event(const Event& e) {
switch (e.type) {
case EventType::Focus:
{
bool active = std::get<EventFocus>(e.variant).active;
circle_element->set_style_enabled(focus_state, active);
if (active) {
queue_update();
}
if (focus_callback != nullptr) {
focus_callback(active);
}
}
break;
case EventType::Update:
if (is_enabled()) {
if (circle_element->is_style_enabled(focus_state)) {
circle_element->set_background_color(recompui::get_pulse_color(750));
queue_update();
}
else {
circle_element->set_background_color(Color{ 204, 204, 204, 255 });
}
}
else {
circle_element->set_background_color(Color{ 102, 102, 102, 255 });
}
break;
case EventType::Navigate:
{
NavDirection dir = std::get<EventNavigate>(e.variant).direction;
if (dir == NavDirection::Left) {
do_step(false);
}
else if (dir == NavDirection::Right) {
do_step(true);
}
}
break;
case EventType::Enable:
{
bool enable_active = std::get<EventEnable>(e.variant).active;
circle_element->set_enabled(enable_active);
if (enable_active) {
set_cursor(Cursor::Pointer);
set_focusable(true);
circle_element->set_background_color(Color{ 204, 204, 204, 255 });
}
else {
set_cursor(Cursor::None);
set_focusable(false);
circle_element->set_background_color(Color{ 102, 102, 102, 255 });
}
}
break;
default:
break;
}
}
Slider::Slider(Element *parent, SliderType type) : Element(parent) {
Slider::Slider(Element *parent, SliderType type) : Element(parent, Events(EventType::Focus, EventType::Update, EventType::Navigate, EventType::Enable)) {
this->type = type;
set_cursor(Cursor::Pointer);
set_display(Display::Flex);
set_flex(1.0f, 1.0f, 100.0f, Unit::Percent);
set_flex_direction(FlexDirection::Row);
set_text_align(TextAlign::Left);
set_min_width(120.0f);
enable_focus();
set_nav_none(NavDirection::Left);
set_nav_none(NavDirection::Right);
ContextId context = get_current_context();
@@ -75,8 +152,10 @@ namespace recompui {
value_label->set_min_width(60.0f);
value_label->set_max_width(60.0f);
slider_element = context.create_element<Element>(this);
slider_element->set_width(slider_width_dp);
slider_element = context.create_element<Clickable>(this, true);
slider_element->set_flex(1.0f, 0.0f);
slider_element->add_pressed_callback([this](float x, float y){ bar_pressed(x, y); focus(); });
slider_element->add_dragged_callback([this](float x, float y, recompui::DragPhase phase){ bar_dragged(x, y, phase); focus(); });
{
bar_element = context.create_element<Clickable>(slider_element, true);
@@ -84,19 +163,20 @@ namespace recompui {
bar_element->set_height(2.0f);
bar_element->set_margin_top(8.0f);
bar_element->set_background_color(Color{ 255, 255, 255, 50 });
bar_element->add_pressed_callback(std::bind(&Slider::bar_clicked, this, std::placeholders::_1, std::placeholders::_2));
bar_element->add_dragged_callback(std::bind(&Slider::bar_dragged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
bar_element->add_pressed_callback([this](float x, float y){ bar_pressed(x, y); focus(); });
bar_element->add_dragged_callback([this](float x, float y, recompui::DragPhase phase){ bar_dragged(x, y, phase); focus(); });
circle_element = context.create_element<Clickable>(slider_element, true);
circle_element = context.create_element<Clickable>(bar_element, true);
circle_element->set_position(Position::Relative);
circle_element->set_width(16.0f);
circle_element->set_height(16.0f);
circle_element->set_margin_top(-8.0f);
circle_element->set_margin_top(-7.0f);
circle_element->set_margin_right(-8.0f);
circle_element->set_margin_left(-8.0f);
circle_element->set_background_color(Color{ 204, 204, 204, 255 });
circle_element->set_border_radius(8.0f);
circle_element->add_dragged_callback(std::bind(&Slider::circle_dragged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
circle_element->add_pressed_callback([this](float, float){ focus(); });
circle_element->add_dragged_callback([this](float x, float y, recompui::DragPhase phase){ circle_dragged(x, y, phase); focus(); });
circle_element->set_cursor(Cursor::Pointer);
}
@@ -142,4 +222,22 @@ namespace recompui {
value_changed_callbacks.emplace_back(callback);
}
} // namespace recompui
void Slider::set_focus_callback(std::function<void(bool)> callback) {
focus_callback = callback;
}
void Slider::do_step(bool increment) {
double new_value = value;
if (increment) {
new_value += step_value;
}
else {
new_value -= step_value;
}
new_value = std::clamp(new_value, min_value, max_value);
if (new_value != value) {
set_value_internal(new_value, false, true);
}
}
} // namespace recompui
+11 -3
View File
@@ -15,23 +15,29 @@ namespace recompui {
private:
SliderType type = SliderType::Percent;
Label *value_label = nullptr;
Element *slider_element = nullptr;
Clickable *slider_element = nullptr;
Clickable *bar_element = nullptr;
Clickable *circle_element = nullptr;
double value = 50.0;
double min_value = 0.0;
double max_value = 100.0;
double step_value = 0.0;
float slider_width_dp = 300.0;
std::vector<std::function<void(double)>> value_changed_callbacks;
std::function<void(bool)> focus_callback = nullptr;
void set_value_internal(double v, bool setup, bool trigger_callbacks);
void bar_clicked(float x, float y);
void bar_pressed(float x, float y);
void bar_dragged(float x, float y, DragPhase phase);
void circle_dragged(float x, float y, DragPhase phase);
void update_value_from_mouse(float x);
void update_circle_position();
void update_label_text();
void set_input_value(const ElementValue& val) override;
ElementValue get_element_value() override { return get_value(); }
protected:
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "Slider"; }
public:
Slider(Element *parent, SliderType type);
@@ -45,6 +51,8 @@ namespace recompui {
void set_step_value(double v);
double get_step_value() const;
void add_value_changed_callback(std::function<void(double)> callback);
void do_step(bool increment);
void set_focus_callback(std::function<void(bool)> callback);
};
} // namespace recompui
+15
View File
@@ -0,0 +1,15 @@
#include "ui_span.h"
#include <cassert>
namespace recompui {
Span::Span(Element *parent) : Element(parent, 0, "span", true) {
set_font_style(FontStyle::Normal);
}
Span::Span(Element *parent, const std::string &text) : Span(parent) {
set_text(text);
}
};
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include "ui_element.h"
#include "ui_label.h"
namespace recompui {
class Span : public Element {
protected:
std::string_view get_type_name() override { return "Span"; }
public:
Span(Element *parent);
Span(Element *parent, const std::string &text);
};
} // namespace recompui
+64 -1
View File
@@ -1,4 +1,5 @@
#include "ui_style.h"
#include "ui_element.h"
#include <cassert>
@@ -169,6 +170,22 @@ namespace recompui {
}
}
static Rml::PropertyId nav_to_property(NavDirection dir) {
switch (dir) {
case NavDirection::Up:
return Rml::PropertyId::NavUp;
case NavDirection::Right:
return Rml::PropertyId::NavRight;
case NavDirection::Down:
return Rml::PropertyId::NavDown;
case NavDirection::Left:
return Rml::PropertyId::NavLeft;
default:
assert(false && "Unknown nav direction.");
return Rml::PropertyId::Invalid;
}
}
void Style::set_property(Rml::PropertyId property_id, const Rml::Property &property) {
property_map[property_id] = property;
}
@@ -181,6 +198,17 @@ namespace recompui {
}
void Style::set_visibility(Visibility visibility) {
switch (visibility) {
case Visibility::Visible:
set_property(Rml::PropertyId::Visibility, Rml::Style::Visibility::Visible);
break;
case Visibility::Hidden:
set_property(Rml::PropertyId::Visibility, Rml::Style::Visibility::Hidden);
break;
}
}
void Style::set_position(Position position) {
switch (position) {
case Position::Absolute:
@@ -401,7 +429,7 @@ namespace recompui {
void Style::set_cursor(Cursor cursor) {
switch (cursor) {
case Cursor::None:
assert(false && "Unimplemented.");
set_property(Rml::PropertyId::Cursor, Rml::Property("", Rml::Unit::STRING));
break;
case Cursor::Pointer:
set_property(Rml::PropertyId::Cursor, Rml::Property("pointer", Rml::Unit::STRING));
@@ -460,6 +488,12 @@ namespace recompui {
case FlexDirection::Column:
set_property(Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Column);
break;
case FlexDirection::RowReverse:
set_property(Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::RowReverse);
break;
case FlexDirection::ColumnReverse:
set_property(Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::ColumnReverse);
break;
default:
assert(false && "Unknown flex direction.");
break;
@@ -545,5 +579,34 @@ namespace recompui {
void Style::set_font_family(std::string_view family) {
set_property(Rml::PropertyId::FontFamily, Rml::Property(Rml::String{ family }, Rml::Unit::UNKNOWN));
}
void Style::set_nav_auto(NavDirection dir) {
set_property(nav_to_property(dir), Rml::Style::Nav::Auto);
}
void Style::set_nav_none(NavDirection dir) {
set_property(nav_to_property(dir), Rml::Style::Nav::None);
}
void Style::set_nav(NavDirection dir, Element* element) {
set_property(nav_to_property(dir), Rml::Property(Rml::String{ "#" + element->get_id() }, Rml::Unit::STRING));
}
void Style::set_nav_manual(NavDirection dir, const std::string& target) {
set_property(nav_to_property(dir), Rml::Property(target, Rml::Unit::STRING));
}
void Style::set_tab_index_auto() {
set_property(Rml::PropertyId::TabIndex, Rml::Style::Nav::Auto);
}
void Style::set_tab_index_none() {
set_property(Rml::PropertyId::TabIndex, Rml::Style::Nav::None);
}
void Style::set_focusable(bool focusable) {
set_property(Rml::PropertyId::Focus, focusable ? Rml::Style::Focus::Auto : Rml::Style::Focus::None);
}
} // namespace recompui
+8
View File
@@ -20,6 +20,7 @@ namespace recompui {
public:
Style();
virtual ~Style();
void set_visibility(Visibility visibility);
void set_position(Position position);
void set_left(float left, Unit unit = Unit::Dp);
void set_top(float top, Unit unit = Unit::Dp);
@@ -93,6 +94,13 @@ namespace recompui {
void set_drag(Drag drag);
void set_tab_index(TabIndex focus);
void set_font_family(std::string_view family);
virtual void set_nav_auto(NavDirection dir);
virtual void set_nav_none(NavDirection dir);
virtual void set_nav(NavDirection dir, Element* element);
virtual void set_nav_manual(NavDirection dir, const std::string& target);
void set_tab_index_auto();
void set_tab_index_none();
void set_focusable(bool focusable);
virtual bool is_element() { return false; }
ResourceId get_resource_id() { return resource_id; }
};
+19 -3
View File
@@ -16,17 +16,30 @@ namespace recompui {
break;
}
case EventType::Focus: {
const EventFocus &event = std::get<EventFocus>(e.variant);
if (focus_callback != nullptr) {
focus_callback(event.active);
}
break;
}
default:
break;
}
}
TextInput::TextInput(Element *parent) : Element(parent, Events(EventType::Text), "input") {
TextInput::TextInput(Element *parent, bool text_visible) : Element(parent, Events(EventType::Text, EventType::Focus), "input") {
if (!text_visible) {
set_attribute("type", "password");
}
set_min_width(60.0f);
set_max_width(400.0f);
set_border_color(Color{ 242, 242, 242, 255 });
set_border_bottom_width(1.0f);
set_padding_bottom(6.0f);
set_focusable(true);
set_nav_auto(NavDirection::Up);
set_nav_auto(NavDirection::Down);
set_tab_index_auto();
}
void TextInput::set_text(std::string_view text) {
@@ -42,4 +55,7 @@ namespace recompui {
text_changed_callbacks.emplace_back(callback);
}
};
void TextInput::set_focus_callback(std::function<void(bool)> callback) {
focus_callback = callback;
}
};
+4 -1
View File
@@ -8,13 +8,16 @@ namespace recompui {
private:
std::string text;
std::vector<std::function<void(const std::string &)>> text_changed_callbacks;
std::function<void(bool)> focus_callback = nullptr;
protected:
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "TextInput"; }
public:
TextInput(Element *parent);
TextInput(Element *parent, bool text_visible = true);
void set_text(std::string_view text);
const std::string &get_text();
void add_text_changed_callback(std::function<void(const std::string &)> callback);
void set_focus_callback(std::function<void(bool)> callback);
};
} // namespace recompui
+23 -2
View File
@@ -6,7 +6,9 @@
namespace recompui {
Toggle::Toggle(Element *parent) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Enable), "button") {
Toggle::Toggle(Element *parent) : Element(parent, Events(EventType::Click, EventType::Focus, EventType::Hover, EventType::Enable), "button") {
enable_focus();
set_width(162.0f);
set_height(72.0f);
set_border_radius(36.0f);
@@ -18,13 +20,19 @@ namespace recompui {
checked_style.set_border_color(Color{ 34, 177, 76, 255 });
hover_style.set_border_color(Color{ 177, 76, 34, 255 });
hover_style.set_background_color(Color{ 206, 120, 68, 76 });
focus_style.set_border_color(Color{ 177, 76, 34, 255 });
focus_style.set_background_color(Color{ 206, 120, 68, 76 });
checked_hover_style.set_border_color(Color{ 34, 177, 76, 255 });
checked_hover_style.set_background_color(Color{ 68, 206, 120, 76 });
checked_focus_style.set_border_color(Color{ 34, 177, 76, 255 });
checked_focus_style.set_background_color(Color{ 68, 206, 120, 76 });
disabled_style.set_border_color(Color{ 177, 76, 34, 128 });
checked_disabled_style.set_border_color(Color{ 34, 177, 76, 128 });
add_style(&checked_style, checked_state);
add_style(&hover_style, hover_state);
add_style(&focus_style, focus_state);
add_style(&checked_hover_style, { checked_state, hover_state });
add_style(&checked_focus_style, { checked_state, focus_state });
add_style(&disabled_style, disabled_state);
add_style(&checked_disabled_style, { checked_state, disabled_state });
@@ -85,15 +93,28 @@ namespace recompui {
break;
case EventType::Hover: {
bool hover_active = std::get<EventHover>(e.variant).active;
bool hover_active = std::get<EventHover>(e.variant).active && is_enabled();
set_style_enabled(hover_state, hover_active);
floater->set_style_enabled(hover_state, hover_active);
break;
}
case EventType::Focus: {
bool focus_active = std::get<EventFocus>(e.variant).active;
set_style_enabled(focus_state, focus_active);
break;
}
case EventType::Enable: {
bool enable_active = std::get<EventEnable>(e.variant).active;
set_style_enabled(disabled_state, !enable_active);
floater->set_style_enabled(disabled_state, !enable_active);
if (enable_active) {
set_cursor(Cursor::Pointer);
set_focusable(true);
}
else {
set_cursor(Cursor::None);
set_focusable(false);
}
break;
}
case EventType::Update: {
+3
View File
@@ -12,7 +12,9 @@ namespace recompui {
std::list<std::function<void(bool)>> checked_callbacks;
Style checked_style;
Style hover_style;
Style focus_style;
Style checked_hover_style;
Style checked_focus_style;
Style disabled_style;
Style checked_disabled_style;
Style floater_checked_style;
@@ -25,6 +27,7 @@ namespace recompui {
// Element overrides.
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "Toggle"; }
public:
Toggle(Element *parent);
void set_checked(bool checked);
+52 -2
View File
@@ -7,6 +7,7 @@ namespace recompui {
constexpr std::string_view checked_state = "checked";
constexpr std::string_view hover_state = "hover";
constexpr std::string_view focus_state = "focus";
constexpr std::string_view disabled_state = "disabled";
struct Color {
@@ -21,6 +22,7 @@ namespace recompui {
Pointer
};
// These two enums must be kept in sync with patches/recompui_event_structs.h!
enum class EventType {
None,
Click,
@@ -30,6 +32,8 @@ namespace recompui {
Drag,
Text,
Update,
Navigate,
MouseButton,
Count
};
@@ -40,6 +44,20 @@ namespace recompui {
End
};
enum class NavDirection {
Up,
Right,
Down,
Left
};
enum class MouseButton {
Left,
Right,
Middle,
Count
};
template <typename Enum, typename = std::enable_if_t<std::is_enum_v<Enum>>>
constexpr uint32_t Events(Enum first) {
return 1u << static_cast<uint32_t>(first);
@@ -77,7 +95,18 @@ namespace recompui {
std::string text;
};
using EventVariant = std::variant<EventClick, EventFocus, EventHover, EventEnable, EventDrag, EventText, std::monostate>;
struct EventNavigate {
NavDirection direction;
};
struct EventMouseButton {
float x;
float y;
MouseButton button;
bool pressed;
};
using EventVariant = std::variant<EventClick, EventFocus, EventHover, EventEnable, EventDrag, EventText, EventNavigate, EventMouseButton, std::monostate>;
struct Event {
EventType type;
@@ -132,6 +161,20 @@ namespace recompui {
e.variant = std::monostate{};
return e;
}
static Event navigate_event(NavDirection direction) {
Event e;
e.type = EventType::Navigate;
e.variant = EventNavigate{ direction };
return e;
}
static Event mousebutton_event(float x, float y, MouseButton button, bool pressed) {
Event e;
e.type = EventType::MouseButton;
e.variant = EventMouseButton{ x, y, button, pressed };
return e;
}
};
enum class Display {
@@ -151,6 +194,11 @@ namespace recompui {
TableCell
};
enum class Visibility {
Visible,
Hidden
};
enum class Position {
Absolute,
Relative
@@ -167,7 +215,9 @@ namespace recompui {
enum class FlexDirection {
Row,
Column
Column,
RowReverse,
ColumnReverse
};
enum class AlignItems {
+335 -84
View File
@@ -1,5 +1,8 @@
#include "recomp_ui.h"
#include "ui_helpers.h"
#include "ui_api_images.h"
#include "core/ui_context.h"
#include "core/ui_resource.h"
@@ -12,6 +15,7 @@
#include "elements/ui_radio.h"
#include "elements/ui_scroll_container.h"
#include "elements/ui_slider.h"
#include "elements/ui_span.h"
#include "elements/ui_style.h"
#include "elements/ui_text_input.h"
#include "elements/ui_toggle.h"
@@ -19,92 +23,11 @@
#include "librecomp/overlays.hpp"
#include "librecomp/helpers.hpp"
#include "librecomp/addresses.hpp"
#include "ultramodern/error_handling.hpp"
using namespace recompui;
constexpr ResourceId root_element_id{ 0xFFFFFFFE };
// Helpers
ContextId get_context(uint8_t* rdram, recomp_context* ctx) {
uint32_t context_id = _arg<0, uint32_t>(rdram, ctx);
return ContextId{ .slot_id = context_id };
}
template <int arg_index>
std::string arg_string(uint8_t* rdram, recomp_context* ctx) {
PTR(char) str = _arg<arg_index, PTR(char)>(rdram, ctx);
// Get the length of the byteswapped string.
size_t len = 0;
while (MEM_B(str, len) != 0x00) {
len++;
}
std::string ret{};
ret.reserve(len + 1);
for (size_t i = 0; i < len; i++) {
ret += (char)MEM_B(str, i);
}
return ret;
}
template <int arg_index>
ResourceId arg_resource_id(uint8_t* rdram, recomp_context* ctx) {
uint32_t slot_id = _arg<arg_index, uint32_t>(rdram, ctx);
return ResourceId{ .slot_id = slot_id };
}
template <int arg_index>
Element* arg_element(uint8_t* rdram, recomp_context* ctx, ContextId ui_context) {
ResourceId resource = arg_resource_id<arg_index>(rdram, ctx);
if (resource == ResourceId::null()) {
return nullptr;
}
else if (resource == root_element_id) {
return ui_context.get_root_element();
}
return resource.as_element();
}
template <int arg_index>
Style* arg_style(uint8_t* rdram, recomp_context* ctx) {
ResourceId resource = arg_resource_id<arg_index>(rdram, ctx);
if (resource == ResourceId::null()) {
return nullptr;
}
else if (resource == root_element_id) {
ContextId ui_context = recompui::get_current_context();
return ui_context.get_root_element();
}
return *resource;
}
template <int arg_index>
Color arg_color(uint8_t* rdram, recomp_context* ctx) {
PTR(u8) color_arg = _arg<arg_index, PTR(u8)>(rdram, ctx);
Color ret{};
ret.r = MEM_B(0, color_arg);
ret.g = MEM_B(1, color_arg);
ret.b = MEM_B(2, color_arg);
ret.a = MEM_B(3, color_arg);
return ret;
}
void return_resource(recomp_context* ctx, ResourceId resource) {
_return<uint32_t>(ctx, resource.slot_id);
}
// Contexts
void recompui_create_context(uint8_t* rdram, recomp_context* ctx) {
(void)rdram;
@@ -144,6 +67,20 @@ void recompui_hide_context(uint8_t* rdram, recomp_context* ctx) {
recompui::hide_context(ui_context);
}
void recompui_set_context_captures_input(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
bool captures_input = _arg<1, int>(rdram, ctx) != 0;
ui_context.set_captures_input(captures_input);
}
void recompui_set_context_captures_mouse(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
bool captures_mouse = _arg<1, int>(rdram, ctx) != 0;
ui_context.set_captures_mouse(captures_mouse);
}
// Resources
void recompui_create_style(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
@@ -160,17 +97,110 @@ void recompui_create_element(uint8_t* rdram, recomp_context* ctx) {
return_resource(ctx, ret->get_resource_id());
}
void recompui_destroy_element(uint8_t* rdram, recomp_context* ctx) {
Style* parent_resource = arg_style<0>(rdram, ctx);
if (!parent_resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to remove child from non-element or element not found in context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* parent = static_cast<Element*>(parent_resource);
ResourceId to_remove = arg_resource_id<1>(rdram, ctx);
if (!parent->remove_child(to_remove)) {
recompui::message_box("Fatal error in mod - attempted to remove child from wrong parent");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
}
void recompui_create_button(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
Element* parent = arg_element<1>(rdram, ctx, ui_context);
std::string text = arg_string<2>(rdram, ctx);
std::string text = _arg_string<2>(rdram, ctx);
uint32_t style = _arg<3, uint32_t>(rdram, ctx);
Button* ret = ui_context.create_element<Button>(parent, text, static_cast<ButtonStyle>(style));
return_resource(ctx, ret->get_resource_id());
}
void recompui_create_label(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
Element* parent = arg_element<1>(rdram, ctx, ui_context);
std::string text = _arg_string<2>(rdram, ctx);
uint32_t style = _arg<3, uint32_t>(rdram, ctx);
Element* ret = ui_context.create_element<Label>(parent, text, static_cast<LabelStyle>(style));
return_resource(ctx, ret->get_resource_id());
}
void recompui_create_span(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
Element* parent = arg_element<1>(rdram, ctx, ui_context);
std::string text = _arg_string<2>(rdram, ctx);
Element* ret = ui_context.create_element<Span>(parent, text);
return_resource(ctx, ret->get_resource_id());
}
void recompui_create_textinput(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
Element* parent = arg_element<1>(rdram, ctx, ui_context);
Element* ret = ui_context.create_element<TextInput>(parent);
return_resource(ctx, ret->get_resource_id());
}
void recompui_create_passwordinput(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
Element* parent = arg_element<1>(rdram, ctx, ui_context);
Element* ret = ui_context.create_element<TextInput>(parent, false);
return_resource(ctx, ret->get_resource_id());
}
void recompui_create_labelradio(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
Element* parent = arg_element<1>(rdram, ctx, ui_context);
PTR(PTR(char)) options = _arg<2, PTR(PTR(char))>(rdram, ctx);
uint32_t num_options = _arg<3, uint32_t>(rdram, ctx);
Radio* ret = ui_context.create_element<Radio>(parent);
for (size_t i = 0; i < num_options; i++) {
ret->add_option(decode_string(rdram, MEM_W(sizeof(uint32_t) * i, options)));
}
return_resource(ctx, ret->get_resource_id());
}
void recompui_create_slider(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
Element* parent = arg_element<1>(rdram, ctx, ui_context);
uint32_t type = _arg<2, uint32_t>(rdram, ctx);
float min_value = arg_float3(rdram, ctx);
float max_value = arg_float4(rdram, ctx);
float step = arg_float5(rdram, ctx);
float initial_value = arg_float6(rdram, ctx);
Slider* ret = ui_context.create_element<Slider>(parent, static_cast<SliderType>(type));
ret->set_min_value(min_value);
ret->set_max_value(max_value);
ret->set_step_value(step);
ret->set_value(initial_value);
return_resource(ctx, ret->get_resource_id());
}
// Position and Layout
void recompui_set_visibility(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
uint32_t visibility = _arg<1, uint32_t>(rdram, ctx);
resource->set_visibility(static_cast<Visibility>(visibility));
}
void recompui_set_position(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
uint32_t position = _arg<1, uint32_t>(rdram, ctx);
@@ -610,6 +640,19 @@ void recompui_set_overflow_y(uint8_t* rdram, recomp_context* ctx) {
}
// Text and Fonts
void recompui_set_text(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
if (resource == nullptr || !resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to set text of non-element or element not found in context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* element = static_cast<Element*>(resource);
element->set_text(_arg_string<1>(rdram, ctx));
}
void recompui_set_font_size(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float size = _arg_float_a1(rdram, ctx);
@@ -696,6 +739,192 @@ void recompui_set_tab_index(uint8_t* rdram, recomp_context* ctx) {
resource->set_tab_index(static_cast<TabIndex>(tab_index));
}
// Values
void recompui_get_input_value_u32(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
if (resource == nullptr || !resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to get value of non-element or element not found in context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* element = static_cast<Element*>(resource);
_return<uint32_t>(ctx, element->get_input_value_u32());
}
void recompui_get_input_value_float(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
if (resource == nullptr || !resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to get value of non-element or element not found in context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* element = static_cast<Element*>(resource);
_return<float>(ctx, element->get_input_value_float());
}
void recompui_get_input_text(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
if (resource == nullptr || !resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to get input text of non-element or element not found in context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* element = static_cast<Element*>(resource);
std::string ret = element->get_input_text();
return_string(rdram, ctx, ret);
}
void recompui_set_input_value_u32(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
uint32_t value = _arg<1, uint32_t>(rdram, ctx);
if (resource == nullptr || !resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to set value of non-element or element not found in context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* element = static_cast<Element*>(resource);
element->set_input_value_u32(value);
}
void recompui_set_input_value_float(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float value = _arg_float_a1(rdram, ctx);
if (resource == nullptr || !resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to set value of non-element or element not found in context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* element = static_cast<Element*>(resource);
element->set_input_value_float(value);
}
void recompui_set_input_text(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
if (resource == nullptr || !resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to set input text of non-element or element not found in context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* element = static_cast<Element*>(resource);
element->set_input_text(_arg_string<1>(rdram, ctx));
}
// Callbacks
void recompui_register_callback(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = recompui::get_current_context();
if (ui_context == ContextId::null()) {
recompui::message_box("Fatal error in mod - attempted to register callback with no active context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Style* resource = arg_style<0>(rdram, ctx);
if (resource == nullptr || !resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to register callback on non-element or element not found in context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* element = static_cast<Element*>(resource);
PTR(void) callback = _arg<1, PTR(void)>(rdram, ctx);
PTR(void) userdata = _arg<2, PTR(void)>(rdram, ctx);
element->register_callback(ui_context, callback, userdata);
}
// Navigation
void recompui_set_nav_auto(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = recompui::get_current_context();
if (ui_context == ContextId::null()) {
recompui::message_box("Fatal error in mod - attempted to set element navigation with no active context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Style* resource = arg_style<0>(rdram, ctx);
if (resource == nullptr || !resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to set navigation on non-element or element not found in context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* element = static_cast<Element*>(resource);
u32 nav_dir = _arg<1, u32>(rdram, ctx);
element->set_nav_auto(static_cast<recompui::NavDirection>(nav_dir));
}
void recompui_set_nav_none(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = recompui::get_current_context();
if (ui_context == ContextId::null()) {
recompui::message_box("Fatal error in mod - attempted to set element navigation with no active context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Style* resource = arg_style<0>(rdram, ctx);
if (resource == nullptr || !resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to set navigation on non-element or element not found in context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* element = static_cast<Element*>(resource);
u32 nav_dir = _arg<1, u32>(rdram, ctx);
element->set_nav_none(static_cast<recompui::NavDirection>(nav_dir));
}
void recompui_set_nav(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = recompui::get_current_context();
if (ui_context == ContextId::null()) {
recompui::message_box("Fatal error in mod - attempted to set element navigation with no active context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Style* resource = arg_style<0>(rdram, ctx);
if (resource == nullptr || !resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to set navigation on non-element or element not found in context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Style* target_resource = arg_style<2>(rdram, ctx);
if (target_resource == nullptr || !target_resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to set element navigation to non-element or target element not found in context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* element = static_cast<Element*>(resource);
Element* target_element = static_cast<Element*>(target_resource);
u32 nav_dir = _arg<1, u32>(rdram, ctx);
element->set_nav(static_cast<recompui::NavDirection>(nav_dir), target_element);
}
#define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name)
void recompui::register_ui_exports() {
@@ -705,9 +934,19 @@ void recompui::register_ui_exports() {
REGISTER_FUNC(recompui_context_root);
REGISTER_FUNC(recompui_show_context);
REGISTER_FUNC(recompui_hide_context);
REGISTER_FUNC(recompui_set_context_captures_input);
REGISTER_FUNC(recompui_set_context_captures_mouse);
REGISTER_FUNC(recompui_create_style);
REGISTER_FUNC(recompui_create_element);
REGISTER_FUNC(recompui_destroy_element);
REGISTER_FUNC(recompui_create_button);
REGISTER_FUNC(recompui_create_label);
// REGISTER_FUNC(recompui_create_span);
REGISTER_FUNC(recompui_create_textinput);
REGISTER_FUNC(recompui_create_passwordinput);
REGISTER_FUNC(recompui_create_labelradio);
REGISTER_FUNC(recompui_create_slider);
REGISTER_FUNC(recompui_set_visibility);
REGISTER_FUNC(recompui_set_position);
REGISTER_FUNC(recompui_set_left);
REGISTER_FUNC(recompui_set_top);
@@ -766,6 +1005,7 @@ void recompui::register_ui_exports() {
REGISTER_FUNC(recompui_set_overflow);
REGISTER_FUNC(recompui_set_overflow_x);
REGISTER_FUNC(recompui_set_overflow_y);
REGISTER_FUNC(recompui_set_text);
REGISTER_FUNC(recompui_set_font_size);
REGISTER_FUNC(recompui_set_letter_spacing);
REGISTER_FUNC(recompui_set_line_height);
@@ -777,4 +1017,15 @@ void recompui::register_ui_exports() {
REGISTER_FUNC(recompui_set_column_gap);
REGISTER_FUNC(recompui_set_drag);
REGISTER_FUNC(recompui_set_tab_index);
REGISTER_FUNC(recompui_get_input_value_u32);
REGISTER_FUNC(recompui_get_input_value_float);
REGISTER_FUNC(recompui_get_input_text);
REGISTER_FUNC(recompui_set_input_value_u32);
REGISTER_FUNC(recompui_set_input_value_float);
REGISTER_FUNC(recompui_set_input_text);
REGISTER_FUNC(recompui_set_nav_auto);
REGISTER_FUNC(recompui_set_nav_none);
REGISTER_FUNC(recompui_set_nav);
REGISTER_FUNC(recompui_register_callback);
register_ui_image_exports();
}
+118
View File
@@ -0,0 +1,118 @@
#include "concurrentqueue.h"
#include "overloaded.h"
#include "recomp_ui.h"
#include "core/ui_context.h"
#include "core/ui_resource.h"
#include "elements/ui_element.h"
#include "elements/ui_button.h"
#include "elements/ui_clickable.h"
#include "elements/ui_container.h"
#include "elements/ui_image.h"
#include "elements/ui_label.h"
#include "elements/ui_radio.h"
#include "elements/ui_scroll_container.h"
#include "elements/ui_slider.h"
#include "elements/ui_style.h"
#include "elements/ui_text_input.h"
#include "elements/ui_toggle.h"
#include "elements/ui_types.h"
#include "librecomp/overlays.hpp"
#include "librecomp/helpers.hpp"
#include "../patches/ui_funcs.h"
struct QueuedCallback {
recompui::ResourceId resource;
recompui::Event event;
recompui::UICallback callback;
};
moodycamel::ConcurrentQueue<QueuedCallback> queued_callbacks{};
void recompui::queue_ui_callback(recompui::ResourceId resource, const Event& e, const UICallback& callback) {
queued_callbacks.enqueue(QueuedCallback{ .resource = resource, .event = e, .callback = callback });
}
bool convert_event(const recompui::Event& in, RecompuiEventData& out) {
bool skip = false;
out = {};
out.type = static_cast<RecompuiEventType>(in.type);
switch (in.type) {
default:
case recompui::EventType::None:
case recompui::EventType::Count:
skip = true;
break;
case recompui::EventType::Click:
{
const recompui::EventClick &click = std::get<recompui::EventClick>(in.variant);
out.data.click.x = click.x;
out.data.click.y = click.y;
}
break;
case recompui::EventType::Focus:
{
const recompui::EventFocus &focus = std::get<recompui::EventFocus>(in.variant);
out.data.focus.active = focus.active;
}
break;
case recompui::EventType::Hover:
{
const recompui::EventHover &hover = std::get<recompui::EventHover>(in.variant);
out.data.hover.active = hover.active;
}
break;
case recompui::EventType::Enable:
{
const recompui::EventEnable &enable = std::get<recompui::EventEnable>(in.variant);
out.data.enable.active = enable.active;
}
break;
case recompui::EventType::Drag:
{
const recompui::EventDrag &drag = std::get<recompui::EventDrag>(in.variant);
out.data.drag.phase = static_cast<RecompuiDragPhase>(drag.phase);
out.data.drag.x = drag.x;
out.data.drag.y = drag.y;
}
break;
case recompui::EventType::Text:
skip = true; // Text events aren't supported in the UI mod API.
break;
case recompui::EventType::Update:
// No data for an update event.
break;
}
return !skip;
}
extern "C" void recomp_run_ui_callbacks(uint8_t* rdram, recomp_context* ctx) {
// Allocate the event on the stack.
gpr stack_frame = ctx->r29;
ctx->r29 -= sizeof(RecompuiEventData);
RecompuiEventData* event_data = TO_PTR(RecompuiEventData, stack_frame);
QueuedCallback cur_callback;
while (queued_callbacks.try_dequeue(cur_callback)) {
if (convert_event(cur_callback.event, *event_data)) {
recompui::ContextId cur_context = cur_callback.callback.context;
cur_context.open();
ctx->r4 = static_cast<int32_t>(cur_callback.resource.slot_id);
ctx->r5 = stack_frame;
ctx->r6 = cur_callback.callback.userdata;
LOOKUP_FUNC(cur_callback.callback.callback)(rdram, ctx);
cur_context.close();
}
}
ctx->r29 += sizeof(RecompuiEventData);
}
+131
View File
@@ -0,0 +1,131 @@
#include <mutex>
#include <unordered_set>
#include "recomp_ui.h"
#include "librecomp/overlays.hpp"
#include "librecomp/helpers.hpp"
#include "ultramodern/error_handling.hpp"
#include "ui_helpers.h"
#include "ui_api_images.h"
#include "elements/ui_image.h"
using namespace recompui;
struct {
std::mutex mutex;
std::unordered_set<uint32_t> textures{};
uint32_t textures_created = 0;
} TextureState;
const std::string mod_texture_prefix = "?/mod_api/";
static std::string get_texture_name(uint32_t texture_id) {
return mod_texture_prefix + std::to_string(texture_id);
}
static uint32_t get_new_texture_id() {
std::lock_guard lock{TextureState.mutex};
uint32_t cur_id = TextureState.textures_created++;
TextureState.textures.emplace(cur_id);
return cur_id;
}
static void release_texture(uint32_t texture_id) {
std::string texture_name = get_texture_name(texture_id);
std::lock_guard lock{TextureState.mutex};
if (TextureState.textures.erase(texture_id) == 0) {
recompui::message_box("Fatal error in mod - attempted to destroy texture that doesn't exist!");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
recompui::release_image(texture_name);
}
thread_local std::vector<char> swapped_image_bytes;
void recompui_create_texture_rgba32(uint8_t* rdram, recomp_context* ctx) {
PTR(void) data_in = _arg<0, PTR(void)>(rdram, ctx);
uint32_t width = _arg<1, uint32_t>(rdram, ctx);
uint32_t height = _arg<2, uint32_t>(rdram, ctx);
uint32_t cur_id = get_new_texture_id();
// The size in bytes of the image's pixel data.
size_t size_bytes = width * height * 4 * sizeof(uint8_t);
swapped_image_bytes.resize(size_bytes);
// Byteswap copy the pixel data.
for (size_t i = 0; i < size_bytes; i++) {
swapped_image_bytes[i] = MEM_B(i, data_in);
}
// Create a texture name from the ID and queue its bytes.
std::string texture_name = get_texture_name(cur_id);
recompui::queue_image_from_bytes_rgba32(texture_name, swapped_image_bytes, width, height);
// Return the new texture ID.
_return(ctx, cur_id);
}
void recompui_create_texture_image_bytes(uint8_t* rdram, recomp_context* ctx) {
PTR(void) data_in = _arg<0, PTR(void)>(rdram, ctx);
uint32_t size_bytes = _arg<1, u32>(rdram, ctx);
uint32_t cur_id = get_new_texture_id();
// The size in bytes of the image's data.
swapped_image_bytes.resize(size_bytes);
// Byteswap copy the image's data.
for (size_t i = 0; i < size_bytes; i++) {
swapped_image_bytes[i] = MEM_B(i, data_in);
}
// Create a texture name from the ID and queue its bytes.
std::string texture_name = get_texture_name(cur_id);
recompui::queue_image_from_bytes_file(texture_name, swapped_image_bytes);
// Return the new texture ID.
_return(ctx, cur_id);
}
void recompui_destroy_texture(uint8_t* rdram, recomp_context* ctx) {
uint32_t texture_id = _arg<0, uint32_t>(rdram, ctx);
release_texture(texture_id);
}
void recompui_create_imageview(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
Element* parent = arg_element<1>(rdram, ctx, ui_context);
uint32_t texture_id = _arg<2, uint32_t>(rdram, ctx);
Element* ret = ui_context.create_element<Image>(parent, get_texture_name(texture_id));
return_resource(ctx, ret->get_resource_id());
}
void recompui_set_imageview_texture(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
uint32_t texture_id = _arg<1, uint32_t>(rdram, ctx);
if (!resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to set texture of non-element");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* element = static_cast<Element*>(resource);
element->set_src(get_texture_name(texture_id));
}
#define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name)
void recompui::register_ui_image_exports() {
REGISTER_FUNC(recompui_create_texture_rgba32);
REGISTER_FUNC(recompui_create_texture_image_bytes);
REGISTER_FUNC(recompui_destroy_texture);
REGISTER_FUNC(recompui_create_imageview);
REGISTER_FUNC(recompui_set_imageview_texture);
}
+10
View File
@@ -0,0 +1,10 @@
#ifndef __UI_API_IMAGES_H__
#define __UI_API_IMAGES_H__
#include <cstdint>
namespace recompui {
void register_ui_image_exports();
}
#endif
+99 -60
View File
@@ -4,6 +4,7 @@
#include "banjo_config.h"
#include "banjo_debug.h"
#include "banjo_render.h"
#include "banjo_support.h"
#include "promptfont.h"
#include "ultramodern/config.hpp"
#include "ultramodern/ultramodern.hpp"
@@ -21,6 +22,26 @@ Rml::DataModelHandle sound_options_model_handle;
// True if controller config menu is open, false if keyboard config menu is open, undefined otherwise
bool configuring_controller = false;
int recompui::config_tab_to_index(recompui::ConfigTab tab) {
switch (tab) {
case recompui::ConfigTab::General:
return 0;
case recompui::ConfigTab::Controls:
return 1;
case recompui::ConfigTab::Graphics:
return 2;
case recompui::ConfigTab::Sound:
return 3;
case recompui::ConfigTab::Mods:
return 4;
case recompui::ConfigTab::Debug:
return 5;
default:
assert(false && "Unknown config tab.");
return 0;
}
}
template <typename T>
void get_option(const T& input, Rml::Variant& output) {
std::string value = "";
@@ -164,7 +185,7 @@ void apply_graphics_config(void) {
void close_config_menu() {
if (ultramodern::renderer::get_graphics_config() != new_options) {
recompui::open_prompt(
recompui::open_choice_prompt(
"Graphics options have changed",
"Would you like to apply or discard the changes?",
"Apply",
@@ -191,7 +212,7 @@ void close_config_menu() {
}
void banjo::open_quit_game_prompt() {
recompui::open_prompt(
recompui::open_choice_prompt(
"Are you sure you want to quit?",
"Any progress since your last save will be lost.",
"Quit",
@@ -376,7 +397,45 @@ recompui::ContextId recompui::get_config_context_id() {
return config_context;
}
// Helper copied from RmlUi to get a named child.
Rml::Element* recompui::get_child_by_tag(Rml::Element* parent, const std::string& tag)
{
// Look for the existing child
for (int i = 0; i < parent->GetNumChildren(); i++)
{
Rml::Element* child = parent->GetChild(i);
if (child->GetTagName() == tag)
return child;
}
return nullptr;
}
class ConfigTabsetListener : public Rml::EventListener {
void ProcessEvent(Rml::Event& event) override {
if (event.GetId() == Rml::EventId::Tabchange) {
int tab_index = event.GetParameter<int>("tab_index", 0);
bool in_mod_tab = (tab_index == recompui::config_tab_to_index(recompui::ConfigTab::Mods));
if (in_mod_tab) {
recompui::set_config_tabset_mod_nav();
}
else {
Rml::ElementTabSet* tabset = recompui::get_config_tabset();
Rml::Element* tabs = recompui::get_child_by_tag(tabset, "tabs");
if (tabs != nullptr) {
size_t num_children = tabs->GetNumChildren();
for (size_t i = 0; i < num_children; i++) {
tabs->GetChild(i)->SetProperty(Rml::PropertyId::NavDown, Rml::Style::Nav::Auto);
}
}
}
}
}
};
class ConfigMenu : public recompui::MenuController {
private:
ConfigTabsetListener config_tabset_listener;
public:
ConfigMenu() {
@@ -384,11 +443,10 @@ public:
~ConfigMenu() override {
}
Rml::ElementDocument* load_document(Rml::Context* context) override {
(void)context;
config_context = recompui::create_context("assets/config_menu.rml");
Rml::ElementDocument* ret = config_context.get_document();
return ret;
void load_document() override {
config_context = recompui::create_context(banjo::get_asset_path("config_menu.rml"));
recompui::update_mod_list(false);
recompui::get_config_tabset()->AddEventListener(Rml::EventId::Tabchange, &config_tabset_listener);
}
void register_events(recompui::UiEventListenerInstancer& listener) override {
recompui::register_event(listener, "apply_options",
@@ -563,7 +621,7 @@ public:
throw std::runtime_error("Failed to make RmlUi data model for the controls config menu");
}
constructor.BindFunc("input_count", [](Rml::Variant& out) { out = recomp::get_num_inputs(); } );
constructor.BindFunc("input_count", [](Rml::Variant& out) { out = static_cast<uint64_t>(recomp::get_num_inputs()); } );
constructor.BindFunc("input_device_is_keyboard", [](Rml::Variant& out) { out = cur_device == recomp::InputDevice::Keyboard; } );
constructor.RegisterTransformFunc("get_input_name", [](const Rml::VariantList& inputs) {
@@ -851,64 +909,45 @@ void recompui::toggle_fullscreen() {
graphics_model_handle.DirtyVariable("wm_option");
}
void recompui::open_prompt(
const std::string& headerText,
const std::string& contentText,
const std::string& confirmLabelText,
const std::string& cancelLabelText,
std::function<void()> confirmCb,
std::function<void()> cancelCb,
ButtonVariant _confirmVariant,
ButtonVariant _cancelVariant,
bool _focusOnCancel,
const std::string& _returnElementId
) {
printf("Prompt opened\n %s (%s): %s %s\n", contentText.c_str(), headerText.c_str(), confirmLabelText.c_str(), cancelLabelText.c_str());
printf(" Autoselected %s\n", confirmLabelText.c_str());
confirmCb();
}
bool recompui::is_prompt_open() {
return false;
}
void recompui::set_config_tab(ConfigTab tab) {
get_config_tabset()->SetActiveTab(config_tab_to_index(tab));
}
Rml::ElementTabSet* recompui::get_config_tabset() {
ContextId config_context = recompui::get_config_context_id();
ContextId old_context = recompui::try_close_current_context();
Rml::ElementDocument *doc = config_context.get_document();
assert(doc != nullptr);
Rml::Element *tabset_el = doc->GetElementById("config_tabset");
assert(tabset_el != nullptr);
Rml::ElementTabSet *tabset = rmlui_dynamic_cast<Rml::ElementTabSet *>(tabset_el);
assert(tabset != nullptr);
if (old_context != ContextId::null()) {
old_context.open();
}
return tabset;
}
Rml::Element* recompui::get_mod_tab() {
ContextId config_context = recompui::get_config_context_id();
ContextId old_context = recompui::try_close_current_context();
Rml::ElementDocument* doc = config_context.get_document();
assert(doc != nullptr);
Rml::Element* tabset_el = doc->GetElementById("config_tabset");
assert(tabset_el != nullptr);
Rml::Element* tab_el = doc->GetElementById("tab_mods");
assert(tab_el != nullptr);
Rml::ElementTabSet* tabset = rmlui_dynamic_cast<Rml::ElementTabSet*>(tabset_el);
assert(tabset != nullptr);
int tab_index = 0;
switch (tab) {
case ConfigTab::General:
tab_index = 0;
break;
case ConfigTab::Controls:
tab_index = 1;
break;
case ConfigTab::Graphics:
tab_index = 2;
break;
case ConfigTab::Sound:
tab_index = 3;
break;
case ConfigTab::Mods:
tab_index = 4;
break;
case ConfigTab::Debug:
tab_index = 5;
break;
default:
assert(false);
return;
if (old_context != ContextId::null()) {
old_context.open();
}
tabset->SetActiveTab(tab_index);
return tab_el;
}
+54 -21
View File
@@ -13,6 +13,9 @@ namespace recompui {
void ConfigOptionElement::process_event(const Event &e) {
switch (e.type) {
case EventType::Hover:
if (hover_callback == nullptr) {
break;
}
hover_callback(this, std::get<EventHover>(e.variant).active);
break;
case EventType::Update:
@@ -36,8 +39,8 @@ ConfigOptionElement::~ConfigOptionElement() {
}
void ConfigOptionElement::set_id(std::string_view id) {
this->id = id;
void ConfigOptionElement::set_option_id(std::string_view id) {
this->option_id = id;
}
void ConfigOptionElement::set_name(std::string_view name) {
@@ -53,6 +56,10 @@ void ConfigOptionElement::set_hover_callback(std::function<void(ConfigOptionElem
hover_callback = callback;
}
void ConfigOptionElement::set_focus_callback(std::function<void(const std::string &, bool)> callback) {
focus_callback = callback;
}
const std::string &ConfigOptionElement::get_description() const {
return description;
}
@@ -60,45 +67,56 @@ const std::string &ConfigOptionElement::get_description() const {
// ConfigOptionSlider
void ConfigOptionSlider::slider_value_changed(double v) {
callback(id, v);
callback(option_id, v);
}
ConfigOptionSlider::ConfigOptionSlider(Element *parent, double value, double min_value, double max_value, double step_value, bool percent, std::function<void(const std::string &, double)> callback) : ConfigOptionElement(parent) {
this->callback = callback;
slider = get_current_context().create_element<Slider>(this, percent ? SliderType::Percent : SliderType::Double);
slider->set_max_width(380.0f);
slider->set_min_value(min_value);
slider->set_max_value(max_value);
slider->set_step_value(step_value);
slider->set_value(value);
slider->add_value_changed_callback(std::bind(&ConfigOptionSlider::slider_value_changed, this, std::placeholders::_1));
slider->add_value_changed_callback([this](double v){ slider_value_changed(v); });
slider->set_focus_callback([this](bool active) {
focus_callback(option_id, active);
});
}
// ConfigOptionTextInput
void ConfigOptionTextInput::text_changed(const std::string &text) {
callback(id, text);
callback(option_id, text);
}
ConfigOptionTextInput::ConfigOptionTextInput(Element *parent, std::string_view value, std::function<void(const std::string &, const std::string &)> callback) : ConfigOptionElement(parent) {
this->callback = callback;
text_input = get_current_context().create_element<TextInput>(this);
text_input->set_max_width(400.0f);
text_input->set_text(value);
text_input->add_text_changed_callback(std::bind(&ConfigOptionTextInput::text_changed, this, std::placeholders::_1));
text_input->add_text_changed_callback([this](const std::string &text){ text_changed(text); });
text_input->set_focus_callback([this](bool active) {
focus_callback(option_id, active);
});
}
// ConfigOptionRadio
void ConfigOptionRadio::index_changed(uint32_t index) {
callback(id, index);
callback(option_id, index);
}
ConfigOptionRadio::ConfigOptionRadio(Element *parent, uint32_t value, const std::vector<std::string> &options, std::function<void(const std::string &, uint32_t)> callback) : ConfigOptionElement(parent) {
this->callback = callback;
radio = get_current_context().create_element<Radio>(this);
radio->add_index_changed_callback(std::bind(&ConfigOptionRadio::index_changed, this, std::placeholders::_1));
radio->set_focus_callback([this](bool active) {
focus_callback(option_id, active);
});
radio->add_index_changed_callback([this](uint32_t index){ index_changed(index); });
for (std::string_view option : options) {
radio->add_option(option);
}
@@ -117,21 +135,22 @@ void ConfigSubMenu::back_button_pressed() {
recompui::hide_context(sub_menu_context);
recompui::show_context(config_context, "");
recompui::focus_mod_configure_button();
}
void ConfigSubMenu::option_hovered(ConfigOptionElement *option, bool active) {
void ConfigSubMenu::set_description_option_element(ConfigOptionElement *option, bool active) {
if (active) {
hover_option_elements.emplace(option);
description_option_element = option;
}
else {
hover_option_elements.erase(option);
else if (description_option_element == option) {
description_option_element = nullptr;
}
if (hover_option_elements.empty()) {
if (description_option_element == nullptr) {
description_label->set_text("");
}
else {
description_label->set_text((*hover_option_elements.begin())->get_description());
description_label->set_text(description_option_element->get_description());
}
}
@@ -147,12 +166,12 @@ ConfigSubMenu::ConfigSubMenu(Element *parent) : Element(parent) {
header_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::FlexStart);
header_container->set_flex_grow(0.0f);
header_container->set_align_items(AlignItems::Center);
header_container->set_padding_left(12.0f);
header_container->set_padding(12.0f);
header_container->set_gap(24.0f);
{
back_button = context.create_element<Button>(header_container, "Back", ButtonStyle::Secondary);
back_button->add_pressed_callback(std::bind(&ConfigSubMenu::back_button_pressed, this));
back_button->add_pressed_callback([this](){ back_button_pressed(); });
title_label = context.create_element<Label>(header_container, "Title", LabelStyle::Large);
}
@@ -167,9 +186,13 @@ ConfigSubMenu::ConfigSubMenu(Element *parent) : Element(parent) {
config_scroll_container = context.create_element<ScrollContainer>(config_container, ScrollDirection::Vertical);
}
description_label = context.create_element<Label>(body_container, "Description", LabelStyle::Small);
description_label = context.create_element<Label>(body_container, "", LabelStyle::Small);
description_label->set_min_width(800.0f);
description_label->set_padding_left(16.0f);
description_label->set_padding_right(16.0f);
}
recompui::get_current_context().set_autofocus_element(back_button);
}
ConfigSubMenu::~ConfigSubMenu() {
@@ -183,14 +206,24 @@ void ConfigSubMenu::enter(std::string_view title) {
void ConfigSubMenu::clear_options() {
config_scroll_container->clear_children();
config_option_elements.clear();
hover_option_elements.clear();
description_option_element = nullptr;
}
void ConfigSubMenu::add_option(ConfigOptionElement *option, std::string_view id, std::string_view name, std::string_view description) {
option->set_id(id);
option->set_option_id(id);
option->set_name(name);
option->set_description(description);
option->set_hover_callback(std::bind(&ConfigSubMenu::option_hovered, this, std::placeholders::_1, std::placeholders::_2));
option->set_hover_callback([this](ConfigOptionElement *option, bool active){ set_description_option_element(option, active); });
option->set_focus_callback([this, option](const std::string &id, bool active) { set_description_option_element(option, active); });
if (config_option_elements.empty()) {
back_button->set_nav(NavDirection::Down, option->get_focus_element());
option->set_nav(NavDirection::Up, back_button);
}
else {
config_option_elements.back()->set_nav(NavDirection::Down, option->get_focus_element());
option->set_nav(NavDirection::Up, config_option_elements.back()->get_focus_element());
}
config_option_elements.emplace_back(option);
}
@@ -233,4 +266,4 @@ ConfigSubMenu *ElementConfigSubMenu::get_config_sub_menu_element() const {
return config_sub_menu;
}
}
}
+21 -6
View File
@@ -16,20 +16,28 @@ namespace recompui {
class ConfigOptionElement : public Element {
protected:
Label *name_label = nullptr;
std::string id;
std::string option_id;
std::string name;
std::string description;
std::function<void(ConfigOptionElement *, bool)> hover_callback = nullptr;
std::function<void(const std::string &, bool)> focus_callback = nullptr;
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "ConfigOptionElement"; }
public:
ConfigOptionElement(Element *parent);
virtual ~ConfigOptionElement();
void set_id(std::string_view id);
void set_option_id(std::string_view id);
void set_name(std::string_view name);
void set_description(std::string_view description);
void set_hover_callback(std::function<void(ConfigOptionElement *, bool)> callback);
void set_focus_callback(std::function<void(const std::string &, bool)> callback);
const std::string &get_description() const;
void set_nav_auto(NavDirection dir) override { get_focus_element()->set_nav_auto(dir); }
void set_nav_none(NavDirection dir) override { get_focus_element()->set_nav_none(dir); }
void set_nav(NavDirection dir, Element* element) override { get_focus_element()->set_nav(dir, element); }
void set_nav_manual(NavDirection dir, const std::string& target) override { get_focus_element()->set_nav_manual(dir, target); }
virtual Element* get_focus_element() { return this; }
};
class ConfigOptionSlider : public ConfigOptionElement {
@@ -38,8 +46,10 @@ protected:
std::function<void(const std::string &, double)> callback;
void slider_value_changed(double v);
std::string_view get_type_name() override { return "ConfigOptionSlider"; }
public:
ConfigOptionSlider(Element *parent, double value, double min_value, double max_value, double step_value, bool percent, std::function<void(const std::string &, double)> callback);
Element* get_focus_element() override { return slider; }
};
class ConfigOptionTextInput : public ConfigOptionElement {
@@ -48,8 +58,10 @@ protected:
std::function<void(const std::string &, const std::string &)> callback;
void text_changed(const std::string &text);
std::string_view get_type_name() override { return "ConfigOptionTextInput"; }
public:
ConfigOptionTextInput(Element *parent, std::string_view value, std::function<void(const std::string &, const std::string &)> callback);
Element* get_focus_element() override { return text_input; }
};
class ConfigOptionRadio : public ConfigOptionElement {
@@ -58,8 +70,10 @@ protected:
std::function<void(const std::string &, uint32_t)> callback;
void index_changed(uint32_t index);
std::string_view get_type_name() override { return "ConfigOptionRadio"; }
public:
ConfigOptionRadio(Element *parent, uint32_t value, const std::vector<std::string> &options, std::function<void(const std::string &, uint32_t)> callback);
Element* get_focus_element() override { return radio; }
};
class ConfigSubMenu : public Element {
@@ -72,12 +86,13 @@ private:
Container *config_container = nullptr;
ScrollContainer *config_scroll_container = nullptr;
std::vector<ConfigOptionElement *> config_option_elements;
std::unordered_set<ConfigOptionElement *> hover_option_elements;
ConfigOptionElement * description_option_element = nullptr;
void back_button_pressed();
void option_hovered(ConfigOptionElement *option, bool active);
void set_description_option_element(ConfigOptionElement *option, bool active);
void add_option(ConfigOptionElement *option, std::string_view id, std::string_view name, std::string_view description);
protected:
std::string_view get_type_name() override { return "ConfigSubMenu"; }
public:
ConfigSubMenu(Element *parent);
virtual ~ConfigSubMenu();
@@ -99,4 +114,4 @@ private:
};
}
#endif
#endif
+154
View File
@@ -0,0 +1,154 @@
#ifndef __UI_HELPERS_H__
#define __UI_HELPERS_H__
#include "librecomp/helpers.hpp"
#include "librecomp/addresses.hpp"
#include "elements/ui_element.h"
#include "elements/ui_types.h"
#include "core/ui_context.h"
#include "core/ui_resource.h"
namespace recompui {
constexpr ResourceId root_element_id{ 0xFFFFFFFE };
inline ContextId get_context(uint8_t* rdram, recomp_context* ctx) {
uint32_t context_id = _arg<0, uint32_t>(rdram, ctx);
return ContextId{ .slot_id = context_id };
}
inline float arg_float2(uint8_t* rdram, recomp_context* ctx) {
union {
float f32;
uint32_t u32;
} val;
val.u32 = _arg<2, uint32_t>(rdram, ctx);
return val.f32;
}
inline float arg_float3(uint8_t* rdram, recomp_context* ctx) {
union {
float f32;
uint32_t u32;
} val;
val.u32 = _arg<3, uint32_t>(rdram, ctx);
return val.f32;
}
inline float arg_float4(uint8_t* rdram, recomp_context* ctx) {
union {
float f32;
uint32_t u32;
} val;
val.u32 = MEM_W(0x10, ctx->r29);
return val.f32;
}
inline float arg_float5(uint8_t* rdram, recomp_context* ctx) {
union {
float f32;
uint32_t u32;
} val;
val.u32 = MEM_W(0x14, ctx->r29);
return val.f32;
}
inline float arg_float6(uint8_t* rdram, recomp_context* ctx) {
union {
float f32;
uint32_t u32;
} val;
val.u32 = MEM_W(0x18, ctx->r29);
return val.f32;
}
template <int arg_index>
ResourceId arg_resource_id(uint8_t* rdram, recomp_context* ctx) {
uint32_t slot_id = _arg<arg_index, uint32_t>(rdram, ctx);
return ResourceId{ .slot_id = slot_id };
}
template <int arg_index>
Element* arg_element(uint8_t* rdram, recomp_context* ctx, ContextId ui_context) {
ResourceId resource = arg_resource_id<arg_index>(rdram, ctx);
if (resource == ResourceId::null()) {
return nullptr;
}
else if (resource == root_element_id) {
return ui_context.get_root_element();
}
return resource.as_element();
}
template <int arg_index>
Style* arg_style(uint8_t* rdram, recomp_context* ctx) {
ResourceId resource = arg_resource_id<arg_index>(rdram, ctx);
if (resource == ResourceId::null()) {
return nullptr;
}
else if (resource == root_element_id) {
ContextId ui_context = recompui::get_current_context();
return ui_context.get_root_element();
}
return *resource;
}
template <int arg_index>
Color arg_color(uint8_t* rdram, recomp_context* ctx) {
PTR(u8) color_arg = _arg<arg_index, PTR(u8)>(rdram, ctx);
Color ret{};
ret.r = MEM_B(0, color_arg);
ret.g = MEM_B(1, color_arg);
ret.b = MEM_B(2, color_arg);
ret.a = MEM_B(3, color_arg);
return ret;
}
inline void return_resource(recomp_context* ctx, ResourceId resource) {
_return<uint32_t>(ctx, resource.slot_id);
}
inline void return_string(uint8_t* rdram, recomp_context* ctx, const std::string& ret) {
gpr addr = (reinterpret_cast<uint8_t*>(recomp::alloc(rdram, ret.size() + 1)) - rdram) + 0xFFFFFFFF80000000ULL;
for (size_t i = 0; i < ret.size(); i++) {
MEM_B(i, addr) = ret[i];
}
MEM_B(ret.size(), addr) = '\x00';
_return<PTR(char)>(ctx, addr);
}
inline std::string decode_string(uint8_t* rdram, PTR(char) str) {
// Get the length of the byteswapped string.
size_t len = 0;
while (MEM_B(str, len) != 0x00) {
len++;
}
std::string ret{};
ret.reserve(len + 1);
for (size_t i = 0; i < len; i++) {
ret += (char)MEM_B(str, i);
}
return ret;
}
}
#endif
+32 -39
View File
@@ -1,5 +1,6 @@
#include "recomp_ui.h"
#include "banjo_config.h"
#include "banjo_support.h"
#include "librecomp/game.hpp"
#include "ultramodern/ultramodern.hpp"
#include "RmlUi/Core.h"
@@ -15,41 +16,36 @@ extern std::vector<recomp::GameEntry> supported_games;
void select_rom() {
nfdnchar_t* native_path = nullptr;
nfdresult_t result = NFD_OpenDialogN(&native_path, nullptr, 0, nullptr);
if (result == NFD_OKAY) {
std::filesystem::path path{native_path};
NFD_FreePathN(native_path);
native_path = nullptr;
recomp::RomValidationError rom_error = recomp::select_rom(path, supported_games[0].game_id);
switch (rom_error) {
case recomp::RomValidationError::Good:
bk_rom_valid = true;
model_handle.DirtyVariable("bk_rom_valid");
break;
case recomp::RomValidationError::FailedToOpen:
recompui::message_box("Failed to open ROM file.");
break;
case recomp::RomValidationError::NotARom:
recompui::message_box("This is not a valid ROM file.");
break;
case recomp::RomValidationError::IncorrectRom:
recompui::message_box("This ROM is not the correct game.");
break;
case recomp::RomValidationError::NotYet:
recompui::message_box("This game isn't supported yet.");
break;
case recomp::RomValidationError::IncorrectVersion:
recompui::message_box(
"This ROM is the correct game, but the wrong version.\nThis project requires the NTSC-U N64 version of the game.");
break;
case recomp::RomValidationError::OtherError:
recompui::message_box("An unknown error has occurred.");
break;
banjo::open_file_dialog([](bool success, const std::filesystem::path& path) {
if (success) {
recomp::RomValidationError rom_error = recomp::select_rom(path, supported_games[0].game_id);
switch (rom_error) {
case recomp::RomValidationError::Good:
bk_rom_valid = true;
model_handle.DirtyVariable("bk_rom_valid");
break;
case recomp::RomValidationError::FailedToOpen:
recompui::message_box("Failed to open ROM file.");
break;
case recomp::RomValidationError::NotARom:
recompui::message_box("This is not a valid ROM file.");
break;
case recomp::RomValidationError::IncorrectRom:
recompui::message_box("This ROM is not the correct game.");
break;
case recomp::RomValidationError::NotYet:
recompui::message_box("This game isn't supported yet.");
break;
case recomp::RomValidationError::IncorrectVersion:
recompui::message_box(
"This ROM is the correct game, but the wrong version.\nThis project requires the NTSC-U N64 version of the game.");
break;
case recomp::RomValidationError::OtherError:
recompui::message_box("An unknown error has occurred.");
break;
}
}
}
});
}
recompui::ContextId launcher_context;
@@ -66,11 +62,8 @@ public:
~LauncherMenu() override {
}
Rml::ElementDocument* load_document(Rml::Context* context) override {
(void)context;
launcher_context = recompui::create_context("assets/launcher.rml");
Rml::ElementDocument* ret = launcher_context.get_document();
return ret;
void load_document() override {
launcher_context = recompui::create_context(banjo::get_asset_path("launcher.rml"));
}
void register_events(recompui::UiEventListenerInstancer& listener) override {
recompui::register_event(listener, "select_rom",
+43 -11
View File
@@ -4,12 +4,13 @@
namespace recompui {
extern const std::string mod_tab_id;
ModDetailsPanel::ModDetailsPanel(Element *parent) : Element(parent) {
set_flex(1.0f, 1.0f, 200.0f);
set_height(100.0f, Unit::Percent);
set_display(Display::Flex);
set_flex_direction(FlexDirection::Column);
set_border_bottom_right_radius(16.0f);
set_background_color(Color{ 190, 184, 219, 25 });
ContextId context = get_current_context();
@@ -19,6 +20,8 @@ ModDetailsPanel::ModDetailsPanel(Element *parent) : Element(parent) {
header_container->set_padding(16.0f);
header_container->set_gap(16.0f);
header_container->set_background_color(Color{ 0, 0, 0, 89 });
header_container->set_border_bottom_width(1.1f);
header_container->set_border_bottom_color(Color{ 255, 255, 255, 25 });
{
thumbnail_container = context.create_element<Container>(header_container, FlexDirection::Column, JustifyContent::SpaceEvenly);
thumbnail_container->set_flex(0.0f, 0.0f);
@@ -39,42 +42,48 @@ ModDetailsPanel::ModDetailsPanel(Element *parent) : Element(parent) {
}
}
body_container = context.create_element<Container>(this, FlexDirection::Column, JustifyContent::FlexStart);
body_container->set_flex(0.0f, 0.0f);
body_container = context.create_element<ScrollContainer>(this, ScrollDirection::Vertical);
body_container->set_text_align(TextAlign::Left);
body_container->set_padding(16.0f);
body_container->set_gap(16.0f);
{
description_label = context.create_element<Label>(body_container, LabelStyle::Normal);
authors_label = context.create_element<Label>(body_container, LabelStyle::Normal);
authors_label->set_margin_bottom(16.0f);
description_label = context.create_element<Label>(body_container, LabelStyle::Normal);
}
spacer_element = context.create_element<Element>(this);
spacer_element->set_flex(1.0f, 0.0f);
buttons_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::SpaceAround);
buttons_container->set_flex(0.0f, 0.0f);
buttons_container->set_padding(16.0f);
buttons_container->set_justify_content(JustifyContent::SpaceBetween);
buttons_container->set_border_top_width(1.1f);
buttons_container->set_border_top_color(Color{ 255, 255, 255, 25 });
buttons_container->set_background_color(Color{ 0, 0, 0, 89 });
{
enable_container = context.create_element<Container>(buttons_container, FlexDirection::Row, JustifyContent::FlexStart);
enable_container->set_align_items(AlignItems::Center);
enable_container->set_gap(16.0f);
{
enable_toggle = context.create_element<Toggle>(enable_container);
enable_toggle->add_checked_callback(std::bind(&ModDetailsPanel::enable_toggle_checked, this, std::placeholders::_1));
enable_toggle->add_checked_callback([this](bool checked){ enable_toggle_checked(checked); });
enable_toggle->set_nav_manual(NavDirection::Up, mod_tab_id);
enable_label = context.create_element<Label>(enable_container, "A currently enabled mod requires this mod", LabelStyle::Annotation);
}
configure_button = context.create_element<Button>(buttons_container, "Configure", recompui::ButtonStyle::Secondary);
configure_button->add_pressed_callback(std::bind(&ModDetailsPanel::configure_button_pressed, this));
configure_button->add_pressed_callback([this](){ configure_button_pressed(); });
configure_button->set_nav_manual(NavDirection::Up, mod_tab_id);
}
clear_mod_navigation();
}
ModDetailsPanel::~ModDetailsPanel() {
}
void ModDetailsPanel::disable_toggle() {
enable_toggle->set_enabled(false);
}
void ModDetailsPanel::set_mod_details(const recomp::mods::ModDetails& details, const std::string &thumbnail, bool toggle_checked, bool toggle_enabled, bool toggle_label_visible, bool configure_enabled) {
cur_details = details;
@@ -83,7 +92,7 @@ void ModDetailsPanel::set_mod_details(const recomp::mods::ModDetails& details, c
title_label->set_text(cur_details.display_name);
version_label->set_text(cur_details.version.to_string());
std::string authors_str = "<i>Authors</i>:";
std::string authors_str = "Authors:";
bool first = true;
for (const std::string& author : details.authors) {
authors_str += (first ? " " : ", ") + author;
@@ -96,6 +105,13 @@ void ModDetailsPanel::set_mod_details(const recomp::mods::ModDetails& details, c
enable_toggle->set_enabled(toggle_enabled);
configure_button->set_enabled(configure_enabled);
enable_label->set_display(toggle_label_visible ? Display::Block : Display::None);
if (configure_enabled) {
enable_toggle->set_nav(NavDirection::Right, configure_button);
}
else {
enable_toggle->set_nav_none(NavDirection::Right);
}
}
void ModDetailsPanel::set_mod_toggled_callback(std::function<void(bool)> callback) {
@@ -106,6 +122,22 @@ void ModDetailsPanel::set_mod_configure_pressed_callback(std::function<void()> c
mod_configure_pressed_callback = callback;
}
void ModDetailsPanel::setup_mod_navigation(Element* nav_target) {
enable_toggle->set_nav(NavDirection::Left, nav_target);
if (enable_toggle->is_enabled()) {
configure_button->set_nav(NavDirection::Left, enable_toggle);
}
else {
configure_button->set_nav(NavDirection::Left, nav_target);
}
}
void ModDetailsPanel::clear_mod_navigation() {
enable_toggle->set_nav_none(NavDirection::Left);
configure_button->set_nav_none(NavDirection::Left);
}
void ModDetailsPanel::enable_toggle_checked(bool checked) {
if (mod_toggled_callback != nullptr) {
mod_toggled_callback(checked);
+9 -2
View File
@@ -7,6 +7,7 @@
#include "elements/ui_image.h"
#include "elements/ui_label.h"
#include "elements/ui_toggle.h"
#include "elements/ui_scroll_container.h"
namespace recompui {
@@ -17,6 +18,13 @@ public:
void set_mod_details(const recomp::mods::ModDetails& details, const std::string &thumbnail, bool toggle_checked, bool toggle_enabled, bool toggle_label_visible, bool configure_enabled);
void set_mod_toggled_callback(std::function<void(bool)> callback);
void set_mod_configure_pressed_callback(std::function<void()> callback);
void setup_mod_navigation(Element* nav_target);
void clear_mod_navigation();
Toggle* get_enable_toggle() { return enable_toggle; }
Button* get_configure_button() { return configure_button; }
void disable_toggle();
protected:
std::string_view get_type_name() override { return "ModDetailsPanel"; }
private:
recomp::mods::ModDetails cur_details;
Container *thumbnail_container = nullptr;
@@ -25,10 +33,9 @@ private:
Container *header_details_container = nullptr;
Label *title_label = nullptr;
Label *version_label = nullptr;
Container *body_container = nullptr;
ScrollContainer *body_container = nullptr;
Label *description_label = nullptr;
Label *authors_label = nullptr;
Element *spacer_element = nullptr;
Container *buttons_container = nullptr;
Container *enable_container = nullptr;
Toggle *enable_toggle = nullptr;
+343
View File
@@ -0,0 +1,343 @@
#include "ui_mod_installer.h"
#include "librecomp/mods.hpp"
namespace recompui {
static const std::string ManifestFilename = "mod.json";
static const char *TextureDatabaseFilename = "rt64.json";
static const std::u8string OldExtension = u8".old";
static const std::u8string NewExtension = u8".new";
static bool is_dynamic_lib(const std::filesystem::path &file_path) {
#if defined(_WIN32)
return file_path.extension() == ".dll";
#elif defined(__linux__)
return file_path.extension() == ".so" || file_path.filename().string().find(".so.") != std::string::npos;
#elif defined(__APPLE__)
return file_path.extension() == ".dylib";
#else
static_assert(false, "Unimplemented for this platform.");
#endif
}
size_t zip_write_func(void *opaque, mz_uint64 offset, const void *bytes, size_t count) {
std::ofstream &stream = *(std::ofstream *)(opaque);
stream.seekp(offset, std::ios::beg);
stream.write((const char *)(bytes), count);
return stream.bad() ? 0 : count;
}
void start_single_mod_installation(const std::filesystem::path &file_path, recomp::mods::ZipModFileHandle &file_handle, std::function<void(std::filesystem::path, size_t, size_t)> progress_callback, ModInstaller::Result &result) {
// Check for the existence of the manifest file.
std::filesystem::path mods_directory = recomp::mods::get_mods_directory();
std::filesystem::path target_path = mods_directory / file_path.filename();
std::filesystem::path target_write_path = target_path.u8string() + NewExtension;
ModInstaller::Installation installation;
bool exists = false;
std::vector<char> manifest_bytes = file_handle.read_file(ManifestFilename, exists);
if (exists) {
// Parse the manifest file to check for its validity.
std::string error;
recomp::mods::ModManifest manifest;
recomp::mods::ModOpenError open_error = parse_manifest(manifest, manifest_bytes, error);
exists = (open_error == recomp::mods::ModOpenError::Good);
if (exists) {
installation.mod_id = manifest.mod_id;
installation.display_name = manifest.display_name;
installation.mod_version = manifest.version;
installation.mod_file = target_path;
}
}
else if (file_path.extension() == ".rtz") {
// When it's an rtz file, check if the texture database file exists.
exists = mz_zip_reader_locate_file(file_handle.archive.get(), TextureDatabaseFilename, nullptr, 0) >= 0;
if (exists) {
installation.mod_id = std::string((const char *)(target_path.stem().u8string().c_str()));
installation.display_name = installation.mod_id;
installation.mod_version = recomp::Version{0, 0, 0, ""};
installation.mod_file = target_path;
}
}
std::error_code ec;
if (exists) {
std::filesystem::copy(file_path, target_write_path, ec);
if (ec) {
result.error_messages.emplace_back("Unable to install " + file_path.filename().string() + " to mod directory.");
return;
}
}
else {
result.error_messages.emplace_back(file_path.string() + " is not a mod.");
std::filesystem::remove(target_write_path, ec);
return;
}
if (std::filesystem::exists(installation.mod_file, ec)) {
installation.needs_overwrite_confirmation = true;
}
if (!installation.needs_overwrite_confirmation) {
// This check isn't really needed as additional_files will be empty for a single mod installation,
// but it's good to have in case this logic ever changes.
for (const std::filesystem::path &path : installation.additional_files) {
if (std::filesystem::exists(path, ec)) {
installation.needs_overwrite_confirmation = true;
break;
}
}
}
result.pending_installations.emplace_back(installation);
}
void start_package_mod_installation(const std::filesystem::path &path, recomp::mods::ZipModFileHandle &file_handle, std::function<void(std::filesystem::path, size_t, size_t)> progress_callback, ModInstaller::Result &result) {
std::error_code ec;
char filename[1024];
std::filesystem::path mods_directory = recomp::mods::get_mods_directory();
mz_zip_archive *zip_archive = file_handle.archive.get();
mz_uint num_files = mz_zip_reader_get_num_files(file_handle.archive.get());
std::list<std::filesystem::path> dynamic_lib_files;
std::list<ModInstaller::Installation>::iterator first_nrm_iterator = result.pending_installations.end();
bool found_mod = false;
for (mz_uint i = 0; i < num_files; i++) {
mz_uint filename_length = mz_zip_reader_get_filename(zip_archive, i, filename, sizeof(filename));
if (filename_length == 0) {
continue;
}
std::filesystem::path target_path = mods_directory / std::u8string_view((const char8_t *)(filename));
if ((target_path.extension() == ".rtz") || (target_path.extension() == ".nrm")) {
found_mod = true;
ModInstaller::Installation installation;
std::filesystem::path target_write_path = target_path.u8string() + NewExtension;
std::ofstream output_stream(target_write_path, std::ios::binary);
if (!output_stream.is_open()) {
result.error_messages.emplace_back("Unable to write to mod directory.");
continue;
}
if (!mz_zip_reader_extract_to_callback(zip_archive, i, &zip_write_func, &output_stream, 0)) {
output_stream.close();
std::filesystem::remove(target_write_path, ec);
result.error_messages.emplace_back("Failed to install " + path.filename().string() + " to mod directory.");
continue;
}
output_stream.close();
if (output_stream.bad()) {
std::filesystem::remove(target_write_path, ec);
result.error_messages.emplace_back("Failed to install " + path.filename().string() + " to mod directory.");
continue;
}
// Try to load the extracted file as a mod file handle.
recomp::mods::ModOpenError open_error;
std::unique_ptr<recomp::mods::ZipModFileHandle> extracted_file_handle = std::make_unique<recomp::mods::ZipModFileHandle>(target_write_path, open_error);
if (open_error != recomp::mods::ModOpenError::Good) {
result.error_messages.emplace_back("Invalid mod (" + target_path.filename().string() + ") in " + path.filename().string() + ".");
extracted_file_handle.reset();
std::filesystem::remove(target_write_path, ec);
continue;
}
// Check for the existence of the manifest file.
bool exists = false;
std::vector<char> manifest_bytes = extracted_file_handle->read_file(ManifestFilename, exists);
if (exists) {
// Parse the manifest file to check for its validity.
std::string error;
recomp::mods::ModManifest manifest;
open_error = parse_manifest(manifest, manifest_bytes, error);
exists = (open_error == recomp::mods::ModOpenError::Good);
if (exists) {
installation.mod_id = manifest.mod_id;
installation.display_name = manifest.display_name;
installation.mod_version = manifest.version;
installation.mod_file = target_path;
}
}
else if (target_path.extension() == ".rtz") {
// When it's an rtz file, check if the texture database file exists.
exists = mz_zip_reader_locate_file(extracted_file_handle->archive.get(), TextureDatabaseFilename, nullptr, 0) >= 0;
if (exists) {
installation.mod_id = std::string((const char *)(target_path.stem().u8string().c_str()));
installation.display_name = installation.mod_id;
installation.mod_version = recomp::Version();
installation.mod_file = target_path;
}
}
if (!exists) {
result.error_messages.emplace_back("Invalid mod (" + target_path.filename().string() + ") in " + path.filename().string() + ".");
extracted_file_handle.reset();
std::filesystem::remove(target_write_path, ec);
continue;
}
if (std::filesystem::exists(installation.mod_file, ec)) {
installation.needs_overwrite_confirmation = true;
}
if (!installation.needs_overwrite_confirmation) {
// This check isn't really needed as additional_files will be empty at this point,
// but it's good to have in case this logic ever changes.
for (const std::filesystem::path &path : installation.additional_files) {
if (std::filesystem::exists(path, ec)) {
installation.needs_overwrite_confirmation = true;
break;
}
}
}
result.pending_installations.emplace_back(installation);
// Store the first nrm found for any dynamic libraries that might be found.
if ((first_nrm_iterator == result.pending_installations.end()) && (target_path.extension() == ".nrm")) {
first_nrm_iterator = std::prev(result.pending_installations.end());
}
}
if (is_dynamic_lib(target_path)) {
std::filesystem::path target_write_path = target_path.u8string() + NewExtension;
std::ofstream output_stream(target_write_path, std::ios::binary);
if (!output_stream.is_open()) {
result.error_messages.emplace_back("Failed to install " + path.filename().string() + " to mod directory.");
continue;
}
if (!mz_zip_reader_extract_to_callback(zip_archive, i, &zip_write_func, &output_stream, 0)) {
output_stream.close();
std::filesystem::remove(target_write_path, ec);
result.error_messages.emplace_back("Failed to install " + path.filename().string() + " to mod directory.");
continue;
}
output_stream.close();
if (output_stream.bad()) {
std::filesystem::remove(target_write_path, ec);
result.error_messages.emplace_back("Failed to install " + path.filename().string() + " to mod directory.");
continue;
}
dynamic_lib_files.emplace_back(target_path);
}
}
if (!dynamic_lib_files.empty()) {
if (first_nrm_iterator != result.pending_installations.end()) {
// Associate all these files to the first mod that is found.
for (const std::filesystem::path &path : dynamic_lib_files) {
first_nrm_iterator->additional_files.emplace_back(path);
// Run verification against for overwrite confirmations.
if (std::filesystem::exists(path, ec)) {
first_nrm_iterator->needs_overwrite_confirmation = true;
}
}
}
else {
// These library files were not required by any mod, just delete them.
for (const std::filesystem::path &path : dynamic_lib_files) {
std::filesystem::path new_path(path.u8string() + NewExtension);
std::filesystem::remove(new_path, ec);
}
}
}
if (!found_mod) {
result.error_messages.emplace_back("No mods found in " + path.filename().string() + ".");
}
}
void remove_and_rename(std::vector<std::string>& error_messages, const std::filesystem::path& path) {
std::error_code ec;
std::filesystem::path old_path(path.u8string() + OldExtension);
std::filesystem::path new_path(path.u8string() + NewExtension);
// Rename the current path to a temporary old path, but only if the current path already exists.
if (std::filesystem::exists(path, ec)) {
std::filesystem::remove(old_path, ec);
std::filesystem::rename(path, old_path, ec);
if (ec) {
// If it fails, remove the new path.
std::filesystem::remove(new_path, ec);
error_messages.emplace_back("Unable to rename " + path.filename().string() + ".");
return;
}
}
// Rename the new path to the current path.
std::filesystem::rename(new_path, path, ec);
if (ec) {
// If it fails, remove the new path and also restore the temporary old path to the current path.
std::filesystem::remove(new_path, ec);
std::filesystem::rename(old_path, path, ec);
error_messages.emplace_back("Unable to rename " + path.filename().string() + ".");
return;
}
// If nothing failed, just remove the temporary old path.
std::filesystem::remove(old_path, ec);
};
void ModInstaller::start_mod_installation(const std::list<std::filesystem::path> &file_paths, std::function<void(std::filesystem::path, size_t, size_t)> progress_callback, Result &result) {
result = Result();
for (const std::filesystem::path &path : file_paths) {
if (is_dynamic_lib(path)) {
result.error_messages.emplace_back("The provided mod(s) must be installed without extracting the ZIP file(s). Please install the mod ZIP file(s) directly.");
return;
}
}
for (const std::filesystem::path &path : file_paths) {
recomp::mods::ModOpenError open_error;
recomp::mods::ZipModFileHandle file_handle(path, open_error);
if (open_error != recomp::mods::ModOpenError::Good) {
result.error_messages.emplace_back(path.filename().string() + " is not a valid zip or mod.");
continue;
}
// First we verify if the container itself isn't a mod already.
// TODO hook into the runtime's container registration to check the extension instead of using hardcoded values.
if ((path.extension() == ".rtz") || (path.extension() == ".nrm")) {
start_single_mod_installation(path, file_handle, progress_callback, result);
}
else {
// Scan the container for compatible mods instead. This is the case for packages made by users or how they're tipically uploaded to Thunderstore.
start_package_mod_installation(path, file_handle, progress_callback, result);
}
}
}
void ModInstaller::cancel_mod_installation(const Result &result, std::vector<std::string>& error_messages) {
error_messages.clear();
std::error_code ec;
// Delete all the files that were extracted for all mods.
for (const Installation &installation : result.pending_installations) {
std::filesystem::path new_path(installation.mod_file.u8string() + NewExtension);
std::filesystem::remove(new_path, ec);
for (const std::filesystem::path &path : installation.additional_files) {
std::filesystem::path new_path(path.u8string() + NewExtension);
std::filesystem::remove(new_path, ec);
}
}
}
void ModInstaller::finish_mod_installation(const Result &result, std::vector<std::string>& error_messages) {
error_messages.clear();
std::error_code ec;
for (const Installation &installation : result.pending_installations) {
// Overwrite the mod files.
remove_and_rename(error_messages, installation.mod_file);
for (const std::filesystem::path &path : installation.additional_files) {
remove_and_rename(error_messages, path);
}
}
}
};
+42
View File
@@ -0,0 +1,42 @@
#ifndef RECOMPUI_MOD_INSTALLER_H
#define RECOMPUI_MOD_INSTALLER_H
#include <librecomp/game.hpp>
#include <unordered_set>
#include <vector>
#include <string>
#include <list>
namespace recompui {
struct ModInstaller {
struct Installation {
std::string mod_id;
std::string display_name;
recomp::Version mod_version;
std::filesystem::path mod_file;
std::list<std::filesystem::path> additional_files;
bool needs_overwrite_confirmation = false;
};
struct Confirmation {
std::string old_display_name;
std::string new_display_name;
std::string old_mod_id;
std::string new_mod_id;
recomp::Version old_version;
recomp::Version new_version;
};
struct Result {
std::list<std::string> error_messages;
std::list<Installation> pending_installations;
};
static void start_mod_installation(const std::list<std::filesystem::path> &file_paths, std::function<void(std::filesystem::path, size_t, size_t)> progress_callback, Result &result);
static void cancel_mod_installation(const Result& result, std::vector<std::string>& errors);
static void finish_mod_installation(const Result &result, std::vector<std::string>& errors);
};
};
#endif
+271 -44
View File
@@ -1,5 +1,8 @@
#include "ui_mod_menu.h"
#include "ui_mod_menu.h"
#include "ui_utils.h"
#include "recomp_ui.h"
#include "banjo_support.h"
#include "banjo_render.h"
#include "librecomp/mods.hpp"
@@ -24,51 +27,65 @@ static bool is_mod_enabled_or_auto(const std::string &mod_id) {
}
// ModEntryView
#define COL_TEXT_DEFAULT 242, 242, 242
#define COL_TEXT_DIM 204, 204, 204
#define COL_SECONDARY 23, 214, 232
constexpr float modEntryHeight = 120.0f;
constexpr float modEntryPadding = 4.0f;
ModEntryView::ModEntryView(Element *parent) : Element(parent) {
extern const std::string mod_tab_id;
const std::string mod_tab_id = "#tab_mods";
ModEntryView::ModEntryView(Element *parent) : Element(parent, Events(EventType::Update)) {
ContextId context = get_current_context();
set_display(Display::Flex);
set_flex_direction(FlexDirection::Row);
set_width(100.0f, Unit::Percent);
set_height_auto();
set_padding_top(4.0f);
set_padding_right(8.0f);
set_padding_bottom(4.0f);
set_padding_left(8.0f);
set_border_width(1.1f);
set_border_color(Color{ 242, 242, 242, 12 });
set_background_color(Color{ 242, 242, 242, 12 });
set_padding(modEntryPadding);
set_border_left_width(2.0f);
set_border_color(Color{ COL_TEXT_DEFAULT, 12 });
set_background_color(Color{ COL_TEXT_DEFAULT, 12 });
set_cursor(Cursor::Pointer);
set_color(Color{ COL_TEXT_DEFAULT, 255 });
checked_style.set_border_color(Color{ 242, 242, 242, 160 });
hover_style.set_border_color(Color{ 242, 242, 242, 64 });
checked_hover_style.set_border_color(Color{ 242, 242, 242, 204 });
checked_style.set_border_color(Color{ COL_TEXT_DEFAULT, 160 });
checked_style.set_color(Color{ 255, 255, 255, 255 });
checked_style.set_background_color(Color{ 26, 24, 32, 255 });
hover_style.set_border_color(Color{ COL_TEXT_DEFAULT, 64 });
checked_hover_style.set_border_color(Color{ COL_TEXT_DEFAULT, 255 });
pulsing_style.set_border_color(Color{ 23, 214, 232, 244 });
{
thumbnail_image = context.create_element<Image>(this, "");
thumbnail_image->set_width(100.0f);
thumbnail_image->set_height(100.0f);
thumbnail_image->set_min_width(100.0f);
thumbnail_image->set_min_height(100.0f);
thumbnail_image->set_width(modEntryHeight);
thumbnail_image->set_height(modEntryHeight);
thumbnail_image->set_min_width(modEntryHeight);
thumbnail_image->set_min_height(modEntryHeight);
thumbnail_image->set_background_color(Color{ 190, 184, 219, 25 });
body_container = context.create_element<Container>(this, FlexDirection::Column, JustifyContent::FlexStart);
body_container = context.create_element<Element>(this);
body_container->set_width_auto();
body_container->set_height(100.0f);
body_container->set_margin_left(16.0f);
body_container->set_overflow(Overflow::Hidden);
body_container->set_padding_top(8.0f);
body_container->set_padding_bottom(8.0f);
body_container->set_max_height(modEntryHeight);
body_container->set_overflow_y(Overflow::Hidden);
{
name_label = context.create_element<Label>(body_container, LabelStyle::Normal);
description_label = context.create_element<Label>(body_container, LabelStyle::Small);
description_label->set_margin_top(4.0f);
description_label->set_color(Color{ COL_TEXT_DIM, 255 });
} // body_container
} // this
add_style(&checked_style, checked_state);
add_style(&hover_style, hover_state);
add_style(&checked_hover_style, { checked_state, hover_state });
add_style(&pulsing_style, { focus_state });
}
ModEntryView::~ModEntryView() {
@@ -92,12 +109,32 @@ void ModEntryView::set_selected(bool selected) {
set_style_enabled(checked_state, selected);
}
void ModEntryView::set_focused(bool focused) {
set_style_enabled(focus_state, focused);
}
void ModEntryView::process_event(const Event &e) {
switch (e.type) {
case EventType::Update:
if (is_style_enabled(focus_state)) {
pulsing_style.set_color(recompui::get_pulse_color(750));
apply_styles();
queue_update();
}
break;
default:
break;
}
}
// ModEntryButton
ModEntryButton::ModEntryButton(Element *parent, uint32_t mod_index) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Focus, EventType::Drag)) {
this->mod_index = mod_index;
set_drag(Drag::Drag);
enable_focus();
ContextId context = get_current_context();
view = context.create_element<ModEntryView>(this);
@@ -131,16 +168,20 @@ void ModEntryButton::set_selected(bool selected) {
view->set_selected(selected);
}
void ModEntryButton::set_focused(bool focused) {
view->set_focused(focused);
view->queue_update();
}
void ModEntryButton::process_event(const Event& e) {
switch (e.type) {
case EventType::Click:
case EventType::Focus:
selected_callback(mod_index);
set_focused(std::get<EventFocus>(e.variant).active);
break;
case EventType::Hover:
view->set_style_enabled(hover_state, std::get<EventHover>(e.variant).active);
break;
case EventType::Focus:
break;
case EventType::Drag:
drag_callback(mod_index, std::get<EventDrag>(e.variant));
break;
@@ -205,13 +246,15 @@ void ModEntrySpacer::set_target_height(float target_height, bool animate_to_targ
// ModMenu
void ModMenu::refresh_mods() {
void ModMenu::refresh_mods(bool scan_mods) {
for (const std::string &thumbnail : loaded_thumbnails) {
recompui::release_image(thumbnail);
}
recomp::mods::scan_mods();
mod_details = recomp::mods::get_mod_details(game_mod_id);
if (scan_mods) {
recomp::mods::scan_mods();
}
mod_details = recomp::mods::get_all_mod_details(game_mod_id);
create_mod_list();
}
@@ -221,13 +264,30 @@ void ModMenu::open_mods_folder() {
std::wstring path_wstr = mods_directory.wstring();
ShellExecuteW(NULL, L"open", path_wstr.c_str(), NULL, NULL, SW_SHOWDEFAULT);
#elif defined(__linux__)
std::string command = "xdg-open " + mods_directory.string() + " &";
std::string command = "xdg-open \"" + mods_directory.string() + "\" &";
std::system(command.c_str());
#elif defined(__APPLE__)
std::string command = "open \"" + mods_directory.string() + "\"";
std::system(command.c_str());
#else
static_assert(false, "Not implemented for this platform.");
#endif
}
void ModMenu::open_install_dialog() {
banjo::open_file_dialog_multiple([](bool success, const std::list<std::filesystem::path>& paths) {
if (success) {
ContextId old_context = recompui::try_close_current_context();
recompui::drop_files(paths);
if (old_context != ContextId::null()) {
old_context.open();
}
}
});
}
void ModMenu::mod_toggled(bool enabled) {
if (active_mod_index >= 0) {
recomp::mods::enable_mod(mod_details[active_mod_index].mod_id, enabled);
@@ -255,11 +315,41 @@ void ModMenu::mod_selected(uint32_t mod_index) {
bool configure_enabled = !config_schema.options.empty();
mod_details_panel->set_mod_details(mod_details[mod_index], thumbnail_src, toggle_checked, toggle_enabled, auto_enabled, configure_enabled);
mod_entry_buttons[active_mod_index]->set_selected(true);
mod_details_panel->setup_mod_navigation(mod_entry_buttons[mod_index]);
// Navigation from the bottom bar.
Button *configure_button = mod_details_panel->get_configure_button();
Toggle *enable_toggle = mod_details_panel->get_enable_toggle();
if (configure_enabled) {
refresh_button->set_nav(NavDirection::Up, configure_button);
mods_folder_button->set_nav(NavDirection::Up, configure_button);
}
else if (toggle_enabled) {
refresh_button->set_nav(NavDirection::Up, enable_toggle);
mods_folder_button->set_nav(NavDirection::Up, enable_toggle);
}
else {
refresh_button->set_nav_manual(NavDirection::Up, mod_tab_id);
mods_folder_button->set_nav_manual(NavDirection::Up, mod_tab_id);
}
// Navigation from the mod list.
if (toggle_enabled) {
mod_entry_buttons[active_mod_index]->set_nav(NavDirection::Right, enable_toggle);
}
else if (configure_enabled) {
mod_entry_buttons[active_mod_index]->set_nav(NavDirection::Right, configure_button);
}
else {
mod_entry_buttons[active_mod_index]->set_nav_none(NavDirection::Right);
}
}
}
void ModMenu::mod_dragged(uint32_t mod_index, EventDrag drag) {
constexpr float spacer_height = 110.0f;
constexpr float spacer_height = modEntryHeight + modEntryPadding * 2.0f;
switch (drag.phase) {
case DragPhase::Start: {
for (size_t i = 0; i < mod_entry_buttons.size(); i++) {
@@ -274,6 +364,7 @@ void ModMenu::mod_dragged(uint32_t mod_index, EventDrag drag) {
float left = mod_entry_buttons[mod_index]->get_absolute_left() - get_absolute_left();
float top = mod_entry_buttons[mod_index]->get_absolute_top() - (height / 2.0f); // TODO: Figure out why this adjustment is even necessary.
mod_entry_buttons[mod_index]->set_display(Display::None);
mod_entry_buttons[mod_index]->set_focused(false);
mod_entry_floating_view->set_display(Display::Flex);
mod_entry_floating_view->set_mod_details(mod_details[mod_index]);
mod_entry_floating_view->set_mod_thumbnail(generate_thumbnail_src_for_mod(mod_details[mod_index].mod_id));
@@ -332,7 +423,7 @@ void ModMenu::mod_dragged(uint32_t mod_index, EventDrag drag) {
// Re-order the mods and update all the details on the menu.
recomp::mods::set_mod_index(game_mod_id, mod_details[mod_index].mod_id, mod_drag_target_index);
mod_details = recomp::mods::get_mod_details(game_mod_id);
mod_details = recomp::mods::get_all_mod_details(game_mod_id);
for (size_t i = 0; i < mod_entry_buttons.size(); i++) {
mod_entry_buttons[i]->set_mod_details(mod_details[i]);
mod_entry_buttons[i]->set_mod_thumbnail(generate_thumbnail_src_for_mod(mod_details[i].mod_id));
@@ -356,6 +447,22 @@ ContextId get_config_sub_menu_context_id() {
return sub_menu_context;
}
bool ModMenu::handle_special_config_options(const recomp::mods::ConfigOption& option, const recomp::mods::ConfigValueVariant& config_value) {
if (banjo::renderer::is_texture_pack_enable_config_option(option, true)) {
const recomp::mods::ConfigOptionEnum &option_enum = std::get<recomp::mods::ConfigOptionEnum>(option.variant);
config_sub_menu->add_radio_option(option.id, option.name, option.description, std::get<uint32_t>(config_value), option_enum.options,
[this](const std::string &id, uint32_t value) {
mod_enum_option_changed(id, value);
mod_hd_textures_enabled_changed(value);
});
return true;
}
return false;
}
void ModMenu::mod_configure_requested() {
if (active_mod_index >= 0) {
// Record the context that was open when this function was called and close it.
@@ -373,19 +480,26 @@ void ModMenu::mod_configure_requested() {
continue;
}
if (handle_special_config_options(option, config_value)) {
continue;
}
switch (option.type) {
case recomp::mods::ConfigOptionType::Enum: {
const recomp::mods::ConfigOptionEnum &option_enum = std::get<recomp::mods::ConfigOptionEnum>(option.variant);
config_sub_menu->add_radio_option(option.id, option.name, option.description, std::get<uint32_t>(config_value), option_enum.options, std::bind(&ModMenu::mod_enum_option_changed, this, std::placeholders::_1, std::placeholders::_2));
config_sub_menu->add_radio_option(option.id, option.name, option.description, std::get<uint32_t>(config_value), option_enum.options,
[this](const std::string &id, uint32_t value){ mod_enum_option_changed(id, value); });
break;
}
case recomp::mods::ConfigOptionType::Number: {
const recomp::mods::ConfigOptionNumber &option_number = std::get<recomp::mods::ConfigOptionNumber>(option.variant);
config_sub_menu->add_slider_option(option.id, option.name, option.description, std::get<double>(config_value), option_number.min, option_number.max, option_number.step, option_number.percent, std::bind(&ModMenu::mod_number_option_changed, this, std::placeholders::_1, std::placeholders::_2));
config_sub_menu->add_slider_option(option.id, option.name, option.description, std::get<double>(config_value), option_number.min, option_number.max, option_number.step, option_number.percent,
[this](const std::string &id, double value){ mod_number_option_changed(id, value); });
break;
}
case recomp::mods::ConfigOptionType::String: {
config_sub_menu->add_text_option(option.id, option.name, option.description, std::get<std::string>(config_value), std::bind(&ModMenu::mod_string_option_changed, this, std::placeholders::_1, std::placeholders::_2));
config_sub_menu->add_text_option(option.id, option.name, option.description, std::get<std::string>(config_value),
[this](const std::string &id, const std::string &value){ mod_string_option_changed(id, value); });
break;
}
default:
@@ -424,6 +538,17 @@ void ModMenu::mod_number_option_changed(const std::string &id, double value) {
}
}
void ModMenu::mod_hd_textures_enabled_changed(uint32_t value) {
if (active_mod_index >= 0) {
if (value) {
banjo::renderer::secondary_enable_texture_pack(mod_details[active_mod_index].mod_id);
}
else {
banjo::renderer::secondary_disable_texture_pack(mod_details[active_mod_index].mod_id);
}
}
}
void ModMenu::create_mod_list() {
ContextId context = get_current_context();
@@ -432,12 +557,14 @@ void ModMenu::create_mod_list() {
mod_entry_buttons.clear();
mod_entry_spacers.clear();
Toggle* enable_toggle = mod_details_panel->get_enable_toggle();
// Create the child elements for the list scroll.
for (size_t mod_index = 0; mod_index < mod_details.size(); mod_index++) {
const std::vector<char> &thumbnail = recomp::mods::get_mod_thumbnail(mod_details[mod_index].mod_id);
std::string thumbnail_name = generate_thumbnail_src_for_mod(mod_details[mod_index].mod_id);
if (!thumbnail.empty()) {
recompui::queue_image_from_bytes(thumbnail_name, thumbnail);
recompui::queue_image_from_bytes_file(thumbnail_name, thumbnail);
loaded_thumbnails.emplace(thumbnail_name);
}
@@ -445,14 +572,28 @@ void ModMenu::create_mod_list() {
mod_entry_spacers.emplace_back(spacer);
ModEntryButton *mod_entry = context.create_element<ModEntryButton>(list_scroll_container, mod_index);
mod_entry->set_mod_selected_callback(std::bind(&ModMenu::mod_selected, this, std::placeholders::_1));
mod_entry->set_mod_drag_callback(std::bind(&ModMenu::mod_dragged, this, std::placeholders::_1, std::placeholders::_2));
mod_entry->set_mod_selected_callback([this](uint32_t mod_index){ mod_selected(mod_index); });
mod_entry->set_mod_drag_callback([this](uint32_t mod_index, recompui::EventDrag drag){ mod_dragged(mod_index, drag); });
mod_entry->set_mod_details(mod_details[mod_index]);
mod_entry->set_mod_thumbnail(thumbnail_name);
mod_entry->set_mod_enabled(is_mod_enabled_or_auto(mod_details[mod_index].mod_id));
mod_entry_buttons.emplace_back(mod_entry);
}
if (!mod_entry_buttons.empty()) {
mod_entry_buttons.front()->set_nav_manual(NavDirection::Up, mod_tab_id);
mod_entry_buttons.back()->set_nav(NavDirection::Down, install_mods_button);
install_mods_button->set_nav(NavDirection::Up, mod_entry_buttons.back());
}
else {
install_mods_button->set_nav_manual(NavDirection::Up, mod_tab_id);
}
Rml::ElementTabSet* tabset = recompui::get_config_tabset();
if (tabset && tabset->GetActiveTab() == recompui::config_tab_to_index(ConfigTab::Mods)) {
recompui::set_config_tabset_mod_nav();
}
// Add one extra spacer at the bottom.
ModEntrySpacer *spacer = context.create_element<ModEntrySpacer>(list_scroll_container);
mod_entry_spacers.emplace_back(spacer);
@@ -467,6 +608,27 @@ void ModMenu::create_mod_list() {
}
}
void ModMenu::process_event(const Event &e) {
if (e.type == EventType::Update) {
if (mods_dirty) {
refresh_mods(mod_scan_queued);
mods_dirty = false;
mod_scan_queued = false;
}
if (ultramodern::is_game_started()) {
install_mods_button->set_enabled(false);
refresh_button->set_enabled(false);
}
if (active_mod_index != -1) {
bool auto_enabled = recomp::mods::is_mod_auto_enabled(mod_details[active_mod_index].mod_id);
bool toggle_enabled = !auto_enabled && (mod_details[active_mod_index].runtime_toggleable || !ultramodern::is_game_started());
if (!toggle_enabled) {
mod_details_panel->disable_toggle();
}
}
}
}
ModMenu::ModMenu(Element *parent) : Element(parent) {
game_mod_id = "bk";
@@ -498,8 +660,8 @@ ModMenu::ModMenu(Element *parent) : Element(parent) {
} // list_container
mod_details_panel = context.create_element<ModDetailsPanel>(body_container);
mod_details_panel->set_mod_toggled_callback(std::bind(&ModMenu::mod_toggled, this, std::placeholders::_1));
mod_details_panel->set_mod_configure_pressed_callback(std::bind(&ModMenu::mod_configure_requested, this));
mod_details_panel->set_mod_toggled_callback([this](bool enabled){ mod_toggled(enabled); });
mod_details_panel->set_mod_configure_pressed_callback([this](){ mod_configure_requested(); });
} // body_container
body_empty_container = context.create_element<Container>(this, FlexDirection::Column, JustifyContent::SpaceBetween);
@@ -511,23 +673,32 @@ ModMenu::ModMenu(Element *parent) : Element(parent) {
context.create_element<Element>(body_empty_container);
} // body_empty_container
footer_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::SpaceBetween);
footer_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::FlexStart);
footer_container->set_width(100.0f, recompui::Unit::Percent);
footer_container->set_align_items(recompui::AlignItems::Center);
footer_container->set_background_color(Color{ 0, 0, 0, 89 });
footer_container->set_border_top_width(1.1f);
footer_container->set_border_top_color(Color{ 255, 255, 255, 25 });
footer_container->set_padding(20.0f);
footer_container->set_gap(20.0f);
footer_container->set_border_bottom_left_radius(16.0f);
footer_container->set_border_bottom_right_radius(16.0f);
{
refresh_button = context.create_element<Button>(footer_container, "Refresh", recompui::ButtonStyle::Primary);
refresh_button->add_pressed_callback(std::bind(&ModMenu::refresh_mods, this));
Button* configure_button = mod_details_panel->get_configure_button();
install_mods_button = context.create_element<Button>(footer_container, "Install Mods", recompui::ButtonStyle::Primary);
install_mods_button->add_pressed_callback([this](){ open_install_dialog(); });
context.create_element<Label>(footer_container, "⚠ UNDER CONSTRUCTION ⚠", LabelStyle::Small);
Element* footer_spacer = context.create_element<Element>(footer_container);
footer_spacer->set_flex(1.0f, 0.0f);
refresh_button = context.create_element<Button>(footer_container, "Refresh", recompui::ButtonStyle::Primary);
refresh_button->add_pressed_callback([this](){ refresh_mods(true); });
refresh_button->set_nav_manual(NavDirection::Up, mod_tab_id);
mods_folder_button = context.create_element<Button>(footer_container, "Open Mods Folder", recompui::ButtonStyle::Primary);
mods_folder_button->add_pressed_callback(std::bind(&ModMenu::open_mods_folder, this));
mods_folder_button->add_pressed_callback([this](){ open_mods_folder(); });
mods_folder_button->set_nav(NavDirection::Up, configure_button);
mods_folder_button->set_nav_manual(NavDirection::Up, mod_tab_id);
} // footer_container
} // this
@@ -536,11 +707,9 @@ ModMenu::ModMenu(Element *parent) : Element(parent) {
mod_entry_floating_view->set_position(Position::Absolute);
mod_entry_floating_view->set_selected(true);
refresh_mods();
context.close();
sub_menu_context = recompui::create_context("assets/config_sub_menu.rml");
sub_menu_context = recompui::create_context(banjo::get_asset_path("config_sub_menu.rml"));
sub_menu_context.open();
Rml::ElementDocument* sub_menu_doc = sub_menu_context.get_document();
Rml::Element* config_sub_menu_generic = sub_menu_doc->GetElementById("config_sub_menu");
@@ -556,6 +725,64 @@ ModMenu::~ModMenu() {
// Placeholder class until the rest of the UI refactor is finished.
recompui::ModMenu* mod_menu;
void update_mod_list(bool scan_mods) {
if (mod_menu) {
recompui::ContextId ui_context = recompui::get_config_context_id();
bool opened = ui_context.open_if_not_already();
mod_menu->set_mods_dirty(scan_mods);
mod_menu->queue_update();
if (opened) {
ui_context.close();
}
}
}
void process_game_started() {
if (mod_menu) {
recompui::ContextId ui_context = recompui::get_config_context_id();
bool opened = ui_context.open_if_not_already();
mod_menu->queue_update();
if (opened) {
ui_context.close();
}
}
}
void set_config_tabset_mod_nav() {
if (mod_menu) {
Rml::ElementTabSet* tabset = recompui::get_config_tabset();
Rml::Element* tabs = recompui::get_child_by_tag(tabset, "tabs");
if (tabs != nullptr) {
size_t num_children = tabs->GetNumChildren();
Element* first_mod_entry = mod_menu->get_first_mod_entry();
if (first_mod_entry != nullptr) {
std::string id = "#" + first_mod_entry->get_id();
for (size_t i = 0; i < num_children; i++) {
tabs->GetChild(i)->SetProperty(Rml::PropertyId::NavDown, Rml::Property{ id, Rml::Unit::STRING });
}
}
else {
for (size_t i = 0; i < num_children; i++) {
tabs->GetChild(i)->SetProperty(Rml::PropertyId::NavDown, Rml::Style::Nav::Auto);
}
}
}
}
}
void focus_mod_configure_button() {
Element* configure_button = mod_menu->get_mod_configure_button();
if (configure_button) {
configure_button->focus();
}
}
ElementModMenu::ElementModMenu(const Rml::String &tag) : Rml::Element(tag) {
SetProperty("width", "100%");
SetProperty("height", "100%");
+22 -4
View File
@@ -18,14 +18,19 @@ public:
void set_mod_thumbnail(const std::string &thumbnail);
void set_mod_enabled(bool enabled);
void set_selected(bool selected);
void set_focused(bool focused);
protected:
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "ModEntryView"; }
private:
Image *thumbnail_image = nullptr;
Container *body_container = nullptr;
Element *body_container = nullptr;
Label *name_label = nullptr;
Label *description_label = nullptr;
Style checked_style;
Style hover_style;
Style checked_hover_style;
Style pulsing_style;
};
class ModEntryButton : public Element {
@@ -38,8 +43,10 @@ public:
void set_mod_thumbnail(const std::string &thumbnail);
void set_mod_enabled(bool enabled);
void set_selected(bool selected);
void set_focused(bool focused);
protected:
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "ModEntryButton"; }
private:
uint32_t mod_index = 0;
ModEntryView *view = nullptr;
@@ -56,6 +63,7 @@ private:
void check_height_distance();
protected:
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "ModEntrySpacer"; }
public:
ModEntrySpacer(Element *parent);
void set_target_height(float target_height, bool animate_to_target);
@@ -65,17 +73,26 @@ class ModMenu : public Element {
public:
ModMenu(Element *parent);
virtual ~ModMenu();
void set_mods_dirty(bool scan_mods) { mods_dirty = true; mod_scan_queued = scan_mods; }
Element* get_first_mod_entry() { return !mod_entry_buttons.empty() ? mod_entry_buttons[0] : nullptr; }
Element* get_mod_configure_button() { return mod_details_panel != nullptr ? mod_details_panel->get_configure_button() : nullptr; }
protected:
std::string_view get_type_name() override { return "ModMenu"; }
private:
void refresh_mods();
void refresh_mods(bool scan_mods);
void open_mods_folder();
void open_install_dialog();
void mod_toggled(bool enabled);
void mod_selected(uint32_t mod_index);
void mod_dragged(uint32_t mod_index, EventDrag drag);
void mod_configure_requested();
bool handle_special_config_options(const recomp::mods::ConfigOption& option, const recomp::mods::ConfigValueVariant& config_value);
void mod_enum_option_changed(const std::string &id, uint32_t value);
void mod_string_option_changed(const std::string &id, const std::string &value);
void mod_number_option_changed(const std::string &id, double value);
void mod_hd_textures_enabled_changed(uint32_t value);
void create_mod_list();
void process_event(const Event &e) override;
Container *body_container = nullptr;
Container *list_container = nullptr;
@@ -83,6 +100,7 @@ private:
ModDetailsPanel *mod_details_panel = nullptr;
Container *body_empty_container = nullptr;
Container *footer_container = nullptr;
Button *install_mods_button = nullptr;
Button *refresh_button = nullptr;
Button *mods_folder_button = nullptr;
int32_t active_mod_index = -1;
@@ -96,6 +114,8 @@ private:
std::vector<recomp::mods::ModDetails> mod_details{};
std::unordered_set<std::string> loaded_thumbnails;
std::string game_mod_id;
bool mods_dirty = false;
bool mod_scan_queued = false;
ConfigSubMenu *config_sub_menu;
};
@@ -104,8 +124,6 @@ class ElementModMenu : public Rml::Element {
public:
ElementModMenu(const Rml::String& tag);
virtual ~ElementModMenu();
private:
ModMenu *mod_menu;
};
} // namespace recompui
+360
View File
@@ -0,0 +1,360 @@
#include <mutex>
#include "recomp_ui.h"
#include "elements/ui_element.h"
#include "elements/ui_label.h"
#include "elements/ui_button.h"
struct {
recompui::ContextId ui_context;
recompui::Label* prompt_header;
recompui::Label* prompt_label;
recompui::Element* prompt_controls;
recompui::Button* confirm_button;
recompui::Button* cancel_button;
std::function<void()> confirm_action;
std::function<void()> cancel_action;
std::string return_element_id;
std::mutex mutex;
} prompt_state;
void run_confirm_callback() {
std::function<void()> confirm_action;
{
std::lock_guard lock{ prompt_state.mutex };
confirm_action = std::move(prompt_state.confirm_action);
}
if (confirm_action) {
confirm_action();
}
recompui::hide_context(prompt_state.ui_context);
// TODO nav: focus on return_element_id
// or just remove it as the usage of the prompt can change now
}
void run_cancel_callback() {
std::function<void()> cancel_action;
{
std::lock_guard lock{ prompt_state.mutex };
cancel_action = std::move(prompt_state.cancel_action);
}
if (cancel_action) {
cancel_action();
}
recompui::hide_context(prompt_state.ui_context);
// TODO nav: focus on return_element_id
// or just remove it as the usage of the prompt can change now
}
void recompui::init_prompt_context() {
ContextId context = create_context();
std::lock_guard lock{ prompt_state.mutex };
context.open();
prompt_state.ui_context = context;
Element* window = context.create_element<Element>(context.get_root_element());
window->set_display(Display::Flex);
window->set_flex_direction(FlexDirection::Column);
window->set_background_color({0, 0, 0, 0});
Element* prompt_overlay = context.create_element<Element>(window);
prompt_overlay->set_background_color(Color{ 190, 184, 219, 25 });
prompt_overlay->set_position(Position::Absolute);
prompt_overlay->set_top(0);
prompt_overlay->set_right(0);
prompt_overlay->set_bottom(0);
prompt_overlay->set_left(0);
Element* prompt_content_wrapper = context.create_element<Element>(window);
prompt_content_wrapper->set_display(Display::Flex);
prompt_content_wrapper->set_position(Position::Absolute);
prompt_content_wrapper->set_top(0);
prompt_content_wrapper->set_right(0);
prompt_content_wrapper->set_bottom(0);
prompt_content_wrapper->set_left(0);
prompt_content_wrapper->set_align_items(AlignItems::Center);
prompt_content_wrapper->set_justify_content(JustifyContent::Center);
Element* prompt_content = context.create_element<Element>(prompt_content_wrapper);
prompt_content->set_display(Display::Flex);
prompt_content->set_position(Position::Relative);
prompt_content->set_flex(1.0f, 1.0f);
prompt_content->set_flex_basis(100, Unit::Percent);
prompt_content->set_flex_direction(FlexDirection::Column);
prompt_content->set_width(100, Unit::Percent);
prompt_content->set_max_width(700, Unit::Dp);
prompt_content->set_height_auto();
prompt_content->set_margin_auto();
prompt_content->set_border_width(1.1, Unit::Dp);
prompt_content->set_border_radius(16, Unit::Dp);
prompt_content->set_border_color(Color{ 255, 255, 255, 51 });
prompt_content->set_background_color(Color{ 8, 7, 13, 229 });
prompt_state.prompt_header = context.create_element<Label>(prompt_content, "", LabelStyle::Large);
prompt_state.prompt_header->set_margin(24, Unit::Dp);
prompt_state.prompt_label = context.create_element<Label>(prompt_content, "", LabelStyle::Small);
prompt_state.prompt_label->set_margin(24, Unit::Dp);
prompt_state.prompt_label->set_margin_top(0);
prompt_state.prompt_controls = context.create_element<Element>(prompt_content);
prompt_state.prompt_controls->set_display(Display::Flex);
prompt_state.prompt_controls->set_flex_direction(FlexDirection::Row);
prompt_state.prompt_controls->set_justify_content(JustifyContent::Center);
prompt_state.prompt_controls->set_padding_top(24, Unit::Dp);
prompt_state.prompt_controls->set_padding_bottom(24, Unit::Dp);
prompt_state.prompt_controls->set_padding_left(12, Unit::Dp);
prompt_state.prompt_controls->set_padding_right(12, Unit::Dp);
prompt_state.prompt_controls->set_border_top_width(1.1, Unit::Dp);
prompt_state.prompt_controls->set_border_top_color({ 255, 255, 255, 25 });
prompt_state.confirm_button = context.create_element<Button>(prompt_state.prompt_controls, "", ButtonStyle::Primary);
prompt_state.confirm_button->set_min_width(185.0f, Unit::Dp);
prompt_state.confirm_button->set_margin_top(0);
prompt_state.confirm_button->set_margin_bottom(0);
prompt_state.confirm_button->set_margin_left(12, Unit::Dp);
prompt_state.confirm_button->set_margin_right(12, Unit::Dp);
prompt_state.confirm_button->set_text_align(TextAlign::Center);
prompt_state.confirm_button->set_color(Color{ 204, 204, 204, 255 });
prompt_state.confirm_button->add_pressed_callback(run_confirm_callback);
Style* confirm_hover_style = prompt_state.confirm_button->get_hover_style();
confirm_hover_style->set_border_color(Color{ 69, 208, 67, 255 });
confirm_hover_style->set_background_color(Color{ 69, 208, 67, 76 });
confirm_hover_style->set_color(Color{ 242, 242, 242, 255 });
Style* confirm_focus_style = prompt_state.confirm_button->get_focus_style();
confirm_focus_style->set_border_color(Color{ 69, 208, 67, 255 });
confirm_focus_style->set_background_color(Color{ 69, 208, 67, 76 });
confirm_focus_style->set_color(Color{ 242, 242, 242, 255 });
prompt_state.cancel_button = context.create_element<Button>(prompt_state.prompt_controls, "", ButtonStyle::Primary);
prompt_state.cancel_button->set_min_width(185.0f, Unit::Dp);
prompt_state.cancel_button->set_margin_top(0);
prompt_state.cancel_button->set_margin_bottom(0);
prompt_state.cancel_button->set_margin_left(12, Unit::Dp);
prompt_state.cancel_button->set_margin_right(12, Unit::Dp);
prompt_state.cancel_button->set_text_align(TextAlign::Center);
prompt_state.cancel_button->set_color(Color{ 204, 204, 204, 255 });
prompt_state.cancel_button->add_pressed_callback(run_cancel_callback);
Style* cancel_hover_style = prompt_state.cancel_button->get_hover_style();
cancel_hover_style->set_border_color(Color{ 248, 96, 57, 255 });
cancel_hover_style->set_background_color(Color{ 248, 96, 57, 76 });
cancel_hover_style->set_color(Color{ 242, 242, 242, 255 });
Style* cancel_focus_style = prompt_state.cancel_button->get_focus_style();
cancel_focus_style->set_border_color(Color{ 248, 96, 57, 255 });
cancel_focus_style->set_background_color(Color{ 248, 96, 57, 76 });
cancel_focus_style->set_color(Color{ 242, 242, 242, 255 });
context.close();
}
void style_button(recompui::Button* button, recompui::ButtonVariant variant) {
recompui::Color button_color{};
switch (variant) {
case recompui::ButtonVariant::Primary:
button_color = { 185, 125, 242, 255 };
break;
case recompui::ButtonVariant::Secondary:
button_color = { 23, 214, 232, 255 };
break;
case recompui::ButtonVariant::Tertiary:
button_color = { 242, 242, 242, 255 };
break;
case recompui::ButtonVariant::Success:
button_color = { 69, 208, 67, 255 };
break;
case recompui::ButtonVariant::Error:
button_color = { 248, 96, 57, 255 };
break;
case recompui::ButtonVariant::Warning:
button_color = { 233, 205, 53, 255 };
break;
default:
assert(false);
break;
}
recompui::Color border_color = button_color;
recompui::Color background_color = button_color;
border_color.a = 0.8f * 255;
background_color.a = 0.05f * 255;
button->set_border_color(border_color);
button->set_background_color(background_color);
recompui::Color hover_border_color = button_color;
recompui::Color hover_background_color = button_color;
hover_border_color.a = 255;
hover_background_color.a = 0.3f * 255;
recompui::Style* hover_style = button->get_hover_style();
hover_style->set_border_color(hover_border_color);
hover_style->set_background_color(hover_background_color);
recompui::Style* focus_style = button->get_focus_style();
focus_style->set_border_color(hover_border_color);
focus_style->set_background_color(hover_background_color);
recompui::Color disabled_color { 255, 255, 255, 0.6f * 255 };
recompui::Style* disabled_style = button->get_disabled_style();
disabled_style->set_color(disabled_color);
}
// Must be called while prompt_state.mutex is locked.
void show_prompt(std::function<void()>& prev_cancel_action, bool focus_on_cancel) {
if (focus_on_cancel) {
prompt_state.ui_context.set_autofocus_element(prompt_state.cancel_button);
}
else {
prompt_state.ui_context.set_autofocus_element(prompt_state.confirm_button);
}
if (!recompui::is_context_shown(prompt_state.ui_context)) {
recompui::show_context(prompt_state.ui_context, "");
}
else {
// Call the previous cancel action to effectively close the previous prompt.
if (prev_cancel_action) {
prev_cancel_action();
}
}
}
void recompui::open_choice_prompt(
const std::string& header_text,
const std::string& content_text,
const std::string& confirm_label_text,
const std::string& cancel_label_text,
std::function<void()> confirm_action,
std::function<void()> cancel_action,
ButtonVariant confirm_variant,
ButtonVariant cancel_variant,
bool focus_on_cancel,
const std::string& return_element_id
) {
std::lock_guard lock{ prompt_state.mutex };
std::function<void()> prev_cancel_action = std::move(prompt_state.cancel_action);
ContextId prev_context = try_close_current_context();
prompt_state.ui_context.open();
prompt_state.prompt_header->set_text(header_text);
prompt_state.prompt_label->set_text(content_text);
prompt_state.prompt_controls->set_display(Display::Flex);
prompt_state.confirm_button->set_display(Display::Block);
prompt_state.cancel_button->set_display(Display::Block);
prompt_state.confirm_button->set_text(confirm_label_text);
prompt_state.cancel_button->set_text(cancel_label_text);
prompt_state.confirm_action = confirm_action;
prompt_state.cancel_action = cancel_action;
prompt_state.return_element_id = return_element_id;
style_button(prompt_state.confirm_button, confirm_variant);
style_button(prompt_state.cancel_button, cancel_variant);
prompt_state.ui_context.close();
if (prev_context != ContextId::null()) {
prev_context.open();
}
show_prompt(prev_cancel_action, focus_on_cancel);
}
void recompui::open_info_prompt(
const std::string& header_text,
const std::string& content_text,
const std::string& okay_label_text,
std::function<void()> okay_action,
ButtonVariant okay_variant,
const std::string& return_element_id
) {
std::lock_guard lock{ prompt_state.mutex };
std::function<void()> prev_cancel_action = std::move(prompt_state.cancel_action);
ContextId prev_context = try_close_current_context();
prompt_state.ui_context.open();
prompt_state.prompt_header->set_text(header_text);
prompt_state.prompt_label->set_text(content_text);
prompt_state.prompt_controls->set_display(Display::Flex);
prompt_state.confirm_button->set_display(Display::None);
prompt_state.cancel_button->set_display(Display::Block);
prompt_state.cancel_button->set_text(okay_label_text);
prompt_state.confirm_action = {};
prompt_state.cancel_action = okay_action;
prompt_state.return_element_id = return_element_id;
style_button(prompt_state.cancel_button, okay_variant);
prompt_state.ui_context.close();
if (prev_context != ContextId::null()) {
prev_context.open();
}
show_prompt(prev_cancel_action, true);
}
void recompui::open_notification(
const std::string& header_text,
const std::string& content_text,
const std::string& return_element_id
) {
std::lock_guard lock{ prompt_state.mutex };
std::function<void()> prev_cancel_action = std::move(prompt_state.cancel_action);
ContextId prev_context = try_close_current_context();
prompt_state.ui_context.open();
prompt_state.prompt_header->set_text(header_text);
prompt_state.prompt_label->set_text(content_text);
prompt_state.prompt_controls->set_display(Display::None);
prompt_state.confirm_button->set_display(Display::None);
prompt_state.cancel_button->set_display(Display::None);
prompt_state.confirm_action = {};
prompt_state.cancel_action = {};
prompt_state.return_element_id = return_element_id;
prompt_state.ui_context.close();
if (prev_context != ContextId::null()) {
prev_context.open();
}
show_prompt(prev_cancel_action, false);
}
void recompui::close_prompt() {
std::lock_guard lock{ prompt_state.mutex };
if (recompui::is_context_shown(prompt_state.ui_context)) {
if (prompt_state.cancel_action) {
prompt_state.cancel_action();
}
recompui::hide_context(prompt_state.ui_context);
}
}
bool recompui::is_prompt_open() {
std::lock_guard lock{ prompt_state.mutex };
return recompui::is_context_shown(prompt_state.ui_context);
}
+80 -13
View File
@@ -22,6 +22,9 @@
#ifdef _WIN32
# include "InterfaceVS.hlsl.dxil.h"
# include "InterfacePS.hlsl.dxil.h"
#elif defined(__APPLE__)
# include "InterfaceVS.hlsl.metal.h"
# include "InterfacePS.hlsl.metal.h"
#endif
#ifdef _WIN32
@@ -31,6 +34,13 @@
# define GET_SHADER_SIZE(name, format) \
((format) == RT64::RenderShaderFormat::SPIRV ? std::size(name##BlobSPIRV) : \
(format) == RT64::RenderShaderFormat::DXIL ? std::size(name##BlobDXIL) : 0)
#elif defined(__APPLE__)
# define GET_SHADER_BLOB(name, format) \
((format) == RT64::RenderShaderFormat::SPIRV ? name##BlobSPIRV : \
(format) == RT64::RenderShaderFormat::METAL ? name##BlobMSL : nullptr)
# define GET_SHADER_SIZE(name, format) \
((format) == RT64::RenderShaderFormat::SPIRV ? std::size(name##BlobSPIRV) : \
(format) == RT64::RenderShaderFormat::METAL ? std::size(name##BlobMSL) : 0)
#else
# define GET_SHADER_BLOB(name, format) \
((format) == RT64::RenderShaderFormat::SPIRV ? name##BlobSPIRV : nullptr)
@@ -62,7 +72,19 @@ T from_bytes_le(const char* input) {
return *reinterpret_cast<const T*>(input);
}
typedef std::pair<std::string, std::vector<char>> ImageFromBytes;
enum class ImageType {
File,
RGBA32
};
struct ImageFromBytes {
ImageType type;
// Dimensions only used for RGBA32 data. Files pull the size from the file data.
uint32_t width;
uint32_t height;
std::string name;
std::vector<char> bytes;
};
namespace recompui {
class RmlRenderInterface_RT64_impl : public Rml::RenderInterfaceCompatibility {
@@ -128,7 +150,7 @@ class RmlRenderInterface_RT64_impl : public Rml::RenderInterfaceCompatibility {
bool scissor_enabled_ = false;
std::vector<std::unique_ptr<RT64::RenderBuffer>> stale_buffers_{};
moodycamel::ConcurrentQueue<ImageFromBytes> image_from_bytes_queue;
std::unordered_map<std::string, std::vector<char>> image_from_bytes_map;
std::unordered_map<std::string, ImageFromBytes> image_from_bytes_map;
public:
RmlRenderInterface_RT64_impl(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
interface_ = interface;
@@ -201,7 +223,19 @@ public:
// Create the pipeline description
RT64::RenderGraphicsPipelineDesc pipeline_desc{};
pipeline_desc.renderTargetBlend[0] = RT64::RenderBlendDesc::AlphaBlend();
// Set up alpha blending for non-premultiplied alpha. RmlUi recommends using premultiplied alpha normally,
// but that would require preprocessing all input files, which would be difficult for user-provided content (such as mods).
// This blending setup produces similar results as premultipled alpha but for normal assets as it multiplies during blending and
// computes the output alpha value the same way that a premultipled alpha blender would.
pipeline_desc.renderTargetBlend[0] = RT64::RenderBlendDesc {
.blendEnabled = true,
.srcBlend = RT64::RenderBlend::SRC_ALPHA,
.dstBlend = RT64::RenderBlend::INV_SRC_ALPHA,
.blendOp = RT64::RenderBlendOperation::ADD,
.srcBlendAlpha = RT64::RenderBlend::ONE,
.dstBlendAlpha = RT64::RenderBlend::INV_SRC_ALPHA,
.blendOpAlpha = RT64::RenderBlendOperation::ADD,
};
pipeline_desc.renderTargetFormat[0] = SwapChainFormat; // TODO: Use whatever format the swap chain was created with.
pipeline_desc.renderTargetCount = 1;
pipeline_desc.cullMode = RT64::RenderCullMode::NONE;
@@ -236,7 +270,7 @@ public:
}
copy_command_queue_ = device->createCommandQueue(RT64::RenderCommandListType::COPY);
copy_command_list_ = device->createCommandList(RT64::RenderCommandListType::COPY);
copy_command_list_ = copy_command_queue_->createCommandList(RT64::RenderCommandListType::COPY);
copy_command_fence_ = device->createCommandFence();
}
@@ -393,11 +427,30 @@ public:
return true;
}
// TODO: This data copy can be avoided when RT64::TextureCache::loadTextureFromBytes's function is updated to only take a pointer and size as the input.
std::vector<uint8_t> data_copy(it->second.data(), it->second.data() + it->second.size());
RT64::Texture* texture = nullptr;
std::unique_ptr<RT64::RenderBuffer> texture_buffer;
ImageFromBytes& img = it->second;
copy_command_list_->begin();
RT64::Texture *texture = RT64::TextureCache::loadTextureFromBytes(device_, copy_command_list_.get(), data_copy, texture_buffer);
switch (img.type) {
case ImageType::RGBA32:
{
// Read the image header (two 32-bit values for width and height respectively).
uint32_t rowPitch = img.width * 4;
size_t byteCount = img.height * rowPitch;
texture = new RT64::Texture();
RT64::TextureCache::setRGBA32(texture, device_, copy_command_list_.get(), reinterpret_cast<const uint8_t*>(img.bytes.data()), byteCount, img.width, img.height, rowPitch, texture_buffer, nullptr);
}
break;
case ImageType::File:
{
// TODO: This data copy can be avoided when RT64::TextureCache::loadTextureFromBytes's function is updated to only take a pointer and size as the input.
std::vector<uint8_t> data_copy(img.bytes.data(), img.bytes.data() + img.bytes.size());
texture = RT64::TextureCache::loadTextureFromBytes(device_, copy_command_list_.get(), data_copy, texture_buffer);
}
break;
}
copy_command_list_->end();
copy_command_queue_->executeCommandLists(copy_command_list_.get(), copy_command_fence_.get());
copy_command_queue_->waitForCommandFence(copy_command_fence_.get());
@@ -585,6 +638,7 @@ public:
list->setGraphicsPipelineLayout(layout_.get());
list->setGraphicsDescriptorSet(sampler_set_.get(), 0);
list->setGraphicsDescriptorSet(screen_descriptor_set_.get(), 1);
list->setScissors(RT64::RenderRect{ 0, 0, window_width_, window_height_ });
RT64::RenderVertexBufferView vertex_view(screen_vertex_buffer_.get(), screen_vertex_buffer_size_);
list->setVertexBuffers(0, &vertex_view, 1, &vertex_slot_);
@@ -604,14 +658,21 @@ public:
list_ = nullptr;
}
void queue_image_from_bytes(const std::string &src, const std::vector<char> &bytes) {
image_from_bytes_queue.enqueue(ImageFromBytes(src, bytes));
void queue_image_from_bytes_file(const std::string &src, const std::vector<char> &bytes) {
// Width and height aren't used for file images, so set them to 0.
image_from_bytes_queue.enqueue(ImageFromBytes{ .type = ImageType::File, .width = 0, .height = 0, .name = src, .bytes = bytes });
}
void queue_image_from_bytes_rgba32(const std::string &src, const std::vector<char> &bytes, uint32_t width, uint32_t height) {
image_from_bytes_queue.enqueue(ImageFromBytes{ .type = ImageType::RGBA32, .width = width, .height = height, .name = src, .bytes = bytes });
}
void flush_image_from_bytes_queue() {
ImageFromBytes image_from_bytes;
while (image_from_bytes_queue.try_dequeue(image_from_bytes)) {
image_from_bytes_map.emplace(image_from_bytes.first, std::move(image_from_bytes.second));
// We can move the name into the map since the name in the actual entry is no longer needed.
// After that, move the entry itself into the map.
image_from_bytes_map.emplace(std::move(image_from_bytes.name), std::move(image_from_bytes));
}
}
};
@@ -647,8 +708,14 @@ void recompui::RmlRenderInterface_RT64::end(RT64::RenderCommandList* list, RT64:
impl->end(list, framebuffer);
}
void recompui::RmlRenderInterface_RT64::queue_image_from_bytes(const std::string &src, const std::vector<char> &bytes) {
void recompui::RmlRenderInterface_RT64::queue_image_from_bytes_file(const std::string &src, const std::vector<char> &bytes) {
assert(static_cast<bool>(impl));
impl->queue_image_from_bytes(src, bytes);
}
impl->queue_image_from_bytes_file(src, bytes);
}
void recompui::RmlRenderInterface_RT64::queue_image_from_bytes_rgba32(const std::string &src, const std::vector<char> &bytes, uint32_t width, uint32_t height) {
assert(static_cast<bool>(impl));
impl->queue_image_from_bytes_rgba32(src, bytes, width, height);
}
+3 -1
View File
@@ -2,6 +2,7 @@
#define __UI_RENDERER_H__
#include <memory>
#include "recomp_ui.h"
namespace RT64 {
struct RenderInterface;
@@ -29,7 +30,8 @@ namespace recompui {
void start(RT64::RenderCommandList* list, int image_width, int image_height);
void end(RT64::RenderCommandList* list, RT64::RenderFramebuffer* framebuffer);
void queue_image_from_bytes(const std::string &src, const std::vector<char> &bytes);
void queue_image_from_bytes_file(const std::string &src, const std::vector<char> &bytes);
void queue_image_from_bytes_rgba32(const std::string &src, const std::vector<char> &bytes, uint32_t width, uint32_t height);
};
} // namespace recompui
+252 -34
View File
@@ -4,6 +4,7 @@
#else
#include <SDL2/SDL_video.h>
#endif
#include <chrono>
#include "rt64_render_hooks.h"
@@ -19,9 +20,11 @@
#include "recomp_input.h"
#include "librecomp/game.hpp"
#include "banjo_config.h"
#include "banjo_support.h"
#include "ui_rml_hacks.hpp"
#include "ui_elements.h"
#include "ui_mod_menu.h"
#include "ui_mod_installer.h"
#include "ui_renderer.h"
bool can_focus(Rml::Element* element) {
@@ -150,7 +153,6 @@ Rml::Element* find_autofocus_element(Rml::Element* start) {
struct ContextDetails {
recompui::ContextId context;
Rml::ElementDocument* document;
bool takes_input;
};
class UIState {
@@ -209,8 +211,6 @@ public:
Rml::Debugger::Initialise(context);
{
const Rml::String directory = "assets/";
struct FontFace {
const char* filename;
bool fallback_face;
@@ -225,14 +225,17 @@ public:
};
for (const FontFace& face : font_faces) {
Rml::LoadFontFace(directory + face.filename, face.fallback_face);
auto font = banjo::get_asset_path(face.filename);
Rml::LoadFontFace(font.string(), face.fallback_face);
}
}
}
void load_documents() {
launcher_menu_controller->load_document(context);
config_menu_controller->load_document(context);
void create_menus() {
recompui::init_styling(banjo::get_asset_path("recomp.rcss"));
launcher_menu_controller->load_document();
config_menu_controller->load_document();
recompui::init_prompt_context();
}
void unload() {
@@ -264,7 +267,7 @@ public:
recompui::set_cursor_visible(mouse_is_active);
}
Rml::ElementDocument* current_document = top_input_document();
Rml::ElementDocument* current_document = top_mouse_document();
if (current_document == nullptr) {
return;
}
@@ -284,7 +287,7 @@ public:
}
void update_focus(bool mouse_moved, bool non_mouse_interacted) {
Rml::ElementDocument* current_document = top_input_document();
Rml::ElementDocument* current_document = top_mouse_document();
if (current_document == nullptr) {
return;
@@ -339,12 +342,10 @@ public:
recompui::message_box("Attemped to show the same context twice");
assert(false);
}
bool takes_input = context.takes_input();
Rml::ElementDocument* document = context.get_document();
shown_contexts.push_back(ContextDetails{
.context = context,
.document = document,
.takes_input = takes_input
.document = document
});
// auto& on_show = context.on_show;
@@ -356,6 +357,10 @@ public:
document->PullToFront();
document->Show();
recompui::Element* default_element = context.get_autofocus_element();
if (default_element) {
default_element->focus();
}
}
void hide_context(recompui::ContextId context) {
@@ -381,8 +386,12 @@ public:
return std::find_if(shown_contexts.begin(), shown_contexts.end(), [context](auto& c){ return c.context == context; }) != shown_contexts.end();
}
bool is_context_taking_input() {
return std::find_if(shown_contexts.begin(), shown_contexts.end(), [](auto& c){ return c.takes_input; }) != shown_contexts.end();
bool is_context_capturing_input() {
return std::find_if(shown_contexts.begin(), shown_contexts.end(), [](auto& c){ return c.context.captures_input(); }) != shown_contexts.end();
}
bool is_context_capturing_mouse() {
return std::find_if(shown_contexts.begin(), shown_contexts.end(), [](auto& c){ return c.context.captures_mouse(); }) != shown_contexts.end();
}
bool is_any_context_shown() {
@@ -392,7 +401,17 @@ public:
Rml::ElementDocument* top_input_document() {
// Iterate backwards and stop at the first context that takes input.
for (auto it = shown_contexts.rbegin(); it != shown_contexts.rend(); it++) {
if (it->takes_input) {
if (it->context.captures_input()) {
return it->document;
}
}
return nullptr;
}
Rml::ElementDocument* top_mouse_document() {
// Iterate backwards and stop at the first context that takes input.
for (auto it = shown_contexts.rbegin(); it != shown_contexts.rend(); it++) {
if (it->context.captures_mouse()) {
return it->document;
}
}
@@ -430,7 +449,7 @@ void init_hook(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
std::locale::global(std::locale::classic());
#endif
ui_state = std::make_unique<UIState>(window, interface, device);
ui_state->load_documents();
ui_state->create_menus();
}
moodycamel::ConcurrentQueue<SDL_Event> ui_event_queue{};
@@ -553,9 +572,28 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
bool config_was_open = recompui::is_context_shown(recompui::get_config_context_id()) || recompui::is_context_shown(recompui::get_config_sub_menu_context_id());
using clock = std::chrono::system_clock;
// TODO move these into a more appropriate place.
constexpr clock::duration start_repeat_delay = std::chrono::milliseconds{500};
constexpr clock::duration repeat_rate = std::chrono::milliseconds{50};
static clock::time_point next_repeat_time = {};
static int latest_controller_key_pressed = SDLK_UNKNOWN;
while (recompui::try_deque_event(cur_event)) {
bool context_taking_input = recompui::is_context_taking_input();
bool context_capturing_input = recompui::is_context_capturing_input();
bool context_capturing_mouse = recompui::is_context_capturing_mouse();
// Handle up button events even when input is disabled to avoid missing them during binding.
if (cur_event.type == SDL_EventType::SDL_CONTROLLERBUTTONUP) {
int sdl_key = cont_button_to_key(cur_event.cbutton);
if (sdl_key == latest_controller_key_pressed) {
latest_controller_key_pressed = SDLK_UNKNOWN;
}
}
if (!recomp::all_input_disabled()) {
bool is_mouse_input = false;
// Implement some additional behavior for specific events on top of what RmlUi normally does with them.
switch (cur_event.type) {
case SDL_EventType::SDL_MOUSEMOTION: {
@@ -580,12 +618,20 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
case SDL_EventType::SDL_MOUSEBUTTONDOWN:
mouse_moved = true;
mouse_clicked = true;
is_mouse_input = true;
break;
case SDL_EventType::SDL_MOUSEBUTTONUP:
case SDL_EventType::SDL_MOUSEWHEEL:
is_mouse_input = true;
break;
case SDL_EventType::SDL_CONTROLLERBUTTONDOWN: {
int rml_key = cont_button_to_key(cur_event.cbutton);
if (context_taking_input && rml_key) {
ui_state->context->ProcessKeyDown(RmlSDL::ConvertKey(rml_key), 0);
int sdl_key = cont_button_to_key(cur_event.cbutton);
if (context_capturing_input && sdl_key) {
ui_state->context->ProcessKeyDown(RmlSDL::ConvertKey(sdl_key), 0);
latest_controller_key_pressed = sdl_key;
next_repeat_time = clock::now() + start_repeat_delay;
}
non_mouse_interacted = true;
cont_interacted = true;
@@ -594,6 +640,11 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
case SDL_EventType::SDL_KEYDOWN:
non_mouse_interacted = true;
kb_interacted = true;
if (cur_event.key.keysym.scancode == SDL_Scancode::SDL_SCANCODE_F8) {
if (banjo::get_debug_mode_enabled()) {
Rml::Debugger::SetVisible(!Rml::Debugger::IsVisible());
}
}
break;
case SDL_EventType::SDL_USEREVENT:
if (cur_event.user.code == SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTY) {
@@ -616,9 +667,11 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
if (!*await_stick_return) {
*await_stick_return = true;
non_mouse_interacted = true;
int rml_key = cont_axis_to_key(cur_event.caxis, axis_value);
if (context_taking_input && rml_key) {
ui_state->context->ProcessKeyDown(RmlSDL::ConvertKey(rml_key), 0);
int sdl_key = cont_axis_to_key(cur_event.caxis, axis_value);
if (context_capturing_input && sdl_key) {
ui_state->context->ProcessKeyDown(RmlSDL::ConvertKey(sdl_key), 0);
latest_controller_key_pressed = sdl_key;
next_repeat_time = clock::now() + start_repeat_delay;
}
}
non_mouse_interacted = true;
@@ -626,12 +679,25 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
}
else if (*await_stick_return && fabsf(axis_value) < 0.15f) {
*await_stick_return = false;
// Stop pressing the current key if the axis that was released was the one triggering key presses.
int sdl_key = cont_axis_to_key(cur_event.caxis, axis_value);
if (sdl_key == latest_controller_key_pressed) {
latest_controller_key_pressed = SDLK_UNKNOWN;
}
}
break;
}
if (context_taking_input) {
RmlSDL::InputEventHandler(ui_state->context, cur_event);
// Send the event to RmlUi if this type of event is being captured.
if (is_mouse_input) {
if (context_capturing_mouse) {
RmlSDL::InputEventHandler(ui_state->context, cur_event);
}
}
else {
if (context_capturing_input) {
RmlSDL::InputEventHandler(ui_state->context, cur_event);
}
}
}
@@ -662,6 +728,15 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
}
} // end dequeue event loop
// Handle controller key repeats.
if (latest_controller_key_pressed != SDLK_UNKNOWN) {
clock::time_point now = clock::now();
if (now >= next_repeat_time) {
ui_state->context->ProcessKeyDown(RmlSDL::ConvertKey(latest_controller_key_pressed), 0);
next_repeat_time += repeat_rate;
}
}
if (cont_interacted || kb_interacted || mouse_clicked) {
recompui::set_cont_active(cont_interacted);
}
@@ -721,16 +796,28 @@ void recompui::message_box(const char* msg) {
}
void recompui::show_context(ContextId context, std::string_view param) {
std::lock_guard lock{ui_state_mutex};
ContextId prev_context = recompui::try_close_current_context();
{
std::lock_guard lock{ ui_state_mutex };
// TODO call the context's on_show callback with the param.
ui_state->show_context(context);
// TODO call the context's on_show callback with the param.
ui_state->show_context(context);
}
if (prev_context != ContextId::null()) {
prev_context.open();
}
}
void recompui::hide_context(ContextId context) {
std::lock_guard lock{ui_state_mutex};
ContextId prev_context = recompui::try_close_current_context();
{
std::lock_guard lock{ ui_state_mutex };
ui_state->hide_context(context);
ui_state->hide_context(context);
}
if (prev_context != ContextId::null()) {
prev_context.open();
}
}
void recompui::hide_all_contexts() {
@@ -751,14 +838,24 @@ bool recompui::is_context_shown(ContextId context) {
return ui_state->is_context_shown(context);
}
bool recompui::is_context_taking_input() {
bool recompui::is_context_capturing_input() {
std::lock_guard lock{ui_state_mutex};
if (!ui_state) {
return false;
}
return ui_state->is_context_taking_input();
return ui_state->is_context_capturing_input();
}
bool recompui::is_context_capturing_mouse() {
std::lock_guard lock{ui_state_mutex};
if (!ui_state) {
return false;
}
return ui_state->is_context_capturing_mouse();
}
bool recompui::is_any_context_shown() {
@@ -783,10 +880,131 @@ Rml::ElementDocument* recompui::create_empty_document() {
return ui_state->context->CreateDocument();
}
void recompui::queue_image_from_bytes(const std::string &src, const std::vector<char> &bytes) {
ui_state->render_interface.queue_image_from_bytes(src, bytes);
void recompui::queue_image_from_bytes_file(const std::string &src, const std::vector<char> &bytes) {
ui_state->render_interface.queue_image_from_bytes_file(src, bytes);
}
void recompui::queue_image_from_bytes_rgba32(const std::string &src, const std::vector<char> &bytes, uint32_t width, uint32_t height) {
ui_state->render_interface.queue_image_from_bytes_rgba32(src, bytes, width, height);
}
void recompui::release_image(const std::string &src) {
Rml::ReleaseTexture(src);
}
void recompui::drop_files(const std::list<std::filesystem::path> &file_list) {
// Prevent mod installation after the game has started.
if (ultramodern::is_game_started()) {
return;
}
recompui::open_notification("Installing Mods", "Please Wait");
// TODO: Needs a progress callback and a prompt for every mod that needs to be confirmed to be overwritten.
// TODO: Run this on a background thread and use the callbacks to advance the state instead of blocking.
ModInstaller::Result result;
ModInstaller::start_mod_installation(file_list, nullptr, result);
recompui::close_prompt();
if (!result.error_messages.empty()) {
std::string error_label = std::accumulate(result.error_messages.begin(), result.error_messages.end(), std::string{},
[](const std::string &lhs, const std::string &rhs)
{
return lhs.empty() ? rhs : lhs + '\n' + rhs;
});
recompui::open_info_prompt("Error Installing Mods", error_label, "OK", {}, recompui::ButtonVariant::Tertiary);
std::vector<std::string> dummy_error_messages{};
ModInstaller::cancel_mod_installation(result, dummy_error_messages);
return;
}
std::vector<ModInstaller::Confirmation> confirmations{};
for (const ModInstaller::Installation& pending_install : result.pending_installations) {
if (pending_install.needs_overwrite_confirmation) {
// Get the mod details for the current mod at this file path.
std::string old_mod_id = recomp::mods::get_mod_id_from_filename(pending_install.mod_file.filename());
std::optional<recomp::mods::ModDetails> old_mod_details = {};
if (!old_mod_id.empty()) {
old_mod_details = recomp::mods::get_details_for_mod(old_mod_id);
}
if (old_mod_details) {
confirmations.emplace_back(ModInstaller::Confirmation {
.old_display_name = old_mod_details->display_name,
.new_display_name = pending_install.display_name,
.old_mod_id = old_mod_details->mod_id,
.new_mod_id = pending_install.mod_id,
.old_version = old_mod_details->version,
.new_version = pending_install.mod_version
});
}
else {
confirmations.emplace_back(ModInstaller::Confirmation {
.old_display_name = "?",
.new_display_name = pending_install.display_name,
.old_mod_id = "",
.new_mod_id = pending_install.mod_id,
.old_version = recomp::Version{0, 0, 0, ""},
.new_version = pending_install.mod_version
});
}
}
}
if (confirmations.empty()) {
std::vector<std::string> error_messages{};
ModInstaller::finish_mod_installation(result, error_messages);
ContextId old_context = recompui::try_close_current_context();
recompui::update_mod_list();
if (old_context != ContextId::null()) {
old_context.open();
}
// TODO show errors
}
else {
std::string prompt_text = std::accumulate(confirmations.begin(), confirmations.end(), std::string{},
[](const std::string &cur_text, const ModInstaller::Confirmation &confirmation)
{
std::string new_text{};
if (confirmation.old_display_name == confirmation.new_display_name) {
new_text = confirmation.old_display_name + " (" + confirmation.old_version.to_string() + " -> " + confirmation.new_version.to_string() + ")";
}
else {
new_text =
confirmation.old_display_name + " (" + confirmation.old_version.to_string() + ") -> " +
confirmation.new_display_name + " (" + confirmation.new_version.to_string() + ")";
}
return cur_text.empty() ? new_text : cur_text + '\n' + new_text;
});
// open prompt where confirm finishes the mod installation with the overwritten files
recompui::open_choice_prompt("Overwrite Mods?",
prompt_text,
"Overwrite",
"Cancel",
[result]() {
std::vector<std::string> error_messages{};
recomp::mods::close_mods();
ModInstaller::finish_mod_installation(result, error_messages);
ContextId old_context = recompui::try_close_current_context();
recompui::update_mod_list();
if (old_context != ContextId::null()) {
old_context.open();
}
// TODO show errors
},
[result]() {
std::vector<std::string> error_messages{};
ModInstaller::cancel_mod_installation(result, error_messages);
// TODO show errors
},
recompui::ButtonVariant::Success,
recompui::ButtonVariant::Error,
true,
""
);
}
}
+20
View File
@@ -0,0 +1,20 @@
#include "ultramodern/ultramodern.hpp"
#include "ui_utils.h"
recompui::Color recompui::lerp_color(const recompui::Color& a, const recompui::Color& b, float factor) {
return recompui::Color{
static_cast<uint8_t>(std::lerp(float(a.r), float(b.r), factor)),
static_cast<uint8_t>(std::lerp(float(a.g), float(b.g), factor)),
static_cast<uint8_t>(std::lerp(float(a.b), float(b.b), factor)),
static_cast<uint8_t>(std::lerp(float(a.a), float(b.a), factor)),
};
}
recompui::Color recompui::get_pulse_color(uint32_t pulse_milliseconds) {
uint64_t millis = std::chrono::duration_cast<std::chrono::milliseconds>(ultramodern::time_since_start()).count();
uint32_t anim_offset = millis % pulse_milliseconds;
float factor = std::abs((2.0f * anim_offset / pulse_milliseconds) - 1.0f);
return lerp_color(Color{ 23, 214, 232, 255 }, Color{ 162, 239, 246, 255 }, factor);
}
+11
View File
@@ -0,0 +1,11 @@
#ifndef __UI_UTILS_H__
#define __UI_UTILS_H__
#include "elements/ui_types.h"
namespace recompui {
Color lerp_color(const Color& a, const Color& b, float factor);
Color get_pulse_color(uint32_t millisecond_period);
}
#endif