mirror of
https://github.com/BanjoRecomp/BanjoRecomp
synced 2026-05-23 14:41:41 -04:00
eda4bcfb66
* Add display name and thumbnail bytes to the game. * Update modern runtime for verification. * Update RecompFrontend to point to main.
816 lines
28 KiB
C++
816 lines
28 KiB
C++
#include <cstdio>
|
|
#include <cassert>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
#include <array>
|
|
#include <filesystem>
|
|
#include <numeric>
|
|
#include <stdexcept>
|
|
#include <cinttypes>
|
|
|
|
#include "nfd.h"
|
|
|
|
#include "ultramodern/ultra64.h"
|
|
#include "ultramodern/ultramodern.hpp"
|
|
#include "ultramodern/config.hpp"
|
|
#define SDL_MAIN_HANDLED
|
|
#ifdef _WIN32
|
|
#include "SDL.h"
|
|
#else
|
|
#include "SDL2/SDL.h"
|
|
#include "SDL2/SDL_syswm.h"
|
|
// Undefine x11 macros that get included by SDL_syswm.h.
|
|
#undef None
|
|
#undef Status
|
|
#undef LockMask
|
|
#undef ControlMask
|
|
#undef Success
|
|
#undef Always
|
|
#endif
|
|
|
|
#include "recompui/recompui.h"
|
|
#include "recompui/program_config.h"
|
|
#include "recompui/renderer.h"
|
|
#include "recompui/config.h"
|
|
#include "util/file.h"
|
|
#include "recompinput/input_events.h"
|
|
#include "recompinput/recompinput.h"
|
|
#include "recompinput/profiles.h"
|
|
#include "banjo_config.h"
|
|
#include "banjo_sound.h"
|
|
#include "banjo_support.h"
|
|
#include "banjo_game.h"
|
|
#include "banjo_launcher.h"
|
|
#include "recomp_data.h"
|
|
#include "ovl_patches.hpp"
|
|
#include "theme.h"
|
|
#include "librecomp/game.hpp"
|
|
#include "librecomp/mods.hpp"
|
|
#include "librecomp/helpers.hpp"
|
|
|
|
#include "../../patches/graphics.h"
|
|
#include "../../patches/input.h"
|
|
#include "../../patches/sound.h"
|
|
#include "../../patches/misc_funcs.h"
|
|
|
|
#ifdef _WIN32
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#include <Windows.h>
|
|
#include <timeapi.h>
|
|
#include "SDL_syswm.h"
|
|
#define APP_ICON_B 1
|
|
#define APP_ICON_K 2
|
|
#endif
|
|
|
|
#include "../../lib/rt64/src/contrib/stb/stb_image.h"
|
|
|
|
const std::string version_string = "1.0.0-rc1";
|
|
|
|
template<typename... Ts>
|
|
void exit_error(const char* str, Ts ...args) {
|
|
// TODO pop up an error
|
|
((void)fprintf(stderr, str, args), ...);
|
|
assert(false);
|
|
|
|
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
|
}
|
|
|
|
ultramodern::gfx_callbacks_t::gfx_data_t create_gfx() {
|
|
SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitorv2");
|
|
SDL_SetHint(SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS, "0");
|
|
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
|
|
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
|
|
SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
|
|
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
|
|
|
|
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC) > 0) {
|
|
exit_error("Failed to initialize SDL2: %s\n", SDL_GetError());
|
|
}
|
|
|
|
fprintf(stdout, "SDL Video Driver: %s\n", SDL_GetCurrentVideoDriver());
|
|
|
|
return {};
|
|
}
|
|
|
|
ultramodern::input::connected_device_info_t get_connected_device_info(int controller_num) {
|
|
if (recompinput::players::is_single_player_mode() || recompinput::players::get_player_is_assigned(controller_num)) {
|
|
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{
|
|
.connected_device = ultramodern::input::Device::None,
|
|
.connected_pak = ultramodern::input::Pak::None,
|
|
};
|
|
}
|
|
|
|
#include "icon_bytes.h"
|
|
|
|
#if defined(__gnu_linux__)
|
|
bool SetImageAsIcon(const char* filename, SDL_Window* window)
|
|
{
|
|
// Read data
|
|
int width, height, bytesPerPixel;
|
|
void* data = stbi_load_from_memory(reinterpret_cast<const uint8_t*>(icon_bytes), sizeof(icon_bytes), &width, &height, &bytesPerPixel, 4);
|
|
|
|
// Calculate pitch
|
|
int pitch;
|
|
pitch = width * 4;
|
|
pitch = (pitch + 3) & ~3;
|
|
|
|
// Setup relevance bitmask
|
|
int Rmask, Gmask, Bmask, Amask;
|
|
|
|
#if SDL_BYTEORDER == SDL_LIL_ENDIAN
|
|
Rmask = 0x000000FF;
|
|
Gmask = 0x0000FF00;
|
|
Bmask = 0x00FF0000;
|
|
Amask = 0xFF000000;
|
|
#else
|
|
Rmask = 0xFF000000;
|
|
Gmask = 0x00FF0000;
|
|
Bmask = 0x0000FF00;
|
|
Amask = 0x000000FF;
|
|
#endif
|
|
|
|
SDL_Surface* surface = nullptr;
|
|
if (data != nullptr) {
|
|
surface = SDL_CreateRGBSurfaceFrom(data, width, height, 32, pitch, Rmask, Gmask,
|
|
Bmask, Amask);
|
|
}
|
|
|
|
if (surface == nullptr) {
|
|
if (data != nullptr) {
|
|
stbi_image_free(data);
|
|
}
|
|
return false;
|
|
} else {
|
|
SDL_SetWindowIcon(window,surface);
|
|
SDL_FreeSurface(surface);
|
|
stbi_image_free(data);
|
|
return true;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
SDL_Window* window;
|
|
|
|
ultramodern::renderer::WindowHandle create_window(ultramodern::gfx_callbacks_t::gfx_data_t) {
|
|
uint32_t flags = SDL_WINDOW_RESIZABLE;
|
|
|
|
#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, 900, flags);
|
|
|
|
if (window == nullptr) {
|
|
exit_error("Failed to create window: %s\n", SDL_GetError());
|
|
}
|
|
|
|
SDL_SysWMinfo wmInfo;
|
|
SDL_VERSION(&wmInfo.version);
|
|
SDL_GetWindowWMInfo(window, &wmInfo);
|
|
|
|
#if defined(_WIN32)
|
|
// There's a 50/50 chance to choose the icon where the smallest variant is either Banjo or Kazooie alone.
|
|
bool choose_kazooie_icon = (rand() % 2 == 0);
|
|
HICON new_icon = LoadIcon(GetModuleHandle(NULL), choose_kazooie_icon ? MAKEINTRESOURCE(APP_ICON_K) : MAKEINTRESOURCE(APP_ICON_B));
|
|
SendMessage(wmInfo.info.win.window, WM_SETICON, ICON_SMALL2, (LPARAM)(new_icon));
|
|
#elif defined(__linux__)
|
|
SetImageAsIcon("icons/app.png", window);
|
|
#endif
|
|
|
|
#if defined(_WIN32)
|
|
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
|
|
}
|
|
|
|
void update_gfx(void*) {
|
|
recompinput::handle_events();
|
|
}
|
|
|
|
static SDL_AudioCVT audio_convert;
|
|
static SDL_AudioDeviceID audio_device = 0;
|
|
|
|
// Samples per channel per second.
|
|
static uint32_t sample_rate = 48000;
|
|
static uint32_t output_sample_rate = 48000;
|
|
// Channel count.
|
|
constexpr uint32_t input_channels = 2;
|
|
static uint32_t output_channels = 2;
|
|
|
|
// Terminology: a frame is a collection of samples for each channel. e.g. 2 input samples is one input frame. This is unrelated to graphical frames.
|
|
|
|
// Number of frames to duplicate for fixing interpolation at the start and end of a chunk.
|
|
constexpr uint32_t duplicated_input_frames = 4;
|
|
// The number of output frames to skip for playback (to avoid playing duplicate inputs twice).
|
|
static uint32_t discarded_output_frames;
|
|
|
|
constexpr uint32_t bytes_per_frame = input_channels * sizeof(float);
|
|
|
|
void queue_samples(int16_t* audio_data, size_t sample_count) {
|
|
// Buffer for holding the output of swapping the audio channels. This is reused across
|
|
// calls to reduce runtime allocations.
|
|
static std::vector<float> swap_buffer;
|
|
static std::array<float, duplicated_input_frames * input_channels> duplicated_sample_buffer;
|
|
|
|
// Make sure the swap buffer is large enough to hold the audio data, including any extra space needed for resampling.
|
|
size_t resampled_sample_count = sample_count + duplicated_input_frames * input_channels;
|
|
size_t max_sample_count = std::max(resampled_sample_count, resampled_sample_count * audio_convert.len_mult);
|
|
if (max_sample_count > swap_buffer.size()) {
|
|
swap_buffer.resize(max_sample_count);
|
|
}
|
|
|
|
// Copy the duplicated frames from last chunk into this chunk
|
|
for (size_t i = 0; i < duplicated_input_frames * input_channels; i++) {
|
|
swap_buffer[i] = duplicated_sample_buffer[i];
|
|
}
|
|
|
|
// Convert the audio from 16-bit values to floats and swap the audio channels into the
|
|
// swap buffer to correct for the address xor caused by endianness handling.
|
|
float cur_main_volume = static_cast<float>(recompui::config::sound::get_main_volume()) / 100.0f; // Get the current main volume, normalized to 0.0-1.0.
|
|
for (size_t i = 0; i < sample_count; i += input_channels) {
|
|
swap_buffer[i + 0 + duplicated_input_frames * input_channels] = audio_data[i + 1] * (0.5f / 32768.0f) * cur_main_volume;
|
|
swap_buffer[i + 1 + duplicated_input_frames * input_channels] = audio_data[i + 0] * (0.5f / 32768.0f) * cur_main_volume;
|
|
}
|
|
|
|
// TODO handle cases where a chunk is smaller than the duplicated frame count.
|
|
assert(sample_count > duplicated_input_frames * input_channels);
|
|
|
|
// Copy the last converted samples into the duplicated sample buffer to reuse in resampling the next queued chunk.
|
|
for (size_t i = 0; i < duplicated_input_frames * input_channels; i++) {
|
|
duplicated_sample_buffer[i] = swap_buffer[i + sample_count];
|
|
}
|
|
|
|
audio_convert.buf = reinterpret_cast<Uint8*>(swap_buffer.data());
|
|
audio_convert.len = (sample_count + duplicated_input_frames * input_channels) * sizeof(swap_buffer[0]);
|
|
|
|
int ret = SDL_ConvertAudio(&audio_convert);
|
|
|
|
if (ret < 0) {
|
|
printf("Error using SDL audio converter: %s\n", SDL_GetError());
|
|
throw std::runtime_error("Error using SDL audio converter");
|
|
}
|
|
|
|
uint64_t cur_queued_microseconds = uint64_t(SDL_GetQueuedAudioSize(audio_device)) / bytes_per_frame * 1000000 / sample_rate;
|
|
uint32_t num_bytes_to_queue = audio_convert.len_cvt - output_channels * discarded_output_frames * sizeof(swap_buffer[0]);
|
|
float* samples_to_queue = swap_buffer.data() + output_channels * discarded_output_frames / 2;
|
|
|
|
// Prevent audio latency from building up by skipping samples in incoming audio when too many samples are already queued.
|
|
// Skip samples based on how many microseconds of samples are queued already.
|
|
uint32_t skip_factor = cur_queued_microseconds / 100000;
|
|
if (skip_factor != 0) {
|
|
uint32_t skip_ratio = 1 << skip_factor;
|
|
num_bytes_to_queue /= skip_ratio;
|
|
for (size_t i = 0; i < num_bytes_to_queue / (output_channels * sizeof(swap_buffer[0])); i++) {
|
|
samples_to_queue[2 * i + 0] = samples_to_queue[2 * skip_ratio * i + 0];
|
|
samples_to_queue[2 * i + 1] = samples_to_queue[2 * skip_ratio * i + 1];
|
|
}
|
|
}
|
|
|
|
// Queue the swapped audio data.
|
|
// Offset the data start by only half the discarded frame count as the other half of the discarded frames are at the end of the buffer.
|
|
SDL_QueueAudio(audio_device, samples_to_queue, num_bytes_to_queue);
|
|
}
|
|
|
|
size_t get_frames_remaining() {
|
|
constexpr float buffer_offset_frames = 1.0f;
|
|
// Get the number of remaining buffered audio bytes.
|
|
uint64_t buffered_byte_count = SDL_GetQueuedAudioSize(audio_device);
|
|
|
|
// Scale the byte count based on the ratio of sample rates and channel counts.
|
|
buffered_byte_count = buffered_byte_count * 2 * sample_rate / output_sample_rate / output_channels;
|
|
|
|
// Adjust the reported count to be some number of refreshes in the future, which helps ensure that
|
|
// there are enough samples even if the audio thread experiences a small amount of lag. This prevents
|
|
// audio popping on games that use the buffered audio byte count to determine how many samples
|
|
// to generate.
|
|
uint32_t frames_per_vi = (sample_rate / 60);
|
|
if (buffered_byte_count > (buffer_offset_frames * bytes_per_frame * frames_per_vi)) {
|
|
buffered_byte_count -= (buffer_offset_frames * bytes_per_frame * frames_per_vi);
|
|
}
|
|
else {
|
|
buffered_byte_count = 0;
|
|
}
|
|
// Convert from byte count to sample count.
|
|
return static_cast<uint32_t>(buffered_byte_count / bytes_per_frame);
|
|
}
|
|
|
|
void update_audio_converter() {
|
|
int ret = SDL_BuildAudioCVT(&audio_convert, AUDIO_F32, input_channels, sample_rate, AUDIO_F32, output_channels, output_sample_rate);
|
|
|
|
if (ret < 0) {
|
|
printf("Error creating SDL audio converter: %s\n", SDL_GetError());
|
|
throw std::runtime_error("Error creating SDL audio converter");
|
|
}
|
|
|
|
// Calculate the number of samples to discard based on the sample rate ratio and the duplicate frame count.
|
|
discarded_output_frames = duplicated_input_frames * output_sample_rate / sample_rate;
|
|
}
|
|
|
|
void set_frequency(uint32_t freq) {
|
|
sample_rate = freq;
|
|
|
|
update_audio_converter();
|
|
}
|
|
|
|
void reset_audio(uint32_t output_freq) {
|
|
SDL_AudioSpec spec_desired{
|
|
.freq = (int)output_freq,
|
|
.format = AUDIO_F32,
|
|
.channels = (Uint8)output_channels,
|
|
.silence = 0, // calculated
|
|
.samples = 0x100, // Fairly small sample count to reduce the latency of internal buffering
|
|
.padding = 0, // unused
|
|
.size = 0, // calculated
|
|
.callback = nullptr,
|
|
.userdata = nullptr
|
|
};
|
|
|
|
|
|
audio_device = SDL_OpenAudioDevice(nullptr, false, &spec_desired, nullptr, 0);
|
|
if (audio_device == 0) {
|
|
exit_error("SDL error opening audio device: %s\n", SDL_GetError());
|
|
}
|
|
SDL_PauseAudioDevice(audio_device, 0);
|
|
|
|
output_sample_rate = output_freq;
|
|
update_audio_converter();
|
|
}
|
|
|
|
extern RspUcodeFunc n_aspMain;
|
|
|
|
RspUcodeFunc* get_rsp_microcode(const OSTask* task) {
|
|
switch (task->t.type) {
|
|
case M_AUDTASK:
|
|
return n_aspMain;
|
|
|
|
default:
|
|
fprintf(stderr, "Unknown task: %" PRIu32 "\n", task->t.type);
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
extern "C" void recomp_entrypoint(uint8_t * rdram, recomp_context * ctx);
|
|
gpr get_entrypoint_address();
|
|
|
|
// array of supported GameEntry objects
|
|
std::vector<recomp::GameEntry> supported_games = {
|
|
{
|
|
.rom_hash = 0x1B67585D56E07F8CULL,
|
|
.internal_name = "Banjo-Kazooie",
|
|
.display_name = "Banjo-Kazooie",
|
|
.game_id = u8"bk.n64.us.1.0",
|
|
.mod_game_id = "bk",
|
|
// Eep16k instead of Eep4k to have room for extra save file data.
|
|
.save_type = recomp::SaveType::Eep16k,
|
|
.thumbnail_bytes = std::span<const char>(icon_bytes),
|
|
.is_enabled = false,
|
|
.decompression_routine = banjo::decompress_bk,
|
|
.has_compressed_code = true,
|
|
.entrypoint_address = get_entrypoint_address(),
|
|
.entrypoint = recomp_entrypoint,
|
|
.on_init_callback = banjo::bk_on_init,
|
|
},
|
|
};
|
|
|
|
// TODO: move somewhere else
|
|
namespace banjo {
|
|
std::string get_game_thread_name(const OSThread* t) {
|
|
std::string name = "[Game] ";
|
|
|
|
switch (t->id) {
|
|
case 0:
|
|
switch (t->priority) {
|
|
case 150:
|
|
name += "PIMGR";
|
|
break;
|
|
|
|
case 80:
|
|
name += "VIMGR";
|
|
break;
|
|
|
|
default:
|
|
name += std::to_string(t->id);
|
|
break;
|
|
}
|
|
break;
|
|
case 1:
|
|
name += "INIT";
|
|
break;
|
|
case 2:
|
|
name += "DEFRAG";
|
|
break;
|
|
case 4:
|
|
name += "AUDIO";
|
|
break;
|
|
case 5:
|
|
name += "RESET";
|
|
break;
|
|
case 6:
|
|
name += "MAIN";
|
|
break;
|
|
case 7:
|
|
name += "CONT";
|
|
break;
|
|
case 8:
|
|
name += "RUMBLE";
|
|
break;
|
|
default:
|
|
name += std::to_string(t->id);
|
|
break;
|
|
}
|
|
|
|
return name;
|
|
}
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
|
|
struct PreloadContext {
|
|
HANDLE handle;
|
|
HANDLE mapping_handle;
|
|
SIZE_T size;
|
|
PVOID view;
|
|
};
|
|
|
|
bool preload_executable(PreloadContext& context) {
|
|
wchar_t module_name[MAX_PATH];
|
|
GetModuleFileNameW(NULL, module_name, MAX_PATH);
|
|
|
|
context.handle = CreateFileW(module_name, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
|
if (context.handle == INVALID_HANDLE_VALUE) {
|
|
fprintf(stderr, "Failed to load executable into memory!");
|
|
context = {};
|
|
return false;
|
|
}
|
|
|
|
LARGE_INTEGER module_size;
|
|
if (!GetFileSizeEx(context.handle, &module_size)) {
|
|
fprintf(stderr, "Failed to get size of executable!");
|
|
CloseHandle(context.handle);
|
|
context = {};
|
|
return false;
|
|
}
|
|
|
|
context.size = module_size.QuadPart;
|
|
|
|
context.mapping_handle = CreateFileMappingW(context.handle, nullptr, PAGE_READONLY, 0, 0, nullptr);
|
|
if (context.mapping_handle == nullptr) {
|
|
fprintf(stderr, "Failed to create file mapping of executable!");
|
|
CloseHandle(context.handle);
|
|
context = {};
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
context.view = MapViewOfFile(context.mapping_handle, FILE_MAP_READ, 0, 0, 0);
|
|
if (context.view == nullptr) {
|
|
fprintf(stderr, "Failed to map view of of executable!");
|
|
CloseHandle(context.mapping_handle);
|
|
CloseHandle(context.handle);
|
|
context = {};
|
|
return false;
|
|
}
|
|
|
|
DWORD pid = GetCurrentProcessId();
|
|
HANDLE process_handle = OpenProcess(PROCESS_SET_QUOTA | PROCESS_QUERY_INFORMATION, FALSE, pid);
|
|
if (process_handle == nullptr) {
|
|
fprintf(stderr, "Failed to open own process!");
|
|
CloseHandle(context.mapping_handle);
|
|
CloseHandle(context.handle);
|
|
context = {};
|
|
return false;
|
|
}
|
|
|
|
SIZE_T minimum_set_size, maximum_set_size;
|
|
if (!GetProcessWorkingSetSize(process_handle, &minimum_set_size, &maximum_set_size)) {
|
|
fprintf(stderr, "Failed to get working set size!");
|
|
CloseHandle(context.mapping_handle);
|
|
CloseHandle(context.handle);
|
|
context = {};
|
|
return false;
|
|
}
|
|
|
|
if (!SetProcessWorkingSetSize(process_handle, minimum_set_size + context.size, maximum_set_size + context.size)) {
|
|
fprintf(stderr, "Failed to set working set size!");
|
|
CloseHandle(context.mapping_handle);
|
|
CloseHandle(context.handle);
|
|
context = {};
|
|
return false;
|
|
}
|
|
|
|
if (VirtualLock(context.view, context.size) == 0) {
|
|
fprintf(stderr, "Failed to lock view of executable! (Error: %08lx)\n", GetLastError());
|
|
CloseHandle(context.mapping_handle);
|
|
CloseHandle(context.handle);
|
|
context = {};
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void release_preload(PreloadContext& context) {
|
|
VirtualUnlock(context.view, context.size);
|
|
CloseHandle(context.mapping_handle);
|
|
CloseHandle(context.handle);
|
|
context = {};
|
|
}
|
|
|
|
#elif defined(__linux__) || defined(APPLE)
|
|
|
|
struct PreloadContext {
|
|
|
|
};
|
|
|
|
bool preload_executable(PreloadContext& context) {
|
|
// Preloading isn't implemented on Linux and MacOS, but it's also unnecessary there, as the OS already preloads the executable.
|
|
// Therefore, we can just consider the executable to be preloaded.
|
|
return true;
|
|
}
|
|
|
|
void release_preload(PreloadContext& context) {
|
|
}
|
|
|
|
#else
|
|
|
|
struct PreloadContext {};
|
|
|
|
bool preload_executable(PreloadContext& context) {
|
|
return false;
|
|
}
|
|
|
|
void release_preload(PreloadContext& context) {
|
|
}
|
|
|
|
#endif
|
|
|
|
void enable_texture_pack(recomp::mods::ModContext& context, const recomp::mods::ModHandle& mod) {
|
|
recompui::renderer::enable_texture_pack(context, mod);
|
|
}
|
|
|
|
void disable_texture_pack(recomp::mods::ModContext&, const recomp::mods::ModHandle& mod) {
|
|
recompui::renderer::disable_texture_pack(mod);
|
|
}
|
|
|
|
void reorder_texture_pack(recomp::mods::ModContext&) {
|
|
recompui::renderer::trigger_texture_pack_update();
|
|
}
|
|
|
|
void on_launcher_init(recompui::LauncherMenu *menu) {
|
|
auto game_options_menu = menu->init_game_options_menu(
|
|
supported_games[0].game_id,
|
|
supported_games[0].mod_game_id,
|
|
supported_games[0].display_name,
|
|
supported_games[0].thumbnail_bytes,
|
|
recompui::GameOptionsMenuLayout::Center
|
|
);
|
|
|
|
game_options_menu->add_default_options();
|
|
|
|
for (auto option : game_options_menu->get_options()) {
|
|
option->set_justify_content(recompui::JustifyContent::FlexEnd);
|
|
option->set_border_radius(0);
|
|
|
|
std::vector<recompui::Style *> hover_focus = {&option->hover_style, &option->focus_style};
|
|
for (auto style : hover_focus) {
|
|
style->set_background_color(recompui::theme::color::Transparent);
|
|
}
|
|
}
|
|
|
|
recompui::Element *menu_container = menu->get_menu_container();
|
|
menu_container->set_width(1440);
|
|
menu_container->unset_left();
|
|
menu_container->set_top(banjo::launcher_options_top_offset);
|
|
menu_container->set_bottom(-banjo::launcher_options_top_offset);
|
|
menu_container->set_right(50, recompui::Unit::Percent);
|
|
menu_container->set_translate_2D(50.0f, 0.0f, recompui::Unit::Percent);
|
|
|
|
game_options_menu->unset_left();
|
|
game_options_menu->set_bottom(50.0f, recompui::Unit::Percent);
|
|
game_options_menu->set_translate_2D(0.0f, 50.0f, recompui::Unit::Percent);
|
|
game_options_menu->set_right(banjo::launcher_options_right_position_start);
|
|
|
|
menu->remove_default_title();
|
|
|
|
banjo::launcher_animation_setup(menu);
|
|
}
|
|
|
|
#define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name)
|
|
|
|
int main(int argc, char** argv) {
|
|
(void)argc;
|
|
(void)argv;
|
|
recomp::Version project_version{};
|
|
if (!recomp::Version::from_string(version_string, project_version)) {
|
|
ultramodern::error_handling::message_box(("Invalid version string: " + version_string).c_str());
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
// Map this executable into memory and lock it, which should keep it in physical memory. This ensures
|
|
// that there are no stutters from the OS having to load new pages of the executable whenever a new code page is run.
|
|
PreloadContext preload_context;
|
|
bool preloaded = preload_executable(preload_context);
|
|
|
|
if (!preloaded) {
|
|
fprintf(stderr, "Failed to preload executable!\n");
|
|
}
|
|
|
|
// Initialize random seed for icon easter egg.
|
|
std::srand(std::time(nullptr));
|
|
|
|
#ifdef _WIN32
|
|
// Set up high resolution timing period.
|
|
timeBeginPeriod(1);
|
|
|
|
// Process arguments.
|
|
for (int i = 1; i < argc; i++)
|
|
{
|
|
if (strcmp(argv[i], "--show-console") == 0)
|
|
{
|
|
if (GetConsoleWindow() == nullptr)
|
|
{
|
|
AllocConsole();
|
|
freopen("CONIN$", "r", stdin);
|
|
freopen("CONOUT$", "w", stderr);
|
|
freopen("CONOUT$", "w", stdout);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Set up console output to accept UTF-8 on windows
|
|
SetConsoleOutputCP(CP_UTF8);
|
|
|
|
// Change to a font that supports Japanese characters
|
|
CONSOLE_FONT_INFOEX cfi;
|
|
cfi.cbSize = sizeof cfi;
|
|
cfi.nFont = 0;
|
|
cfi.dwFontSize.X = 0;
|
|
cfi.dwFontSize.Y = 16;
|
|
cfi.FontFamily = FF_DONTCARE;
|
|
cfi.FontWeight = FW_NORMAL;
|
|
wcscpy_s(cfi.FaceName, L"NSimSun");
|
|
SetCurrentConsoleFontEx(GetStdHandle(STD_OUTPUT_HANDLE), FALSE, &cfi);
|
|
#endif
|
|
|
|
#ifdef _WIN32
|
|
// Force wasapi on Windows, as there seems to be some issue with sample queueing with directsound currently.
|
|
SDL_setenv("SDL_AUDIODRIVER", "wasapi", true);
|
|
#endif
|
|
|
|
#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 native file dialogs.
|
|
NFD_Init();
|
|
|
|
// Initialize SDL audio and set the output frequency.
|
|
SDL_InitSubSystem(SDL_INIT_AUDIO);
|
|
reset_audio(48000);
|
|
|
|
// Source controller mappings file
|
|
std::u8string controller_db_path = (recompui::file::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());
|
|
}
|
|
|
|
recompui::programconfig::set_program_name(banjo::program_name);
|
|
recompui::programconfig::set_program_id(banjo::program_id);
|
|
recompui::register_primary_font("Suplexmentary Comic NC.ttf", "Suplexmentary Comic NC");
|
|
recomp::register_config_path(recompui::file::get_app_folder_path());
|
|
|
|
// Register supported games and patches
|
|
for (const auto& game : supported_games) {
|
|
recomp::register_game(game);
|
|
}
|
|
|
|
REGISTER_FUNC(recomp_get_window_resolution);
|
|
REGISTER_FUNC(recomp_get_target_aspect_ratio);
|
|
REGISTER_FUNC(recomp_get_target_framerate);
|
|
REGISTER_FUNC(recomp_get_cutscene_aspect_ratio);
|
|
REGISTER_FUNC(recomp_get_analog_cam_enabled);
|
|
REGISTER_FUNC(recomp_get_right_analog_inputs);
|
|
REGISTER_FUNC(recomp_get_bgm_volume);
|
|
// REGISTER_FUNC(recomp_get_gyro_deltas);
|
|
// REGISTER_FUNC(recomp_get_mouse_deltas);
|
|
REGISTER_FUNC(recomp_get_inverted_axes);
|
|
REGISTER_FUNC(recomp_get_analog_inverted_axes);
|
|
recompui::register_ui_exports();
|
|
recomputil::register_data_api_exports();
|
|
recomptheme::set_custom_theme();
|
|
|
|
banjo::register_bk_overlays();
|
|
banjo::register_bk_patches();
|
|
|
|
// Register extensions for two types: Props and ActorMarkers.
|
|
recomputil::init_extended_object_data(2);
|
|
|
|
recompinput::players::set_single_player_mode(true);
|
|
|
|
banjo::init_config();
|
|
|
|
recompui::register_launcher_init_callback(on_launcher_init);
|
|
recompui::register_launcher_update_callback(banjo::launcher_animation_update);
|
|
|
|
recomp::rsp::callbacks_t rsp_callbacks{
|
|
.get_rsp_microcode = get_rsp_microcode,
|
|
};
|
|
|
|
ultramodern::renderer::callbacks_t renderer_callbacks{
|
|
.create_render_context = [](uint8_t* rdram, ultramodern::renderer::WindowHandle window_handle, bool developer_mode) {
|
|
auto presentation_mode = ultramodern::renderer::PresentationMode::PresentEarly;
|
|
return recompui::renderer::create_render_context(rdram, window_handle, presentation_mode, developer_mode);
|
|
},
|
|
};
|
|
|
|
ultramodern::gfx_callbacks_t gfx_callbacks{
|
|
.create_gfx = create_gfx,
|
|
.create_window = create_window,
|
|
.update_gfx = update_gfx,
|
|
};
|
|
|
|
ultramodern::audio_callbacks_t audio_callbacks{
|
|
.queue_samples = queue_samples,
|
|
.get_frames_remaining = get_frames_remaining,
|
|
.set_frequency = set_frequency,
|
|
};
|
|
|
|
ultramodern::input::callbacks_t input_callbacks{
|
|
.poll_input = recompinput::poll_inputs,
|
|
.get_input = recompinput::profiles::get_n64_input,
|
|
.set_rumble = recompinput::set_rumble,
|
|
.get_connected_device_info = get_connected_device_info,
|
|
};
|
|
|
|
ultramodern::events::callbacks_t thread_callbacks{
|
|
.vi_callback = recompinput::update_rumble,
|
|
.gfx_init_callback = nullptr,
|
|
};
|
|
|
|
ultramodern::error_handling::callbacks_t error_handling_callbacks{
|
|
.message_box = recompui::message_box,
|
|
};
|
|
|
|
ultramodern::threads::callbacks_t threads_callbacks{
|
|
.get_game_thread_name = banjo::get_game_thread_name,
|
|
};
|
|
|
|
// Register the texture pack content type with rt64.json as its content file.
|
|
recomp::mods::ModContentType texture_pack_content_type{
|
|
.content_filename = "rt64.json",
|
|
.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);
|
|
|
|
// Register the .rtz texture pack file format with the previous content type as its only allowed content type.
|
|
recomp::mods::register_mod_container_type("rtz", std::vector{ texture_pack_content_type_id }, false);
|
|
|
|
recomp::start(
|
|
project_version,
|
|
{},
|
|
rsp_callbacks,
|
|
renderer_callbacks,
|
|
audio_callbacks,
|
|
input_callbacks,
|
|
gfx_callbacks,
|
|
thread_callbacks,
|
|
error_handling_callbacks,
|
|
threads_callbacks
|
|
);
|
|
|
|
NFD_Quit();
|
|
|
|
if (preloaded) {
|
|
release_preload(preload_context);
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
// End high resolution timing period.
|
|
timeEndPeriod(1);
|
|
#endif
|
|
|
|
return EXIT_SUCCESS;
|
|
}
|