mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-04 08:48:45 -04:00
mods: save service (#2256)
* initial save service updated from encounter's impl * Review cleanup --------- Co-authored-by: Luke Street <luke@street.dev>
This commit is contained in:
@@ -340,6 +340,39 @@ Change callbacks fire on the game thread whenever the value changes at runtime (
|
||||
Writes that store the same value are silent. Values applied from `config.json` or `--cvar` at registration do
|
||||
**not** fire callbacks; read the value after `register_var` for the starting state.
|
||||
|
||||
### SaveService (`mods/svc/save.h`)
|
||||
|
||||
Stores named binary blobs for each save slot. Blob names are scoped to the calling mod, and each mod may store up to
|
||||
`SAVE_BLOB_BUDGET_BYTES` per slot. The service copies data passed to `set_blob`.
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(SaveService, svc_save);
|
||||
|
||||
struct MySaveData {
|
||||
uint32_t version;
|
||||
uint32_t counter;
|
||||
};
|
||||
|
||||
MySaveData state{1, 42};
|
||||
svc_save->set_blob(mod_ctx, "state", &state, sizeof(state));
|
||||
|
||||
MySaveData loaded{};
|
||||
size_t loadedSize = sizeof(loaded);
|
||||
if (svc_save->get_blob(mod_ctx, "state", &loaded, &loadedSize) == MOD_OK &&
|
||||
loadedSize == sizeof(loaded)) {
|
||||
apply_state(loaded);
|
||||
}
|
||||
```
|
||||
|
||||
`set_blob`, `get_blob`, and `delete_blob` operate on the current slot, which is available after creating or loading a
|
||||
save and unavailable at file select. Blob changes are written with the next game save. File-select copy and erase
|
||||
operations update the blob data as well. Use `peek_blob` to read the calling mod's data from any slot; it uses the same
|
||||
buffer contract as `get_blob`. Pass a `NULL` buffer to either read function to query the blob size.
|
||||
|
||||
`observe_saves` registers callbacks for new, loaded, and written saves. New-save callbacks run after the slot's blobs
|
||||
are cleared. Observers are removed automatically when the mod is detached, so the output handle is only needed for
|
||||
manual unregistration. Save callbacks run on the game thread.
|
||||
|
||||
### UiService (`mods/svc/ui.h`)
|
||||
|
||||
Integrate seamlessly with Dusklight's UI system: add controls and buttons to your mod's detail pane in the Mods window,
|
||||
|
||||
Vendored
+1
-1
Submodule extern/aurora updated: 0bddb86249...6c4c27f9e8
@@ -1502,6 +1502,8 @@ set(DUSK_FILES
|
||||
src/dusk/mods/svc/ui.hpp
|
||||
src/dusk/mods/svc/window.cpp
|
||||
src/dusk/mods/svc/window.hpp
|
||||
src/dusk/mods/svc/save.cpp
|
||||
src/dusk/mods/svc/save.hpp
|
||||
src/dusk/mouse.cpp
|
||||
src/dusk/scope_guard.hpp
|
||||
src/dusk/settings.cpp
|
||||
@@ -1581,6 +1583,7 @@ set(DUSK_FILES
|
||||
src/dusk/update_check.cpp
|
||||
src/dusk/update_check.hpp
|
||||
src/dusk/version.cpp
|
||||
src/dusk/utilities.cpp
|
||||
src/helpers/batch.cpp
|
||||
src/helpers/endian.cpp
|
||||
src/helpers/offset_ptr.cpp
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include <mods/api.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
#define SAVE_SERVICE_ID "dev.twilitrealm.dusklight.save"
|
||||
#define SAVE_SERVICE_MAJOR 1u
|
||||
#define SAVE_SERVICE_MINOR 0u
|
||||
|
||||
/* 0 is never a valid handle. */
|
||||
typedef uint64_t SaveObserverHandle;
|
||||
|
||||
/* Maximum combined blob size per mod and save slot. */
|
||||
#define SAVE_BLOB_BUDGET_BYTES 65536u
|
||||
|
||||
/*
|
||||
* Per-slot mod storage.
|
||||
*
|
||||
* Blobs are scoped to the calling mod and saved alongside each slot. Current-slot calls return
|
||||
* MOD_UNAVAILABLE when no slot is active.
|
||||
*
|
||||
* Callbacks run on the game thread. Observer registrations are removed when the calling mod is
|
||||
* detached.
|
||||
*/
|
||||
|
||||
/* slot is the save-file index (0..2). */
|
||||
typedef void (*SaveEventFn)(ModContext* ctx, uint32_t slot, void* user_data);
|
||||
|
||||
typedef struct SaveService {
|
||||
ServiceHeader header;
|
||||
|
||||
/* Store a copy in the current slot. Returns MOD_UNAVAILABLE if the limit would be exceeded. */
|
||||
ModResult (*set_blob)(ModContext* ctx, const char* name, const void* data, size_t size);
|
||||
|
||||
/*
|
||||
* Read a blob from the current slot. Pass NULL for buf to query its size. Otherwise,
|
||||
* inout_size is the buffer capacity on input and the blob size on success. Returns
|
||||
* MOD_UNAVAILABLE if the blob does not exist.
|
||||
*/
|
||||
ModResult (*get_blob)(ModContext* ctx, const char* name, void* buf, size_t* inout_size);
|
||||
|
||||
ModResult (*delete_blob)(ModContext* ctx, const char* name);
|
||||
|
||||
/*
|
||||
* Register save lifecycle callbacks. At least one callback is required. on_new_save runs
|
||||
* after clearing the slot's blobs, on_save_loaded after activating the slot, and
|
||||
* on_save_written after a successful game save. out_handle may be NULL.
|
||||
*/
|
||||
ModResult (*observe_saves)(ModContext* ctx, SaveEventFn on_new_save, SaveEventFn on_save_loaded,
|
||||
SaveEventFn on_save_written, void* user_data, SaveObserverHandle* out_handle);
|
||||
|
||||
ModResult (*unobserve_saves)(ModContext* ctx, SaveObserverHandle handle);
|
||||
|
||||
/* Read the calling mod's blob from any slot. Uses the get_blob buffer contract. */
|
||||
ModResult (*peek_blob)(
|
||||
ModContext* ctx, uint32_t slot, const char* name, void* buf, size_t* inout_size);
|
||||
|
||||
} SaveService;
|
||||
|
||||
MOD_DECLARE_SERVICE(SaveService, svc_save, SAVE_SERVICE_ID, SAVE_SERVICE_MAJOR, SAVE_SERVICE_MINOR);
|
||||
@@ -28,6 +28,7 @@
|
||||
#if TARGET_PC
|
||||
#include "dusk/menu_pointer.h"
|
||||
#include "helpers/string.hpp"
|
||||
#include "dusk/mods/svc/save.hpp"
|
||||
|
||||
namespace {
|
||||
constexpr u8 pointer_target(u8 group, u8 index) noexcept {
|
||||
@@ -255,6 +256,10 @@ dFile_select_c::~dFile_select_c() {
|
||||
void dFile_select_c::_create() {
|
||||
int i;
|
||||
|
||||
#if TARGET_PC
|
||||
dusk::mods::svc::save_no_slot();
|
||||
#endif
|
||||
|
||||
mDoGph_gInf_c::setFadeColor(static_cast<JUtility::TColor&>(g_blackColor));
|
||||
|
||||
stick = JKR_NEW STControl(2, 2, 1, 1, 0.9f, 0.5f, 0, 0x2000);
|
||||
@@ -1390,6 +1395,9 @@ void dFile_select_c::menuSelectStart() {
|
||||
mIsSelectEnd = true;
|
||||
mDataSelProc = DATASELPROC_NEXT_MODE_WAIT;
|
||||
dComIfGs_setDataNum(mSelectNum);
|
||||
#if TARGET_PC
|
||||
dusk::mods::svc::save_slot_loaded(mSelectNum, &mSaveData[mSelectNum]);
|
||||
#endif
|
||||
} else if (mSelectMenuNum == 0) {
|
||||
mSelIcon->setAlphaRate(0.0f);
|
||||
yesnoMenuMoveAnmInitSet(0x473, 0x47d);
|
||||
@@ -1740,6 +1748,9 @@ void dFile_select_c::nameInput2() {
|
||||
case 2:
|
||||
dComIfGs_setHorseName(mpName->getInputStrPtr());
|
||||
mIsSelectEnd = true;
|
||||
#if TARGET_PC
|
||||
dusk::mods::svc::save_slot_new(mSelectNum);
|
||||
#endif
|
||||
mDataSelProc = DATASELPROC_NEXT_MODE_WAIT;
|
||||
}
|
||||
}
|
||||
@@ -2666,6 +2677,9 @@ void dFile_select_c::DataEraseWait2() {
|
||||
mDataSelProc = DATASELPROC_ERROR_MSG_PANE_MOVE;
|
||||
} else if (field_0x03b4 == 1) {
|
||||
mDoAud_seStart(Z2SE_SY_FILE_DELETE_OK, NULL, 0, 0);
|
||||
#if TARGET_PC
|
||||
dusk::mods::svc::save_slot_erased(mSelectNum);
|
||||
#endif
|
||||
field_0x03b1 = 0;
|
||||
mDeleteEfPane[mSelectNum]->alphaAnimeStart(0);
|
||||
mFileInfoNoDatBasePane[mSelectNum]->alphaAnimeStart(0);
|
||||
@@ -2769,6 +2783,9 @@ void dFile_select_c::DataCopyWait2() {
|
||||
mDataSelProc = DATASELPROC_ERROR_MSG_PANE_MOVE;
|
||||
} else if (field_0x03b4 == 1) {
|
||||
mDoAud_seStart(Z2SE_SY_FILE_COPY_OK, NULL, 0, 0);
|
||||
#if TARGET_PC
|
||||
dusk::mods::svc::save_slot_copied(mCpDataNum, mCpDataToNum);
|
||||
#endif
|
||||
field_0x03b1 = 0;
|
||||
mCopyEfPane[mSelectNum]->alphaAnimeStart(0);
|
||||
mCopyEfPane[mCpDataToNum]->alphaAnimeStart(0);
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include "dusk/menu_pointer.h"
|
||||
#include "dusk/settings.h"
|
||||
#include "dusk/mods/svc/save.hpp"
|
||||
#endif
|
||||
|
||||
static int SelStartFrameTbl[3] = {
|
||||
@@ -1468,6 +1469,10 @@ void dMenu_save_c::memCardDataSaveWait2() {
|
||||
dComIfGs_setDataNum(mSelectedFile);
|
||||
dComIfGs_setNoFile(0);
|
||||
|
||||
#if TARGET_PC
|
||||
dusk::mods::svc::save_slot_written(mSelectedFile, mSaveBuffer + mSelectedFile * QUEST_LOG_SIZE);
|
||||
#endif
|
||||
|
||||
if (mUseType == TYPE_WHITE_EVENT || mUseType == TYPE_BLACK_EVENT) {
|
||||
headerTxtSet(0x530); // Saved.
|
||||
mWarning->closeInit();
|
||||
|
||||
+11
-5
@@ -25,8 +25,9 @@
|
||||
|
||||
#ifdef TARGET_PC
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/version.hpp"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/mods/svc/save.hpp"
|
||||
#include "dusk/version.hpp"
|
||||
#include "m_Do/m_Do_MemCard.h"
|
||||
#endif
|
||||
|
||||
@@ -771,15 +772,20 @@ void dScnLogo_c::nextSceneChange() {
|
||||
status = mDoMemCd_LoadSync(buf, sizeof(buf), 0);
|
||||
// Wait until the card is loaded
|
||||
} while (status == 0);
|
||||
|
||||
|
||||
const uint32_t saveSlot = dusk::SaveRequested - 1;
|
||||
if (status == 1) {
|
||||
dComIfGs_setCardToMemory(buf, dusk::SaveRequested - 1);
|
||||
dComIfGs_setCardToMemory(buf, saveSlot);
|
||||
} else {
|
||||
dComIfGs_init();
|
||||
}
|
||||
|
||||
|
||||
dComIfGs_setNoFile(dusk::SaveRequested);
|
||||
dComIfGs_setDataNum(dusk::SaveRequested-1);
|
||||
dComIfGs_setDataNum(saveSlot);
|
||||
if (status == 1) {
|
||||
dusk::mods::svc::save_slot_loaded(
|
||||
saveSlot, buf + saveSlot * SAVEDATA_SIZE);
|
||||
}
|
||||
|
||||
dComIfGs_gameStart();
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "dusk/autosave.h"
|
||||
#include "dusk/ui/ui.hpp"
|
||||
#include "imgui/ImGuiConsole.hpp"
|
||||
#include "mods/svc/save.hpp"
|
||||
|
||||
bool shouldAutoSave = false;
|
||||
u8 mSaveBuffer[QUEST_LOG_SIZE * 3];
|
||||
@@ -105,6 +106,9 @@ void waitingForWrite() {
|
||||
}
|
||||
|
||||
void endAutoSave() {
|
||||
const int slot = dComIfGs_getDataNum();
|
||||
dusk::mods::svc::save_slot_written(slot, mSaveBuffer + slot * QUEST_LOG_SIZE);
|
||||
|
||||
dusk::ui::push_toast({
|
||||
.type = "autosave",
|
||||
.duration = std::chrono::milliseconds(1500),
|
||||
@@ -114,4 +118,4 @@ void endAutoSave() {
|
||||
|
||||
void toggleAutoSave(bool enabled) {
|
||||
shouldAutoSave = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,6 +211,7 @@ void ModLoader::init_services() {
|
||||
&svc::g_cameraModule,
|
||||
&svc::g_windowModule,
|
||||
&svc::g_gfxModule,
|
||||
&svc::g_saveModule,
|
||||
})
|
||||
{
|
||||
svc::register_module(*module);
|
||||
|
||||
@@ -73,5 +73,6 @@ extern const ServiceModule g_gameModule;
|
||||
extern const ServiceModule g_cameraModule;
|
||||
extern const ServiceModule g_windowModule;
|
||||
extern const ServiceModule g_gfxModule;
|
||||
extern const ServiceModule g_saveModule;
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
#include "save.hpp"
|
||||
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "d/d_save.h"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "dusk/utilities.hpp"
|
||||
#include "mods/svc/save.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <string_view>
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::save");
|
||||
|
||||
constexpr uint32_t kSlotCount = 3;
|
||||
constexpr size_t kQuestLogSize = 0xA94;
|
||||
static_assert(kQuestLogSize == QUEST_LOG_SIZE);
|
||||
constexpr int kSidecarVersion = 1;
|
||||
constexpr const char* kSidecarName = "mod_saves.json";
|
||||
constexpr size_t kMaxBlobNameLength = 256;
|
||||
|
||||
using BlobMap = std::map<std::string, std::vector<uint8_t>>;
|
||||
|
||||
struct SlotStore {
|
||||
bool snapshotValid = false;
|
||||
uint32_t snapshotCrc = 0;
|
||||
std::map<std::string, BlobMap> mods;
|
||||
};
|
||||
|
||||
struct SaveObserverRecord {
|
||||
uint64_t handle = 0;
|
||||
LoadedMod* mod = nullptr;
|
||||
SaveEventFn onNewSave = nullptr;
|
||||
SaveEventFn onLoaded = nullptr;
|
||||
SaveEventFn onWritten = nullptr;
|
||||
void* userData = nullptr;
|
||||
};
|
||||
|
||||
std::array<SlotStore, kSlotCount> s_slots;
|
||||
int32_t s_currentSlot = -1;
|
||||
bool s_sidecarLoaded = false;
|
||||
std::vector<SaveObserverRecord> s_observers;
|
||||
uint64_t s_nextHandle = 1;
|
||||
|
||||
std::filesystem::path sidecar_path() {
|
||||
return dusk::ConfigPath / kSidecarName;
|
||||
}
|
||||
|
||||
void load_sidecar() {
|
||||
if (s_sidecarLoaded) {
|
||||
return;
|
||||
}
|
||||
s_sidecarLoaded = true;
|
||||
std::ifstream in{sidecar_path()};
|
||||
if (!in.is_open()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const auto json = nlohmann::json::parse(in);
|
||||
if (json.value("version", 0) != kSidecarVersion) {
|
||||
Log.warn(
|
||||
"mod save sidecar has unknown version {}; ignoring it", json.value("version", 0));
|
||||
return;
|
||||
}
|
||||
const auto& slots = json.at("slots");
|
||||
for (uint32_t slot = 0; slot < kSlotCount && slot < slots.size(); ++slot) {
|
||||
auto& store = s_slots[slot];
|
||||
const auto& slotJson = slots[slot];
|
||||
if (slotJson.contains("snapshot_crc32")) {
|
||||
store.snapshotValid = true;
|
||||
store.snapshotCrc = slotJson["snapshot_crc32"].get<uint32_t>();
|
||||
}
|
||||
const auto modsJson = slotJson.value("mods", nlohmann::json::object());
|
||||
for (const auto& [modId, blobs] : modsJson.items()) {
|
||||
for (const auto& [name, encoded] : blobs.items()) {
|
||||
std::vector<uint8_t> bytes;
|
||||
if (!utils::base64_decode(encoded.get<std::string>(), bytes)) {
|
||||
Log.warn("mod save sidecar: bad blob '{}/{}' in slot {}; dropped", modId,
|
||||
name, slot);
|
||||
continue;
|
||||
}
|
||||
s_slots[slot].mods[modId][name] = std::move(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
Log.error("failed to read mod save sidecar: {}", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void flush_sidecar() {
|
||||
nlohmann::json slots = nlohmann::json::array();
|
||||
for (const auto& store : s_slots) {
|
||||
nlohmann::json slotJson = nlohmann::json::object();
|
||||
if (store.snapshotValid) {
|
||||
slotJson["snapshot_crc32"] = store.snapshotCrc;
|
||||
}
|
||||
nlohmann::json mods = nlohmann::json::object();
|
||||
for (const auto& [modId, blobs] : store.mods) {
|
||||
if (blobs.empty()) {
|
||||
continue;
|
||||
}
|
||||
nlohmann::json blobsJson = nlohmann::json::object();
|
||||
for (const auto& [name, bytes] : blobs) {
|
||||
blobsJson[name] = utils::base64_encode(bytes);
|
||||
}
|
||||
mods[modId] = std::move(blobsJson);
|
||||
}
|
||||
slotJson["mods"] = std::move(mods);
|
||||
slots.push_back(std::move(slotJson));
|
||||
}
|
||||
const nlohmann::json json{{"version", kSidecarVersion}, {"slots", std::move(slots)}};
|
||||
|
||||
const auto path = sidecar_path();
|
||||
const auto tempPath = path.string() + ".tmp";
|
||||
try {
|
||||
{
|
||||
std::ofstream out{tempPath, std::ios::trunc};
|
||||
out << json.dump(2);
|
||||
if (!out.good()) {
|
||||
throw std::runtime_error("write failed");
|
||||
}
|
||||
}
|
||||
std::filesystem::rename(tempPath, path);
|
||||
} catch (const std::exception& e) {
|
||||
Log.error("failed to write mod save sidecar: {}", e.what());
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(tempPath, ec);
|
||||
}
|
||||
}
|
||||
|
||||
void notify(uint32_t slot, SaveEventFn SaveObserverRecord::* which, const char* what) {
|
||||
// Callbacks may unregister observers.
|
||||
const auto observers = s_observers;
|
||||
for (const auto& observer : observers) {
|
||||
if (!observer.mod->active || observer.*which == nullptr) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
(observer.*which)(observer.mod->context.get(), slot, observer.userData);
|
||||
} catch (const std::exception& e) {
|
||||
fail_mod(*observer.mod, MOD_ERROR,
|
||||
fmt::format("exception in {} save callback: {}", what, e.what()));
|
||||
} catch (...) {
|
||||
fail_mod(*observer.mod, MOD_ERROR,
|
||||
fmt::format("unknown exception in {} save callback", what));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void save_slot_new(uint32_t slot) {
|
||||
if (slot >= kSlotCount) {
|
||||
return;
|
||||
}
|
||||
load_sidecar();
|
||||
auto& store = s_slots[slot];
|
||||
store.mods.clear();
|
||||
store.snapshotValid = false;
|
||||
s_currentSlot = static_cast<int32_t>(slot);
|
||||
Log.info("new save in slot {}; mod blob store cleared", slot);
|
||||
notify(slot, &SaveObserverRecord::onNewSave, "new-save");
|
||||
}
|
||||
|
||||
void save_slot_loaded(uint32_t slot, const void* slotData) {
|
||||
if (slot >= kSlotCount) {
|
||||
return;
|
||||
}
|
||||
load_sidecar();
|
||||
auto& store = s_slots[slot];
|
||||
if (store.snapshotValid && slotData != nullptr) {
|
||||
const auto crc = utils::crc32(slotData, kQuestLogSize);
|
||||
if (crc != store.snapshotCrc) {
|
||||
Log.warn("slot {} save data does not match the mod sidecar snapshot; mod save "
|
||||
"data may be stale (card file changed externally?)",
|
||||
slot);
|
||||
}
|
||||
}
|
||||
s_currentSlot = static_cast<int32_t>(slot);
|
||||
notify(slot, &SaveObserverRecord::onLoaded, "save-loaded");
|
||||
}
|
||||
|
||||
void save_slot_written(uint32_t slot, const void* slotData) {
|
||||
if (slot >= kSlotCount) {
|
||||
return;
|
||||
}
|
||||
load_sidecar();
|
||||
auto& store = s_slots[slot];
|
||||
if (slotData != nullptr) {
|
||||
store.snapshotValid = true;
|
||||
store.snapshotCrc = utils::crc32(slotData, kQuestLogSize);
|
||||
}
|
||||
flush_sidecar();
|
||||
notify(slot, &SaveObserverRecord::onWritten, "save-written");
|
||||
}
|
||||
|
||||
void save_slot_copied(uint32_t fromSlot, uint32_t toSlot) {
|
||||
if (fromSlot >= kSlotCount || toSlot >= kSlotCount || fromSlot == toSlot) {
|
||||
return;
|
||||
}
|
||||
load_sidecar();
|
||||
s_slots[toSlot] = s_slots[fromSlot];
|
||||
flush_sidecar();
|
||||
Log.info("mod save data copied with slot {} -> {}", fromSlot, toSlot);
|
||||
}
|
||||
|
||||
void save_slot_erased(uint32_t slot) {
|
||||
if (slot >= kSlotCount) {
|
||||
return;
|
||||
}
|
||||
load_sidecar();
|
||||
s_slots[slot] = SlotStore{};
|
||||
flush_sidecar();
|
||||
Log.info("mod save data erased with slot {}", slot);
|
||||
}
|
||||
|
||||
void save_no_slot() {
|
||||
s_currentSlot = -1;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
BlobMap* current_blobs(const LoadedMod& mod, bool create) {
|
||||
if (s_currentSlot < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
load_sidecar();
|
||||
auto& mods = s_slots[s_currentSlot].mods;
|
||||
if (!create) {
|
||||
const auto it = mods.find(mod.metadata.id);
|
||||
return it != mods.end() ? &it->second : nullptr;
|
||||
}
|
||||
return &mods[mod.metadata.id];
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ModResult save_set_blob(LoadedMod& mod, const char* name, const void* data, size_t size) {
|
||||
auto* blobs = current_blobs(mod, true);
|
||||
if (blobs == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
size_t total = size;
|
||||
for (const auto& [blobName, bytes] : *blobs) {
|
||||
if (blobName != name) {
|
||||
total += bytes.size();
|
||||
}
|
||||
}
|
||||
if (total > SAVE_BLOB_BUDGET_BYTES) {
|
||||
Log.error("[{}] save blob '{}' rejected: {} bytes would exceed the {}-byte budget",
|
||||
mod.metadata.id, name, total, SAVE_BLOB_BUDGET_BYTES);
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
const auto* bytes = static_cast<const uint8_t*>(data);
|
||||
(*blobs)[name] = std::vector<uint8_t>{bytes, bytes + size};
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult save_get_blob(LoadedMod& mod, const char* name, void* buf, size_t& inoutSize) {
|
||||
auto* blobs = current_blobs(mod, false);
|
||||
if (blobs == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
const auto it = blobs->find(name);
|
||||
if (it == blobs->end()) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
if (buf == nullptr) {
|
||||
inoutSize = it->second.size();
|
||||
return MOD_OK;
|
||||
}
|
||||
if (inoutSize < it->second.size()) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
std::memcpy(buf, it->second.data(), it->second.size());
|
||||
inoutSize = it->second.size();
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult save_delete_blob(LoadedMod& mod, const char* name) {
|
||||
auto* blobs = current_blobs(mod, false);
|
||||
if (blobs == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
return blobs->erase(name) != 0 ? MOD_OK : MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
ModResult save_observe(LoadedMod& mod, SaveEventFn onNewSave, SaveEventFn onLoaded,
|
||||
SaveEventFn onWritten, void* userData, uint64_t& outHandle) {
|
||||
auto& observer = s_observers.emplace_back();
|
||||
observer.handle = s_nextHandle++;
|
||||
observer.mod = &mod;
|
||||
observer.onNewSave = onNewSave;
|
||||
observer.onLoaded = onLoaded;
|
||||
observer.onWritten = onWritten;
|
||||
observer.userData = userData;
|
||||
outHandle = observer.handle;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult save_unobserve(LoadedMod& mod, uint64_t handle) {
|
||||
const auto removed = std::erase_if(s_observers,
|
||||
[&](const auto& observer) { return observer.handle == handle && observer.mod == &mod; });
|
||||
return removed != 0 ? MOD_OK : MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
ModResult save_peek_blob(
|
||||
LoadedMod& mod, uint32_t slot, const char* name, void* buf, size_t& inoutSize) {
|
||||
if (slot >= kSlotCount) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
load_sidecar();
|
||||
const auto& mods = s_slots[slot].mods;
|
||||
const auto modIt = mods.find(mod.metadata.id);
|
||||
if (modIt == mods.end()) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
const auto it = modIt->second.find(name);
|
||||
if (it == modIt->second.end()) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
if (buf == nullptr) {
|
||||
inoutSize = it->second.size();
|
||||
return MOD_OK;
|
||||
}
|
||||
if (inoutSize < it->second.size()) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
std::memcpy(buf, it->second.data(), it->second.size());
|
||||
inoutSize = it->second.size();
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
void save_remove_mod(LoadedMod& mod) {
|
||||
std::erase_if(s_observers, [&](const auto& observer) { return observer.mod == &mod; });
|
||||
// Blob data persists across mod reloads.
|
||||
}
|
||||
|
||||
namespace {
|
||||
bool is_valid_blob_name(const char* name) {
|
||||
if (name == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const std::string_view view{name};
|
||||
return !view.empty() && view.size() <= kMaxBlobNameLength;
|
||||
}
|
||||
|
||||
ModResult save_set_blob_(ModContext* context, const char* name, const void* data, size_t size) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || !is_valid_blob_name(name) || (data == nullptr && size != 0) ||
|
||||
size > SAVE_BLOB_BUDGET_BYTES)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return save_set_blob(*mod, name, data, size);
|
||||
}
|
||||
|
||||
ModResult save_get_blob_(ModContext* context, const char* name, void* buf, size_t* inoutSize) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || !is_valid_blob_name(name) || inoutSize == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return save_get_blob(*mod, name, buf, *inoutSize);
|
||||
}
|
||||
|
||||
ModResult save_delete_blob_(ModContext* context, const char* name) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || !is_valid_blob_name(name)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return save_delete_blob(*mod, name);
|
||||
}
|
||||
|
||||
ModResult save_observe_saves_(ModContext* context, SaveEventFn onNewSave, SaveEventFn onLoaded,
|
||||
SaveEventFn onWritten, void* userData, SaveObserverHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || (onNewSave == nullptr && onLoaded == nullptr && onWritten == nullptr)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
uint64_t handle = 0;
|
||||
const auto result = save_observe(*mod, onNewSave, onLoaded, onWritten, userData, handle);
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = handle;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ModResult save_unobserve_saves_(ModContext* context, SaveObserverHandle handle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || handle == 0) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return save_unobserve(*mod, handle);
|
||||
}
|
||||
|
||||
ModResult save_peek_blob_(
|
||||
ModContext* context, uint32_t slot, const char* name, void* buf, size_t* inoutSize) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || !is_valid_blob_name(name) || inoutSize == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return save_peek_blob(*mod, slot, name, buf, *inoutSize);
|
||||
}
|
||||
|
||||
constexpr SaveService s_saveService{
|
||||
.header = SERVICE_HEADER(SaveService, SAVE_SERVICE_MAJOR, SAVE_SERVICE_MINOR),
|
||||
.set_blob = save_set_blob_,
|
||||
.get_blob = save_get_blob_,
|
||||
.delete_blob = save_delete_blob_,
|
||||
.observe_saves = save_observe_saves_,
|
||||
.unobserve_saves = save_unobserve_saves_,
|
||||
.peek_blob = save_peek_blob_,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
constinit const ServiceModule g_saveModule{
|
||||
.id = SAVE_SERVICE_ID,
|
||||
.majorVersion = SAVE_SERVICE_MAJOR,
|
||||
.minorVersion = SAVE_SERVICE_MINOR,
|
||||
.service = &s_saveService,
|
||||
.modDetached = save_remove_mod,
|
||||
};
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
|
||||
void save_slot_new(uint32_t slot);
|
||||
void save_slot_loaded(uint32_t slot, const void* slotData);
|
||||
void save_slot_written(uint32_t slot, const void* slotData);
|
||||
void save_slot_copied(uint32_t fromSlot, uint32_t toSlot);
|
||||
void save_slot_erased(uint32_t slot);
|
||||
void save_no_slot();
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
+1
-1
@@ -908,7 +908,7 @@ void AIInit(u8* stack) {
|
||||
// In a real scenario, it would set up the audio interface and prepare it for use.
|
||||
}
|
||||
|
||||
void AIInitDMA(u32 start_addr, u32 length) {
|
||||
void AIInitDMA(uintptr_t start_addr, u32 length) {
|
||||
STUB_LOG();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
#include "utilities.hpp"
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace dusk::utils {
|
||||
namespace {
|
||||
|
||||
constexpr char kBase64Chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
constexpr std::array<uint32_t, 256> generate_crc32_table() {
|
||||
std::array<uint32_t, 256> table{};
|
||||
for (uint32_t i = 0; i < 256; ++i) {
|
||||
uint32_t ch = i;
|
||||
for (size_t j = 0; j < 8; ++j) {
|
||||
ch = (ch & 1) != 0 ? 0xEDB88320 ^ ch >> 1 : ch >> 1;
|
||||
}
|
||||
table[i] = ch;
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
constexpr std::array<uint32_t, 256> kCrc32Table = generate_crc32_table();
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string base64_encode(const std::vector<uint8_t>& data) {
|
||||
std::string out;
|
||||
out.reserve((data.size() + 2) / 3 * 4);
|
||||
for (size_t i = 0; i < data.size(); i += 3) {
|
||||
const uint32_t rest = data.size() - i;
|
||||
uint32_t chunk = data[i] << 16;
|
||||
if (rest > 1) {
|
||||
chunk |= data[i + 1] << 8;
|
||||
}
|
||||
if (rest > 2) {
|
||||
chunk |= data[i + 2];
|
||||
}
|
||||
out.push_back(kBase64Chars[chunk >> 18 & 0x3F]);
|
||||
out.push_back(kBase64Chars[chunk >> 12 & 0x3F]);
|
||||
out.push_back(rest > 1 ? kBase64Chars[chunk >> 6 & 0x3F] : '=');
|
||||
out.push_back(rest > 2 ? kBase64Chars[chunk & 0x3F] : '=');
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool base64_decode(const std::string& text, std::vector<uint8_t>& out) {
|
||||
if (text.size() % 4 != 0) {
|
||||
return false;
|
||||
}
|
||||
static const auto lookup = [] {
|
||||
std::array<int8_t, 256> table;
|
||||
table.fill(-1);
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
table[static_cast<uint8_t>(kBase64Chars[i])] = static_cast<int8_t>(i);
|
||||
}
|
||||
return table;
|
||||
}();
|
||||
out.clear();
|
||||
out.reserve(text.size() / 4 * 3);
|
||||
for (size_t i = 0; i < text.size(); i += 4) {
|
||||
uint32_t chunk = 0;
|
||||
int pads = 0;
|
||||
for (size_t j = 0; j < 4; ++j) {
|
||||
const char c = text[i + j];
|
||||
if (c == '=' && i + 4 == text.size() && j >= 2) {
|
||||
++pads;
|
||||
chunk <<= 6;
|
||||
continue;
|
||||
}
|
||||
const int8_t value = lookup[static_cast<uint8_t>(c)];
|
||||
if (value < 0 || pads != 0) {
|
||||
return false;
|
||||
}
|
||||
chunk = chunk << 6 | static_cast<uint32_t>(value);
|
||||
}
|
||||
out.push_back(chunk >> 16 & 0xFF);
|
||||
if (pads < 2) {
|
||||
out.push_back(chunk >> 8 & 0xFF);
|
||||
}
|
||||
if (pads < 1) {
|
||||
out.push_back(chunk & 0xFF);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t crc32(const void* data, size_t size) {
|
||||
const auto* bytes = static_cast<const uint8_t*>(data);
|
||||
uint32_t crc = ~0u;
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
crc = crc >> 8 ^ kCrc32Table[static_cast<uint8_t>(crc ^ bytes[i])];
|
||||
}
|
||||
return ~crc;
|
||||
}
|
||||
} // namespace dusk::utils
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifdef crc32
|
||||
// miniz defines crc32 as an alias.
|
||||
#undef crc32
|
||||
#endif
|
||||
|
||||
namespace dusk::utils {
|
||||
std::string base64_encode(const std::vector<uint8_t>& data);
|
||||
bool base64_decode(const std::string& text, std::vector<uint8_t>& out);
|
||||
uint32_t crc32(const void* data, size_t size);
|
||||
} // namespace dusk::utils
|
||||
Reference in New Issue
Block a user