mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-29 07:48:11 -04:00
Merge with origin/main
This commit is contained in:
@@ -41,6 +41,15 @@ bool isActionBound(ActionBinds action, u32 port) {
|
||||
return getActionBindButton(action, port) != PAD_NATIVE_BUTTON_INVALID;
|
||||
}
|
||||
|
||||
bool isActionBoundAnyPort(ActionBinds action) {
|
||||
for (u32 port = 0; port < PAD_CHANMAX; ++port) {
|
||||
if (isActionBound(action, port)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void updateActionBindings() {
|
||||
for (u32 port = 0; port < PAD_CHANMAX; ++port) {
|
||||
// Move the current press to the previous frame
|
||||
|
||||
@@ -31,6 +31,7 @@ using ActionBindsMap = std::unordered_map<ActionBinds, ActionBindData>;
|
||||
ActionBindsMap& getActionBinds();
|
||||
|
||||
bool isActionBound(ActionBinds action, u32 port);
|
||||
bool isActionBoundAnyPort(ActionBinds action);
|
||||
|
||||
void updateActionBindings();
|
||||
|
||||
|
||||
@@ -23,9 +23,20 @@ bool LoadDolAsset(void* dst, std::initializer_list<OffsetVersion> virtualAddress
|
||||
*/
|
||||
bool LoadRelAsset(void* dst, const char* dvdPath, std::initializer_list<OffsetVersion> offset, s32 size);
|
||||
|
||||
template <typename T, size_t N>
|
||||
bool LoadRelAsset(T (&dst)[N], const char* dvdPath, std::initializer_list<OffsetVersion> offset) {
|
||||
return LoadRelAsset(static_cast<void*>(dst), dvdPath, offset, static_cast<s32>(sizeof(dst)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load bytes from a REL inside RELS.arc
|
||||
*/
|
||||
bool LoadArchivedRelAsset(void* dst, u32 memType, const char* relFileName, std::initializer_list<OffsetVersion> offset, s32 size);
|
||||
|
||||
template <typename T, size_t N>
|
||||
bool LoadArchivedRelAsset(T (&dst)[N], u32 memType, const char* relFileName,
|
||||
std::initializer_list<OffsetVersion> offset) {
|
||||
return LoadArchivedRelAsset(static_cast<void*>(dst), memType, relFileName, offset, static_cast<s32>(sizeof(dst)));
|
||||
}
|
||||
|
||||
} // namespace dusk
|
||||
|
||||
@@ -147,6 +147,10 @@ void begin_frame(FrameInterpMode mode, bool is_sim_frame, float step) {
|
||||
g_enabled = mode != FrameInterpMode::Off;
|
||||
g_is_sim_frame = is_sim_frame;
|
||||
g_step = std::clamp(step, 0.0f, 1.0f);
|
||||
if (!g_enabled) {
|
||||
g_interpolating = false;
|
||||
clear_replacements();
|
||||
}
|
||||
}
|
||||
|
||||
bool is_enabled() {
|
||||
|
||||
+86
-55
@@ -1,104 +1,135 @@
|
||||
#include "dusk/game_clock.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <aurora/time.hpp>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <unordered_map>
|
||||
#include <dusk/frame_interpolation.h>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace dusk::game_clock {
|
||||
|
||||
using clock = std::chrono::steady_clock;
|
||||
using native_clock = aurora::time::native_clock;
|
||||
using game_clock = aurora::time::game_clock;
|
||||
|
||||
FrameTiming g_frameTiming;
|
||||
|
||||
namespace {
|
||||
bool s_initialized = false;
|
||||
clock::time_point s_previous_sample{};
|
||||
clock::time_point s_current_snapshot_time{};
|
||||
bool s_fixedStepActive = false;
|
||||
bool s_simTickActive = false;
|
||||
native_clock::time_point s_previousNativeSample{};
|
||||
game_clock::time_point s_latestGameSample{};
|
||||
game_clock::time_point s_currentSnapshotTime{};
|
||||
game_clock::time_point s_pendingSimTime{};
|
||||
|
||||
std::unordered_map<uintptr_t, clock::time_point> s_interval_last_sample;
|
||||
std::unordered_map<uintptr_t, game_clock::time_point> s_intervalLastSample;
|
||||
|
||||
constexpr clock::duration kSimPeriodDuration =
|
||||
std::chrono::duration_cast<clock::duration>(std::chrono::duration<float>(sim_pace()));
|
||||
constexpr clock::duration kAbnormalGapResetThreshold = std::chrono::milliseconds(250);
|
||||
constexpr int kMaxSimTicksPerFrame = 2;
|
||||
constexpr game_clock::duration kSimPeriodDuration =
|
||||
std::chrono::duration_cast<game_clock::duration>(std::chrono::duration<float>(kSimPeriod));
|
||||
constexpr native_clock::duration kAbnormalGapResetThreshold = std::chrono::milliseconds(250);
|
||||
constexpr int kMaxSimTicksPerFrame = static_cast<int>(aurora::time::kMaximumTimeScale) * 4;
|
||||
} // namespace
|
||||
|
||||
void ensure_initialized() {
|
||||
void initialize() {
|
||||
if (s_initialized) {
|
||||
return;
|
||||
}
|
||||
s_previous_sample = clock::now();
|
||||
s_current_snapshot_time = s_previous_sample;
|
||||
s_previousNativeSample = native_clock::now();
|
||||
s_latestGameSample = game_clock::now();
|
||||
s_currentSnapshotTime = s_latestGameSample;
|
||||
s_pendingSimTime = s_latestGameSample;
|
||||
s_initialized = true;
|
||||
}
|
||||
|
||||
void reset_frame_timer() {
|
||||
s_previous_sample = clock::now();
|
||||
s_current_snapshot_time = s_previous_sample - kSimPeriodDuration;
|
||||
void reset() {
|
||||
s_previousNativeSample = native_clock::now();
|
||||
s_latestGameSample = game_clock::now();
|
||||
s_currentSnapshotTime = s_latestGameSample - kSimPeriodDuration;
|
||||
s_pendingSimTime = s_currentSnapshotTime;
|
||||
s_simTickActive = false;
|
||||
}
|
||||
|
||||
MainLoopPacer advance_main_loop() {
|
||||
ensure_initialized();
|
||||
const FrameTiming& advance() {
|
||||
const auto nativeNow = native_clock::now();
|
||||
const auto gameNow = game_clock::now();
|
||||
const auto nativeFrameGap = nativeNow - s_previousNativeSample;
|
||||
s_previousNativeSample = nativeNow;
|
||||
s_latestGameSample = gameNow;
|
||||
|
||||
const clock::time_point now = clock::now();
|
||||
const clock::duration frame_gap = now - s_previous_sample;
|
||||
const float presentation_dt = std::chrono::duration<float>(frame_gap).count();
|
||||
s_previous_sample = now;
|
||||
auto& out = g_frameTiming;
|
||||
out = {.dt = std::chrono::duration<float>().count()};
|
||||
|
||||
MainLoopPacer out{};
|
||||
out.presentation_dt_seconds = presentation_dt;
|
||||
const float timeScale = aurora::time::scale();
|
||||
const bool interpolating =
|
||||
getSettings().game.enableFrameInterpolation.getValue() != FrameInterpMode::Off;
|
||||
const bool separatePresentation = interpolating || timeScale != 1.0f;
|
||||
out.interpolating = interpolating;
|
||||
out.separatePresentation = separatePresentation;
|
||||
s_fixedStepActive = separatePresentation;
|
||||
|
||||
const bool should_interpolate = dusk::getSettings().game.enableFrameInterpolation.getValue() !=
|
||||
dusk::FrameInterpMode::Off &&
|
||||
!dusk::getTransientSettings().skipFrameRateLimit;
|
||||
out.is_interpolating = should_interpolate;
|
||||
out.sim_pace = sim_pace();
|
||||
|
||||
if (!should_interpolate) {
|
||||
s_current_snapshot_time = now;
|
||||
out.sim_ticks_to_run = 1;
|
||||
if (!separatePresentation) {
|
||||
s_currentSnapshotTime = gameNow;
|
||||
out.numSimTicks = 1;
|
||||
return out;
|
||||
}
|
||||
|
||||
if (frame_gap > kAbnormalGapResetThreshold) {
|
||||
s_current_snapshot_time = now - kSimPeriodDuration;
|
||||
out.sim_ticks_to_run = 0;
|
||||
const auto simulationTarget = interpolating ? gameNow - kSimPeriodDuration : gameNow;
|
||||
if (timeScale == 0.f || nativeFrameGap > kAbnormalGapResetThreshold) {
|
||||
s_currentSnapshotTime = simulationTarget;
|
||||
out.numSimTicks = 0;
|
||||
return out;
|
||||
}
|
||||
|
||||
int sim_ticks_to_run = 0;
|
||||
clock::time_point projected_snapshot_time = s_current_snapshot_time;
|
||||
const clock::time_point render_time = now - kSimPeriodDuration;
|
||||
while (sim_ticks_to_run < kMaxSimTicksPerFrame && projected_snapshot_time < render_time) {
|
||||
projected_snapshot_time += kSimPeriodDuration;
|
||||
sim_ticks_to_run++;
|
||||
int numSimTicks = 0;
|
||||
auto projectedSnapshotTime = s_currentSnapshotTime;
|
||||
while (numSimTicks < kMaxSimTicksPerFrame) {
|
||||
const bool tickDue = interpolating ?
|
||||
projectedSnapshotTime < simulationTarget :
|
||||
projectedSnapshotTime + kSimPeriodDuration <= simulationTarget;
|
||||
if (!tickDue) {
|
||||
break;
|
||||
}
|
||||
projectedSnapshotTime += kSimPeriodDuration;
|
||||
numSimTicks++;
|
||||
}
|
||||
out.sim_ticks_to_run = sim_ticks_to_run;
|
||||
out.numSimTicks = numSimTicks;
|
||||
return out;
|
||||
}
|
||||
|
||||
void begin_sim_tick() {
|
||||
s_pendingSimTime =
|
||||
s_fixedStepActive ? s_currentSnapshotTime + kSimPeriodDuration : s_latestGameSample;
|
||||
s_simTickActive = true;
|
||||
}
|
||||
|
||||
void commit_sim_tick() {
|
||||
ensure_initialized();
|
||||
s_current_snapshot_time += kSimPeriodDuration;
|
||||
if (s_simTickActive) {
|
||||
s_currentSnapshotTime = s_pendingSimTime;
|
||||
s_simTickActive = false;
|
||||
} else {
|
||||
s_currentSnapshotTime += kSimPeriodDuration;
|
||||
}
|
||||
}
|
||||
|
||||
float sample_interpolation_step() {
|
||||
ensure_initialized();
|
||||
const float step =
|
||||
std::chrono::duration<float>(clock::now() - s_current_snapshot_time).count() / sim_pace();
|
||||
std::chrono::duration<float>(game_clock::now() - s_currentSnapshotTime).count() /
|
||||
kSimPeriod;
|
||||
return std::clamp(step, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
float consume_interval(const void* consumer) {
|
||||
ensure_initialized();
|
||||
const uintptr_t key = reinterpret_cast<uintptr_t>(consumer);
|
||||
const clock::time_point now = clock::now();
|
||||
|
||||
float dt = ui_initial_dt();
|
||||
const auto it = s_interval_last_sample.find(key);
|
||||
if (it != s_interval_last_sample.end()) {
|
||||
const auto key = reinterpret_cast<uintptr_t>(consumer);
|
||||
const auto now = s_simTickActive ? s_pendingSimTime : game_clock::now();
|
||||
const float timeScale = aurora::time::scale();
|
||||
float dt = kUiInitialDt * timeScale;
|
||||
if (const auto it = s_intervalLastSample.find(key); it != s_intervalLastSample.end()) {
|
||||
dt = std::chrono::duration<float>(now - it->second).count();
|
||||
dt = std::min(dt, ui_maximum_dt());
|
||||
const float maximumDt = std::max(kUiMaximumDt * timeScale, kSimPeriod);
|
||||
dt = std::min(dt, maximumDt);
|
||||
}
|
||||
s_interval_last_sample[key] = now;
|
||||
s_intervalLastSample[key] = now;
|
||||
return dt;
|
||||
}
|
||||
|
||||
|
||||
+18
-13
@@ -2,22 +2,27 @@
|
||||
|
||||
namespace dusk::game_clock {
|
||||
|
||||
void ensure_initialized();
|
||||
void reset_frame_timer();
|
||||
// Amount of time that a simulation tick advances
|
||||
constexpr float kSimPeriod = 1.0f / 30.0f;
|
||||
constexpr float kUiMaximumDt = 0.05f;
|
||||
constexpr float kUiInitialDt = 1.0f / 60.0f;
|
||||
|
||||
constexpr float sim_pace() { return 1.0f / 30.0f; }
|
||||
constexpr float period_for_original_frames(float frame_count) { return frame_count * sim_pace(); }
|
||||
constexpr float ui_maximum_dt() { return 0.05f; }
|
||||
constexpr float ui_initial_dt() { return 1.0f / 60.0f; }
|
||||
|
||||
struct MainLoopPacer {
|
||||
float presentation_dt_seconds;
|
||||
bool is_interpolating;
|
||||
int sim_ticks_to_run;
|
||||
float sim_pace;
|
||||
struct FrameTiming {
|
||||
// Amount of time elapsed in seconds since the last advance
|
||||
float dt;
|
||||
// Whether interpolation is active
|
||||
bool interpolating;
|
||||
// Run simulation and presentation separately (for interpolation or time scaling)
|
||||
bool separatePresentation;
|
||||
// Number of simulation ticks to run
|
||||
int numSimTicks;
|
||||
};
|
||||
extern FrameTiming g_frameTiming;
|
||||
|
||||
MainLoopPacer advance_main_loop();
|
||||
void initialize();
|
||||
void reset();
|
||||
const FrameTiming& advance();
|
||||
void begin_sim_tick();
|
||||
void commit_sim_tick();
|
||||
float sample_interpolation_step();
|
||||
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
#include "aurora/texture.hpp"
|
||||
#include "dusk/texture_replacements.hpp"
|
||||
#include "fmt/format.h"
|
||||
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <span>
|
||||
#include <numbers>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr u16 kMapIconResolutionMultiplier = 4;
|
||||
constexpr u16 kMapImageSide = 16 * kMapIconResolutionMultiplier;
|
||||
constexpr u32 kMapImageTotalPixels = kMapImageSide * kMapImageSide;
|
||||
|
||||
// give higher priority to user and mod replacements
|
||||
constexpr auto kInternalTextureReplacementPriority = dusk::texture_replacements::kUserTextureReplacementPriority - 1;
|
||||
|
||||
typedef std::function<u8(size_t, size_t)> PaintI8Fn;
|
||||
|
||||
enum class ArcIndex : int {
|
||||
Circle16 = 82, // map_icon_circle16x16_4i.bti - simple circle
|
||||
Circle = 76, // im_map_icon_circle_4i.bti - outlined circle
|
||||
Nijumaru = 78, // im_map_icon_nijumaru_4i.bti - concentric rings
|
||||
Enter = 77, // im_map_icon_enter_4i.bti - outlined octagram
|
||||
TryForce = 81, // im_map_icon_try_force_4i.bti - outlined circle with triangle
|
||||
};
|
||||
|
||||
struct Replacement {
|
||||
ArcIndex index;
|
||||
PaintI8Fn painter;
|
||||
};
|
||||
|
||||
struct Icon {
|
||||
u8* origData = nullptr;
|
||||
std::unique_ptr<u8[]> newData;
|
||||
std::string label;
|
||||
std::optional<aurora::texture::ReplacementRegistration> reg;
|
||||
};
|
||||
|
||||
bool s_initialized = false;
|
||||
bool s_active = false;
|
||||
std::unordered_map<int, Icon> s_icons;
|
||||
|
||||
void paint_i8(std::span<u8> dst, size_t width, PaintI8Fn paint) {
|
||||
assert(width % 8 == 0 && dst.size() % 32 == 0);
|
||||
|
||||
const auto blocksAcross = width >> 3;
|
||||
|
||||
for (size_t i = 0; i < dst.size(); i++) {
|
||||
// 8x4 block swizzling for I8
|
||||
const auto blockIdx = i >> 5;
|
||||
const auto localIdx = i & 31;
|
||||
|
||||
const auto blockY = blockIdx / blocksAcross;
|
||||
const auto blockX = blockIdx % blocksAcross;
|
||||
|
||||
const auto localY = localIdx >> 3;
|
||||
const auto localX = localIdx & 7;
|
||||
|
||||
const auto x = (blockX << 3) + localX;
|
||||
const auto y = (blockY << 2) + localY;
|
||||
|
||||
dst[i] = paint(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
void draw_all_replacements() {
|
||||
constexpr auto center = kMapImageSide / 2.0f;
|
||||
constexpr auto radiusSq = center * center;
|
||||
|
||||
// clang-format off
|
||||
const auto replacements = std::to_array<Replacement>({
|
||||
{
|
||||
ArcIndex::Circle16,
|
||||
[=](auto x, auto y) {
|
||||
const auto dx = (x + 0.5f) - center;
|
||||
const auto dy = (y + 0.5f) - center;
|
||||
return (dx * dx + dy * dy < radiusSq) ? 0x11 : 0;
|
||||
}
|
||||
},
|
||||
{
|
||||
ArcIndex::Circle,
|
||||
[=](auto x, auto y) {
|
||||
constexpr auto innerRadius = kMapImageSide * 3.0f / 8.0f;
|
||||
constexpr auto innerRadiusSq = innerRadius * innerRadius;
|
||||
|
||||
const auto dx = (x + 0.5f) - center;
|
||||
const auto dy = (y + 0.5f) - center;
|
||||
const auto dSq = dx * dx + dy * dy;
|
||||
|
||||
return dSq < radiusSq ? (dSq < innerRadiusSq ? 0x22 : 0x11) : 0;
|
||||
}
|
||||
},
|
||||
{
|
||||
ArcIndex::Nijumaru,
|
||||
[=](auto x, auto y) {
|
||||
constexpr u8 nijumaruRings[] = {0x11, 0x22, 0x11, 0x11, 0x22, 0x22};
|
||||
|
||||
const auto dx = (x + 0.5f) - center;
|
||||
const auto dy = (y + 0.5f) - center;
|
||||
const auto dSq = dx * dx + dy * dy;
|
||||
|
||||
if (dSq < radiusSq) {
|
||||
auto ringIndex = static_cast<size_t>(std::trunc(std::sqrt(dSq) / kMapImageSide * 12));
|
||||
ringIndex = std::min(ringIndex, sizeof(nijumaruRings) - 1);
|
||||
return nijumaruRings[ringIndex];
|
||||
}
|
||||
return u8{0};
|
||||
}
|
||||
},
|
||||
{
|
||||
ArcIndex::Enter,
|
||||
[=](auto x, auto y) {
|
||||
constexpr auto outlineWidth = kMapImageSide / 6.0f;
|
||||
|
||||
const auto adx = std::abs((x + 0.5f) - center);
|
||||
const auto ady = std::abs((y + 0.5f) - center);
|
||||
const auto dist =
|
||||
std::min(adx + ady, std::max(adx, ady) * std::numbers::sqrt2_v<float>) -
|
||||
kMapImageSide / 2.0f;
|
||||
|
||||
return dist > 0.0f ? 0 : (dist > -outlineWidth ? 0x22 : 0x33);
|
||||
}
|
||||
},
|
||||
{
|
||||
ArcIndex::TryForce,
|
||||
[=](auto x, auto y) {
|
||||
constexpr auto innerRadiusNorm = 5.0f / 12.0f;
|
||||
constexpr auto innerRadius = kMapImageSide * innerRadiusNorm;
|
||||
constexpr auto innerRadiusSq = innerRadius * innerRadius;
|
||||
constexpr auto triRadius = kMapImageSide * innerRadiusNorm / 2.0f;
|
||||
|
||||
const auto dx = (x + 0.5f) - center;
|
||||
const auto dy = (y + 0.5f) - center;
|
||||
const auto dSq = dx * dx + dy * dy;
|
||||
const auto triSideDist = (std::numbers::sqrt3_v<float> * std::abs(dx) - dy) * 0.5f;
|
||||
const auto insideTri = std::max(dy, triSideDist) < triRadius;
|
||||
|
||||
return insideTri ? 0x22 : (dSq < radiusSq ? (dSq < innerRadiusSq ? 0x33 : 0x22) : 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
// clang-format on
|
||||
|
||||
for (const auto r : replacements) {
|
||||
auto pixels = std::make_unique_for_overwrite<u8[]>(kMapImageTotalPixels);
|
||||
paint_i8(std::span{pixels.get(), kMapImageTotalPixels}, kMapImageSide, r.painter);
|
||||
|
||||
auto& icon = s_icons[static_cast<int>(r.index)];
|
||||
icon.newData = std::move(pixels);
|
||||
icon.label = fmt::format("hq minimap icon {}", static_cast<int>(r.index));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace dusk::hq_minimap {
|
||||
|
||||
void register_pointer(int idx, u8* ptr) {
|
||||
if (s_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
s_icons[idx].origData = ptr;
|
||||
}
|
||||
|
||||
void set_active(bool active) {
|
||||
s_active = active;
|
||||
}
|
||||
|
||||
void update() {
|
||||
if (!s_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& [idx, icon] : s_icons) {
|
||||
const bool shouldBeRegistered = s_active && icon.origData && icon.newData;
|
||||
|
||||
if (shouldBeRegistered && !icon.reg) {
|
||||
aurora::texture::ReplacementKey key{aurora::texture::TexturePointerKey{icon.origData}};
|
||||
aurora::texture::RawTextureReplacement repl{
|
||||
.bytes = std::span{icon.newData.get(), kMapImageTotalPixels},
|
||||
.width = kMapImageSide,
|
||||
.height = kMapImageSide,
|
||||
.mipCount = 1,
|
||||
.gxFormat = GX_TF_I8,
|
||||
.label = icon.label,
|
||||
};
|
||||
icon.reg = aurora::texture::register_replacement(
|
||||
key, repl, {.priority = kInternalTextureReplacementPriority});
|
||||
} else if (!shouldBeRegistered && icon.reg) {
|
||||
aurora::texture::unregister_replacement(*icon.reg);
|
||||
icon.reg.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void initialize_if_needed() {
|
||||
if (s_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
draw_all_replacements();
|
||||
s_initialized = true;
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
} // namespace dusk::hq_minimap
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
namespace dusk::hq_minimap {
|
||||
|
||||
/// Adds a mapping of an image resource index (into `Always.arc`) to the pointer to its image data.
|
||||
/// If `initialize_if_needed` has been called, this is a no-op. Pointers are expected to be stable
|
||||
/// and valid for the program's entire lifetime.
|
||||
void register_pointer(int idx, u8* ptr);
|
||||
|
||||
/// Sets whether HQ minimap texture replacements should be active or not. Does not manage
|
||||
/// replacement registrations itself; see `update`.
|
||||
void set_active(bool active);
|
||||
|
||||
/// Registers or unregisters texture replacements depending on active state.
|
||||
void update();
|
||||
|
||||
/// Called once after registering image pointers, in which their HQ replacements are procedurally
|
||||
/// drawn and `update` is called. Further calls are no-ops.
|
||||
void initialize_if_needed();
|
||||
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <aurora/aurora.h>
|
||||
#include <chrono>
|
||||
#include <numeric>
|
||||
#include <string_view>
|
||||
#include <chrono>
|
||||
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
#include "imgui.h"
|
||||
@@ -20,6 +21,7 @@
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include "dusk/livesplit.h"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/presentation.hpp"
|
||||
#include "dusk/settings.h"
|
||||
#include "dusk/ui/ui.hpp"
|
||||
#include "dusk/gamemode.hpp"
|
||||
@@ -38,6 +40,8 @@ using namespace std::string_literals;
|
||||
using namespace std::string_view_literals;
|
||||
|
||||
namespace {
|
||||
constexpr float kTurboTimeScale = 4.f;
|
||||
|
||||
ImGuiWindow* FindDragScrollWindow(ImGuiWindow* window) {
|
||||
while (window != nullptr) {
|
||||
const bool canScrollX = window->ScrollMax.x > 0.0f;
|
||||
@@ -236,10 +240,32 @@ namespace dusk {
|
||||
}
|
||||
|
||||
void ImGuiConsole::UpdateSettings() {
|
||||
getTransientSettings().skipFrameRateLimit = getSettings().game.enableTurboKeybind &&
|
||||
(ImGui::IsKeyDown(ImGuiKey_Tab) || getActionBindHoldAnyPort(ActionBinds::TURBO_SPEED_BUTTON));
|
||||
static bool previousTurboActive = false;
|
||||
static bool previousSlowActive = false;
|
||||
static float previousTimeScale = 1.0f;
|
||||
|
||||
if (dusk::frame_interp::get_ui_tick_pending() && mDoMain::developmentMode == 1 && (mDoCPd_c::getHold(PAD_1) & (PAD_TRIGGER_R | PAD_TRIGGER_L)) == (PAD_TRIGGER_R | PAD_TRIGGER_L) && mDoCPd_c::getTrigY(PAD_1)) {
|
||||
const bool turboBound = isActionBoundAnyPort(ActionBinds::TURBO_SPEED_BUTTON);
|
||||
const bool turboActive =
|
||||
getSettings().game.enableTurboKeybind &&
|
||||
(turboBound ? getActionBindHoldAnyPort(ActionBinds::TURBO_SPEED_BUTTON) :
|
||||
ImGui::IsKeyDown(ImGuiKey_Tab));
|
||||
const bool slowDown = turboActive && ImGui::GetIO().KeyShift;
|
||||
if (turboActive != previousTurboActive) {
|
||||
getTransientSettings().turboMode = turboActive;
|
||||
presentation::update_frame_rate_preference();
|
||||
if (turboActive) {
|
||||
previousTimeScale = aurora_get_timescale();
|
||||
aurora_set_timescale(slowDown ? 1.f / kTurboTimeScale : kTurboTimeScale);
|
||||
} else {
|
||||
aurora_set_timescale(previousTimeScale);
|
||||
}
|
||||
} else if (turboActive && slowDown != previousSlowActive) {
|
||||
aurora_set_timescale(slowDown ? 1.f / kTurboTimeScale : kTurboTimeScale);
|
||||
}
|
||||
previousTurboActive = turboActive;
|
||||
previousSlowActive = slowDown;
|
||||
|
||||
if (frame_interp::get_ui_tick_pending() && mDoMain::developmentMode == 1 && (mDoCPd_c::getHold(PAD_1) & (PAD_TRIGGER_R | PAD_TRIGGER_L)) == (PAD_TRIGGER_R | PAD_TRIGGER_L) && mDoCPd_c::getTrigY(PAD_1)) {
|
||||
getTransientSettings().moveLinkActive = !getTransientSettings().moveLinkActive;
|
||||
}
|
||||
if (mDoMain::developmentMode != 1) {
|
||||
@@ -271,7 +297,7 @@ namespace dusk {
|
||||
m_isHidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool showMenu = !m_isHidden;
|
||||
|
||||
// The menu bar renders with ImGuiCol_WindowBg behind it. We just want ImGuiCol_MenuBarBg,
|
||||
|
||||
@@ -26,10 +26,6 @@
|
||||
#include <TargetConditionals.h>
|
||||
#endif
|
||||
|
||||
namespace aurora::gx {
|
||||
extern bool enableLodBias;
|
||||
}
|
||||
|
||||
namespace dusk {
|
||||
ImGuiMenuTools::ImGuiMenuTools() {}
|
||||
|
||||
@@ -77,7 +73,6 @@ namespace dusk {
|
||||
getSettings().game.disableWaterRefraction.setValue(disableWaterRefraction);
|
||||
config::save();
|
||||
}
|
||||
ImGui::Checkbox("Enable LOD Bias", &aurora::gx::enableLodBias);
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
|
||||
@@ -40,10 +40,25 @@ constexpr auto AcceptedDiscs = std::to_array<borealis::disc::AcceptedDisc>({
|
||||
.gameId = "GZ2P01",
|
||||
.expectedHash = borealis::disc::parse_xxh3_128("9ef597588b0035ca9e91b333fa9a8a7e"),
|
||||
},
|
||||
{
|
||||
.gameId = "RZDE01", .revision = 0,
|
||||
.expectedHash = borealis::disc::parse_xxh3_128("b3d91fbea59e5c66934d04c01566728e"),
|
||||
},
|
||||
{
|
||||
.gameId = "RZDE01", .revision = 2,
|
||||
.expectedHash = borealis::disc::parse_xxh3_128("c3ec420921a1b36d6ae43f576491d25c"),
|
||||
},
|
||||
{
|
||||
.gameId = "RZDJ01",
|
||||
.expectedHash = borealis::disc::parse_xxh3_128("d3866821c7fc6999e6e8bbef8b6875aa"),
|
||||
},
|
||||
{
|
||||
.gameId = "RZDP01",
|
||||
.expectedHash = borealis::disc::parse_xxh3_128("6095a924a57e5fb4294ac96fb85a09a1"),
|
||||
},
|
||||
});
|
||||
|
||||
constexpr auto RecognizedGameIds =
|
||||
std::to_array<std::string_view>({"RZDE01", "RZDJ01", "RZDK01", "RZDP01"});
|
||||
constexpr auto RecognizedGameIds = std::to_array<std::string_view>({"RZDK01"});
|
||||
|
||||
constexpr borealis::disc::Catalog DiscCatalog{
|
||||
.acceptedDiscs = AcceptedDiscs,
|
||||
@@ -92,6 +107,7 @@ void update_info(const borealis::disc::Result& result, DiscInfo& info) noexcept
|
||||
if (!result.metadata.gameId.empty()) {
|
||||
info.platform = result.metadata.platform;
|
||||
info.region = region_from_game_id(result.metadata.gameId);
|
||||
info.revision = result.metadata.revision;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ using VerificationStatus = borealis::disc::Progress;
|
||||
struct DiscInfo {
|
||||
Platform platform = Platform::Unknown;
|
||||
Region region = Region::NorthAmerica;
|
||||
std::uint8_t revision = 0;
|
||||
};
|
||||
|
||||
ValidationError inspect(const char* path, DiscInfo& info);
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
#include "dusk/language.hpp"
|
||||
|
||||
#include "dusk/version.hpp"
|
||||
|
||||
namespace dusk::language {
|
||||
namespace {
|
||||
|
||||
constexpr GameLanguage kEnglishOnly[] = {GameLanguage::English};
|
||||
constexpr GameLanguage kJapaneseOnly[] = {GameLanguage::Japanese};
|
||||
constexpr GameLanguage kPalLanguages[] = {
|
||||
GameLanguage::English,
|
||||
GameLanguage::German,
|
||||
GameLanguage::French,
|
||||
GameLanguage::Spanish,
|
||||
GameLanguage::Italian,
|
||||
};
|
||||
constexpr GameLanguage kWiiUsaLanguages[] = {
|
||||
GameLanguage::English,
|
||||
GameLanguage::French,
|
||||
GameLanguage::Spanish,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::span<const GameLanguage> available_languages(const iso::DiscInfo& info) noexcept {
|
||||
switch (info.region) {
|
||||
case iso::Region::Japan:
|
||||
return kJapaneseOnly;
|
||||
case iso::Region::Europe:
|
||||
return kPalLanguages;
|
||||
case iso::Region::NorthAmerica:
|
||||
if (info.platform == iso::Platform::Wii && info.revision == 2) {
|
||||
return kWiiUsaLanguages;
|
||||
}
|
||||
return kEnglishOnly;
|
||||
default:
|
||||
return kEnglishOnly;
|
||||
}
|
||||
}
|
||||
|
||||
const char* language_name(GameLanguage language) noexcept {
|
||||
switch (language) {
|
||||
case GameLanguage::English:
|
||||
return "English";
|
||||
case GameLanguage::German:
|
||||
return "German";
|
||||
case GameLanguage::French:
|
||||
return "French";
|
||||
case GameLanguage::Spanish:
|
||||
return "Spanish";
|
||||
case GameLanguage::Italian:
|
||||
return "Italian";
|
||||
case GameLanguage::Japanese:
|
||||
return "Japanese";
|
||||
}
|
||||
return "English";
|
||||
}
|
||||
|
||||
const char* msg_folder() noexcept {
|
||||
using namespace version;
|
||||
|
||||
switch (getSettings().game.language.getValue()) {
|
||||
case GameLanguage::German:
|
||||
return "Msgde";
|
||||
case GameLanguage::French:
|
||||
return "Msgfr";
|
||||
case GameLanguage::Spanish:
|
||||
return "Msgsp";
|
||||
case GameLanguage::Italian:
|
||||
return "Msgit";
|
||||
case GameLanguage::Japanese:
|
||||
return "Msgjp";
|
||||
case GameLanguage::English:
|
||||
default:
|
||||
return isRegionPal() ? "Msguk" : "Msgus";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dusk::language
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "dusk/iso_validate.hpp"
|
||||
#include "dusk/settings.h"
|
||||
|
||||
#include <span>
|
||||
|
||||
namespace dusk::language {
|
||||
|
||||
std::span<const GameLanguage> available_languages(const iso::DiscInfo& info) noexcept;
|
||||
|
||||
const char* language_name(GameLanguage language) noexcept;
|
||||
const char* msg_folder() noexcept;
|
||||
|
||||
} // namespace dusk::language
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
class fopAc_ac_c;
|
||||
|
||||
namespace dusk::mods {
|
||||
|
||||
uint8_t item_check(const char* name, uint8_t itemNo, fopAc_ac_c* giver);
|
||||
uint8_t item_check_tagged(uint32_t giveTag, uint8_t itemNo, fopAc_ac_c* giver);
|
||||
|
||||
uint8_t item_check_chest(uint8_t boxNo, uint8_t itemNo, fopAc_ac_c* chest);
|
||||
uint8_t item_check_boss(uint8_t itemNo, fopAc_ac_c* boss);
|
||||
uint8_t item_check_freestanding(uint8_t bitNo, uint8_t itemNo, fopAc_ac_c* item);
|
||||
uint8_t item_check_poe(uint8_t bitNo, uint8_t itemNo, fopAc_ac_c* poe);
|
||||
uint8_t item_check_shop(uint8_t itemNo, fopAc_ac_c* giver);
|
||||
uint8_t item_check_bug(uint8_t insectId, uint8_t itemNo, fopAc_ac_c* agitha);
|
||||
uint8_t item_check_sky_character(uint8_t itemNo, fopAc_ac_c* statue);
|
||||
|
||||
uint32_t item_give_tag(const char* name);
|
||||
uint32_t item_give_tag_chest(uint8_t boxNo);
|
||||
uint32_t item_give_tag_boss();
|
||||
uint32_t item_give_tag_freestanding(uint8_t bitNo);
|
||||
uint32_t item_give_tag_poe(uint8_t bitNo);
|
||||
uint32_t item_give_tag_shop(uint8_t itemNo);
|
||||
uint32_t item_give_tag_bug(uint8_t insectId);
|
||||
uint32_t item_give_tag_sky_character();
|
||||
|
||||
void item_check_enqueue(const char* name, uint8_t itemNo);
|
||||
void item_check_enqueue_poe(uint8_t bitNo, uint8_t itemNo);
|
||||
|
||||
void item_granted(uint8_t itemNo, uint32_t giveTag, fopAc_ac_c* giver);
|
||||
|
||||
bool item_give_queue_dispatching();
|
||||
uint32_t item_give_queue_take_tag();
|
||||
|
||||
} // namespace dusk::mods
|
||||
@@ -0,0 +1,294 @@
|
||||
#include "item.hpp"
|
||||
|
||||
#include "dusk/mod_loader.hpp"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "dusk/mods/svc/item.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "d/d_com_inf_game.h"
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <exception>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::mods {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log{"dusk::mods::item_checks"};
|
||||
|
||||
struct CheckOverride {
|
||||
std::string name;
|
||||
uint8_t itemNo = 0;
|
||||
};
|
||||
|
||||
struct CheckResolver {
|
||||
ItemCheckHandle handle = 0;
|
||||
std::string name;
|
||||
ItemCheckResolveFn fn = nullptr;
|
||||
void* userData = nullptr;
|
||||
};
|
||||
|
||||
struct ModItemChecks {
|
||||
std::vector<CheckOverride> overrides;
|
||||
std::vector<CheckResolver> resolvers;
|
||||
};
|
||||
|
||||
struct PendingResolve {
|
||||
LoadedMod* mod = nullptr;
|
||||
bool fixedValue = false;
|
||||
uint8_t itemNo = 0;
|
||||
ItemCheckResolveFn fn = nullptr;
|
||||
void* userData = nullptr;
|
||||
};
|
||||
|
||||
std::unordered_map<LoadedMod*, ModItemChecks> s_modChecks;
|
||||
std::unordered_set<std::string> s_warnedCollisions;
|
||||
ItemCheckHandle s_nextCheckHandle = 1;
|
||||
|
||||
const char* current_stage_name() {
|
||||
const char* stageName = dComIfGp_getStartStageName();
|
||||
return stageName != nullptr ? stageName : "";
|
||||
}
|
||||
|
||||
std::string chest_check_name(uint8_t boxNo) {
|
||||
return fmt::format("chest:{}:{}", current_stage_name(), boxNo);
|
||||
}
|
||||
|
||||
std::string boss_check_name() {
|
||||
return fmt::format("boss:{}", current_stage_name());
|
||||
}
|
||||
|
||||
std::string freestanding_check_name(uint8_t bitNo) {
|
||||
return fmt::format("freestanding:{}:{}", current_stage_name(), bitNo);
|
||||
}
|
||||
|
||||
std::string poe_check_name(uint8_t bitNo) {
|
||||
return fmt::format("poe:{}:{}", current_stage_name(), bitNo);
|
||||
}
|
||||
|
||||
std::string shop_check_name(uint8_t itemNo) {
|
||||
return fmt::format("shop:{}:{}", current_stage_name(), itemNo);
|
||||
}
|
||||
|
||||
std::string bug_check_name(uint8_t insectId) {
|
||||
return fmt::format("bug:{}", insectId);
|
||||
}
|
||||
|
||||
std::string sky_character_check_name() {
|
||||
return fmt::format("skychar:{}:{}", current_stage_name(), dStage_roomControl_c::getStayNo());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint8_t item_check(const char* name, uint8_t itemNo, fopAc_ac_c* giver) {
|
||||
if (name == nullptr || *name == '\0' || s_modChecks.empty()) {
|
||||
return itemNo;
|
||||
}
|
||||
|
||||
// Callbacks may change registrations, so copy the applicable chain before invoking one.
|
||||
std::vector<PendingResolve> resolves;
|
||||
LoadedMod* previousOverrideOwner = nullptr;
|
||||
for (auto& mod : ModLoader::instance().mods()) {
|
||||
if (!mod.active) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto modIt = s_modChecks.find(&mod);
|
||||
if (modIt == s_modChecks.end()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const auto& checkOverride : modIt->second.overrides) {
|
||||
if (checkOverride.name != name) {
|
||||
continue;
|
||||
}
|
||||
if (previousOverrideOwner != nullptr && s_warnedCollisions.emplace(name).second) {
|
||||
Log.warn("check '{}' is overridden by [{}] and [{}]; [{}] wins by load order", name,
|
||||
previousOverrideOwner->metadata.id, mod.metadata.id, mod.metadata.id);
|
||||
}
|
||||
previousOverrideOwner = &mod;
|
||||
resolves.push_back({.mod = &mod, .fixedValue = true, .itemNo = checkOverride.itemNo});
|
||||
}
|
||||
|
||||
for (const auto& resolver : modIt->second.resolvers) {
|
||||
if (resolver.name.empty() || resolver.name == name) {
|
||||
resolves.push_back({.mod = &mod, .fn = resolver.fn, .userData = resolver.userData});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ItemCheckInfo info{
|
||||
.name = name,
|
||||
.giver_actor = giver,
|
||||
.vanilla_item = itemNo,
|
||||
.current_item = itemNo,
|
||||
};
|
||||
for (const auto& resolve : resolves) {
|
||||
if (!resolve.mod->active) {
|
||||
continue;
|
||||
}
|
||||
if (resolve.fixedValue) {
|
||||
info.current_item = resolve.itemNo;
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t resolvedItem = info.current_item;
|
||||
try {
|
||||
if (resolve.fn(resolve.mod->context.get(), &info, &resolvedItem, resolve.userData)) {
|
||||
info.current_item = resolvedItem;
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
fail_mod(*resolve.mod, MOD_ERROR,
|
||||
fmt::format("Exception in item check resolver for '{}': {}", name, e.what()));
|
||||
} catch (...) {
|
||||
fail_mod(*resolve.mod, MOD_ERROR,
|
||||
fmt::format("Unknown exception in item check resolver for '{}'", name));
|
||||
}
|
||||
}
|
||||
return info.current_item;
|
||||
}
|
||||
|
||||
uint8_t item_check_chest(uint8_t boxNo, uint8_t itemNo, fopAc_ac_c* chest) {
|
||||
if (s_modChecks.empty()) {
|
||||
return itemNo;
|
||||
}
|
||||
const auto name = chest_check_name(boxNo);
|
||||
return item_check(name.c_str(), itemNo, chest);
|
||||
}
|
||||
|
||||
uint8_t item_check_boss(uint8_t itemNo, fopAc_ac_c* boss) {
|
||||
if (s_modChecks.empty()) {
|
||||
return itemNo;
|
||||
}
|
||||
const auto name = boss_check_name();
|
||||
return item_check(name.c_str(), itemNo, boss);
|
||||
}
|
||||
|
||||
uint8_t item_check_freestanding(uint8_t bitNo, uint8_t itemNo, fopAc_ac_c* item) {
|
||||
if (s_modChecks.empty()) {
|
||||
return itemNo;
|
||||
}
|
||||
const auto name = freestanding_check_name(bitNo);
|
||||
return item_check(name.c_str(), itemNo, item);
|
||||
}
|
||||
|
||||
uint8_t item_check_poe(uint8_t bitNo, uint8_t itemNo, fopAc_ac_c* poe) {
|
||||
if (s_modChecks.empty()) {
|
||||
return itemNo;
|
||||
}
|
||||
const auto name = poe_check_name(bitNo);
|
||||
return item_check(name.c_str(), itemNo, poe);
|
||||
}
|
||||
|
||||
uint8_t item_check_shop(uint8_t itemNo, fopAc_ac_c* giver) {
|
||||
if (s_modChecks.empty()) {
|
||||
return itemNo;
|
||||
}
|
||||
const auto name = shop_check_name(itemNo);
|
||||
return item_check(name.c_str(), itemNo, giver);
|
||||
}
|
||||
|
||||
uint8_t item_check_bug(uint8_t insectId, uint8_t itemNo, fopAc_ac_c* agitha) {
|
||||
if (s_modChecks.empty()) {
|
||||
return itemNo;
|
||||
}
|
||||
const auto name = bug_check_name(insectId);
|
||||
return item_check(name.c_str(), itemNo, agitha);
|
||||
}
|
||||
|
||||
uint8_t item_check_sky_character(uint8_t itemNo, fopAc_ac_c* statue) {
|
||||
if (s_modChecks.empty()) {
|
||||
return itemNo;
|
||||
}
|
||||
const auto name = sky_character_check_name();
|
||||
return item_check(name.c_str(), itemNo, statue);
|
||||
}
|
||||
|
||||
uint32_t item_give_tag_chest(uint8_t boxNo) {
|
||||
return item_give_tag(chest_check_name(boxNo).c_str());
|
||||
}
|
||||
|
||||
uint32_t item_give_tag_boss() {
|
||||
return item_give_tag(boss_check_name().c_str());
|
||||
}
|
||||
|
||||
uint32_t item_give_tag_freestanding(uint8_t bitNo) {
|
||||
return item_give_tag(freestanding_check_name(bitNo).c_str());
|
||||
}
|
||||
|
||||
uint32_t item_give_tag_poe(uint8_t bitNo) {
|
||||
return item_give_tag(poe_check_name(bitNo).c_str());
|
||||
}
|
||||
|
||||
uint32_t item_give_tag_shop(uint8_t itemNo) {
|
||||
return item_give_tag(shop_check_name(itemNo).c_str());
|
||||
}
|
||||
|
||||
uint32_t item_give_tag_bug(uint8_t insectId) {
|
||||
return item_give_tag(bug_check_name(insectId).c_str());
|
||||
}
|
||||
|
||||
uint32_t item_give_tag_sky_character() {
|
||||
return item_give_tag(sky_character_check_name().c_str());
|
||||
}
|
||||
|
||||
void item_check_enqueue_poe(uint8_t bitNo, uint8_t itemNo) {
|
||||
item_check_enqueue(poe_check_name(bitNo).c_str(), itemNo);
|
||||
}
|
||||
|
||||
namespace svc {
|
||||
|
||||
ModResult item_check_set_override(LoadedMod& mod, const char* name, uint8_t itemNo) {
|
||||
auto& checks = s_modChecks[&mod];
|
||||
for (auto& checkOverride : checks.overrides) {
|
||||
if (checkOverride.name == name) {
|
||||
checkOverride.itemNo = itemNo;
|
||||
return MOD_OK;
|
||||
}
|
||||
}
|
||||
checks.overrides.push_back({.name = name, .itemNo = itemNo});
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult item_check_clear_override(LoadedMod& mod, const char* name) {
|
||||
const auto modIt = s_modChecks.find(&mod);
|
||||
if (modIt == s_modChecks.end()) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
const auto removed = std::erase_if(modIt->second.overrides,
|
||||
[&](const auto& checkOverride) { return checkOverride.name == name; });
|
||||
return removed != 0 ? MOD_OK : MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
ModResult item_check_add_resolver(LoadedMod& mod, const char* name, ItemCheckResolveFn fn,
|
||||
void* userData, ItemCheckHandle& outHandle) {
|
||||
auto& resolver = s_modChecks[&mod].resolvers.emplace_back();
|
||||
resolver.handle = s_nextCheckHandle++;
|
||||
resolver.name = name != nullptr ? name : "";
|
||||
resolver.fn = fn;
|
||||
resolver.userData = userData;
|
||||
outHandle = resolver.handle;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult item_check_remove_resolver(LoadedMod& mod, ItemCheckHandle handle) {
|
||||
const auto modIt = s_modChecks.find(&mod);
|
||||
if (modIt == s_modChecks.end()) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
const auto removed = std::erase_if(
|
||||
modIt->second.resolvers, [&](const auto& resolver) { return resolver.handle == handle; });
|
||||
return removed != 0 ? MOD_OK : MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
void item_checks_remove_mod(LoadedMod& mod) {
|
||||
s_modChecks.erase(&mod);
|
||||
s_warnedCollisions.clear();
|
||||
}
|
||||
|
||||
} // namespace svc
|
||||
} // namespace dusk::mods
|
||||
@@ -0,0 +1,343 @@
|
||||
#include "item.hpp"
|
||||
|
||||
#include "dusk/mod_loader.hpp"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "dusk/mods/svc/item.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "d/actor/d_a_alink.h"
|
||||
#include "d/d_com_inf_game.h"
|
||||
#include "d/d_item.h"
|
||||
#include "d/d_item_data.h"
|
||||
#include "f_op/f_op_actor_mng.h"
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <deque>
|
||||
#include <exception>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::mods {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log{"dusk::mods::item_gives"};
|
||||
|
||||
// deque keeps previously returned c_str pointers valid if a callback interns another name.
|
||||
std::deque<std::string> s_giveNames;
|
||||
std::unordered_map<std::string, uint32_t> s_giveNameIds;
|
||||
|
||||
const char* item_give_name(uint32_t tag) {
|
||||
if (tag == 0 || tag > s_giveNames.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return s_giveNames[tag - 1].c_str();
|
||||
}
|
||||
|
||||
struct GiveObserver {
|
||||
ItemGiveHandle handle = 0;
|
||||
ItemGiveObserveFn fn = nullptr;
|
||||
void* userData = nullptr;
|
||||
};
|
||||
|
||||
struct PendingObserver {
|
||||
LoadedMod* mod = nullptr;
|
||||
ItemGiveObserveFn fn = nullptr;
|
||||
void* userData = nullptr;
|
||||
};
|
||||
|
||||
std::unordered_map<LoadedMod*, std::vector<GiveObserver>> s_modObservers;
|
||||
ItemGiveHandle s_nextGiveHandle = 1;
|
||||
size_t s_observerCount = 0;
|
||||
|
||||
void notify_gives(const char* checkName, uint8_t itemNo, fopAc_ac_c* giver, ItemGiveOrigin origin) {
|
||||
if (s_observerCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Callbacks may change registrations, so copy them before invoking one.
|
||||
std::vector<PendingObserver> observers;
|
||||
for (auto& mod : ModLoader::instance().mods()) {
|
||||
if (!mod.active) {
|
||||
continue;
|
||||
}
|
||||
const auto modIt = s_modObservers.find(&mod);
|
||||
if (modIt == s_modObservers.end()) {
|
||||
continue;
|
||||
}
|
||||
for (const auto& observer : modIt->second) {
|
||||
observers.push_back({.mod = &mod, .fn = observer.fn, .userData = observer.userData});
|
||||
}
|
||||
}
|
||||
|
||||
const ItemGiveInfo info{
|
||||
.check_name = checkName,
|
||||
.giver_actor = giver,
|
||||
.item = itemNo,
|
||||
.origin = static_cast<uint8_t>(origin),
|
||||
};
|
||||
for (const auto& observer : observers) {
|
||||
if (!observer.mod->active) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
observer.fn(observer.mod->context.get(), &info, observer.userData);
|
||||
} catch (const std::exception& e) {
|
||||
fail_mod(*observer.mod, MOD_ERROR,
|
||||
fmt::format("Exception in item give observer: {}", e.what()));
|
||||
} catch (...) {
|
||||
fail_mod(*observer.mod, MOD_ERROR, "Unknown exception in item give observer");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constexpr size_t kGiveQueueLimit = 64;
|
||||
constexpr int kGiveMaxRetries = 5;
|
||||
|
||||
struct QueuedGive {
|
||||
LoadedMod* owner = nullptr;
|
||||
uint32_t tag = 0;
|
||||
uint8_t itemNo = 0;
|
||||
bool silent = false;
|
||||
bool resolveAtDispatch = false;
|
||||
};
|
||||
|
||||
std::deque<QueuedGive> s_giveQueue;
|
||||
QueuedGive s_inFlightGive{};
|
||||
uint8_t s_inFlightItem = 0;
|
||||
int s_inFlightRetries = 0;
|
||||
bool s_inFlight = false;
|
||||
bool s_inFlightSpawned = false;
|
||||
bool s_dispatchingSilent = false;
|
||||
|
||||
bool safe_to_dispatch() {
|
||||
daAlink_c* link = daAlink_getAlinkActorClass();
|
||||
if (link == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// make sure player is in a safe action and not already in an event before dispatching
|
||||
switch (link->mProcID) {
|
||||
case daAlink_c::PROC_WAIT:
|
||||
case daAlink_c::PROC_TIRED_WAIT:
|
||||
case daAlink_c::PROC_MOVE:
|
||||
case daAlink_c::PROC_WOLF_WAIT:
|
||||
case daAlink_c::PROC_WOLF_TIRED_WAIT:
|
||||
case daAlink_c::PROC_WOLF_MOVE:
|
||||
case daAlink_c::PROC_ATN_MOVE:
|
||||
case daAlink_c::PROC_WOLF_ATN_AC_MOVE:
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
if (link->checkEventRun()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int itemId = 0;
|
||||
return link->mMsgFlow.getEventId(&itemId) == 0;
|
||||
}
|
||||
|
||||
bool resolve_queued_give(const QueuedGive& give, ItemGiveOrigin origin, uint8_t& outItem) {
|
||||
outItem = give.itemNo;
|
||||
if (give.resolveAtDispatch) {
|
||||
outItem = item_check(item_give_name(give.tag), give.itemNo, nullptr);
|
||||
}
|
||||
if (outItem != dItemNo_NONE_e) {
|
||||
return true;
|
||||
}
|
||||
|
||||
notify_gives(item_give_name(give.tag), dItemNo_NONE_e, nullptr, origin);
|
||||
return false;
|
||||
}
|
||||
|
||||
void dispatch_silent_give(const QueuedGive& give) {
|
||||
uint8_t itemNo = 0;
|
||||
if (!resolve_queued_give(give, ITEM_GIVE_ORIGIN_QUEUE_SILENT, itemNo)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.debug("dispatching silent item {:#x} for '{}'", itemNo,
|
||||
item_give_name(give.tag) != nullptr ? item_give_name(give.tag) : "");
|
||||
s_dispatchingSilent = true;
|
||||
execItemGet(itemNo, give.tag, nullptr);
|
||||
s_dispatchingSilent = false;
|
||||
}
|
||||
|
||||
void dispatch_demo_give() {
|
||||
Log.debug("dispatching item {:#x} for '{}'", s_inFlightItem,
|
||||
item_give_name(s_inFlightGive.tag) != nullptr ? item_give_name(s_inFlightGive.tag) : "");
|
||||
|
||||
daAlink_c* link = daAlink_getAlinkActorClass();
|
||||
dComIfGp_getEvent()->setGtItm(s_inFlightItem);
|
||||
link->procCoGetItemInit();
|
||||
const s16 eventIndex = dComIfGp_getEventManager().getEventIdx(link, "DEFAULT_GETITEM", 0xFF);
|
||||
fopAcM_orderChangeEventId(link, eventIndex, 1, 0xFFFF);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint32_t item_give_tag(const char* name) {
|
||||
if (name == nullptr || *name == '\0') {
|
||||
return 0;
|
||||
}
|
||||
if (const auto it = s_giveNameIds.find(name); it != s_giveNameIds.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
s_giveNames.emplace_back(name);
|
||||
const auto tag = static_cast<uint32_t>(s_giveNames.size());
|
||||
s_giveNameIds.emplace(s_giveNames.back(), tag);
|
||||
return tag;
|
||||
}
|
||||
|
||||
uint8_t item_check_tagged(uint32_t giveTag, uint8_t itemNo, fopAc_ac_c* giver) {
|
||||
const char* name = item_give_name(giveTag);
|
||||
return name != nullptr ? item_check(name, itemNo, giver) : itemNo;
|
||||
}
|
||||
|
||||
void item_check_enqueue(const char* name, uint8_t itemNo) {
|
||||
if (s_giveQueue.size() >= kGiveQueueLimit) {
|
||||
Log.warn("item give queue is full; dropping check '{}'", name != nullptr ? name : "");
|
||||
return;
|
||||
}
|
||||
s_giveQueue.push_back({
|
||||
.tag = item_give_tag(name),
|
||||
.itemNo = itemNo,
|
||||
.resolveAtDispatch = true,
|
||||
});
|
||||
}
|
||||
|
||||
void item_granted(uint8_t itemNo, uint32_t giveTag, fopAc_ac_c* giver) {
|
||||
ItemGiveOrigin origin = ITEM_GIVE_ORIGIN_GAME;
|
||||
if (s_dispatchingSilent) {
|
||||
origin = ITEM_GIVE_ORIGIN_QUEUE_SILENT;
|
||||
} else if (s_inFlight && itemNo == s_inFlightItem && giveTag == s_inFlightGive.tag) {
|
||||
origin = ITEM_GIVE_ORIGIN_QUEUE;
|
||||
s_inFlight = false;
|
||||
s_inFlightSpawned = false;
|
||||
}
|
||||
notify_gives(item_give_name(giveTag), itemNo, giver, origin);
|
||||
}
|
||||
|
||||
bool item_give_queue_dispatching() {
|
||||
return s_inFlight && !s_inFlightSpawned;
|
||||
}
|
||||
|
||||
uint32_t item_give_queue_take_tag() {
|
||||
if (!item_give_queue_dispatching()) {
|
||||
return 0;
|
||||
}
|
||||
s_inFlightSpawned = true;
|
||||
return s_inFlightGive.tag;
|
||||
}
|
||||
|
||||
namespace svc {
|
||||
|
||||
void item_gives_tick() {
|
||||
if ((!s_inFlight && s_giveQueue.empty()) || !safe_to_dispatch()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_inFlight) {
|
||||
if (++s_inFlightRetries > kGiveMaxRetries) {
|
||||
Log.error("item {:#x} for '{}' did not complete after {} attempts; dropping it",
|
||||
s_inFlightItem,
|
||||
item_give_name(s_inFlightGive.tag) != nullptr ? item_give_name(s_inFlightGive.tag) :
|
||||
"",
|
||||
kGiveMaxRetries);
|
||||
s_inFlight = false;
|
||||
s_inFlightSpawned = false;
|
||||
return;
|
||||
}
|
||||
s_inFlightSpawned = false;
|
||||
dispatch_demo_give();
|
||||
return;
|
||||
}
|
||||
|
||||
while (!s_giveQueue.empty() && s_giveQueue.front().silent) {
|
||||
const QueuedGive give = s_giveQueue.front();
|
||||
s_giveQueue.pop_front();
|
||||
dispatch_silent_give(give);
|
||||
}
|
||||
if (s_giveQueue.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QueuedGive give = s_giveQueue.front();
|
||||
uint8_t itemNo = 0;
|
||||
if (!resolve_queued_give(give, ITEM_GIVE_ORIGIN_QUEUE, itemNo)) {
|
||||
s_giveQueue.pop_front();
|
||||
return;
|
||||
}
|
||||
|
||||
s_giveQueue.pop_front();
|
||||
s_inFlightGive = give;
|
||||
s_inFlightItem = itemNo;
|
||||
s_inFlightRetries = 0;
|
||||
s_inFlight = true;
|
||||
s_inFlightSpawned = false;
|
||||
dispatch_demo_give();
|
||||
}
|
||||
|
||||
void item_gives_clear() {
|
||||
if (!s_giveQueue.empty() || s_inFlight) {
|
||||
Log.info("dropping {} pending item give(s)",
|
||||
s_giveQueue.size() + static_cast<size_t>(s_inFlight));
|
||||
}
|
||||
s_giveQueue.clear();
|
||||
s_inFlight = false;
|
||||
s_inFlightSpawned = false;
|
||||
}
|
||||
|
||||
ModResult item_give_enqueue(LoadedMod& mod, const char* checkName, uint8_t itemNo, uint32_t flags) {
|
||||
if (s_giveQueue.size() >= kGiveQueueLimit) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
s_giveQueue.push_back({
|
||||
.owner = &mod,
|
||||
.tag = item_give_tag(checkName),
|
||||
.itemNo = itemNo,
|
||||
.silent = (flags & ITEM_GIVE_SILENT) != 0,
|
||||
.resolveAtDispatch = (flags & ITEM_GIVE_RESOLVE) != 0,
|
||||
});
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult item_give_add_observer(
|
||||
LoadedMod& mod, ItemGiveObserveFn fn, void* userData, ItemGiveHandle& outHandle) {
|
||||
auto& observer = s_modObservers[&mod].emplace_back();
|
||||
observer.handle = s_nextGiveHandle++;
|
||||
observer.fn = fn;
|
||||
observer.userData = userData;
|
||||
outHandle = observer.handle;
|
||||
++s_observerCount;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult item_give_remove_observer(LoadedMod& mod, ItemGiveHandle handle) {
|
||||
const auto modIt = s_modObservers.find(&mod);
|
||||
if (modIt == s_modObservers.end()) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
const auto removed = std::erase_if(
|
||||
modIt->second, [&](const auto& observer) { return observer.handle == handle; });
|
||||
s_observerCount -= removed;
|
||||
return removed != 0 ? MOD_OK : MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
void item_gives_remove_mod(LoadedMod& mod) {
|
||||
if (const auto modIt = s_modObservers.find(&mod); modIt != s_modObservers.end()) {
|
||||
s_observerCount -= modIt->second.size();
|
||||
s_modObservers.erase(modIt);
|
||||
}
|
||||
std::erase_if(s_giveQueue, [&](const QueuedGive& give) { return give.owner == &mod; });
|
||||
if (s_inFlight && s_inFlightGive.owner == &mod) {
|
||||
// The event system already owns this grant, so it can no longer be canceled safely.
|
||||
s_inFlightGive.owner = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace svc
|
||||
} // namespace dusk::mods
|
||||
@@ -899,6 +899,7 @@ bool ModLoader::activate_mod(LoadedMod& mod) {
|
||||
}
|
||||
|
||||
void ModLoader::deactivate_mod(LoadedMod& mod) {
|
||||
svc::modules_mod_deactivating(mod);
|
||||
if (mod.initialized && mod.native && mod.native->fn_shutdown) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_TRACE, "calling mod_shutdown");
|
||||
try {
|
||||
|
||||
+92
-26
@@ -9,8 +9,10 @@
|
||||
|
||||
#include <aurora/gfx.hpp>
|
||||
#include <aurora/webgpu.hpp>
|
||||
#include <dolphin/gx/GXAurora.h>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -126,23 +128,34 @@ GfxSlot* resolve_owned_slot_locked(LoadedMod& mod, uint64_t handle, GfxSlotKind
|
||||
return &entry->value;
|
||||
}
|
||||
|
||||
void collect_mod_types_locked(LoadedMod& owner, std::vector<aurora::gfx::DrawTypeId>& drawIds,
|
||||
void take_mod_types_locked(LoadedMod& owner, std::vector<aurora::gfx::DrawTypeId>& drawIds,
|
||||
std::vector<aurora::gfx::EncoderTaskId>& taskIds) {
|
||||
s_slots.for_each([&](uint64_t, const auto& entry) {
|
||||
std::vector<uint64_t> drawHandles;
|
||||
std::vector<uint64_t> taskHandles;
|
||||
s_slots.for_each([&](uint64_t handle, const auto& entry) {
|
||||
if (entry.owner != &owner) {
|
||||
return;
|
||||
}
|
||||
const auto& slot = entry.value;
|
||||
if (slot.kind == GfxSlotKind::DrawType && slot.auroraDrawId != aurora::gfx::InvalidDrawType)
|
||||
{
|
||||
drawIds.push_back(slot.auroraDrawId);
|
||||
drawHandles.push_back(handle);
|
||||
} else if ((slot.kind == GfxSlotKind::ComputeType ||
|
||||
slot.kind == GfxSlotKind::PresentTarget) &&
|
||||
slot.auroraTaskId != aurora::gfx::InvalidEncoderTask)
|
||||
{
|
||||
taskIds.push_back(slot.auroraTaskId);
|
||||
taskHandles.push_back(handle);
|
||||
}
|
||||
});
|
||||
for (const auto handle : drawHandles) {
|
||||
auto* entry = s_slots.find(handle);
|
||||
drawIds.push_back(std::exchange(entry->value.auroraDrawId, aurora::gfx::InvalidDrawType));
|
||||
}
|
||||
for (const auto handle : taskHandles) {
|
||||
auto* entry = s_slots.find(handle);
|
||||
taskIds.push_back(
|
||||
std::exchange(entry->value.auroraTaskId, aurora::gfx::InvalidEncoderTask));
|
||||
}
|
||||
}
|
||||
|
||||
void unregister_aurora_types(const std::vector<aurora::gfx::DrawTypeId>& drawIds,
|
||||
@@ -155,6 +168,49 @@ void unregister_aurora_types(const std::vector<aurora::gfx::DrawTypeId>& drawIds
|
||||
}
|
||||
}
|
||||
|
||||
void gfx_mod_deactivating(LoadedMod& mod) {
|
||||
std::vector<aurora::gfx::DrawTypeId> drawIds;
|
||||
std::vector<aurora::gfx::EncoderTaskId> taskIds;
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
take_mod_types_locked(mod, drawIds, taskIds);
|
||||
}
|
||||
unregister_aurora_types(drawIds, taskIds);
|
||||
if (!drawIds.empty() || !taskIds.empty()) {
|
||||
aurora::gfx::synchronize();
|
||||
}
|
||||
}
|
||||
|
||||
GfxAttachmentSemantic gfx_attachment_semantic(aurora::gfx::ColorAttachmentSemantic semantic) {
|
||||
switch (semantic) {
|
||||
case aurora::gfx::ColorAttachmentSemantic::SceneColor:
|
||||
return GFX_ATTACHMENT_SCENE_COLOR;
|
||||
case aurora::gfx::ColorAttachmentSemantic::Normal:
|
||||
return GFX_ATTACHMENT_NORMAL;
|
||||
case aurora::gfx::ColorAttachmentSemantic::Auxiliary:
|
||||
return GFX_ATTACHMENT_AUXILIARY;
|
||||
}
|
||||
return GFX_ATTACHMENT_AUXILIARY;
|
||||
}
|
||||
|
||||
GfxRenderTargetLayout gfx_render_target_layout(const aurora::gfx::RenderTargetLayout& layout) {
|
||||
GfxRenderTargetLayout result = GFX_RENDER_TARGET_LAYOUT_INIT;
|
||||
result.key = layout.key;
|
||||
result.color_attachment_count =
|
||||
std::min<uint32_t>(layout.colorAttachmentCount, GFX_MAX_COLOR_ATTACHMENTS);
|
||||
for (uint32_t i = 0; i < result.color_attachment_count; ++i) {
|
||||
result.color_attachments[i] = {
|
||||
.semantic = gfx_attachment_semantic(layout.colorAttachments[i].semantic),
|
||||
.format = static_cast<WGPUTextureFormat>(layout.colorAttachments[i].format),
|
||||
.width = layout.colorAttachments[i].width,
|
||||
.height = layout.colorAttachments[i].height,
|
||||
};
|
||||
}
|
||||
result.depth_stencil_format = static_cast<WGPUTextureFormat>(layout.depthStencilFormat);
|
||||
result.sample_count = layout.sampleCount;
|
||||
return result;
|
||||
}
|
||||
|
||||
void draw_trampoline(const aurora::gfx::DrawContext& ctx, const wgpu::RenderPassEncoder& pass,
|
||||
const void* payload, size_t payloadSize, void* userdata) {
|
||||
const auto handle = static_cast<uint64_t>(reinterpret_cast<uintptr_t>(userdata));
|
||||
@@ -184,12 +240,12 @@ void draw_trampoline(const aurora::gfx::DrawContext& ctx, const wgpu::RenderPass
|
||||
.index_buffer = ctx.indexBuffer.Get(),
|
||||
.uniform_buffer = ctx.uniformBuffer.Get(),
|
||||
.storage_buffer = ctx.storageBuffer.Get(),
|
||||
.color_format = static_cast<WGPUTextureFormat>(ctx.colorFormat),
|
||||
.depth_format = static_cast<WGPUTextureFormat>(ctx.depthFormat),
|
||||
.sample_count = ctx.sampleCount,
|
||||
.target_width = ctx.targetWidth,
|
||||
.target_height = ctx.targetHeight,
|
||||
.color_format = static_cast<WGPUTextureFormat>(
|
||||
ctx.layout.colorAttachments[GFX_SCENE_COLOR_ATTACHMENT_INDEX].format),
|
||||
.depth_format = static_cast<WGPUTextureFormat>(ctx.layout.depthStencilFormat),
|
||||
.sample_count = ctx.layout.sampleCount,
|
||||
.uses_reversed_z = aurora::gfx::uses_reversed_z(),
|
||||
.layout = gfx_render_target_layout(ctx.layout),
|
||||
};
|
||||
|
||||
std::string failure;
|
||||
@@ -778,8 +834,10 @@ ModResult gfx_unregister_present_target(LoadedMod& mod, uint64_t handle) {
|
||||
auroraId = slot->auroraTaskId;
|
||||
}
|
||||
|
||||
aurora::gfx::unregister_encoder_task_type(auroraId);
|
||||
aurora::gfx::synchronize();
|
||||
if (auroraId != aurora::gfx::InvalidEncoderTask) {
|
||||
aurora::gfx::unregister_encoder_task_type(auroraId);
|
||||
aurora::gfx::synchronize();
|
||||
}
|
||||
|
||||
std::optional<GfxSlotMap::Entry> removed;
|
||||
{
|
||||
@@ -903,6 +961,7 @@ void gfx_run_stage(
|
||||
.game_viewport = gameViewport,
|
||||
};
|
||||
|
||||
AuroraGXSync();
|
||||
for (const auto& entry : entries) {
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
@@ -924,6 +983,7 @@ void gfx_run_stage(
|
||||
fail_mod(*entry.owner, MOD_ERROR, "unknown exception in gfx stage callback");
|
||||
}
|
||||
|
||||
AuroraGXSync();
|
||||
if (aurora::gfx::is_offscreen() != wasOffscreen) {
|
||||
aurora::gfx::ResolvedTargets discarded;
|
||||
aurora::gfx::resolve_pass(
|
||||
@@ -935,17 +995,8 @@ void gfx_run_stage(
|
||||
}
|
||||
}
|
||||
|
||||
void gfx_remove_mod(LoadedMod& mod) {
|
||||
std::vector<aurora::gfx::DrawTypeId> drawIds;
|
||||
std::vector<aurora::gfx::EncoderTaskId> taskIds;
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
collect_mod_types_locked(mod, drawIds, taskIds);
|
||||
}
|
||||
unregister_aurora_types(drawIds, taskIds);
|
||||
if (!drawIds.empty() || !taskIds.empty()) {
|
||||
aurora::gfx::synchronize();
|
||||
}
|
||||
void gfx_mod_detached(LoadedMod& mod) {
|
||||
gfx_mod_deactivating(mod);
|
||||
|
||||
std::vector<GfxSlotMap::Entry> entries;
|
||||
{
|
||||
@@ -966,7 +1017,7 @@ void gfx_remove_mod(LoadedMod& mod) {
|
||||
}
|
||||
}
|
||||
|
||||
void gfx_drain_worker_failures() {
|
||||
void gfx_frame_begin() {
|
||||
std::vector<WorkerFailure> failures;
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
@@ -979,7 +1030,7 @@ void gfx_drain_worker_failures() {
|
||||
for (const auto& failure : failures) {
|
||||
for (auto& mod : ModLoader::instance().mods()) {
|
||||
if (mod.metadata.id == failure.modId && mod.active) {
|
||||
gfx_remove_mod(mod);
|
||||
gfx_mod_detached(mod);
|
||||
fail_mod(mod, MOD_ERROR, failure.message);
|
||||
break;
|
||||
}
|
||||
@@ -1030,6 +1081,19 @@ ModResult gfx_get_device_info(ModContext* context, GfxDeviceInfo* outInfo) {
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult gfx_get_scene_target_layout(ModContext* context, GfxRenderTargetLayout* outLayout) {
|
||||
if (outLayout == nullptr || outLayout->struct_size < sizeof(GfxRenderTargetLayout) ||
|
||||
mod_from_context(context) == nullptr)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
const uint32_t structSize = outLayout->struct_size;
|
||||
*outLayout = gfx_render_target_layout(aurora::gfx::scene_render_target_layout());
|
||||
outLayout->struct_size = structSize;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
void* gfx_get_proc_address(ModContext* context, const char* name) {
|
||||
if (mod_from_context(context) == nullptr || name == nullptr) {
|
||||
return nullptr;
|
||||
@@ -1327,6 +1391,7 @@ constexpr GfxService s_gfxService{
|
||||
.resize_present_target = gfx_resize_present_target_impl,
|
||||
.unregister_present_target = gfx_unregister_present_target_impl,
|
||||
.push_present = gfx_push_present_impl,
|
||||
.get_scene_target_layout = gfx_get_scene_target_layout,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@@ -1336,8 +1401,9 @@ constinit const ServiceModule g_gfxModule{
|
||||
.majorVersion = GFX_SERVICE_MAJOR,
|
||||
.minorVersion = GFX_SERVICE_MINOR,
|
||||
.service = &s_gfxService,
|
||||
.modDetached = gfx_remove_mod,
|
||||
.frameBegin = gfx_drain_worker_failures,
|
||||
.modDeactivating = gfx_mod_deactivating,
|
||||
.modDetached = gfx_mod_detached,
|
||||
.frameBegin = gfx_frame_begin,
|
||||
};
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
#include "item.hpp"
|
||||
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "dusk/mods/item.hpp"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
|
||||
#include "d/d_item_data.h"
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
constexpr size_t kMaxCheckNameLength = 256;
|
||||
constexpr uint32_t kGiveFlagMask = ITEM_GIVE_SILENT | ITEM_GIVE_RESOLVE;
|
||||
|
||||
bool is_valid_check_name(const char* name) {
|
||||
if (name == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const std::string_view view{name};
|
||||
return !view.empty() && view.size() <= kMaxCheckNameLength;
|
||||
}
|
||||
|
||||
ModResult item_set_check_override(ModContext* context, const char* name, uint8_t itemNo) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || !is_valid_check_name(name)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return item_check_set_override(*mod, name, itemNo);
|
||||
}
|
||||
|
||||
ModResult item_clear_check_override(ModContext* context, const char* name) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || !is_valid_check_name(name)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return item_check_clear_override(*mod, name);
|
||||
}
|
||||
|
||||
ModResult item_set_check_resolver(ModContext* context, const char* name, ItemCheckResolveFn fn,
|
||||
void* userData, ItemCheckHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || fn == nullptr || (name != nullptr && !is_valid_check_name(name))) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
ItemCheckHandle handle = 0;
|
||||
const auto result = item_check_add_resolver(*mod, name, fn, userData, handle);
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = handle;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ModResult item_clear_check_resolver(ModContext* context, ItemCheckHandle handle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || handle == 0) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return item_check_remove_resolver(*mod, handle);
|
||||
}
|
||||
|
||||
ModResult item_resolve_check(
|
||||
ModContext* context, const char* name, uint8_t originalItemNo, uint8_t* outItem) {
|
||||
if (mod_from_context(context) == nullptr || !is_valid_check_name(name) || outItem == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
*outItem = item_check(name, originalItemNo, nullptr);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult item_give_item(
|
||||
ModContext* context, const char* checkName, uint8_t itemNo, uint32_t flags) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || (checkName != nullptr && !is_valid_check_name(checkName)) ||
|
||||
(flags & ~kGiveFlagMask) != 0)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if ((flags & ITEM_GIVE_RESOLVE) != 0) {
|
||||
if (checkName == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
} else if (itemNo == dItemNo_NONE_e) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return item_give_enqueue(*mod, checkName, itemNo, flags);
|
||||
}
|
||||
|
||||
ModResult item_observe_gives(
|
||||
ModContext* context, ItemGiveObserveFn fn, void* userData, ItemGiveHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || fn == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
ItemGiveHandle handle = 0;
|
||||
const auto result = item_give_add_observer(*mod, fn, userData, handle);
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = handle;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ModResult item_unobserve_gives(ModContext* context, ItemGiveHandle handle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || handle == 0) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return item_give_remove_observer(*mod, handle);
|
||||
}
|
||||
|
||||
constexpr ItemService s_itemService{
|
||||
.header = SERVICE_HEADER(ItemService, ITEM_SERVICE_MAJOR, ITEM_SERVICE_MINOR),
|
||||
.set_check_override = item_set_check_override,
|
||||
.clear_check_override = item_clear_check_override,
|
||||
.set_check_resolver = item_set_check_resolver,
|
||||
.clear_check_resolver = item_clear_check_resolver,
|
||||
.resolve_check = item_resolve_check,
|
||||
.give_item = item_give_item,
|
||||
.observe_gives = item_observe_gives,
|
||||
.unobserve_gives = item_unobserve_gives,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
constinit const ServiceModule g_itemModule{
|
||||
.id = ITEM_SERVICE_ID,
|
||||
.majorVersion = ITEM_SERVICE_MAJOR,
|
||||
.minorVersion = ITEM_SERVICE_MINOR,
|
||||
.service = &s_itemService,
|
||||
.modDetached =
|
||||
[](LoadedMod& mod) {
|
||||
item_checks_remove_mod(mod);
|
||||
item_gives_remove_mod(mod);
|
||||
},
|
||||
.frameEnd = item_gives_tick,
|
||||
.shutdown = item_gives_clear,
|
||||
};
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/svc/item.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace dusk::mods {
|
||||
|
||||
struct LoadedMod;
|
||||
|
||||
namespace svc {
|
||||
|
||||
ModResult item_check_set_override(LoadedMod& mod, const char* name, uint8_t itemNo);
|
||||
ModResult item_check_clear_override(LoadedMod& mod, const char* name);
|
||||
ModResult item_check_add_resolver(LoadedMod& mod, const char* name, ItemCheckResolveFn fn,
|
||||
void* userData, ItemCheckHandle& outHandle);
|
||||
ModResult item_check_remove_resolver(LoadedMod& mod, ItemCheckHandle handle);
|
||||
void item_checks_remove_mod(LoadedMod& mod);
|
||||
|
||||
ModResult item_give_enqueue(LoadedMod& mod, const char* checkName, uint8_t itemNo, uint32_t flags);
|
||||
ModResult item_give_add_observer(
|
||||
LoadedMod& mod, ItemGiveObserveFn fn, void* userData, ItemGiveHandle& outHandle);
|
||||
ModResult item_give_remove_observer(LoadedMod& mod, ItemGiveHandle handle);
|
||||
void item_gives_remove_mod(LoadedMod& mod);
|
||||
void item_gives_tick();
|
||||
void item_gives_clear();
|
||||
|
||||
} // namespace svc
|
||||
} // namespace dusk::mods
|
||||
@@ -150,6 +150,14 @@ ModResult register_module(const ServiceModule& module) {
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
void modules_mod_deactivating(LoadedMod& mod) {
|
||||
for (const auto* module : s_modules | std::views::reverse) {
|
||||
if (module->modDeactivating != nullptr) {
|
||||
module->modDeactivating(mod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void modules_mod_detached(LoadedMod& mod) {
|
||||
for (const auto* module : s_modules | std::views::reverse) {
|
||||
if (module->modDetached != nullptr) {
|
||||
@@ -213,6 +221,7 @@ void ModLoader::init_services() {
|
||||
&svc::g_gfxModule,
|
||||
&svc::g_saveModule,
|
||||
&svc::g_stageModule,
|
||||
&svc::g_itemModule,
|
||||
&svc::g_gamemodeModule,
|
||||
})
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@ struct ServiceRecord {
|
||||
};
|
||||
|
||||
// A host service and its lifecycle hooks. Every hook is optional. Frame and lifecycle hooks run in
|
||||
// registration order, modDetached in reverse registration order.
|
||||
// registration order, teardown hooks in reverse registration order.
|
||||
struct ServiceModule {
|
||||
const char* id = nullptr;
|
||||
uint16_t majorVersion = 0;
|
||||
@@ -28,6 +28,9 @@ struct ServiceModule {
|
||||
|
||||
// One-time setup, at registration (ModLoader::init_services).
|
||||
void (*initialize)() = nullptr;
|
||||
// A mod is beginning deactivation: stop callbacks that may execute concurrently. Service state
|
||||
// remains registered so mod_shutdown may release it normally.
|
||||
void (*modDeactivating)(LoadedMod& mod) = nullptr;
|
||||
// A mod is going away (deactivation or failed activation): drop all state held for it.
|
||||
// Runs after the mod's mod_shutdown and before its library unloads, so pointers into
|
||||
// the mod are still valid but must not be called.
|
||||
@@ -55,6 +58,7 @@ const ServiceRecord* find_service(
|
||||
const ServiceRecord* find_service_record(const char* serviceId, uint16_t majorVersion);
|
||||
|
||||
ModResult register_module(const ServiceModule& module);
|
||||
void modules_mod_deactivating(LoadedMod& mod);
|
||||
void modules_mod_detached(LoadedMod& mod);
|
||||
void modules_lifecycle_applied();
|
||||
void modules_frame_begin();
|
||||
@@ -75,6 +79,7 @@ extern const ServiceModule g_windowModule;
|
||||
extern const ServiceModule g_gfxModule;
|
||||
extern const ServiceModule g_saveModule;
|
||||
extern const ServiceModule g_stageModule;
|
||||
extern const ServiceModule g_itemModule;
|
||||
extern const ServiceModule g_gamemodeModule;
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "save.hpp"
|
||||
|
||||
#include "item.hpp"
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
@@ -164,6 +165,7 @@ void save_slot_new(uint32_t slot) {
|
||||
store.mods.clear();
|
||||
store.snapshotValid = false;
|
||||
s_currentSlot = static_cast<int32_t>(slot);
|
||||
item_gives_clear();
|
||||
Log.info("new save in slot {}; mod blob store cleared", slot);
|
||||
notify(slot, &SaveObserverRecord::onNewSave, "new-save");
|
||||
}
|
||||
@@ -183,6 +185,7 @@ void save_slot_loaded(uint32_t slot, const void* slotData) {
|
||||
}
|
||||
}
|
||||
s_currentSlot = static_cast<int32_t>(slot);
|
||||
item_gives_clear();
|
||||
notify(slot, &SaveObserverRecord::onLoaded, "save-loaded");
|
||||
}
|
||||
|
||||
@@ -222,6 +225,7 @@ void save_slot_erased(uint32_t slot) {
|
||||
|
||||
void save_no_slot() {
|
||||
s_currentSlot = -1;
|
||||
item_gives_clear();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -10,6 +10,10 @@ namespace dusk::presentation {
|
||||
namespace {
|
||||
|
||||
float preferred_frame_rate() {
|
||||
if (getTransientSettings().turboMode) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
switch (getSettings().game.enableFrameInterpolation.getValue()) {
|
||||
case FrameInterpMode::Off:
|
||||
return 30.0f;
|
||||
|
||||
@@ -51,6 +51,7 @@ UserSettings g_userSettings = {
|
||||
.sunsSong {"game.sunsSong", false},
|
||||
.autoSave {"game.autoSave", false},
|
||||
.enhancedMapMenus {"game.enhancedMapMenus", false},
|
||||
.aimingReticle {"game.aimingReticle", false},
|
||||
|
||||
// Preferences
|
||||
.enableMirrorMode {"game.enableMirrorMode", false},
|
||||
@@ -75,6 +76,7 @@ UserSettings g_userSettings = {
|
||||
.resampler {"game.resampler", Resampler::Bilinear},
|
||||
.enableMapBackground {"game.enableMapBackground", true},
|
||||
.disableCutscenePillarboxing {"game.disableCutscenePillarboxing", false},
|
||||
.enableHighQualityMinimapTextures {"game.enableHighQualityMinimapTextures", true},
|
||||
|
||||
// Audio
|
||||
.noLowHpSound {"game.noLowHpSound", false},
|
||||
@@ -256,6 +258,7 @@ void registerSettings() {
|
||||
Register(g_userSettings.game.sunsSong);
|
||||
Register(g_userSettings.game.autoSave);
|
||||
Register(g_userSettings.game.enhancedMapMenus);
|
||||
Register(g_userSettings.game.aimingReticle);
|
||||
Register(g_userSettings.game.enableMirrorMode);
|
||||
Register(g_userSettings.game.invertCameraXAxis);
|
||||
Register(g_userSettings.game.invertCameraYAxis);
|
||||
@@ -282,6 +285,7 @@ void registerSettings() {
|
||||
Register(g_userSettings.game.shadowResolutionMultiplier);
|
||||
Register(g_userSettings.game.enableMapBackground);
|
||||
Register(g_userSettings.game.disableCutscenePillarboxing);
|
||||
Register(g_userSettings.game.enableHighQualityMinimapTextures);
|
||||
Register(g_userSettings.game.enableFastIronBoots);
|
||||
Register(g_userSettings.game.canTransformAnywhere);
|
||||
Register(g_userSettings.game.fastRoll);
|
||||
@@ -394,7 +398,7 @@ static TransientSettings g_transientSettings = {
|
||||
.colliderViewOpacity = 50.0f,
|
||||
.drawRange = 100.0f,
|
||||
},
|
||||
.skipFrameRateLimit = false,
|
||||
.turboMode = false,
|
||||
};
|
||||
|
||||
TransientSettings& getTransientSettings() {
|
||||
|
||||
+5
-2
@@ -33,6 +33,7 @@ enum class GameLanguage : u8 {
|
||||
French = OS_LANGUAGE_FRENCH,
|
||||
Spanish = OS_LANGUAGE_SPANISH,
|
||||
Italian = OS_LANGUAGE_ITALIAN,
|
||||
Japanese = 6,
|
||||
};
|
||||
|
||||
enum class DiscVerificationState : u8 {
|
||||
@@ -89,7 +90,7 @@ struct ConfigEnumRange<Resampler> {
|
||||
template <>
|
||||
struct ConfigEnumRange<GameLanguage> {
|
||||
static constexpr auto min = GameLanguage::English;
|
||||
static constexpr auto max = GameLanguage::Italian;
|
||||
static constexpr auto max = GameLanguage::Japanese;
|
||||
};
|
||||
|
||||
template <>
|
||||
@@ -183,6 +184,7 @@ struct UserSettings {
|
||||
ConfigVar<bool> sunsSong;
|
||||
ConfigVar<bool> autoSave;
|
||||
ConfigVar<bool> enhancedMapMenus;
|
||||
ConfigVar<bool> aimingReticle;
|
||||
|
||||
// Preferences
|
||||
ConfigVar<bool> enableMirrorMode;
|
||||
@@ -207,6 +209,7 @@ struct UserSettings {
|
||||
ConfigVar<Resampler> resampler;
|
||||
ConfigVar<bool> enableMapBackground;
|
||||
ConfigVar<bool> disableCutscenePillarboxing;
|
||||
ConfigVar<bool> enableHighQualityMinimapTextures;
|
||||
|
||||
// Audio
|
||||
ConfigVar<bool> noLowHpSound;
|
||||
@@ -327,7 +330,7 @@ struct CollisionViewSettings {
|
||||
|
||||
struct TransientSettings {
|
||||
CollisionViewSettings collisionView;
|
||||
bool skipFrameRateLimit;
|
||||
bool turboMode;
|
||||
bool moveLinkActive;
|
||||
bool stateShareLoadActive;
|
||||
};
|
||||
|
||||
+12
-6
@@ -9,18 +9,23 @@ namespace dusk::speedrun {
|
||||
struct SpeedrunInfo {
|
||||
void startRun() {
|
||||
m_isRunStarted = true;
|
||||
m_startTimestamp = OSGetTime();
|
||||
m_rtaStartTimestamp = OSGetNativeTime();
|
||||
m_igtStartTimestamp = OSGetTime();
|
||||
}
|
||||
|
||||
void stopRun() {
|
||||
m_isRunStarted = false;
|
||||
m_endTimestamp = OSGetTime() - m_startTimestamp;
|
||||
m_rtaTimer = OSGetNativeTime() - m_rtaStartTimestamp;
|
||||
if (!m_isPauseIGT) {
|
||||
m_igtTimer = OSGetTime() - m_igtStartTimestamp - m_totalLoadTime;
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
m_isRunStarted = false;
|
||||
m_startTimestamp = 0;
|
||||
m_endTimestamp = 0;
|
||||
m_rtaStartTimestamp = 0;
|
||||
m_rtaTimer = 0;
|
||||
m_igtStartTimestamp = 0;
|
||||
m_isPauseIGT = false;
|
||||
m_loadStartTimestamp = 0;
|
||||
m_totalLoadTime = 0;
|
||||
@@ -28,8 +33,9 @@ struct SpeedrunInfo {
|
||||
}
|
||||
|
||||
bool m_isRunStarted = false;
|
||||
OSTime m_startTimestamp = 0;
|
||||
OSTime m_endTimestamp = 0;
|
||||
OSTime m_rtaStartTimestamp = 0;
|
||||
OSTime m_rtaTimer = 0;
|
||||
OSTime m_igtStartTimestamp = 0;
|
||||
|
||||
bool m_isPauseIGT = false;
|
||||
OSTime m_loadStartTimestamp = 0;
|
||||
|
||||
+25
-16
@@ -203,10 +203,19 @@ void remove_element(Rml::Element*& elem) noexcept {
|
||||
|
||||
} // namespace
|
||||
|
||||
static std::string FormatTime(OSTime ticks) {
|
||||
OSCalendarTime t;
|
||||
OSTicksToCalendarTime(ticks, &t);
|
||||
return fmt::format("{0:02}:{1:02}:{2:02}.{3:03}", t.hour, t.min, t.sec, t.msec);
|
||||
static std::string FormatElapsedTime(OSTime ticksElapsed) {
|
||||
using namespace std::chrono;
|
||||
|
||||
milliseconds ms{OSTicksToMilliseconds(ticksElapsed)};
|
||||
|
||||
const hours hr = duration_cast<hours>(ms);
|
||||
ms -= hr;
|
||||
const minutes min = duration_cast<minutes>(ms);
|
||||
ms -= min;
|
||||
const seconds sec = duration_cast<seconds>(ms);
|
||||
ms -= sec;
|
||||
|
||||
return fmt::format("{0:02}:{1:02}:{2:02}.{3:03}", hr.count(), min.count(), sec.count(), ms.count());
|
||||
}
|
||||
|
||||
Overlay::Overlay() : Document(kDocumentSource, true, DocumentScope::Overlay) {
|
||||
@@ -316,34 +325,34 @@ void Overlay::update() {
|
||||
if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) &&
|
||||
mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigY(PAD_1))
|
||||
{
|
||||
if (dusk::speedrun::g_speedrunInfo.m_isRunStarted) {
|
||||
dusk::speedrun::g_speedrunInfo.m_endTimestamp = OSGetTime() - dusk::speedrun::g_speedrunInfo.m_startTimestamp;
|
||||
dusk::speedrun::g_speedrunInfo.m_isRunStarted = false;
|
||||
if (speedrun::g_speedrunInfo.m_isRunStarted) {
|
||||
speedrun::g_speedrunInfo.stopRun();
|
||||
}
|
||||
}
|
||||
|
||||
OSTime elapsedTime = 0;
|
||||
if (dusk::speedrun::g_speedrunInfo.m_isRunStarted) {
|
||||
elapsedTime = OSGetTime() - dusk::speedrun::g_speedrunInfo.m_startTimestamp;
|
||||
} else if (dusk::speedrun::g_speedrunInfo.m_endTimestamp != 0) {
|
||||
elapsedTime = dusk::speedrun::g_speedrunInfo.m_endTimestamp;
|
||||
OSTime rtaElapsedTime = 0;
|
||||
if (speedrun::g_speedrunInfo.m_isRunStarted) {
|
||||
rtaElapsedTime = OSGetNativeTime() - speedrun::g_speedrunInfo.m_rtaStartTimestamp;
|
||||
} else if (speedrun::g_speedrunInfo.m_rtaTimer != 0) {
|
||||
rtaElapsedTime = speedrun::g_speedrunInfo.m_rtaTimer;
|
||||
}
|
||||
|
||||
if (!dusk::speedrun::g_speedrunInfo.m_isPauseIGT) {
|
||||
dusk::speedrun::g_speedrunInfo.m_igtTimer = elapsedTime - dusk::speedrun::g_speedrunInfo.m_totalLoadTime;
|
||||
if (speedrun::g_speedrunInfo.m_isRunStarted && !speedrun::g_speedrunInfo.m_isRunStarted) {
|
||||
speedrun::g_speedrunInfo.m_igtTimer = OSGetTime() - speedrun::g_speedrunInfo.m_igtStartTimestamp -
|
||||
speedrun::g_speedrunInfo.m_totalLoadTime;
|
||||
}
|
||||
|
||||
mSpeedrunTimer->SetAttribute("open", "");
|
||||
|
||||
if (getSettings().game.showSpeedrunRTATimer) {
|
||||
mSpeedrunRta->SetAttribute("open", "");
|
||||
mSpeedrunRta->SetInnerRML(escape(fmt::format("RTA {}", FormatTime(elapsedTime))));
|
||||
mSpeedrunRta->SetInnerRML(escape(fmt::format("RTA {}", FormatElapsedTime(rtaElapsedTime))));
|
||||
} else {
|
||||
mSpeedrunRta->RemoveAttribute("open");
|
||||
}
|
||||
|
||||
mSpeedrunIgt->SetInnerRML(
|
||||
escape(fmt::format("IGT {}", FormatTime(dusk::speedrun::g_speedrunInfo.m_igtTimer))));
|
||||
escape(fmt::format("IGT {}", FormatElapsedTime(speedrun::g_speedrunInfo.m_igtTimer))));
|
||||
} else {
|
||||
mSpeedrunTimer->RemoveAttribute("open");
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "dusk/data.hpp"
|
||||
#include "dusk/gamemode.hpp"
|
||||
#include "dusk/iso_validate.hpp"
|
||||
#include "dusk/language.hpp"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/settings.h"
|
||||
#include "dusk/ui/menu_bar.hpp"
|
||||
@@ -293,7 +294,7 @@ std::string get_error_msg(iso::ValidationError error) {
|
||||
case iso::ValidationError::WrongGame:
|
||||
return "The selected game is not supported by Dusklight.";
|
||||
case iso::ValidationError::WrongVersion:
|
||||
return "Dusklight currently supports GameCube USA and PAL disc images only.";
|
||||
return "Dusklight does not currently support the Wii's Korean version.";
|
||||
case iso::ValidationError::Canceled:
|
||||
return "Disc verification was canceled. Dusklight cannot guarantee the selected disc "
|
||||
"image is compatible.";
|
||||
@@ -319,6 +320,24 @@ void persist_disc_choice(const std::string& path, iso::ValidationError validatio
|
||||
}
|
||||
}
|
||||
|
||||
void apply_language_for_disc(const iso::DiscInfo& info) {
|
||||
const auto langs = language::available_languages(info);
|
||||
auto& language = getSettings().game.language;
|
||||
const GameLanguage previous = language.getValue();
|
||||
if (std::ranges::find(langs, previous) != langs.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const GameLanguage fallback = langs.front();
|
||||
language.setValue(fallback);
|
||||
config::save();
|
||||
|
||||
auto& state = prelaunch_state();
|
||||
state.initialLanguage = fallback;
|
||||
state.unavailableLanguage = previous;
|
||||
state.pendingLanguageUnavailableNotice = true;
|
||||
}
|
||||
|
||||
void apply_valid_disc_result(
|
||||
const std::string& path, const iso::DiscInfo& info, iso::ValidationError validation) {
|
||||
auto& state = prelaunch_state();
|
||||
@@ -331,6 +350,7 @@ void apply_valid_disc_result(
|
||||
state.activeDiscInfo = info;
|
||||
}
|
||||
persist_disc_choice(path, validation);
|
||||
apply_language_for_disc(info);
|
||||
}
|
||||
|
||||
void apply_disc_verification_result(const DiscVerificationResult& result) {
|
||||
@@ -550,6 +570,7 @@ void refresh_configured_disc_state() noexcept {
|
||||
if (state.configuredDiscPath == state.activeDiscPath) {
|
||||
state.activeDiscInfo = info;
|
||||
}
|
||||
apply_language_for_disc(info);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -634,6 +655,36 @@ void try_push_verification_modal(Document& host) {
|
||||
}));
|
||||
}
|
||||
|
||||
void try_push_language_unavailable_modal(Document& host) {
|
||||
auto& state = prelaunch_state();
|
||||
|
||||
if (!state.pendingLanguageUnavailableNotice) {
|
||||
return;
|
||||
}
|
||||
state.pendingLanguageUnavailableNotice = false;
|
||||
|
||||
const Rml::String bodyRml = fmt::format(
|
||||
"<b>{}</b> is not available on this disc. Language has been reset to <b>{}</b>.",
|
||||
language::language_name(state.unavailableLanguage),
|
||||
language::language_name(getSettings().game.language.getValue()));
|
||||
|
||||
auto dismiss = [](Modal& modal) { modal.pop(); };
|
||||
|
||||
host.push(std::make_unique<Modal>(Modal::Props{
|
||||
.title = "Language unavailable",
|
||||
.bodyRml = bodyRml,
|
||||
.actions =
|
||||
{
|
||||
ModalAction{
|
||||
.label = "OK",
|
||||
.onPressed = dismiss,
|
||||
},
|
||||
},
|
||||
.onDismiss = dismiss,
|
||||
.icon = "warning",
|
||||
}));
|
||||
}
|
||||
|
||||
void ensure_initialized() noexcept {
|
||||
auto& state = prelaunch_state();
|
||||
if (state.initialized) {
|
||||
@@ -946,6 +997,7 @@ void Prelaunch::update() {
|
||||
|
||||
if (top_document() == this) {
|
||||
try_push_verification_modal(*this);
|
||||
try_push_language_unavailable_modal(*this);
|
||||
}
|
||||
|
||||
const auto& state = prelaunch_state();
|
||||
@@ -1019,6 +1071,9 @@ void Prelaunch::update() {
|
||||
break;
|
||||
case iso::Region::NorthAmerica:
|
||||
innerRML += "USA";
|
||||
if (state.activeDiscInfo.platform == iso::Platform::Wii) {
|
||||
innerRML += fmt::format(" Rev. {}", state.activeDiscInfo.revision);
|
||||
}
|
||||
break;
|
||||
case iso::Region::Korea:
|
||||
innerRML += "KOR";
|
||||
|
||||
@@ -54,6 +54,8 @@ struct PrelaunchState {
|
||||
std::string activeDiscPath;
|
||||
iso::DiscInfo activeDiscInfo{};
|
||||
GameLanguage initialLanguage = GameLanguage::English;
|
||||
GameLanguage unavailableLanguage = GameLanguage::English;
|
||||
bool pendingLanguageUnavailableNotice = false;
|
||||
std::string initialGraphicsBackend;
|
||||
int initialCardFileType = 0;
|
||||
std::string errorString;
|
||||
@@ -69,5 +71,6 @@ void refresh_configured_disc_state() noexcept;
|
||||
void open_iso_picker() noexcept;
|
||||
bool is_restart_pending() noexcept;
|
||||
void try_push_verification_modal(Document& host);
|
||||
void try_push_language_unavailable_modal(Document& host);
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
+33
-28
@@ -11,6 +11,7 @@
|
||||
#include "dusk/data.hpp"
|
||||
#include "dusk/imgui/ImGuiEngine.hpp"
|
||||
#include "dusk/io.hpp"
|
||||
#include "dusk/language.hpp"
|
||||
#include "dusk/presentation.hpp"
|
||||
#include <borealis/io.hpp>
|
||||
#include <borealis/file_select.hpp>
|
||||
@@ -53,14 +54,6 @@
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
constexpr std::array kLanguageNames = {
|
||||
"English",
|
||||
"German",
|
||||
"French",
|
||||
"Spanish",
|
||||
"Italian",
|
||||
};
|
||||
|
||||
constexpr std::array kCardFileTypes = {
|
||||
"Card Image",
|
||||
"GCI Folder",
|
||||
@@ -220,6 +213,18 @@ AuroraBackend configured_backend() {
|
||||
return configuredBackend;
|
||||
}
|
||||
|
||||
bool is_graphics_backend_restart_pending() {
|
||||
return getSettings().backend.graphicsBackend.getValue() !=
|
||||
prelaunch_state().initialGraphicsBackend;
|
||||
}
|
||||
|
||||
Rml::String graphics_backend_display_name() {
|
||||
if (is_graphics_backend_restart_pending()) {
|
||||
return Rml::String{backend_name(configured_backend())};
|
||||
}
|
||||
return Rml::String{backend_name(aurora_get_backend())};
|
||||
}
|
||||
|
||||
Rml::String configured_data_path_display_name() {
|
||||
const auto path = data::abbreviated_path_string(data::configured_data_path());
|
||||
if (path.empty()) {
|
||||
@@ -562,18 +567,15 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.key = "Language",
|
||||
.getValue =
|
||||
[] {
|
||||
const auto& state = prelaunch_state();
|
||||
if (!state.configuredDiscCanLaunch || state.configuredDiscInfo.region != iso::Region::Europe) {
|
||||
return kLanguageNames[0];
|
||||
}
|
||||
const u8 idx = static_cast<u8>(getSettings().game.language.getValue());
|
||||
return kLanguageNames[idx];
|
||||
return language::language_name(getSettings().game.language.getValue());
|
||||
},
|
||||
.isDisabled =
|
||||
[] {
|
||||
const auto& state = prelaunch_state();
|
||||
return !state.configuredDiscCanLaunch ||
|
||||
state.configuredDiscInfo.region != iso::Region::Europe;
|
||||
if (!state.configuredDiscCanLaunch) {
|
||||
return true;
|
||||
}
|
||||
return language::available_languages(state.configuredDiscInfo).size() <= 1;
|
||||
},
|
||||
.isModified =
|
||||
[] {
|
||||
@@ -582,18 +584,22 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
},
|
||||
}),
|
||||
rightPane, [](Pane& pane) {
|
||||
for (int i = 0; i < kLanguageNames.size(); i++) {
|
||||
const auto& state = prelaunch_state();
|
||||
const auto languages = state.configuredDiscCanLaunch
|
||||
? language::available_languages(state.configuredDiscInfo)
|
||||
: language::available_languages({});
|
||||
for (const GameLanguage language : languages) {
|
||||
pane.add_button({
|
||||
.text = kLanguageNames[i],
|
||||
.text = language::language_name(language),
|
||||
.isSelected =
|
||||
[i] {
|
||||
[language] {
|
||||
return getSettings().game.language.getValue() ==
|
||||
static_cast<GameLanguage>(i);
|
||||
language;
|
||||
},
|
||||
})
|
||||
.on_pressed([i] {
|
||||
.on_pressed([language] {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
getSettings().game.language.setValue(static_cast<GameLanguage>(i));
|
||||
getSettings().game.language.setValue(language);
|
||||
config::save();
|
||||
});
|
||||
}
|
||||
@@ -602,12 +608,8 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
leftPane.register_control(
|
||||
leftPane.add_select_button({
|
||||
.key = "Graphics Backend",
|
||||
.getValue = [] { return Rml::String{backend_name(aurora_get_backend())}; },
|
||||
.isModified =
|
||||
[] {
|
||||
return getSettings().backend.graphicsBackend.getValue() !=
|
||||
prelaunch_state().initialGraphicsBackend;
|
||||
},
|
||||
.getValue = [] { return graphics_backend_display_name(); },
|
||||
.isModified = [] { return is_graphics_backend_restart_pending(); },
|
||||
}),
|
||||
rightPane, [](Pane& pane) {
|
||||
const auto availableBackends = available_backends();
|
||||
@@ -1225,6 +1227,8 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
"Allows Wolf Link to howl and change the time of day.");
|
||||
addOption("Quick Transform (R+Y)", getSettings().game.enableQuickTransform,
|
||||
"Transform instantly by pressing R and Y simultaneously.");
|
||||
addOption("Aiming Reticle", getSettings().game.aimingReticle,
|
||||
"Shows the aiming reticle for bow and slingshot.");
|
||||
|
||||
leftPane.add_section("Speedrunning");
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.speedrunMode,
|
||||
@@ -1553,6 +1557,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
void SettingsWindow::update() {
|
||||
if (mPrelaunch && top_document() == this) {
|
||||
try_push_verification_modal(*this);
|
||||
try_push_language_unavailable_modal(*this);
|
||||
}
|
||||
|
||||
Window::update();
|
||||
|
||||
+15
-3
@@ -26,10 +26,18 @@ void init() {
|
||||
|
||||
if (game == "GZ2E"sv) {
|
||||
gameVersion = GameVersion::GcnUsa;
|
||||
} else if (game == "GZ2P") {
|
||||
} else if (game == "GZ2P"sv) {
|
||||
gameVersion = GameVersion::GcnPal;
|
||||
} else if (game == "GZ2J") {
|
||||
} else if (game == "GZ2J"sv) {
|
||||
gameVersion = GameVersion::GcnJpn;
|
||||
} else if (game == "RZDE"sv && diskId.gameVersion == 0) {
|
||||
gameVersion = GameVersion::WiiUsaRev0;
|
||||
} else if (game == "RZDE"sv && diskId.gameVersion == 2) {
|
||||
gameVersion = GameVersion::WiiUsa;
|
||||
} else if (game == "RZDP"sv) {
|
||||
gameVersion = GameVersion::WiiPal;
|
||||
} else if (game == "RZDJ"sv) {
|
||||
gameVersion = GameVersion::WiiJpn;
|
||||
} else {
|
||||
// TODO: Handle remaining valid versions.
|
||||
DuskLog.fatal("Unknown/unsupported game version in disc: {}", game);
|
||||
@@ -52,8 +60,12 @@ bool isWii() {
|
||||
|| getGameVersion() == GameVersion::WiiKor;
|
||||
}
|
||||
|
||||
bool isJpnOrLessThanWiiJpn() {
|
||||
return isRegionJpn() || getGameVersion() < GameVersion::WiiJpn;
|
||||
}
|
||||
|
||||
bool isPalOrAtLeastWiiR2() {
|
||||
return getGameVersion() == GameVersion::GcnPal || getGameVersion() >= GameVersion::WiiUsa;
|
||||
return isRegionPal() || (isWii() && getGameVersion() != GameVersion::WiiUsaRev0);
|
||||
}
|
||||
|
||||
bool isRegionJpn() {
|
||||
|
||||
+19
-8
@@ -5,18 +5,19 @@
|
||||
*/
|
||||
namespace dusk::version {
|
||||
enum class GameVersion : u8 {
|
||||
GcnUsa = VERSION_GCN_USA,
|
||||
GcnPal = VERSION_GCN_PAL,
|
||||
GcnJpn = VERSION_GCN_JPN,
|
||||
WiiUsaRev0 = VERSION_WII_USA_R0,
|
||||
WiiUsa = VERSION_WII_USA_R2,
|
||||
WiiPal = VERSION_WII_PAL,
|
||||
WiiJpn = VERSION_WII_JPN,
|
||||
WiiKor = VERSION_WII_KOR,
|
||||
WiiUsaRev0,
|
||||
WiiPal,
|
||||
WiiJpn,
|
||||
GcnUsa,
|
||||
GcnPal,
|
||||
GcnJpn,
|
||||
WiiUsa,
|
||||
WiiKor,
|
||||
};
|
||||
|
||||
bool isGcn();
|
||||
bool isWii();
|
||||
bool isJpnOrLessThanWiiJpn();
|
||||
bool isPalOrAtLeastWiiR2();
|
||||
|
||||
bool isRegionPal();
|
||||
@@ -61,4 +62,14 @@ namespace dusk::version {
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T platformSelect(const T& gcn, const T& wii) {
|
||||
return isGcn() ? gcn : wii;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T regionSelect(const T& usa, const T& pal, const T& jpn) {
|
||||
return isRegionUsa() ? usa : isRegionPal() ? pal : jpn;
|
||||
}
|
||||
} // namespace dusk::version
|
||||
|
||||
Reference in New Issue
Block a user