mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-05-24 06:50:43 -04:00
Merge branch 'main' of https://github.com/TwilitRealm/dusk into randomizer
This commit is contained in:
@@ -154,6 +154,7 @@ bool daAlink_c::checkGyroAimContext() {
|
||||
case PROC_BOW_SUBJECT:
|
||||
case PROC_BOOMERANG_SUBJECT:
|
||||
case PROC_COPY_ROD_SUBJECT:
|
||||
case PROC_HAWK_SUBJECT:
|
||||
case PROC_HOOKSHOT_SUBJECT:
|
||||
case PROC_SWIM_HOOKSHOT_SUBJECT:
|
||||
case PROC_HORSE_BOW_SUBJECT:
|
||||
|
||||
@@ -761,6 +761,11 @@ static void koro2_game(fshop_class* i_this) {
|
||||
sp5C.x = mDoCPd_c::getStickX3D(PAD_1);
|
||||
sp5C.y = 0.0f;
|
||||
sp5C.z = mDoCPd_c::getStickY(PAD_1);
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.enableMirrorMode) {
|
||||
sp5C.x = -sp5C.x;
|
||||
}
|
||||
#endif
|
||||
MtxPosition(&sp5C, &sp68);
|
||||
|
||||
f32 reg_f31 = sp68.x;
|
||||
@@ -782,20 +787,15 @@ static void koro2_game(fshop_class* i_this) {
|
||||
reg_f30 = 0.0f;
|
||||
}
|
||||
|
||||
s16 gyro_ax = 0;
|
||||
s16 gyro_az = 0;
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.enableGyroRollgoal) {
|
||||
s16 rg_add_x;
|
||||
s16 rg_add_z;
|
||||
dusk::gyro::rollgoalTableOffset(rg_add_x, rg_add_z);
|
||||
s16 tgt_x = static_cast<s16>(reg_f30 * (-6000.0f + JREG_F(7))) + rg_add_x;
|
||||
s16 tgt_z = static_cast<s16>(reg_f31 * (-6000.0f + JREG_F(8))) + rg_add_z;
|
||||
cLib_addCalcAngleS2(&i_this->field_0x4020.x, tgt_x, 4, 0x200);
|
||||
cLib_addCalcAngleS2(&i_this->field_0x4020.z, tgt_z, 4, 0x200);
|
||||
dusk::gyro::rollgoalTableOffset(gyro_ax, gyro_az);
|
||||
}
|
||||
#else
|
||||
cLib_addCalcAngleS2(&i_this->field_0x4020.x, reg_f30 * (-6000.0f + JREG_F(7)), 4, 0x200);
|
||||
cLib_addCalcAngleS2(&i_this->field_0x4020.z, reg_f31 * (-6000.0f + JREG_F(8)), 4, 0x200);
|
||||
#endif
|
||||
cLib_addCalcAngleS2(&i_this->field_0x4020.x, reg_f30 * (-6000.0f + JREG_F(7)) + gyro_ax, 4, 0x200);
|
||||
cLib_addCalcAngleS2(&i_this->field_0x4020.z, reg_f31 * (-6000.0f + JREG_F(8)) + gyro_az, 4, 0x200);
|
||||
}
|
||||
#if TARGET_PC
|
||||
if (i_this->field_0x4010 != 2) {
|
||||
|
||||
+10
-2
@@ -17,6 +17,7 @@ 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()));
|
||||
constexpr clock::duration kAbnormalGapResetThreshold = std::chrono::milliseconds(250);
|
||||
constexpr int kMaxSimTicksPerFrame = 2;
|
||||
|
||||
void ensure_initialized() {
|
||||
@@ -30,14 +31,15 @@ void ensure_initialized() {
|
||||
|
||||
void reset_frame_timer() {
|
||||
s_previous_sample = clock::now();
|
||||
s_current_snapshot_time = s_previous_sample;
|
||||
s_current_snapshot_time = s_previous_sample - kSimPeriodDuration;
|
||||
}
|
||||
|
||||
MainLoopPacer advance_main_loop() {
|
||||
ensure_initialized();
|
||||
|
||||
const clock::time_point now = clock::now();
|
||||
const float presentation_dt = std::chrono::duration<float>(now - s_previous_sample).count();
|
||||
const clock::duration frame_gap = now - s_previous_sample;
|
||||
const float presentation_dt = std::chrono::duration<float>(frame_gap).count();
|
||||
s_previous_sample = now;
|
||||
|
||||
MainLoopPacer out{};
|
||||
@@ -54,6 +56,12 @@ MainLoopPacer advance_main_loop() {
|
||||
return out;
|
||||
}
|
||||
|
||||
if (frame_gap > kAbnormalGapResetThreshold) {
|
||||
s_current_snapshot_time = now - kSimPeriodDuration;
|
||||
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;
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
namespace dusk::gyro {
|
||||
namespace {
|
||||
constexpr s32 kRollgoalTableMaxOffset = 12000;
|
||||
constexpr s32 kRollgoalTableMaxOffset = 6500;
|
||||
constexpr float kGyroEmaAlphaMin = 0.05f;
|
||||
constexpr float kGyroEmaAlphaMax = 1.0f;
|
||||
|
||||
|
||||
@@ -5,14 +5,22 @@
|
||||
#include "imgui.h"
|
||||
#include "fmt/format.h"
|
||||
#include "absl/strings/escaping.h"
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
#include "d/d_com_inf_game.h"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/io.hpp"
|
||||
#include "dusk/logging.h"
|
||||
#include "../file_select.hpp"
|
||||
#include "aurora/lib/window.hpp"
|
||||
|
||||
#include <unordered_set>
|
||||
#include <zstd.h>
|
||||
|
||||
namespace dusk {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct StateSharePacket {
|
||||
char stageName[8];
|
||||
@@ -23,9 +31,65 @@ struct StateSharePacket {
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
static constexpr size_t PACKET_TOTAL = sizeof(StateSharePacket) + sizeof(dSv_info_c);
|
||||
static constexpr size_t PACKET_TOTAL = sizeof(StateSharePacket) + sizeof(dSv_info_c);
|
||||
static constexpr size_t PACKET_SAVE_ONLY = sizeof(StateSharePacket) + sizeof(dSv_save_c);
|
||||
static constexpr auto STATES_FILENAME = "states.json";
|
||||
|
||||
void ImGuiStateShare::copyState() {
|
||||
static bool ValidateEncodedState(const std::string&);
|
||||
|
||||
void ImGuiStateShare::onMergeFileSelected(void* userdata, const char* path, const char* /*error*/) {
|
||||
auto* self = static_cast<ImGuiStateShare*>(userdata);
|
||||
if (path != nullptr) {
|
||||
self->m_pendingMergePath = path;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
static std::string GetStatesFilePath() {
|
||||
return (dusk::ConfigPath / STATES_FILENAME).string();
|
||||
}
|
||||
|
||||
void ImGuiStateShare::loadStatesFile() {
|
||||
m_loaded = true;
|
||||
const std::filesystem::path filePath = dusk::ConfigPath / STATES_FILENAME;
|
||||
if (!std::filesystem::exists(filePath)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const std::string pathStr = filePath.string();
|
||||
auto data = io::FileStream::ReadAllBytes(pathStr.c_str());
|
||||
auto j = json::parse(data);
|
||||
if (!j.is_array()) {
|
||||
return;
|
||||
}
|
||||
for (const auto& entry : j) {
|
||||
if (!entry.contains("name") || !entry.contains("data")) {
|
||||
continue;
|
||||
}
|
||||
SavedStateEntry s;
|
||||
s.name = entry["name"].get<std::string>();
|
||||
s.encoded = entry["data"].get<std::string>();
|
||||
m_states.push_back(std::move(s));
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
m_statusMsg = fmt::format("Failed to load states: {}", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiStateShare::saveStatesFile() {
|
||||
json j = json::array();
|
||||
for (const auto& s : m_states) {
|
||||
j.push_back(json{{"name", s.name}, {"data", s.encoded}});
|
||||
}
|
||||
try {
|
||||
io::FileStream::WriteAllText(GetStatesFilePath().c_str(), j.dump(2));
|
||||
} catch (const std::exception& e) {
|
||||
m_statusMsg = fmt::format("Failed to save states: {}", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
std::string ImGuiStateShare::encodeCurrentState() {
|
||||
StateSharePacket pkt = {};
|
||||
strncpy(pkt.stageName, dComIfGp_getStartStageName(), 7);
|
||||
pkt.roomNo = dComIfGp_getStartStageRoomNo();
|
||||
@@ -40,26 +104,25 @@ void ImGuiStateShare::copyState() {
|
||||
std::string compressed(bound, '\0');
|
||||
compressed.resize(ZSTD_compress(compressed.data(), bound, raw.data(), raw.size(), 1));
|
||||
|
||||
std::string encoded = absl::Base64Escape(compressed);
|
||||
ImGui::SetClipboardText(encoded.c_str());
|
||||
m_statusMsg = "Copied to clipboard.";
|
||||
return absl::Base64Escape(compressed);
|
||||
}
|
||||
|
||||
bool ImGuiStateShare::pasteState() {
|
||||
const char* clip = ImGui::GetClipboardText();
|
||||
if (!clip || clip[0] == '\0') {
|
||||
m_statusMsg = "Clipboard is empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ImGuiStateShare::applyEncodedState(const std::string& encoded, const std::string& name) {
|
||||
std::string decoded;
|
||||
if (!absl::Base64Unescape(clip, &decoded)) {
|
||||
if (!absl::Base64Unescape(encoded, &decoded)) {
|
||||
m_statusMsg = "Invalid base64.";
|
||||
return false;
|
||||
}
|
||||
|
||||
unsigned long long dSize = ZSTD_getFrameContentSize(decoded.data(), decoded.size());
|
||||
if (dSize == ZSTD_CONTENTSIZE_ERROR || dSize == ZSTD_CONTENTSIZE_UNKNOWN || dSize < PACKET_TOTAL) {
|
||||
if (dSize == ZSTD_CONTENTSIZE_ERROR || dSize == ZSTD_CONTENTSIZE_UNKNOWN) {
|
||||
m_statusMsg = "Not a valid state string.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool isFull = (dSize == PACKET_TOTAL);
|
||||
const bool isPartial = (dSize == PACKET_SAVE_ONLY);
|
||||
if (!isFull && !isPartial) {
|
||||
m_statusMsg = "Not a valid state string.";
|
||||
return false;
|
||||
}
|
||||
@@ -75,45 +138,261 @@ bool ImGuiStateShare::pasteState() {
|
||||
memcpy(&pkt, raw.data(), sizeof(pkt));
|
||||
pkt.stageName[7] = '\0';
|
||||
|
||||
memcpy(&g_dComIfG_gameInfo.info, raw.data() + sizeof(pkt), sizeof(dSv_info_c));
|
||||
if (isFull) {
|
||||
memcpy(&g_dComIfG_gameInfo.info, raw.data() + sizeof(pkt), sizeof(dSv_info_c));
|
||||
m_pendingInfo = g_dComIfG_gameInfo.info;
|
||||
m_pendingSavedata.reset();
|
||||
} else {
|
||||
memcpy(&g_dComIfG_gameInfo.info.mSavedata, raw.data() + sizeof(pkt), sizeof(dSv_save_c));
|
||||
m_pendingSavedata = g_dComIfG_gameInfo.info.mSavedata;
|
||||
m_pendingInfo.reset();
|
||||
}
|
||||
|
||||
s16 spawnPoint = pkt.startPoint == -4 ? -1 : pkt.startPoint;
|
||||
|
||||
if (spawnPoint == -1) {
|
||||
dComIfGs_setRestartRoomParam(pkt.roomNo & 0x3F);
|
||||
}
|
||||
|
||||
dComIfGp_setNextStage(pkt.stageName, spawnPoint, pkt.roomNo, pkt.layer);
|
||||
m_pendingInfo = g_dComIfG_gameInfo.info;
|
||||
|
||||
m_statusMsg = fmt::format("Warping to {} room {} layer {}.", pkt.stageName, (int)pkt.roomNo, (int)pkt.layer);
|
||||
if (name.empty()) {
|
||||
m_statusMsg = fmt::format("{} room {} layer {}.", pkt.stageName, (int)pkt.roomNo, (int)pkt.layer);
|
||||
} else {
|
||||
m_statusMsg = fmt::format("{}: {} room {} layer {}.", name, pkt.stageName, (int)pkt.roomNo, (int)pkt.layer);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImGuiStateShare::tickPendingApply() {
|
||||
if (!m_pendingInfo.has_value() || dComIfGp_isEnableNextStage())
|
||||
if (!m_pendingInfo.has_value() && !m_pendingSavedata.has_value()) {
|
||||
return;
|
||||
g_dComIfG_gameInfo.info = *m_pendingInfo;
|
||||
m_pendingInfo.reset();
|
||||
}
|
||||
if (dComIfGp_isEnableNextStage()) {
|
||||
return;
|
||||
}
|
||||
if (m_pendingInfo.has_value()) {
|
||||
g_dComIfG_gameInfo.info = *m_pendingInfo;
|
||||
m_pendingInfo.reset();
|
||||
} else {
|
||||
g_dComIfG_gameInfo.info.mSavedata = *m_pendingSavedata;
|
||||
m_pendingSavedata.reset();
|
||||
}
|
||||
dComIfGp_offOxygenShowFlag();
|
||||
dComIfGp_setMaxOxygen(600);
|
||||
dComIfGp_setOxygen(600);
|
||||
}
|
||||
|
||||
static bool ValidateEncodedState(const std::string& encoded) {
|
||||
std::string decoded;
|
||||
if (!absl::Base64Unescape(encoded, &decoded)) {
|
||||
return false;
|
||||
}
|
||||
unsigned long long dSize = ZSTD_getFrameContentSize(decoded.data(), decoded.size());
|
||||
return dSize == PACKET_TOTAL || dSize == PACKET_SAVE_ONLY;
|
||||
}
|
||||
|
||||
void ImGuiStateShare::mergeFromFile(const std::string& path) {
|
||||
try {
|
||||
auto data = io::FileStream::ReadAllBytes(path.c_str());
|
||||
auto j = json::parse(data);
|
||||
if (!j.is_array()) {
|
||||
m_statusMsg = "File does not contain a JSON array.";
|
||||
return;
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> existingNames;
|
||||
for (const auto& s : m_states) {
|
||||
existingNames.insert(s.name);
|
||||
}
|
||||
|
||||
int added = 0;
|
||||
int skipped = 0;
|
||||
for (const auto& entry : j) {
|
||||
if (!entry.contains("name") || !entry.contains("data")) {
|
||||
++skipped;
|
||||
continue;
|
||||
}
|
||||
const std::string name = entry["name"].get<std::string>();
|
||||
const std::string encoded = entry["data"].get<std::string>();
|
||||
if (!ValidateEncodedState(encoded)) {
|
||||
++skipped;
|
||||
continue;
|
||||
}
|
||||
if (existingNames.count(name)) {
|
||||
++skipped;
|
||||
continue;
|
||||
}
|
||||
SavedStateEntry s;
|
||||
s.name = name;
|
||||
s.encoded = encoded;
|
||||
existingNames.insert(s.name);
|
||||
m_states.push_back(std::move(s));
|
||||
++added;
|
||||
}
|
||||
|
||||
if (added > 0) {
|
||||
saveStatesFile();
|
||||
}
|
||||
m_statusMsg = fmt::format("Merged: {} added, {} skipped.", added, skipped);
|
||||
} catch (const std::exception& e) {
|
||||
m_statusMsg = fmt::format("Failed to load file: {}", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiStateShare::draw(bool& open) {
|
||||
if (dusk::IsGameLaunched)
|
||||
if (dusk::IsGameLaunched) {
|
||||
tickPendingApply();
|
||||
}
|
||||
|
||||
if (!open)
|
||||
if (!m_loaded) {
|
||||
loadStatesFile();
|
||||
}
|
||||
|
||||
if (!m_pendingMergePath.empty()) {
|
||||
mergeFromFile(m_pendingMergePath);
|
||||
m_pendingMergePath.clear();
|
||||
}
|
||||
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::SetNextWindowSizeConstraints(ImVec2(400, 0), ImVec2(FLT_MAX, FLT_MAX));
|
||||
if (!ImGui::Begin("State Share", &open, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dusk::IsGameLaunched) ImGui::BeginDisabled();
|
||||
if (ImGui::Button("Copy State")) copyState();
|
||||
const bool gameRunning = dusk::IsGameLaunched;
|
||||
|
||||
const float rowH = ImGui::GetTextLineHeightWithSpacing();
|
||||
const float listH = rowH * 8 + ImGui::GetStyle().FramePadding.y * 2;
|
||||
ImGui::BeginChild("##states", ImVec2(0, listH), true);
|
||||
|
||||
if (m_states.empty()) {
|
||||
ImGui::TextDisabled("No saved states. Save or import one below.");
|
||||
}
|
||||
|
||||
int toDelete = -1;
|
||||
for (int i = 0; i < (int)m_states.size(); ++i) {
|
||||
ImGui::PushID(i);
|
||||
|
||||
if (m_renamingIndex == i) {
|
||||
ImGui::SetNextItemWidth(150);
|
||||
bool done = ImGui::InputText("##rename", m_renameBuffer, sizeof(m_renameBuffer),
|
||||
ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll);
|
||||
if (done) {
|
||||
if (m_renameBuffer[0] != '\0') {
|
||||
m_states[i].name = m_renameBuffer;
|
||||
}
|
||||
m_renamingIndex = -1;
|
||||
saveStatesFile();
|
||||
} else if (ImGui::IsItemDeactivated()) {
|
||||
m_renamingIndex = -1;
|
||||
}
|
||||
} else {
|
||||
ImGui::Selectable(m_states[i].name.c_str(), false, ImGuiSelectableFlags_None, ImVec2(150, 0));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Double-click to rename");
|
||||
if (ImGui::IsMouseDoubleClicked(0)) {
|
||||
m_renamingIndex = i;
|
||||
strncpy(m_renameBuffer, m_states[i].name.c_str(), sizeof(m_renameBuffer) - 1);
|
||||
m_renameBuffer[sizeof(m_renameBuffer) - 1] = '\0';
|
||||
ImGui::SetKeyboardFocusHere(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (!gameRunning) { ImGui::BeginDisabled(); }
|
||||
if (ImGui::Button("Load")) {
|
||||
applyEncodedState(m_states[i].encoded, m_states[i].name);
|
||||
}
|
||||
if (!gameRunning) { ImGui::EndDisabled(); }
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Copy")) {
|
||||
ImGui::SetClipboardText(m_states[i].encoded.c_str());
|
||||
m_statusMsg = fmt::format("'{}' copied to clipboard.", m_states[i].name);
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Del")) {
|
||||
toDelete = i;
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
if (toDelete >= 0) {
|
||||
if (m_renamingIndex == toDelete) { m_renamingIndex = -1; }
|
||||
m_states.erase(m_states.begin() + toDelete);
|
||||
saveStatesFile();
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
// Toolbar
|
||||
if (!gameRunning) { ImGui::BeginDisabled(); }
|
||||
if (ImGui::Button("Save")) {
|
||||
SavedStateEntry entry;
|
||||
entry.name = fmt::format("State {}", m_states.size() + 1);
|
||||
entry.encoded = encodeCurrentState();
|
||||
m_states.push_back(std::move(entry));
|
||||
saveStatesFile();
|
||||
m_statusMsg = fmt::format("Saved as '{}'.", m_states.back().name);
|
||||
}
|
||||
if (!gameRunning) { ImGui::EndDisabled(); }
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Import State")) pasteState();
|
||||
if (!dusk::IsGameLaunched) ImGui::EndDisabled();
|
||||
if (ImGui::Button("Import Clipboard")) {
|
||||
const char* clip = ImGui::GetClipboardText();
|
||||
if (!clip || clip[0] == '\0') {
|
||||
m_statusMsg = "Clipboard is empty.";
|
||||
} else {
|
||||
std::string clipStr = clip;
|
||||
if (!ValidateEncodedState(clipStr)) {
|
||||
m_statusMsg = "Clipboard does not contain a valid state.";
|
||||
} else {
|
||||
SavedStateEntry entry;
|
||||
entry.name = fmt::format("Imported {}", m_states.size() + 1);
|
||||
entry.encoded = std::move(clipStr);
|
||||
m_states.push_back(std::move(entry));
|
||||
saveStatesFile();
|
||||
m_statusMsg = fmt::format("Imported as '{}'.", m_states.back().name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Load Pack")) {
|
||||
static constexpr SDL_DialogFileFilter filter = {"State pack", "json"};
|
||||
ShowFileSelect(&onMergeFileSelected, this, aurora::window::get_sdl_window(), &filter, 1, nullptr, false);
|
||||
}
|
||||
|
||||
if (!m_states.empty()) {
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Clear All")) {
|
||||
ImGui::OpenPopup("##clearall");
|
||||
}
|
||||
|
||||
if (ImGui::BeginPopup("##clearall")) {
|
||||
ImGui::Text("Delete all saved states?");
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("Yes, clear all")) {
|
||||
m_states.clear();
|
||||
m_renamingIndex = -1;
|
||||
saveStatesFile();
|
||||
m_statusMsg = "All states cleared.";
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel")) {
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_statusMsg.empty()) {
|
||||
ImGui::Spacing();
|
||||
@@ -125,8 +404,9 @@ void ImGuiStateShare::draw(bool& open) {
|
||||
}
|
||||
|
||||
void ImGuiMenuTools::ShowStateShare() {
|
||||
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F8, m_showStateShare))
|
||||
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F8, m_showStateShare)) {
|
||||
return;
|
||||
}
|
||||
m_stateShare.draw(m_showStateShare);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,21 +4,38 @@
|
||||
#include "d/d_save.h"
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk {
|
||||
class ImGuiStateShare {
|
||||
public:
|
||||
void draw(bool& open);
|
||||
|
||||
private:
|
||||
void copyState();
|
||||
bool pasteState();
|
||||
void tickPendingApply();
|
||||
struct SavedStateEntry {
|
||||
std::string name;
|
||||
std::string encoded;
|
||||
};
|
||||
|
||||
class ImGuiStateShare {
|
||||
public:
|
||||
void draw(bool& open);
|
||||
|
||||
private:
|
||||
std::string encodeCurrentState();
|
||||
bool applyEncodedState(const std::string& encoded, const std::string& name = {});
|
||||
void tickPendingApply();
|
||||
void loadStatesFile();
|
||||
void saveStatesFile();
|
||||
void mergeFromFile(const std::string& path);
|
||||
static void onMergeFileSelected(void* userdata, const char* path, const char* error);
|
||||
|
||||
std::vector<SavedStateEntry> m_states;
|
||||
std::string m_statusMsg;
|
||||
std::optional<dSv_info_c> m_pendingInfo;
|
||||
std::optional<dSv_save_c> m_pendingSavedata;
|
||||
int m_renamingIndex = -1;
|
||||
char m_renameBuffer[128] = {};
|
||||
bool m_loaded = false;
|
||||
std::string m_pendingMergePath;
|
||||
};
|
||||
|
||||
std::string m_statusMsg;
|
||||
std::optional<dSv_info_c> m_pendingInfo;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -351,8 +351,13 @@ void mDoExt_modelUpdateDL(J3DModel* i_model) {
|
||||
|
||||
void mDoExt_modelEntryDL(J3DModel* i_model) {
|
||||
#if TARGET_PC
|
||||
if (!dusk::frame_interp::is_sim_frame())
|
||||
if (!dusk::frame_interp::is_sim_frame()) {
|
||||
// FRAME INTERP NOTE: This fixes issue #355 where some lights would flicker.
|
||||
// This is likely better solved by updating J3DMaterial::needsInterpCallBack,
|
||||
// but it's unclear what exactly needs to be added.
|
||||
i_model->diff();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
modelMtxErrorCheck(i_model);
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Convert a folder of TPGZ saves to a states.json
|
||||
|
||||
Usage:
|
||||
python saves_to_states_json.py path/to/saves [prefix]
|
||||
|
||||
Requirements:
|
||||
pip install zstandard
|
||||
"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import struct
|
||||
import sys
|
||||
import zstandard
|
||||
from pathlib import Path
|
||||
|
||||
SAVE_C_SIZE = 0x958
|
||||
|
||||
PACKET_FORMAT = "<8sbbh"
|
||||
|
||||
RETURN_PLACE_OFF = 0x058
|
||||
NAME_OFF = RETURN_PLACE_OFF + 0x00
|
||||
ROOM_OFF = RETURN_PLACE_OFF + 0x09
|
||||
SPAWN_POINT_OFF = RETURN_PLACE_OFF + 0x08
|
||||
|
||||
folder = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).parent
|
||||
out_path = folder / "states.json"
|
||||
|
||||
if len(sys.argv) > 2:
|
||||
prefix = sys.argv[2]
|
||||
else:
|
||||
prefix = None
|
||||
|
||||
cctx = zstandard.ZstdCompressor(level=1)
|
||||
states = []
|
||||
|
||||
for bin_path in sorted(folder.glob("*.bin")):
|
||||
raw = bin_path.read_bytes()
|
||||
save_c = raw[:SAVE_C_SIZE]
|
||||
if len(save_c) < SAVE_C_SIZE:
|
||||
print(f" skip {bin_path.name}: too small ({len(save_c)} bytes)")
|
||||
continue
|
||||
|
||||
stage_name = save_c[NAME_OFF:NAME_OFF + 8]
|
||||
room_no = struct.unpack_from("b", save_c, ROOM_OFF)[0]
|
||||
spawn_point = struct.unpack_from("B", save_c, SPAWN_POINT_OFF)[0]
|
||||
|
||||
pkt = struct.pack(PACKET_FORMAT, stage_name, room_no, -1, spawn_point)
|
||||
payload = pkt + save_c
|
||||
encoded = base64.b64encode(cctx.compress(payload)).decode("ascii")
|
||||
|
||||
stage_str = stage_name.rstrip(b"\x00").decode("ascii", errors="replace")
|
||||
print(f" {bin_path.stem:30s} stage={stage_str!r} room={room_no} point={spawn_point}")
|
||||
states.append({"name": f"({prefix}) {bin_path.stem}" if prefix else bin_path.stem, "data": encoded})
|
||||
|
||||
out_path.write_text(json.dumps(states, indent=2))
|
||||
print(f"\nWrote {len(states)} states to {out_path}")
|
||||
Reference in New Issue
Block a user