#include "commands.hpp" #include "JSystem/JUtility/JUTGamePad.h" #include "SSystem/SComponent/c_math.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_actor.h" #include "d/d_com_inf_game.h" #include "d/d_camera.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 #include #include #include #include #include "fmt/format.h" namespace dusk { namespace { static constexpr int kMaxHistory = 64; static std::vector SplitArgs(std::string_view input) { std::vector 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 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{v} : std::nullopt; } static std::optional 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{v} : std::nullopt; } static const char* ActorShortName(s16 profname) { const char* n = dStage_getName(profname, -1); return n ? n : "?"; } // Resolves @found / @link / @ 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(proc); } static std::optional 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{entry->procname} : std::nullopt; } static std::optional ParseXYZ(const std::vector& 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(*x, *y, *z)} : std::nullopt; } static bool TryAngle( s16& out, const std::vector& 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(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(p); if (fpcBs_Is_JustOfType(g_fpcNd_type, proc->subtype)) { fpcLyIt_OnlyHere(&static_cast(p)->layer, callback, ctx); } } struct ListContext { s16 targetId; std::vector* output; }; struct FindContext { s16 targetId; std::vector matches; }; static int ListActorCallback(void* p, void* ctx) { auto* proc = static_cast(p); auto* context = static_cast(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(p); auto* context = static_cast(ctx); if (fopAcM_IsActor(proc) && proc->profname == context->targetId) { context->matches.push_back(proc); } recurseLayer(p, FindActorCallback, ctx); return 1; } struct CameraFlyState { bool active = false; cXyz startEye; cXyz startCenter; cXyz endEye; cXyz endCenter; float duration = 0.0f; float elapsed = 0.0f; }; CameraFlyState s_cameraFly; static dCamera_c* getCamera() { return dCam_getBody(); } static cXyz anglesToDir(s16 h, s16 v) { return cXyz(cM_scos(v) * cM_ssin(h), cM_ssin(v), cM_scos(v) * cM_scos(h)); } static void cameraToAngles(dCamera_c* cam, s16& h, s16& v) { const cXyz eye = cam->iEye(); const cXyz ctr = cam->iCenter(); const float dx = ctr.x - eye.x; const float dy = ctr.y - eye.y; const float dz = ctr.z - eye.z; const float hDist = sqrtf(dx * dx + dz * dz); h = cM_atan2s(dx, dz); v = cM_atan2s(dy, hDist); } static float smoothstep(float t) { t = std::clamp(t, 0.0f, 1.0f); return t * t * (3.0f - 2.0f * t); } static constexpr float kCamTargetDist = 100.0f; } // namespace bool isCameraDetached() { return s_cameraFly.active; } void processCameraCommands() { if (!s_cameraFly.active) { return; } dCamera_c* cam = getCamera(); if (cam == nullptr) { s_cameraFly.active = false; return; } cXyz eye, center; if (s_cameraFly.duration <= 0.0f) { eye = s_cameraFly.endEye; center = s_cameraFly.endCenter; } else { s_cameraFly.elapsed += dusk::game_clock::sim_pace(); const float t = smoothstep(s_cameraFly.elapsed / s_cameraFly.duration); eye = s_cameraFly.startEye + (s_cameraFly.endEye - s_cameraFly.startEye) * t; center = s_cameraFly.startCenter + (s_cameraFly.endCenter - s_cameraFly.startCenter) * t; if (s_cameraFly.elapsed >= s_cameraFly.duration) { eye = s_cameraFly.endEye; center = s_cameraFly.endCenter; s_cameraFly.startEye = s_cameraFly.endEye; s_cameraFly.startCenter = s_cameraFly.endCenter; s_cameraFly.duration = 0.0f; } } cam->Reset(center, eye); cam->mDebugFlyCam.initialized = false; } void runCommand(std::string_view cmdLine, CommandState& state, const CommandOutput& output) { if (state.echoEnabled) { 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 [angle] | tp @ [ [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 [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 [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 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 "); 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 == "freeze" || cmd == "unfreeze") { const bool doFreeze = (cmd == "freeze"); if (args.size() < 2) { g_dComIfAc_gameInfo.mPause = doFreeze; output(doFreeze ? "Global freeze on" : "Global freeze off"); return; } auto* ac = ParseActorArg(args[1], state.foundProcId, output); if (ac == nullptr) { return; } if (doFreeze) { fpcM_PauseEnable(ac, 1); output(fmt::format(FMT_STRING("Froze actor {}"), (unsigned int)ac->id)); } else { fpcM_PauseDisable(ac, 1); output(fmt::format(FMT_STRING("Unfroze actor {}"), (unsigned int)ac->id)); } 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 == "camera") { const std::string sub = args.size() >= 2 ? args[1] : ""; if (sub == "attach") { s_cameraFly.active = false; output("Camera attached"); return; } auto* cam = getCamera(); if (cam == nullptr) { output("Error: camera not available"); return; } if (sub == "detach") { s_cameraFly.active = true; s_cameraFly.duration = 0.0f; s_cameraFly.startEye = s_cameraFly.endEye = cam->iEye(); s_cameraFly.startCenter = s_cameraFly.endCenter = cam->iCenter(); output("Camera detached"); return; } if (sub == "tp") { // camera tp @ if (args.size() >= 3 && !args[2].empty() && args[2][0] == '@') { auto* ac = ParseActorArg(args[2], state.foundProcId, output); if (ac == nullptr) { return; } const cXyz actorPos = ac->current.pos; const cXyz dir = anglesToDir(ac->shape_angle.y, 0); const cXyz newEye = cXyz(actorPos.x - dir.x * kCamTargetDist, actorPos.y - dir.y * kCamTargetDist + 50.0f, actorPos.z - dir.z * kCamTargetDist); s_cameraFly.active = true; s_cameraFly.duration = 0.0f; s_cameraFly.startEye = s_cameraFly.endEye = newEye; s_cameraFly.startCenter = s_cameraFly.endCenter = actorPos; output(fmt::format(FMT_STRING("Camera moved to actor {}"), ac->id)); return; } // camera tp x y z [h] [v] if (args.size() < 5) { output("Usage: camera tp [h_angle] [v_angle] | camera tp @"); return; } const auto pos = ParseXYZ(args, 2); if (!pos) { output("Error: invalid coordinates"); return; } s16 h = 0, v = 0; cameraToAngles(cam, h, v); if (args.size() >= 6) { const auto hv = ParseLong(args[5]); if (!hv) { output("Error: invalid h_angle"); return; } h = (s16)*hv; } if (args.size() >= 7) { const auto vv = ParseLong(args[6]); if (!vv) { output("Error: invalid v_angle"); return; } v = (s16)*vv; } const cXyz dir = anglesToDir(h, v); const cXyz center = cXyz(pos->x + dir.x * kCamTargetDist, pos->y + dir.y * kCamTargetDist, pos->z + dir.z * kCamTargetDist); s_cameraFly.active = true; s_cameraFly.duration = 0.0f; s_cameraFly.startEye = s_cameraFly.endEye = *pos; s_cameraFly.startCenter = s_cameraFly.endCenter = center; output(fmt::format(FMT_STRING("Camera teleported to ({:.2f}, {:.2f}, {:.2f})"), pos->x, pos->y, pos->z)); return; } if (sub == "pos") { s16 h = 0, v = 0; cameraToAngles(cam, h, v); const cXyz eye = cam->iEye(); output(fmt::format(FMT_STRING("Camera: ({:.2f}, {:.2f}, {:.2f}) h={} v={}"), eye.x, eye.y, eye.z, (int)h, (int)v)); return; } if (sub == "fly") { // camera fly