mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-04 11:19:58 -04:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 692c04e936 | |||
| e17d0f21fc | |||
| 91e77b1051 | |||
| 0b1a2c10b6 | |||
| e855e6471f | |||
| 848f635798 |
+12
-7
@@ -244,7 +244,7 @@ set(DOLZEL_FILES
|
||||
src/CaptureScreen.cpp
|
||||
)
|
||||
if(DEBUG)
|
||||
list(APPEND DOLZEL_FILES src/d/d_event_debug.cpp)
|
||||
list(APPEND DOLZEL_FILES src/d/d_event_debug.cpp)
|
||||
endif(DEBUG)
|
||||
|
||||
set(Z2AUDIOLIB_FILES
|
||||
@@ -1404,10 +1404,10 @@ set(REL_FILES
|
||||
)
|
||||
|
||||
set(DOLPHIN_FILES
|
||||
libs/dolphin/src/gf/GFGeometry.cpp
|
||||
libs/dolphin/src/gf/GFLight.cpp
|
||||
libs/dolphin/src/gf/GFPixel.cpp
|
||||
libs/dolphin/src/gf/GFTev.cpp
|
||||
libs/dolphin/src/gf/GFGeometry.cpp
|
||||
libs/dolphin/src/gf/GFLight.cpp
|
||||
libs/dolphin/src/gf/GFPixel.cpp
|
||||
libs/dolphin/src/gf/GFTev.cpp
|
||||
)
|
||||
|
||||
set(DUSK_FILES
|
||||
@@ -1429,12 +1429,15 @@ set(DUSK_FILES
|
||||
src/dusk/file_select.cpp
|
||||
src/dusk/file_select.hpp
|
||||
src/dusk/frame_interpolation.cpp
|
||||
src/dusk/commands.cpp
|
||||
src/dusk/commands.hpp
|
||||
src/dusk/game_clock.cpp
|
||||
src/dusk/game_combos.cpp
|
||||
src/dusk/globals.cpp
|
||||
src/dusk/gyro.cpp
|
||||
src/dusk/mouse.cpp
|
||||
src/dusk/gamepad_color.cpp
|
||||
src/dusk/autosave.cpp
|
||||
src/dusk/gamepad_color.cpp
|
||||
src/dusk/autosave.cpp
|
||||
src/dusk/http/http.hpp
|
||||
src/dusk/io.cpp
|
||||
src/dusk/layout.cpp
|
||||
@@ -1468,6 +1471,8 @@ set(DUSK_FILES
|
||||
src/dusk/imgui/ImGuiStateShare.cpp
|
||||
src/dusk/ui/achievements.cpp
|
||||
src/dusk/ui/achievements.hpp
|
||||
src/dusk/ui/command_console.cpp
|
||||
src/dusk/ui/command_console.hpp
|
||||
src/dusk/ui/bool_button.cpp
|
||||
src/dusk/ui/bool_button.hpp
|
||||
src/dusk/ui/button.cpp
|
||||
|
||||
@@ -1351,6 +1351,9 @@ enum dStage_SaveTbl {
|
||||
|
||||
const char* dStage_getName2(s16, s8);
|
||||
dStage_objectNameInf* dStage_searchName(const char*);
|
||||
#if TARGET_PC
|
||||
dStage_objectNameInf* dStage_searchNameCI(const char*);
|
||||
#endif
|
||||
static int dStage_stageKeepTresureInit(dStage_dt_c*, void*, int, void*);
|
||||
static int dStage_filiInfo2Init(dStage_dt_c*, void*, int, void*);
|
||||
static int dStage_mapPathInitCommonLayer(dStage_dt_c*, void*, int, void*);
|
||||
|
||||
@@ -23,4 +23,8 @@ float sample_interpolation_step();
|
||||
|
||||
float consume_interval(const void* consumer);
|
||||
|
||||
// Runtime sim rate override (default 30 hz). Resets the frame timer.
|
||||
void set_sim_rate(float hz);
|
||||
float get_sim_rate();
|
||||
|
||||
} // namespace dusk::game_clock
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <dolphin/types.h>
|
||||
|
||||
namespace dusk {
|
||||
|
||||
struct GameCombo {
|
||||
using ConditionFn = bool(*)();
|
||||
using ActionFn = void(*)();
|
||||
|
||||
u32 holdMask; // all of these must be held (getHold)
|
||||
u32 trigMask; // at least one must be newly triggered (getTrig); 0 = fires every frame while holdMask is held
|
||||
bool strict; // if true: (held & ~trigMask) must equal holdMask exactly, no extra buttons held
|
||||
ConditionFn condition; // extra game-state guard; nullptr = no extra check
|
||||
ActionFn action; // runs when the combo is fired
|
||||
u32 consumeMask; // buttons to clear after firing; 0 = pass-through
|
||||
bool exclusive; // stop evaluating future combos if this fires
|
||||
};
|
||||
|
||||
void processGameCombos();
|
||||
|
||||
} // namespace dusk
|
||||
@@ -266,6 +266,8 @@ struct UserSettings {
|
||||
ConfigVar<bool> removeQuestMapMarkers;
|
||||
ConfigVar<bool> showInputViewer;
|
||||
ConfigVar<bool> showInputViewerGyro;
|
||||
ConfigVar<bool> enableMoveLinkCombo;
|
||||
ConfigVar<bool> enableTeleportCombo;
|
||||
} game;
|
||||
|
||||
struct {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
*, *:before, *:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
console {
|
||||
position: absolute;
|
||||
bottom: 10dp;
|
||||
left: 10dp;
|
||||
width: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: rgba(0, 0, 0, 60%);
|
||||
pointer-events: auto;
|
||||
font-family: "Noto Mono";
|
||||
font-size: 14dp;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
output {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
max-height: 480dp;
|
||||
padding: 4dp 8dp;
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
output[open] {
|
||||
height: 480dp;
|
||||
max-height: 480dp;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
line {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
line.cmd {
|
||||
color: #FFD966;
|
||||
}
|
||||
|
||||
console input {
|
||||
display: none;
|
||||
width: 100%;
|
||||
background-color: rgba(0, 0, 0, 40%);
|
||||
border: 0dp;
|
||||
border-top: 1dp rgba(255, 255, 255, 20%);
|
||||
color: #FFFFFF;
|
||||
font-family: "Noto Mono";
|
||||
font-size: 14dp;
|
||||
padding: 4dp 8dp;
|
||||
}
|
||||
|
||||
console[open] input {
|
||||
display: block;
|
||||
}
|
||||
@@ -1536,6 +1536,27 @@ dStage_objectNameInf* dStage_searchName(char const* objName) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
dStage_objectNameInf* dStage_searchNameCI(char const* objName) {
|
||||
dStage_objectNameInf* obj = l_objectName;
|
||||
|
||||
for (u32 i = 0; i < ARRAY_SIZEU(l_objectName); i++) {
|
||||
const char* a = obj->name;
|
||||
const char* b = objName;
|
||||
while (*a && *b && tolower((unsigned char)*a) == tolower((unsigned char)*b)) {
|
||||
++a;
|
||||
++b;
|
||||
}
|
||||
if (*a == '\0' && *b == '\0') {
|
||||
return obj;
|
||||
}
|
||||
obj++;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
#endif
|
||||
|
||||
const char* dStage_getName(s16 procName, s8 argument) {
|
||||
static char tmp_name[dStage_NAME_LENGTH];
|
||||
|
||||
|
||||
@@ -0,0 +1,617 @@
|
||||
#include "commands.hpp"
|
||||
|
||||
#include "JSystem/JUtility/JUTGamePad.h"
|
||||
#include "SSystem/SComponent/c_sxyz.h"
|
||||
#include "SSystem/SComponent/c_xyz.h"
|
||||
#include "c/c_damagereaction.h"
|
||||
#include "d/actor/d_a_alink.h"
|
||||
#include "d/d_com_inf_game.h"
|
||||
#include "d/d_kankyo.h"
|
||||
#include "d/d_stage.h"
|
||||
#include "dusk/game_clock.h"
|
||||
#include "f_op/f_op_actor_mng.h"
|
||||
#include "f_pc/f_pc_layer.h"
|
||||
#include "f_pc/f_pc_layer_iter.h"
|
||||
#include "f_pc/f_pc_manager.h"
|
||||
#include "f_pc/f_pc_node.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "fmt/format.h"
|
||||
|
||||
namespace dusk {
|
||||
namespace {
|
||||
|
||||
static constexpr int kMaxHistory = 64;
|
||||
|
||||
static std::vector<std::string> SplitArgs(std::string_view input) {
|
||||
std::vector<std::string> args;
|
||||
std::string cur;
|
||||
for (char c : input) {
|
||||
if (c == ' ' || c == '\t') {
|
||||
if (!cur.empty()) {
|
||||
args.push_back(std::move(cur));
|
||||
cur.clear();
|
||||
}
|
||||
} else {
|
||||
cur += c;
|
||||
}
|
||||
}
|
||||
if (!cur.empty()) {
|
||||
args.push_back(std::move(cur));
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
static std::optional<long> ParseLong(const std::string& s) {
|
||||
if (s.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
char* end = nullptr;
|
||||
const long v = std::strtol(s.c_str(), &end, 0);
|
||||
return (end != s.c_str() && *end == '\0') ? std::optional<long>{v} : std::nullopt;
|
||||
}
|
||||
|
||||
static std::optional<float> ParseFloat(const std::string& s) {
|
||||
if (s.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
char* end = nullptr;
|
||||
const float v = std::strtof(s.c_str(), &end);
|
||||
return (end != s.c_str() && *end == '\0') ? std::optional<float>{v} : std::nullopt;
|
||||
}
|
||||
|
||||
static const char* ActorShortName(s16 profname) {
|
||||
const char* n = dStage_getName(profname, -1);
|
||||
return n ? n : "?";
|
||||
}
|
||||
|
||||
// Resolves @found / @link / @<id> to a process pointer, outputting an error on failure
|
||||
static base_process_class* ParseProcArg(
|
||||
const std::string& s, unsigned int foundProcId, const CommandOutput& output) {
|
||||
if (s.empty() || s[0] != '@') {
|
||||
output("Error: proc reference must start with @");
|
||||
return nullptr;
|
||||
}
|
||||
const std::string inner = s.substr(1);
|
||||
unsigned int id;
|
||||
if (inner == "found") {
|
||||
if (foundProcId == 0) {
|
||||
output("Error: @found is not set");
|
||||
return nullptr;
|
||||
}
|
||||
id = foundProcId;
|
||||
} else if (inner == "link") {
|
||||
auto* player = dComIfGp_getPlayer(0);
|
||||
if (player == nullptr) {
|
||||
output("Error: player not available");
|
||||
return nullptr;
|
||||
}
|
||||
id = (unsigned int)fpcM_GetID(player);
|
||||
} else {
|
||||
const auto v = ParseLong(inner);
|
||||
if (!v) {
|
||||
output("Error: invalid proc ID");
|
||||
return nullptr;
|
||||
}
|
||||
id = (unsigned int)*v;
|
||||
}
|
||||
auto* proc = fpcM_SearchByID(id);
|
||||
if (proc == nullptr) {
|
||||
output(fmt::format(FMT_STRING("Error: proc {} not found"), id));
|
||||
return nullptr;
|
||||
}
|
||||
return proc;
|
||||
}
|
||||
|
||||
// Like ParseProcArg but ensures the proc is an actor
|
||||
static fopAc_ac_c* ParseActorArg(
|
||||
const std::string& s, unsigned int foundProcId, const CommandOutput& output) {
|
||||
auto* proc = ParseProcArg(s, foundProcId, output);
|
||||
if (proc == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
if (!fopAcM_IsActor(proc)) {
|
||||
output("Error: proc is not an actor");
|
||||
return nullptr;
|
||||
}
|
||||
return static_cast<fopAc_ac_c*>(proc);
|
||||
}
|
||||
|
||||
static std::optional<s16> ParseActorId(const std::string& s) {
|
||||
if (const auto v = ParseLong(s)) {
|
||||
return (s16)*v;
|
||||
}
|
||||
const auto* entry = dStage_searchNameCI(s.c_str());
|
||||
return entry ? std::optional<s16>{entry->procname} : std::nullopt;
|
||||
}
|
||||
|
||||
static std::optional<cXyz> ParseXYZ(const std::vector<std::string>& args, size_t i) {
|
||||
const auto x = ParseFloat(args[i]), y = ParseFloat(args[i + 1]), z = ParseFloat(args[i + 2]);
|
||||
return (x && y && z) ? std::optional<cXyz>{cXyz(*x, *y, *z)} : std::nullopt;
|
||||
}
|
||||
|
||||
static bool TryAngle(
|
||||
s16& out, const std::vector<std::string>& args, size_t i, const CommandOutput& output) {
|
||||
if (args.size() <= i) {
|
||||
return true;
|
||||
}
|
||||
const auto a = ParseLong(args[i]);
|
||||
if (!a) {
|
||||
output("Error: invalid angle");
|
||||
return false;
|
||||
}
|
||||
out = (s16)*a;
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::string actorLine(const base_process_class* proc) {
|
||||
const auto* ac = static_cast<const fopAc_ac_c*>(proc);
|
||||
return fmt::format(FMT_STRING("procId={} 0x{:04X} ({}) @ ({:.2f}, {:.2f}, {:.2f}) room={}"),
|
||||
(unsigned int)proc->id, (unsigned int)(u16)proc->profname, ActorShortName(proc->profname),
|
||||
ac->current.pos.x, ac->current.pos.y, ac->current.pos.z, (int)ac->current.roomNo);
|
||||
}
|
||||
|
||||
static void recurseLayer(void* p, int (*callback)(void*, void*), void* ctx) {
|
||||
auto* proc = static_cast<base_process_class*>(p);
|
||||
if (fpcBs_Is_JustOfType(g_fpcNd_type, proc->subtype)) {
|
||||
fpcLyIt_OnlyHere(&static_cast<process_node_class*>(p)->layer, callback, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
struct ListContext {
|
||||
s16 targetId;
|
||||
std::vector<std::string>* output;
|
||||
};
|
||||
struct FindContext {
|
||||
s16 targetId;
|
||||
std::vector<base_process_class*> matches;
|
||||
};
|
||||
|
||||
static int ListActorCallback(void* p, void* ctx) {
|
||||
auto* proc = static_cast<base_process_class*>(p);
|
||||
auto* context = static_cast<ListContext*>(ctx);
|
||||
if (fopAcM_IsActor(proc) && (context->targetId < 0 || proc->profname == context->targetId)) {
|
||||
context->output->push_back(" " + actorLine(proc));
|
||||
}
|
||||
recurseLayer(p, ListActorCallback, ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int FindActorCallback(void* p, void* ctx) {
|
||||
auto* proc = static_cast<base_process_class*>(p);
|
||||
auto* context = static_cast<FindContext*>(ctx);
|
||||
if (fopAcM_IsActor(proc) && proc->profname == context->targetId) {
|
||||
context->matches.push_back(proc);
|
||||
}
|
||||
recurseLayer(p, FindActorCallback, ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void runCommand(std::string_view cmdLine, CommandState& state, const CommandOutput& output) {
|
||||
output(fmt::format(FMT_STRING("> {}"), cmdLine));
|
||||
|
||||
if (!cmdLine.empty()) {
|
||||
if (state.history.empty() || state.history.back() != cmdLine) {
|
||||
state.history.push_back(std::string(cmdLine));
|
||||
if ((int)state.history.size() > kMaxHistory) {
|
||||
state.history.erase(state.history.begin());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto args = SplitArgs(cmdLine);
|
||||
if (args.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto& cmd = args[0];
|
||||
|
||||
auto requirePlayer = [&]() -> daAlink_c* {
|
||||
auto* p = (daAlink_c*)dComIfGp_getPlayer(0);
|
||||
if (p == nullptr) {
|
||||
output("Error: player not available");
|
||||
}
|
||||
return p;
|
||||
};
|
||||
|
||||
if (cmd == "tp") {
|
||||
auto* player = requirePlayer();
|
||||
if (player == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.size() >= 2 && args[1].starts_with('@')) {
|
||||
auto* ac = ParseActorArg(args[1], state.foundProcId, output);
|
||||
if (ac == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.size() >= 3 && args[2].starts_with('@')) {
|
||||
auto* destAc = ParseActorArg(args[2], state.foundProcId, output);
|
||||
if (destAc == nullptr) {
|
||||
return;
|
||||
}
|
||||
const cXyz destPos = destAc->current.pos;
|
||||
ac->current.pos = destPos;
|
||||
output(fmt::format(FMT_STRING("Moved actor {} to ({:.2f}, {:.2f}, {:.2f})"), ac->id,
|
||||
destPos.x, destPos.y, destPos.z));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.size() >= 5) {
|
||||
const auto pos = ParseXYZ(args, 2);
|
||||
if (!pos) {
|
||||
output("Error: invalid coordinates");
|
||||
return;
|
||||
}
|
||||
ac->current.pos = *pos;
|
||||
if (!TryAngle(ac->shape_angle.y, args, 5, output)) {
|
||||
return;
|
||||
}
|
||||
output(fmt::format(FMT_STRING("Moved actor {} to ({:.2f}, {:.2f}, {:.2f})"), ac->id,
|
||||
pos->x, pos->y, pos->z));
|
||||
return;
|
||||
}
|
||||
|
||||
player->current.pos = ac->current.pos;
|
||||
output(fmt::format(FMT_STRING("Teleported to actor {} ({:.2f}, {:.2f}, {:.2f})"),
|
||||
ac->id, ac->current.pos.x, ac->current.pos.y, ac->current.pos.z));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.size() < 4) {
|
||||
output("Usage: tp <x> <y> <z> [angle] | tp @<procId> [<x> <y> <z> [angle] | @link]");
|
||||
return;
|
||||
}
|
||||
const auto pos = ParseXYZ(args, 1);
|
||||
if (!pos) {
|
||||
output("Error: invalid coordinates");
|
||||
return;
|
||||
}
|
||||
player->current.pos = *pos;
|
||||
if (!TryAngle(player->shape_angle.y, args, 4, output)) {
|
||||
return;
|
||||
}
|
||||
output(fmt::format(
|
||||
FMT_STRING("Teleported to ({:.2f}, {:.2f}, {:.2f})"), pos->x, pos->y, pos->z));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "spawn") {
|
||||
if (args.size() < 2) {
|
||||
output("Usage: spawn <actorId> [params] [x y z] [angle]");
|
||||
return;
|
||||
}
|
||||
auto* player = requirePlayer();
|
||||
if (player == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto actorId = ParseActorId(args[1]);
|
||||
if (!actorId) {
|
||||
output("Error: unknown actor ID or name");
|
||||
return;
|
||||
}
|
||||
|
||||
long paramsL = -1;
|
||||
if (args.size() >= 3) {
|
||||
const auto p = ParseLong(args[2]);
|
||||
if (!p) {
|
||||
output("Error: invalid params");
|
||||
return;
|
||||
}
|
||||
paramsL = *p;
|
||||
}
|
||||
|
||||
cXyz pos = player->current.pos;
|
||||
if (args.size() >= 6) {
|
||||
const auto spawnPos = ParseXYZ(args, 3);
|
||||
if (!spawnPos) {
|
||||
output("Error: invalid spawn coordinates");
|
||||
return;
|
||||
}
|
||||
pos = *spawnPos;
|
||||
}
|
||||
|
||||
s16 angleY = 0;
|
||||
if (!TryAngle(angleY, args, 6, output)) {
|
||||
return;
|
||||
}
|
||||
csXyz angle;
|
||||
angle.set(0, angleY, 0);
|
||||
cXyz scale(1.0f, 1.0f, 1.0f);
|
||||
|
||||
layer_class* savedLayer = fpcLy_CurrentLayer();
|
||||
base_process_class* playScene = fpcM_SearchByName(fpcNm_PLAY_SCENE_e);
|
||||
if (playScene != nullptr) {
|
||||
fpcLy_SetCurrentLayer(&((process_node_class*)playScene)->layer);
|
||||
}
|
||||
unsigned int result = fopAcM_create(
|
||||
*actorId, (u32)paramsL, &pos, player->current.roomNo, &angle, &scale, (s8)-1);
|
||||
fpcLy_SetCurrentLayer(savedLayer);
|
||||
|
||||
output(result != 0 ? fmt::format(FMT_STRING("Spawned actorId=0x{:04X} procId={}"),
|
||||
(unsigned int)(u16)*actorId, result) :
|
||||
fmt::format(FMT_STRING("Failed to spawn actorId=0x{:04X}"),
|
||||
(unsigned int)(u16)*actorId));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "reset") {
|
||||
JUTGamePad::C3ButtonReset::sResetSwitchPushing = true;
|
||||
output("Soft reset triggered");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "warp") {
|
||||
if (args.size() < 4) {
|
||||
output("Usage: warp <stageName> <point> <roomNo> [layer=-1]");
|
||||
output(" e.g. warp F_SP121 0 0");
|
||||
return;
|
||||
}
|
||||
const auto pointL = ParseLong(args[2]), roomL = ParseLong(args[3]);
|
||||
if (!pointL || !roomL) {
|
||||
output("Error: invalid point or room number");
|
||||
return;
|
||||
}
|
||||
long layerL = -1;
|
||||
if (args.size() >= 5) {
|
||||
const auto l = ParseLong(args[4]);
|
||||
if (!l) {
|
||||
output("Error: invalid layer");
|
||||
return;
|
||||
}
|
||||
layerL = *l;
|
||||
}
|
||||
state.lastWarpStage = args[1];
|
||||
dComIfGp_setNextStage(state.lastWarpStage.c_str(), (s16)*pointL, (s8)*roomL, (s8)layerL);
|
||||
output(fmt::format(FMT_STRING("Warping to {} point={} room={} layer={}"), args[1],
|
||||
(int)(s16)*pointL, (int)(s8)*roomL, (int)(s8)layerL));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "list") {
|
||||
s16 targetId = -1;
|
||||
if (args.size() >= 2) {
|
||||
const auto id = ParseActorId(args[1]);
|
||||
if (!id) {
|
||||
output("Error: unknown actor ID or name");
|
||||
return;
|
||||
}
|
||||
targetId = *id;
|
||||
}
|
||||
std::vector<std::string> results;
|
||||
ListContext ctx{targetId, &results};
|
||||
fpcLyIt_OnlyHere(fpcLy_RootLayer(), ListActorCallback, &ctx);
|
||||
output(results.empty() ? "No matching actors found" :
|
||||
fmt::format(FMT_STRING("Found {} actor(s):"), results.size()));
|
||||
for (const auto& r : results) {
|
||||
output(r);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "killall") {
|
||||
if (args.size() < 2) {
|
||||
output("Usage: killall <actorId|name>");
|
||||
return;
|
||||
}
|
||||
const auto targetId = ParseActorId(args[1]);
|
||||
if (!targetId) {
|
||||
output("Error: unknown actor ID or name");
|
||||
return;
|
||||
}
|
||||
FindContext ctx{*targetId, {}};
|
||||
fpcLyIt_OnlyHere(fpcLy_RootLayer(), FindActorCallback, &ctx);
|
||||
for (auto* proc : ctx.matches) {
|
||||
fpcM_Delete(proc);
|
||||
}
|
||||
output(fmt::format(FMT_STRING("Deleted {} actor(s) of type {} ({})"),
|
||||
(int)ctx.matches.size(), (unsigned int)(u16)*targetId, ActorShortName(*targetId)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "heal") {
|
||||
auto* player = requirePlayer();
|
||||
if (player == nullptr) {
|
||||
return;
|
||||
}
|
||||
const u16 maxLife = dComIfGs_getMaxLife() / 5 * 4;
|
||||
u16 newLife = maxLife;
|
||||
if (args.size() >= 2) {
|
||||
const auto amount = ParseLong(args[1]);
|
||||
if (!amount) {
|
||||
output("Error: invalid amount");
|
||||
return;
|
||||
}
|
||||
newLife =
|
||||
(u16)std::max(0L, std::min((long)maxLife, (long)dComIfGs_getLife() + *amount));
|
||||
}
|
||||
dComIfGs_setLife(newLife);
|
||||
output(fmt::format(FMT_STRING("Health: {}/{}"), (int)newLife, (int)maxLife));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "kill") {
|
||||
if (args.size() >= 2) {
|
||||
auto* proc = ParseProcArg(args[1], state.foundProcId, output);
|
||||
if (proc == nullptr) {
|
||||
return;
|
||||
}
|
||||
fpcM_Delete(proc);
|
||||
output(fmt::format(FMT_STRING("Deleted proc {}"), proc->id));
|
||||
} else {
|
||||
auto* player = requirePlayer();
|
||||
if (player == nullptr) {
|
||||
return;
|
||||
}
|
||||
dComIfGs_setLife(0);
|
||||
output("Set Link's health to 0");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "rate") {
|
||||
if (args.size() >= 2) {
|
||||
const auto hz = ParseLong(args[1]);
|
||||
if (!hz || *hz <= 0) {
|
||||
output("Error: rate must be a positive integer");
|
||||
return;
|
||||
}
|
||||
dusk::game_clock::set_sim_rate((float)*hz);
|
||||
}
|
||||
output(fmt::format(FMT_STRING("Sim rate: {} hz"), (int)dusk::game_clock::get_sim_rate()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "ebf") {
|
||||
if (args.size() >= 2) {
|
||||
const auto val = ParseLong(args[1]);
|
||||
if (!val || *val < 0 || *val > 255) {
|
||||
output("Error: value must be 0-255");
|
||||
return;
|
||||
}
|
||||
cDmr_SkipInfo = (u8)*val;
|
||||
}
|
||||
output(fmt::format(FMT_STRING("EBF = {}"), (int)cDmr_SkipInfo));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "time") {
|
||||
if (args.size() >= 2) {
|
||||
const auto t = ParseFloat(args[1]);
|
||||
if (!t || *t < 0.0f || *t > 360.0f) {
|
||||
output("Error: time must be a float between 0 and 360");
|
||||
return;
|
||||
}
|
||||
dKy_instant_timechg(*t);
|
||||
}
|
||||
output(fmt::format(FMT_STRING("Time: {:.2f} ({}:{:02d})"), dComIfGs_getTime(),
|
||||
dKy_getdaytime_hour(), dKy_getdaytime_minute()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "rupees") {
|
||||
const u16 maxRupees = dComIfGs_getRupeeMax();
|
||||
if (args.size() >= 2) {
|
||||
const auto amount = ParseLong(args[1]);
|
||||
if (!amount) {
|
||||
output("Error: invalid amount");
|
||||
return;
|
||||
}
|
||||
dComIfGs_setRupee((u16)std::max(0L, std::min((long)maxRupees, *amount)));
|
||||
}
|
||||
output(fmt::format(FMT_STRING("Rupees: {}/{}"), (int)dComIfGs_getRupee(), (int)maxRupees));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "find") {
|
||||
if (args.size() < 2) {
|
||||
if (state.foundProcId == 0) {
|
||||
output("@found is not set");
|
||||
return;
|
||||
}
|
||||
auto* proc = fpcM_SearchByID(state.foundProcId);
|
||||
output(proc != nullptr && fopAcM_IsActor(proc) ?
|
||||
"@found = " + actorLine(proc) :
|
||||
fmt::format(FMT_STRING("@found = {} (no longer exists)"),
|
||||
(unsigned int)state.foundProcId));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto targetId = ParseActorId(args[1]);
|
||||
if (!targetId) {
|
||||
output("Error: unknown actor ID or name");
|
||||
return;
|
||||
}
|
||||
|
||||
int targetN = 1;
|
||||
if (args.size() >= 3) {
|
||||
const auto n = ParseLong(args[2]);
|
||||
if (!n || *n < 1) {
|
||||
output("Error: index must be >= 1");
|
||||
return;
|
||||
}
|
||||
targetN = (int)*n;
|
||||
}
|
||||
|
||||
FindContext ctx{*targetId, {}};
|
||||
fpcLyIt_OnlyHere(fpcLy_RootLayer(), FindActorCallback, &ctx);
|
||||
|
||||
if (ctx.matches.empty()) {
|
||||
output(fmt::format(FMT_STRING("No actors found for '{}'"), args[1]));
|
||||
return;
|
||||
}
|
||||
|
||||
std::sort(ctx.matches.begin(), ctx.matches.end(),
|
||||
[](const base_process_class* a, const base_process_class* b) { return a->id < b->id; });
|
||||
|
||||
if (targetN > (int)ctx.matches.size()) {
|
||||
output(fmt::format(
|
||||
FMT_STRING("Error: only {} actor(s) of that type exist"), (int)ctx.matches.size()));
|
||||
return;
|
||||
}
|
||||
|
||||
auto* picked = ctx.matches[(size_t)(targetN - 1)];
|
||||
state.foundProcId = picked->id;
|
||||
output(fmt::format(
|
||||
FMT_STRING("@found [{}/{}] {}"), targetN, (int)ctx.matches.size(), actorLine(picked)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "transform") {
|
||||
auto* player = requirePlayer();
|
||||
if (player == nullptr) { return; }
|
||||
player->procCoMetamorphoseInit();
|
||||
output("Transforming");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "pos") {
|
||||
auto* player = requirePlayer();
|
||||
if (player == nullptr) {
|
||||
return;
|
||||
}
|
||||
output(fmt::format(FMT_STRING("pos: {:.4f} {:.4f} {:.4f}"), player->current.pos.x,
|
||||
player->current.pos.y, player->current.pos.z));
|
||||
output(
|
||||
fmt::format(FMT_STRING("stage: {} room: {} entry: {}"), dComIfGp_getStartStageName(),
|
||||
(int)player->current.roomNo, (int)dComIfGp_getStartStagePoint()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "help") {
|
||||
output("@<ref> = @<procId> | @found | @link");
|
||||
output("");
|
||||
output("ebf [0-255] Get or set cDmr_SkipInfo");
|
||||
output("find <id|name> [n=1] Store nth actor as @found");
|
||||
output("heal [amount] Heal to max, or by relative amount");
|
||||
output("kill Set Link health to 0");
|
||||
output("kill @<ref> Delete proc");
|
||||
output("killall <id|name> Delete all actors of a type");
|
||||
output("list [id|name] List actors in scene");
|
||||
output("pos Print player position and stage");
|
||||
output("rate [hz] Get or set sim rate (1-1000, default 30)");
|
||||
output("reset Soft reset");
|
||||
output("rupees [amount] Get or set rupee count");
|
||||
output("spawn <id|name> [params] [x y z] [angle] Spawn actor");
|
||||
output("time [0-360] Get or set time of day");
|
||||
output("tp <x> <y> <z> [angle] Teleport Link to coords");
|
||||
output("tp @<ref> Teleport Link to actor");
|
||||
output("tp @<ref> <x> <y> <z> [angle] Move actor to coords");
|
||||
output("tp @<ref> @<ref> Move actor to actor");
|
||||
output("transform Force transform");
|
||||
output("warp <stage> <point> <room> [layer] Warp to stage");
|
||||
return;
|
||||
}
|
||||
|
||||
output(fmt::format(FMT_STRING("Unknown command '{}' (try 'help')"), cmd));
|
||||
}
|
||||
|
||||
} // namespace dusk
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk {
|
||||
|
||||
using CommandOutput = std::function<void(std::string)>;
|
||||
|
||||
struct CommandState {
|
||||
std::vector<std::string> history;
|
||||
unsigned int foundProcId = 0;
|
||||
std::string lastWarpStage;
|
||||
};
|
||||
|
||||
// Execute a single command line. Calls output() for every line of response.
|
||||
// Manages history internally; callers need only hold a CommandState.
|
||||
void runCommand(std::string_view cmdLine, CommandState& state, const CommandOutput& output);
|
||||
|
||||
} // namespace dusk
|
||||
+20
-9
@@ -16,8 +16,9 @@ clock::time_point s_current_snapshot_time{};
|
||||
|
||||
std::unordered_map<uintptr_t, clock::time_point> s_interval_last_sample;
|
||||
|
||||
constexpr clock::duration kSimPeriodDuration =
|
||||
std::chrono::duration_cast<clock::duration>(std::chrono::duration<float>(sim_pace()));
|
||||
float s_sim_rate_hz = 30.0f;
|
||||
clock::duration s_sim_period_duration = std::chrono::duration_cast<clock::duration>(std::chrono::duration<float>(sim_pace()));
|
||||
|
||||
constexpr clock::duration kAbnormalGapResetThreshold = std::chrono::milliseconds(250);
|
||||
constexpr int kMaxSimTicksPerFrame = 2;
|
||||
|
||||
@@ -32,7 +33,17 @@ void ensure_initialized() {
|
||||
|
||||
void reset_frame_timer() {
|
||||
s_previous_sample = clock::now();
|
||||
s_current_snapshot_time = s_previous_sample - kSimPeriodDuration;
|
||||
s_current_snapshot_time = s_previous_sample - s_sim_period_duration;
|
||||
}
|
||||
|
||||
void set_sim_rate(float hz) {
|
||||
s_sim_rate_hz = std::max(1.0f, std::min(hz, 1000.0f));
|
||||
s_sim_period_duration = std::chrono::duration_cast<clock::duration>(std::chrono::duration<float>(1.0f / s_sim_rate_hz));
|
||||
reset_frame_timer();
|
||||
}
|
||||
|
||||
float get_sim_rate() {
|
||||
return s_sim_rate_hz;
|
||||
}
|
||||
|
||||
MainLoopPacer advance_main_loop() {
|
||||
@@ -45,12 +56,12 @@ MainLoopPacer advance_main_loop() {
|
||||
|
||||
MainLoopPacer out{};
|
||||
out.presentation_dt_seconds = presentation_dt;
|
||||
out.sim_pace = 1.0f / s_sim_rate_hz;
|
||||
|
||||
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;
|
||||
@@ -59,16 +70,16 @@ MainLoopPacer advance_main_loop() {
|
||||
}
|
||||
|
||||
if (frame_gap > kAbnormalGapResetThreshold) {
|
||||
s_current_snapshot_time = now - kSimPeriodDuration;
|
||||
s_current_snapshot_time = now - s_sim_period_duration;
|
||||
out.sim_ticks_to_run = 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;
|
||||
const clock::time_point render_time = now - s_sim_period_duration;
|
||||
while (sim_ticks_to_run < kMaxSimTicksPerFrame && projected_snapshot_time < render_time) {
|
||||
projected_snapshot_time += kSimPeriodDuration;
|
||||
projected_snapshot_time += s_sim_period_duration;
|
||||
sim_ticks_to_run++;
|
||||
}
|
||||
out.sim_ticks_to_run = sim_ticks_to_run;
|
||||
@@ -77,13 +88,13 @@ MainLoopPacer advance_main_loop() {
|
||||
|
||||
void commit_sim_tick() {
|
||||
ensure_initialized();
|
||||
s_current_snapshot_time += kSimPeriodDuration;
|
||||
s_current_snapshot_time += s_sim_period_duration;
|
||||
}
|
||||
|
||||
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>(clock::now() - s_current_snapshot_time).count() / (1.0f / s_sim_rate_hz);
|
||||
return std::clamp(step, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
#include "dusk/game_combos.h"
|
||||
|
||||
#include "SSystem/SComponent/c_API_controller_pad.h"
|
||||
#include "SSystem/SComponent/c_xyz.h"
|
||||
#include "d/actor/d_a_alink.h"
|
||||
#include "d/d_com_inf_game.h"
|
||||
#include "dusk/settings.h"
|
||||
#include "m_Do/m_Do_controller_pad.h"
|
||||
|
||||
namespace dusk {
|
||||
namespace {
|
||||
|
||||
cXyz s_savedTeleportPos{};
|
||||
s16 s_savedTeleportAngle = 0;
|
||||
bool s_hasTeleportPos = false;
|
||||
|
||||
static daAlink_c* getPlayer() {
|
||||
return (daAlink_c*)dComIfGp_getPlayer(0);
|
||||
}
|
||||
|
||||
static void consumeButtons(u32 mask) {
|
||||
mDoCPd_c::getCpadInfo(PAD_1).mPressedButtonFlags &= ~mask;
|
||||
mDoCPd_c::getCpadInfo(PAD_1).mButtonFlags &= ~mask;
|
||||
}
|
||||
|
||||
// Table: holdMask, trigMask, strict, condition, action, consumeMask, exclusive
|
||||
static const GameCombo kCombos[] = {
|
||||
// Move Link (L+R+Y), pass-through, non-exclusive
|
||||
{
|
||||
PAD_TRIGGER_R | PAD_TRIGGER_L,
|
||||
PAD_BUTTON_Y,
|
||||
false,
|
||||
[] { return (bool)getSettings().game.enableMoveLinkCombo; },
|
||||
[] { getTransientSettings().moveLinkActive = !getTransientSettings().moveLinkActive; },
|
||||
0,
|
||||
false,
|
||||
},
|
||||
// Quick Transform (R+Y, strictly only R held)
|
||||
{
|
||||
PAD_TRIGGER_R,
|
||||
PAD_BUTTON_Y,
|
||||
true,
|
||||
[] { return getPlayer() != nullptr; },
|
||||
[] { getPlayer()->handleQuickTransform(); },
|
||||
PAD_BUTTON_Y,
|
||||
false,
|
||||
},
|
||||
// Wolf Howl (R+X)
|
||||
{
|
||||
PAD_TRIGGER_R,
|
||||
PAD_BUTTON_X,
|
||||
false,
|
||||
[] { return getPlayer() != nullptr; },
|
||||
[] { getPlayer()->handleWolfHowl(); },
|
||||
PAD_BUTTON_X,
|
||||
false,
|
||||
},
|
||||
// Teleport save (R+D-pad Up), consumes D-pad Up, exclusive
|
||||
{
|
||||
PAD_TRIGGER_R,
|
||||
PAD_BUTTON_UP,
|
||||
false,
|
||||
[] { return getSettings().game.enableTeleportCombo && getPlayer() != nullptr; },
|
||||
[] {
|
||||
auto* p = getPlayer();
|
||||
s_savedTeleportPos = p->current.pos;
|
||||
s_savedTeleportAngle = p->shape_angle.y;
|
||||
s_hasTeleportPos = true;
|
||||
},
|
||||
PAD_BUTTON_UP,
|
||||
true,
|
||||
},
|
||||
// Teleport load (R+D-pad Down), consumes D-pad Down, exclusive
|
||||
{
|
||||
PAD_TRIGGER_R,
|
||||
PAD_BUTTON_DOWN,
|
||||
false,
|
||||
[] {
|
||||
return getSettings().game.enableTeleportCombo && s_hasTeleportPos &&
|
||||
getPlayer() != nullptr;
|
||||
},
|
||||
[] {
|
||||
auto* p = getPlayer();
|
||||
p->current.pos = s_savedTeleportPos;
|
||||
p->shape_angle.y = s_savedTeleportAngle;
|
||||
p->mNormalSpeed = 0.0f;
|
||||
},
|
||||
PAD_BUTTON_DOWN,
|
||||
true,
|
||||
},
|
||||
// Moon Jump (R+A, hold), continuous, pass-through, non-exclusive
|
||||
{
|
||||
PAD_TRIGGER_R | PAD_BUTTON_A,
|
||||
0,
|
||||
false,
|
||||
[] { return getSettings().game.moonJump && getPlayer() != nullptr; },
|
||||
[] { getPlayer()->speed.y = 56.0f; },
|
||||
0,
|
||||
false,
|
||||
},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void processGameCombos() {
|
||||
if (!getSettings().game.enableMoveLinkCombo) {
|
||||
getTransientSettings().moveLinkActive = false;
|
||||
}
|
||||
|
||||
const u32 held = mDoCPd_c::getHold(PAD_1);
|
||||
const u32 trig = mDoCPd_c::getTrig(PAD_1);
|
||||
|
||||
for (const auto& combo : kCombos) {
|
||||
if ((held & combo.holdMask) != combo.holdMask) {
|
||||
continue;
|
||||
}
|
||||
if (combo.strict && (held & ~combo.trigMask) != combo.holdMask) {
|
||||
continue;
|
||||
}
|
||||
if (combo.trigMask != 0 && !(trig & combo.trigMask)) {
|
||||
continue;
|
||||
}
|
||||
if (combo.condition != nullptr && !combo.condition()) {
|
||||
continue;
|
||||
}
|
||||
combo.action();
|
||||
if (combo.consumeMask != 0) {
|
||||
consumeButtons(combo.consumeMask);
|
||||
}
|
||||
if (combo.exclusive) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dusk
|
||||
@@ -238,12 +238,6 @@ namespace dusk {
|
||||
getTransientSettings().skipFrameRateLimit = getSettings().game.enableTurboKeybind &&
|
||||
(ImGui::IsKeyDown(ImGuiKey_Tab) || getActionBindHoldAnyPort(ActionBinds::TURBO_SPEED_BUTTON));
|
||||
|
||||
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)) {
|
||||
getTransientSettings().moveLinkActive = !getTransientSettings().moveLinkActive;
|
||||
}
|
||||
if (mDoMain::developmentMode != 1) {
|
||||
getTransientSettings().moveLinkActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiConsole::PreDraw() {
|
||||
|
||||
@@ -144,7 +144,9 @@ UserSettings g_userSettings = {
|
||||
.recordingMode {"game.recordingMode", false},
|
||||
.removeQuestMapMarkers {"game.removeQuestMapMarkers", false},
|
||||
.showInputViewer {"game.showInputViewer", false},
|
||||
.showInputViewerGyro {"game.showInputViewerGyro", false}
|
||||
.showInputViewerGyro {"game.showInputViewerGyro", false},
|
||||
.enableMoveLinkCombo {"game.enableMoveLinkCombo", false},
|
||||
.enableTeleportCombo {"game.enableTeleportCombo", false}
|
||||
},
|
||||
|
||||
.backend = {
|
||||
@@ -275,6 +277,8 @@ void registerSettings() {
|
||||
Register(g_userSettings.game.removeQuestMapMarkers);
|
||||
Register(g_userSettings.game.showInputViewer);
|
||||
Register(g_userSettings.game.showInputViewerGyro);
|
||||
Register(g_userSettings.game.enableMoveLinkCombo);
|
||||
Register(g_userSettings.game.enableTeleportCombo);
|
||||
Register(g_userSettings.game.fastSpinner);
|
||||
Register(g_userSettings.game.infiniteHearts);
|
||||
Register(g_userSettings.game.infiniteArrows);
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
#include "command_console.hpp"
|
||||
|
||||
#include <RmlUi/Core.h>
|
||||
#include <RmlUi/Core/Elements/ElementFormControlInput.h>
|
||||
#include <SDL3/SDL_keyboard.h>
|
||||
#include <aurora/rmlui.hpp>
|
||||
#include <imgui.h>
|
||||
|
||||
#include "dusk/settings.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "fmt/format.h"
|
||||
#include "ui.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
static const Rml::String kDocumentSource = R"RML(
|
||||
<rml>
|
||||
<head>
|
||||
<link type="text/rcss" href="res/rml/command_console.rcss" />
|
||||
</head>
|
||||
<body>
|
||||
<console id="console">
|
||||
<output id="console-output"></output>
|
||||
<input id="console-input" type="text" maxlength="255" />
|
||||
</console>
|
||||
</body>
|
||||
</rml>
|
||||
)RML";
|
||||
|
||||
static bool isCommand(std::string_view text) {
|
||||
return text.size() >= 2 && text[0] == '>' && text[1] == ' ';
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CommandConsole::CommandConsole() : Document(kDocumentSource) {
|
||||
mConsole = mDocument ? mDocument->GetElementById("console") : nullptr;
|
||||
mOutput = mDocument ? mDocument->GetElementById("console-output") : nullptr;
|
||||
auto* rawInput = mDocument ? mDocument->GetElementById("console-input") : nullptr;
|
||||
mInput = rmlui_dynamic_cast<Rml::ElementFormControlInput*>(rawInput);
|
||||
|
||||
listen(
|
||||
Rml::EventId::Keydown,
|
||||
[this](Rml::Event& event) {
|
||||
if (!mInputActive) {
|
||||
return;
|
||||
}
|
||||
const auto key = static_cast<Rml::Input::KeyIdentifier>(event.GetParameter<int>("key_identifier", Rml::Input::KI_UNKNOWN));
|
||||
if (key == Rml::Input::KI_RETURN) {
|
||||
executeFromInput();
|
||||
event.StopImmediatePropagation();
|
||||
} else if (key == Rml::Input::KI_ESCAPE) {
|
||||
hide(true);
|
||||
event.StopImmediatePropagation();
|
||||
} else if (key == Rml::Input::KI_UP) {
|
||||
navigateHistory(-1);
|
||||
event.StopImmediatePropagation();
|
||||
} else if (key == Rml::Input::KI_DOWN) {
|
||||
navigateHistory(+1);
|
||||
event.StopImmediatePropagation();
|
||||
}
|
||||
},
|
||||
true);
|
||||
}
|
||||
|
||||
bool CommandConsole::handle_nav_command(Rml::Event&, NavCommand) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void CommandConsole::update() {
|
||||
if (!getSettings().backend.enableAdvancedSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float dt = std::max(ImGui::GetIO().DeltaTime, 0.0f);
|
||||
for (auto& line : mOutputLines) {
|
||||
line.remain -= dt;
|
||||
}
|
||||
const auto [first, last] =
|
||||
std::ranges::remove_if(mOutputLines, [](const OutputLine& l) { return l.remain <= 0.0f; });
|
||||
mOutputLines.erase(first, last);
|
||||
|
||||
if (mOutputLines.empty() && !mInputActive) {
|
||||
Document::hide(mPendingClose);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mOutput == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
Rml::String html;
|
||||
if (mInputActive) {
|
||||
for (const auto& msg : mMsgHistory) {
|
||||
html += isCommand(msg) ? "<line class=\"cmd\">" + escape(msg) + "</line>" : "<line>" + escape(msg) + "</line>";
|
||||
}
|
||||
mOutput->SetAttribute("open", "");
|
||||
} else {
|
||||
const int total = (int)mOutputLines.size();
|
||||
const int startIdx = std::max(0, total - kMaxVisibleLines);
|
||||
for (int i = startIdx; i < total; ++i) {
|
||||
const auto& line = mOutputLines[i];
|
||||
const float alpha = line.remain < kFadeSeconds ? line.remain / kFadeSeconds : 1.0f;
|
||||
const Rml::String cls = isCommand(line.text) ? " class=\"cmd\"" : "";
|
||||
html += fmt::format(FMT_STRING("<line{} style=\"opacity: {:.3f}\">{}</line>"), cls,
|
||||
alpha, escape(line.text));
|
||||
}
|
||||
mOutput->RemoveAttribute("open");
|
||||
}
|
||||
|
||||
mOutput->SetInnerRML(html);
|
||||
|
||||
if (mScrollToBottom && mInputActive) {
|
||||
mOutput->SetScrollTop(1e9f);
|
||||
mScrollToBottom = false;
|
||||
}
|
||||
}
|
||||
|
||||
void CommandConsole::show() {
|
||||
if (mDocument != nullptr) {
|
||||
mDocument->Show(Rml::ModalFlag::None, Rml::FocusFlag::None, Rml::ScrollFlag::None);
|
||||
}
|
||||
mInputActive = true;
|
||||
mScrollToBottom = true;
|
||||
if (mConsole != nullptr) {
|
||||
mConsole->SetAttribute("open", "");
|
||||
}
|
||||
focus();
|
||||
}
|
||||
|
||||
bool CommandConsole::focus() {
|
||||
if (mInput != nullptr) {
|
||||
mInput->SetValue("");
|
||||
aurora::rmlui::set_input_type(aurora::rmlui::InputType::Text);
|
||||
return mInput->Focus(true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void CommandConsole::hide(bool close) {
|
||||
mInputActive = false;
|
||||
mHistoryPos = -1;
|
||||
if (mConsole != nullptr) {
|
||||
mConsole->RemoveAttribute("open");
|
||||
}
|
||||
if (mInput != nullptr) {
|
||||
mInput->SetValue("");
|
||||
}
|
||||
mPendingClose = close;
|
||||
// Immediately refocus
|
||||
if (auto* doc = top_document()) {
|
||||
doc->focus();
|
||||
}
|
||||
}
|
||||
|
||||
void CommandConsole::executeFromInput() {
|
||||
if (mInput == nullptr) {
|
||||
return;
|
||||
}
|
||||
const Rml::String value = mInput->GetValue();
|
||||
hide(true);
|
||||
if (!value.empty()) {
|
||||
runCommand(value, mState, [this](std::string text) { ConsolePrint(std::move(text)); });
|
||||
}
|
||||
mScrollToBottom = true;
|
||||
}
|
||||
|
||||
void CommandConsole::navigateHistory(int dir) {
|
||||
if (mState.history.empty() || mInput == nullptr) {
|
||||
return;
|
||||
}
|
||||
const int prev = mHistoryPos;
|
||||
if (dir < 0) {
|
||||
if (mHistoryPos == -1) {
|
||||
mHistoryPos = (int)mState.history.size() - 1;
|
||||
} else if (mHistoryPos > 0) {
|
||||
--mHistoryPos;
|
||||
}
|
||||
} else {
|
||||
if (mHistoryPos != -1 && ++mHistoryPos >= (int)mState.history.size()) {
|
||||
mHistoryPos = -1;
|
||||
}
|
||||
}
|
||||
if (prev != mHistoryPos) {
|
||||
const char* str = mHistoryPos >= 0 ? mState.history[mHistoryPos].c_str() : "";
|
||||
mInput->SetValue(str);
|
||||
const int end = static_cast<int>(Rml::StringUtilities::LengthUTF8(mInput->GetValue()));
|
||||
mInput->SetSelectionRange(end, end);
|
||||
}
|
||||
}
|
||||
|
||||
void CommandConsole::ConsolePrint(std::string text) {
|
||||
mMsgHistory.push_back(text);
|
||||
if ((int)mMsgHistory.size() > kMaxMsgHistory) {
|
||||
mMsgHistory.erase(mMsgHistory.begin());
|
||||
}
|
||||
mOutputLines.push_back({std::move(text), kDurationNormal});
|
||||
mScrollToBottom = true;
|
||||
if ((int)mOutputLines.size() > kMaxStoredLines) {
|
||||
mOutputLines.erase(mOutputLines.begin());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include "document.hpp"
|
||||
#include "dusk/commands.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Rml {
|
||||
class ElementFormControlInput;
|
||||
}
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class CommandConsole : public Document {
|
||||
public:
|
||||
CommandConsole();
|
||||
|
||||
void update() override;
|
||||
void show() override;
|
||||
bool focus() override;
|
||||
void hide(bool close) override;
|
||||
|
||||
private:
|
||||
struct OutputLine {
|
||||
std::string text;
|
||||
float remain;
|
||||
};
|
||||
|
||||
static constexpr float kDurationNormal = 6.0f;
|
||||
static constexpr float kFadeSeconds = 0.8f;
|
||||
static constexpr int kMaxStoredLines = 64;
|
||||
static constexpr int kMaxVisibleLines = 24;
|
||||
static constexpr int kMaxMsgHistory = 500;
|
||||
|
||||
Rml::Element* mConsole = nullptr;
|
||||
Rml::Element* mOutput = nullptr;
|
||||
Rml::ElementFormControlInput* mInput = nullptr;
|
||||
|
||||
std::vector<OutputLine> mOutputLines;
|
||||
std::vector<std::string> mMsgHistory;
|
||||
int mHistoryPos = -1;
|
||||
bool mInputActive = false;
|
||||
bool mScrollToBottom = false;
|
||||
|
||||
CommandState mState;
|
||||
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
|
||||
|
||||
void ConsolePrint(std::string text);
|
||||
void executeFromInput();
|
||||
void navigateHistory(int dir);
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -68,6 +68,7 @@ void Document::show() {
|
||||
focus();
|
||||
}
|
||||
}
|
||||
mPendingClose = false;
|
||||
}
|
||||
|
||||
void Document::hide(bool close) {
|
||||
|
||||
@@ -1524,6 +1524,20 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
"Recording Mode",
|
||||
"Disables the game HUD and all background music.<br/><br/>Useful for recording footage.");
|
||||
});
|
||||
|
||||
add_tab("Tools", [this](Rml::Element* content) {
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
|
||||
|
||||
leftPane.add_section("Link");
|
||||
add_speedrun_disabled_option(leftPane, rightPane, getSettings().game.enableMoveLinkCombo,
|
||||
"Move Link (L+R+Y)",
|
||||
"Enables the L+R+Y button combo to toggle freely repositioning Link.");
|
||||
add_speedrun_disabled_option(leftPane, rightPane, getSettings().game.enableTeleportCombo,
|
||||
"Teleport (R+D-pad Up/Down)",
|
||||
"R+D-pad Up stores Link's current position.<br/>"
|
||||
"R+D-pad Down teleports Link back to it.");
|
||||
});
|
||||
}
|
||||
|
||||
void SettingsWindow::update() {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <ranges>
|
||||
|
||||
#include "aurora/lib/window.hpp"
|
||||
#include "command_console.hpp"
|
||||
#include "dusk/io.hpp"
|
||||
#include "input.hpp"
|
||||
#include "prelaunch.hpp"
|
||||
@@ -172,6 +173,19 @@ void handle_event(const SDL_Event& event) noexcept {
|
||||
sConnectedGamepads.erase(event.gdevice.which);
|
||||
}
|
||||
input::handle_event(event);
|
||||
// TODO: don't overlap with PAD bindings?
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_SLASH) {
|
||||
bool found = false;
|
||||
for (auto& doc : sDocumentStack) {
|
||||
if (auto* console = dynamic_cast<CommandConsole*>(doc.get())) {
|
||||
console->show();
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
push_document(std::make_unique<CommandConsole>(), true, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Document& push_document(std::unique_ptr<Document> doc, bool show, bool passive) noexcept {
|
||||
|
||||
@@ -756,23 +756,7 @@ static void duskExecute() {
|
||||
isRecording = false;
|
||||
}
|
||||
|
||||
if (mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getTrigX(PAD_1)) {
|
||||
if (const auto link = g_dComIfG_gameInfo.play.getPlayer(0)) {
|
||||
dynamic_cast<daAlink_c*>(link)->handleWolfHowl();
|
||||
}
|
||||
}
|
||||
|
||||
if ((mDoCPd_c::getHold(PAD_1) & (PAD_TRIGGER_R | PAD_TRIGGER_L)) == PAD_TRIGGER_R && mDoCPd_c::getTrigY(PAD_1)) {
|
||||
if (const auto link = g_dComIfG_gameInfo.play.getPlayer(0)) {
|
||||
dynamic_cast<daAlink_c*>(link)->handleQuickTransform();
|
||||
}
|
||||
}
|
||||
|
||||
if (dusk::getSettings().game.moonJump && (mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1))) {
|
||||
if (const auto link = g_dComIfG_gameInfo.play.getPlayer(0)) {
|
||||
link->speed.y = 56.0f;
|
||||
}
|
||||
}
|
||||
|
||||
if (dusk::getSettings().game.fastSpinner && mDoCPd_c::getHoldR(PAD_1)) {
|
||||
if (const auto link = g_dComIfG_gameInfo.play.getPlayer(0)) {
|
||||
|
||||
+16
-3
@@ -55,12 +55,14 @@
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include "dusk/game_clock.h"
|
||||
#include "dusk/gyro.h"
|
||||
#include "dusk/game_combos.h"
|
||||
#include "dusk/mouse.h"
|
||||
#include "dusk/imgui/ImGuiConsole.hpp"
|
||||
#include "dusk/imgui/ImGuiEngine.hpp"
|
||||
#include "dusk/iso_validate.hpp"
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/ui/command_console.hpp"
|
||||
#include "dusk/ui/menu_bar.hpp"
|
||||
#include "dusk/ui/overlay.hpp"
|
||||
#include "dusk/ui/prelaunch.hpp"
|
||||
@@ -294,6 +296,7 @@ void main01(void) {
|
||||
mDoCPd_c::read();
|
||||
dusk::mouse::read();
|
||||
dusk::gyro::read(pacing.sim_pace);
|
||||
dusk::processGameCombos();
|
||||
fapGm_Execute();
|
||||
mDoAud_Execute();
|
||||
dusk::game_clock::commit_sim_tick();
|
||||
@@ -310,13 +313,14 @@ void main01(void) {
|
||||
dusk::frame_interp::end_presentation_camera();
|
||||
dusk::frame_interp::set_ui_tick_pending(false);
|
||||
} else {
|
||||
dusk::frame_interp::begin_frame(dusk::FrameInterpMode::Off, true, 0.0f);
|
||||
dusk::frame_interp::set_ui_tick_pending(true);
|
||||
|
||||
// Game Inputs
|
||||
mDoCPd_c::read();
|
||||
dusk::mouse::read();
|
||||
dusk::gyro::read(pacing.presentation_dt_seconds);
|
||||
dusk::processGameCombos();
|
||||
|
||||
dusk::frame_interp::begin_frame(dusk::FrameInterpMode::Off, true, 0.0f);
|
||||
dusk::frame_interp::set_ui_tick_pending(true);
|
||||
|
||||
// EXECUTE GAME LOGIC & RENDER
|
||||
// This calls mDoGph_Painter -> JFWDisplay -> GX Functions
|
||||
@@ -338,6 +342,15 @@ void main01(void) {
|
||||
|
||||
Limiter::duration_t sleepTime = main_loop_limiter.Sleep(target_ns);
|
||||
dusk::frameUsagePct = 100.0f * (1.0f - static_cast<float>(sleepTime) / static_cast<float>(target_ns));
|
||||
} else if (!pacing.is_interpolating && !dusk::getTransientSettings().skipFrameRateLimit) {
|
||||
// Non-interp: throttle display rate to the configured sim rate so /rate works
|
||||
const double sim_fps = static_cast<double>(dusk::game_clock::get_sim_rate());
|
||||
const Limiter::duration_t sim_target_ns = static_cast<Limiter::duration_t>(1'000'000'000.0 / sim_fps);
|
||||
main_loop_limiter.Sleep(sim_target_ns);
|
||||
} else if (dusk::getTransientSettings().skipFrameRateLimit) {
|
||||
// Turbo: cap at 120 hz rather than running fully unlimited
|
||||
constexpr Limiter::duration_t kTurboTargetNs = 1'000'000'000LL / 120LL;
|
||||
main_loop_limiter.Sleep(kTurboTargetNs);
|
||||
} else {
|
||||
main_loop_limiter.Reset();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user