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:
TakaRikka
2026-08-03 19:58:09 -07:00
committed by GitHub
parent 519305b7d4
commit 76079b6294
15 changed files with 701 additions and 8 deletions
+33
View File
@@ -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,
+1 -1
+3
View File
@@ -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
+63
View File
@@ -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);
+17
View File
@@ -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);
+5
View File
@@ -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
View File
@@ -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();
+5 -1
View File
@@ -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;
}
}
+1
View File
@@ -211,6 +211,7 @@ void ModLoader::init_services() {
&svc::g_cameraModule,
&svc::g_windowModule,
&svc::g_gfxModule,
&svc::g_saveModule,
})
{
svc::register_module(*module);
+1
View File
@@ -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
+434
View File
@@ -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
+14
View File
@@ -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
View File
@@ -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();
}
+95
View File
@@ -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
+17
View File
@@ -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