diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index b74392b5..143b5a31 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -20,6 +20,7 @@ #include "system/system.h" #include "console/console.h" #include "save/save.h" +#include "save/settings.h" engine_t ENGINE; @@ -38,6 +39,10 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { errorChain(inputInit()); errorChain(assetInit()); errorChain(saveInit()); + // Must run after saveInit() - on GameCube, settingsInit() reuses the + // memory card mount saveInit() already established rather than mounting + // it a second time (see settingsInitDolphin()). + errorChain(settingsInit()); errorChain(localeManagerInit()); errorChain(displayInit()); errorChain(uiInit()); @@ -89,6 +94,7 @@ errorret_t engineDispose(void) { errorChain(uiDispose()); consoleDispose(); errorChain(displayDispose()); + errorChain(settingsDispose()); errorChain(saveDispose()); errorChain(assetDispose()); diff --git a/src/dusk/save/CMakeLists.txt b/src/dusk/save/CMakeLists.txt index 55b35252..467beb8a 100644 --- a/src/dusk/save/CMakeLists.txt +++ b/src/dusk/save/CMakeLists.txt @@ -8,4 +8,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC save.c savestream.c + settings.c + settingsstream.c ) diff --git a/src/dusk/save/save.c b/src/dusk/save/save.c index 34873373..2365b6c3 100644 --- a/src/dusk/save/save.c +++ b/src/dusk/save/save.c @@ -16,13 +16,6 @@ save_t SAVE; errorret_t saveInit(void) { memoryZero(&SAVE, sizeof(save_t)); - // Establishes the default for a slot that hasn't actually been loaded - // from disk yet - saveLoad() overwrites this the moment a real file is - // found, so this only matters for a brand new save. - for(uint8_t i = 0; i < SAVE_FILE_COUNT_MAX; i++) { - SAVE.files[i].deadzone = SAVE_DEADZONE_DEFAULT; - } - #ifdef saveInitPlatform // A missing/unreachable save medium is expected, recoverable state, // not a reason to fail booting the whole game - log it and carry on diff --git a/src/dusk/save/savefile.h b/src/dusk/save/savefile.h index 024bed92..5d4b74c4 100644 --- a/src/dusk/save/savefile.h +++ b/src/dusk/save/savefile.h @@ -40,15 +40,6 @@ */ #define SAVE_GLOBAL_ITEM_COUNT_MAX 64 -/** - * Default gamepad deadzone for a save slot that's never actually been - * loaded from disk yet (see saveInit(), which stamps this onto every - * slot up front) - defined here, rather than by the input system, since - * the save file is now the single source of truth for this value (see - * savefile_t.deadzone) - nothing else stores or defaults it. - */ -#define SAVE_DEADZONE_DEFAULT 0.1f - /** * Maximum number of story flags the save format can hold - see * rpg/story/storyflag.h. Bounded/fixed here (with real headroom over the @@ -69,12 +60,6 @@ typedef struct { char_t playerName[SAVE_PLAYER_NAME_MAX]; /** Per-global-ID "already collected" flags - see globalitemstore.h. */ bool_t globalItemCollected[SAVE_GLOBAL_ITEM_COUNT_MAX]; - /** - * User-configured gamepad deadzone (0.0f-1.0f) - the save file is the - * only place this lives; read it directly via saveGet(SAVE_ACTIVE_SLOT) - * ->deadzone rather than caching it anywhere else. - */ - float_t deadzone; /** * Story flag values, indexed by storyflag_t - the save file is the only * place these live; read/write via storyFlagGet()/storyFlagSet() (see diff --git a/src/dusk/save/savestream.c b/src/dusk/save/savestream.c index ca1e19d4..938766e9 100644 --- a/src/dusk/save/savestream.c +++ b/src/dusk/save/savestream.c @@ -334,7 +334,6 @@ errorret_t saveFileLoad(savestream_t *stream, savefile_t *file) { for(size_t i = 0; i < SAVE_GLOBAL_ITEM_COUNT_MAX; i++) { saveFileReadBool(stream, &file->globalItemCollected[i]); } - saveFileReadFloat(stream, &file->deadzone); for(size_t i = 0; i < SAVE_STORY_FLAG_COUNT_MAX; i++) { saveFileReadUInt8(stream, &file->storyFlags[i]); } @@ -348,7 +347,6 @@ errorret_t saveFileWrite(savestream_t *stream, savefile_t *file) { for(size_t i = 0; i < SAVE_GLOBAL_ITEM_COUNT_MAX; i++) { saveFileWriteBool(stream, &file->globalItemCollected[i]); } - saveFileWriteFloat(stream, &file->deadzone); for(size_t i = 0; i < SAVE_STORY_FLAG_COUNT_MAX; i++) { saveFileWriteUInt8(stream, &file->storyFlags[i]); } diff --git a/src/dusk/save/settings.c b/src/dusk/save/settings.c new file mode 100644 index 00000000..6abf0299 --- /dev/null +++ b/src/dusk/save/settings.c @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "save/settings.h" +#include "save/settingsstream.h" +#include "util/memory.h" +#include "error/error.h" + +settings_t SETTINGS; + +static errorret_t settingsLoad(void) { + settingsstream_t stream; + memoryZero(&stream, sizeof(settingsstream_t)); + + #ifdef settingsStreamOpenReadPlatform + errorret_t openRet = settingsStreamOpenReadPlatform(&stream); + SETTINGS.available = errorIsOk(openRet); + errorChain(openRet); + #endif + + if(!stream.found) errorOk(); + + errorret_t ret = settingsFileLoad(&stream, &SETTINGS.file); + + #ifdef settingsStreamClosePlatform + settingsStreamClosePlatform(&stream); + #endif + + if(errorIsOk(ret)) ret = settingsStreamVerifyChecksumImpl(&stream); + SETTINGS.file.exists = errorIsOk(ret); + errorChain(ret); + errorOk(); +} + +errorret_t settingsInit(void) { + memoryZero(&SETTINGS, sizeof(settings_t)); + SETTINGS.file.deadzone = SETTINGS_DEADZONE_DEFAULT; + + #ifdef settingsInitPlatform + // A missing/unreachable storage medium is expected, recoverable state, + // not a reason to fail booting the whole game - log it and carry on + // with SETTINGS.available false instead of chaining the error upward. + errorret_t result = settingsInitPlatform(); + SETTINGS.available = errorIsOk(result); + if(!SETTINGS.available) errorCatch(errorPrint(result)); + #else + SETTINGS.available = false; + #endif + + if(SETTINGS.available) { + errorret_t loadRet = settingsLoad(); + if(errorIsNotOk(loadRet)) errorCatch(errorPrint(loadRet)); + } + + errorOk(); +} + +bool_t settingsIsAvailable(void) { + return SETTINGS.available; +} + +errorret_t settingsDispose(void) { + #ifdef settingsDisposePlatform + errorChain(settingsDisposePlatform()); + #endif + errorOk(); +} + +errorret_t settingsSave(void) { + memoryCopy( + SETTINGS.file.header, SETTINGS_FILE_HEADER, SETTINGS_FILE_HEADER_SIZE + ); + SETTINGS.file.version = SETTINGS_FILE_VERSION; + + settingsstream_t stream; + memoryZero(&stream, sizeof(settingsstream_t)); + + #ifdef settingsStreamOpenWritePlatform + errorret_t openRet = settingsStreamOpenWritePlatform(&stream); + SETTINGS.available = errorIsOk(openRet); + errorChain(openRet); + #endif + + errorret_t ret = settingsFileWrite(&stream, &SETTINGS.file); + + if(errorIsOk(ret)) { + ret = settingsStreamFinalizeWriteImpl(&stream); + } + + #ifdef settingsStreamClosePlatform + settingsStreamClosePlatform(&stream); + #endif + + SETTINGS.file.exists = errorIsOk(ret); + errorChain(ret); + errorOk(); +} + +settingsfile_t * settingsGet(void) { + return &SETTINGS.file; +} diff --git a/src/dusk/save/settings.h b/src/dusk/save/settings.h new file mode 100644 index 00000000..31fe40d8 --- /dev/null +++ b/src/dusk/save/settings.h @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "settingsfile.h" +#include "save/settingsplatform.h" + +typedef struct { + /** The single settings file - see settingsfile.h. */ + settingsfile_t file; + /** Platform-specific settings storage state (paths, card handles, etc.). */ + settingsplatform_t platform; + /** + * True if the settings storage medium was reachable the last time it + * was checked - at settingsInit(), and refreshed by every subsequent + * settingsSave() attempt. Missing storage is expected (e.g. no memory + * card/stick inserted), not fatal - see settingsIsAvailable(). + */ + bool_t available; +} settings_t; + +extern settings_t SETTINGS; + +/** + * Initializes the settings system and immediately loads the settings file + * if one exists, so SETTINGS.file reflects the last-saved values as soon + * as this returns - unlike save.h's per-slot game saves (which need an + * explicit "new game vs continue" choice before loading), there's no such + * ambiguity for device-wide settings, so this always loads eagerly. + * + * Never fails the way settingsSave() can - if the storage medium isn't + * reachable, that's logged and reflected in settingsIsAvailable() rather + * than treated as fatal, since the game should still be playable with + * just the built-in defaults. + * + * @return An error code only for unexpected platform failures. + */ +errorret_t settingsInit(void); + +/** + * Checks whether the settings storage medium was reachable as of the last + * load/save attempt. + * + * @return true if the settings medium was available last time it was + * checked. + */ +bool_t settingsIsAvailable(void); + +/** + * Disposes of the settings system. + * + * @return An error code if disposal fails. + */ +errorret_t settingsDispose(void); + +/** + * Writes the current settings to persistent storage. Always synchronous - + * unlike save.h's game saves, no platform needs a multi-frame native + * dialog for this, so there's no callback/busy-polling to deal with. + * + * @return An error code if the write fails. + */ +errorret_t settingsSave(void); + +/** + * Gets a pointer to the live settings data. Modify fields directly, then + * call settingsSave() to persist them. + * + * @return A pointer to the settings file. + */ +settingsfile_t * settingsGet(void); diff --git a/src/dusk/save/settingsfile.h b/src/dusk/save/settingsfile.h new file mode 100644 index 00000000..0537c81e --- /dev/null +++ b/src/dusk/save/settingsfile.h @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +/** Settings file format version. Increment on breaking change. */ +#define SETTINGS_FILE_VERSION 1 + +/** Magic bytes that identify a Dusk settings file. */ +#define SETTINGS_FILE_HEADER "DST" + +/** Byte length of the magic header (excludes the null terminator). */ +#define SETTINGS_FILE_HEADER_SIZE (sizeof(SETTINGS_FILE_HEADER) - 1) + +/** + * Default gamepad deadzone for a settings file that's never actually been + * loaded from disk yet (see settingsInit(), which stamps this on first + * boot) - the settings file is the single source of truth for this value + * (see settingsfile_t.deadzone); nothing else stores or defaults it. + */ +#define SETTINGS_DEADZONE_DEFAULT 0.1f + +/** + * Device/user-wide preferences, independent of any individual game save + * slot (see savefile.h) - there's exactly one of these, not one per slot, + * since a setting like gamepad deadzone shouldn't reset or diverge just + * because the player started a new game in a different slot. + */ +typedef struct { + /** Magic header bytes read from the file; must equal SETTINGS_FILE_HEADER. */ + char_t header[SETTINGS_FILE_HEADER_SIZE]; + /** Format version read from the file; used to branch on older layouts. */ + uint32_t version; + /** Runtime flag - true if settings were successfully loaded or written. */ + bool_t exists; + /** + * User-configured gamepad deadzone (0.0f-1.0f) - the settings file is + * the only place this lives; read it directly via settingsGet()->deadzone + * rather than caching it anywhere else. + */ + float_t deadzone; +} settingsfile_t; diff --git a/src/dusk/save/settingsstream.c b/src/dusk/save/settingsstream.c new file mode 100644 index 00000000..857357b8 --- /dev/null +++ b/src/dusk/save/settingsstream.c @@ -0,0 +1,164 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "save/settingsstream.h" +#include "util/crypt.h" +#include "util/endian.h" + +errorret_t settingsStreamReadBytesRawImpl( + settingsstream_t *stream, void *buf, const size_t len +) { + #ifdef settingsStreamReadBytesPlatform + errorChain(settingsStreamReadBytesPlatform(stream, buf, len)); + #endif + errorOk(); +} + +errorret_t settingsStreamWriteBytesRawImpl( + settingsstream_t *stream, const void *buf, const size_t len +) { + #ifdef settingsStreamWriteBytesPlatform + errorChain(settingsStreamWriteBytesPlatform(stream, buf, len)); + #endif + errorOk(); +} + +errorret_t settingsStreamReadBytesImpl( + settingsstream_t *stream, void *buf, const size_t len +) { + errorChain(settingsStreamReadBytesRawImpl(stream, buf, len)); + cryptCRC32Update(&stream->checksum, buf, len); + errorOk(); +} + +errorret_t settingsStreamWriteBytesImpl( + settingsstream_t *stream, const void *buf, const size_t len +) { + cryptCRC32Update(&stream->checksum, buf, len); + errorChain(settingsStreamWriteBytesRawImpl(stream, buf, len)); + errorOk(); +} + +errorret_t settingsStreamFinalizeWriteImpl(settingsstream_t *stream) { + uint32_t finalCRC = cryptCRC32End(stream->checksum); + uint32_t leChecksum = endianLittleToHost32(finalCRC); + + #ifdef settingsStreamSeekPlatform + errorChain(settingsStreamSeekPlatform(stream, SETTINGS_FILE_HEADER_SIZE)); + #endif + + errorChain(settingsStreamWriteBytesRawImpl( + stream, &leChecksum, sizeof(uint32_t) + )); + errorOk(); +} + +errorret_t settingsStreamVerifyChecksumImpl(settingsstream_t *stream) { + uint32_t computed = cryptCRC32End(stream->checksum); + if(computed != stream->expectedChecksum) { + errorThrow("Settings file has invalid checksum"); + } + errorOk(); +} + +errorret_t settingsStreamReadHeaderImpl( + settingsstream_t *stream, char_t header[SETTINGS_FILE_HEADER_SIZE] +) { + errorChain(settingsStreamReadBytesRawImpl( + stream, header, SETTINGS_FILE_HEADER_SIZE + )); + + if( + header[0] != SETTINGS_FILE_HEADER[0] || + header[1] != SETTINGS_FILE_HEADER[1] || + header[2] != SETTINGS_FILE_HEADER[2] + ) { + errorThrow("Settings file has invalid header"); + } + + uint32_t leChecksum; + errorChain(settingsStreamReadBytesRawImpl( + stream, &leChecksum, sizeof(uint32_t) + )); + stream->expectedChecksum = endianLittleToHost32(leChecksum); + stream->checksum = cryptCRC32Begin(); + errorOk(); +} + +errorret_t settingsStreamWriteHeaderImpl( + settingsstream_t *stream, const char_t header[SETTINGS_FILE_HEADER_SIZE] +) { + errorChain(settingsStreamWriteBytesRawImpl( + stream, header, SETTINGS_FILE_HEADER_SIZE + )); + + uint32_t placeholder = 0; + errorChain(settingsStreamWriteBytesRawImpl( + stream, &placeholder, sizeof(uint32_t) + )); + stream->checksum = cryptCRC32Begin(); + errorOk(); +} + +errorret_t settingsStreamReadVersionImpl(settingsstream_t *stream, uint32_t *out) { + uint32_t raw; + errorChain(settingsStreamReadBytesImpl(stream, &raw, sizeof(uint32_t))); + *out = endianLittleToHost32(raw); + errorOk(); +} + +errorret_t settingsStreamWriteVersionImpl( + settingsstream_t *stream, const uint32_t *input +) { + uint32_t raw = endianLittleToHost32(*input); + errorChain(settingsStreamWriteBytesImpl(stream, &raw, sizeof(uint32_t))); + errorOk(); +} + +errorret_t settingsStreamReadBoolImpl(settingsstream_t *stream, bool_t *out) { + uint8_t raw; + errorChain(settingsStreamReadBytesImpl(stream, &raw, sizeof(uint8_t))); + *out = (bool_t)(raw != 0); + errorOk(); +} + +errorret_t settingsStreamWriteBoolImpl( + settingsstream_t *stream, const bool_t *input +) { + uint8_t raw = *input ? 1 : 0; + errorChain(settingsStreamWriteBytesImpl(stream, &raw, sizeof(uint8_t))); + errorOk(); +} + +errorret_t settingsStreamReadFloatImpl(settingsstream_t *stream, float_t *out) { + float_t raw; + errorChain(settingsStreamReadBytesImpl(stream, &raw, sizeof(float_t))); + *out = endianLittleToHostFloat(raw); + errorOk(); +} + +errorret_t settingsStreamWriteFloatImpl( + settingsstream_t *stream, const float_t *input +) { + float_t raw = endianLittleToHostFloat(*input); + errorChain(settingsStreamWriteBytesImpl(stream, &raw, sizeof(float_t))); + errorOk(); +} + +errorret_t settingsFileLoad(settingsstream_t *stream, settingsfile_t *file) { + settingsFileReadHeader(stream, file->header); + settingsFileReadVersion(stream, &file->version); + settingsFileReadFloat(stream, &file->deadzone); + errorOk(); +} + +errorret_t settingsFileWrite(settingsstream_t *stream, settingsfile_t *file) { + settingsFileWriteHeader(stream, file->header); + settingsFileWriteVersion(stream, &file->version); + settingsFileWriteFloat(stream, &file->deadzone); + errorOk(); +} diff --git a/src/dusk/save/settingsstream.h b/src/dusk/save/settingsstream.h new file mode 100644 index 00000000..890e400b --- /dev/null +++ b/src/dusk/save/settingsstream.h @@ -0,0 +1,208 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "settingsfile.h" +#include "save/settingsplatform.h" + +typedef struct { + bool_t found; + uint32_t checksum; + uint32_t expectedChecksum; + settingsplatformstream_t platform; +} settingsstream_t; + +/** + * Reads bytes from the platform stream without updating the CRC. + * + * @param stream Active stream. + * @param buf Destination buffer. + * @param len Number of bytes to read. + * @return An error if the read fails. + */ +errorret_t settingsStreamReadBytesRawImpl( + settingsstream_t *stream, void *buf, const size_t len +); + +/** + * Writes bytes to the platform stream without updating the CRC. + * + * @param stream Active stream. + * @param buf Source buffer. + * @param len Number of bytes to write. + * @return An error if the write fails. + */ +errorret_t settingsStreamWriteBytesRawImpl( + settingsstream_t *stream, const void *buf, const size_t len +); + +/** + * Reads bytes from the platform stream and accumulates them into the CRC. + * + * @param stream Active stream. + * @param buf Destination buffer. + * @param len Number of bytes to read. + * @return An error if the read fails. + */ +errorret_t settingsStreamReadBytesImpl( + settingsstream_t *stream, void *buf, const size_t len +); + +/** + * Updates the CRC then writes bytes to the platform stream. + * + * @param stream Active stream. + * @param buf Source buffer. + * @param len Number of bytes to write. + * @return An error if the write fails. + */ +errorret_t settingsStreamWriteBytesImpl( + settingsstream_t *stream, const void *buf, const size_t len +); + +/** + * Finalizes a write stream: computes the final CRC32, seeks to the + * checksum field in the header, and writes it in little-endian order. + * + * @param stream Active write stream. + * @return An error if the seek or write fails. + */ +errorret_t settingsStreamFinalizeWriteImpl(settingsstream_t *stream); + +/** + * Verifies that the CRC32 accumulated during loading matches the value + * stored in the file header. + * + * @param stream Active read stream (loading must be complete). + * @return An error if the checksum does not match. + */ +errorret_t settingsStreamVerifyChecksumImpl(settingsstream_t *stream); + +/** + * Reads and validates the magic header, then reads the stored CRC32 and + * resets the running accumulator. + * + * @param stream Active read stream. + * @param header Buffer of SETTINGS_FILE_HEADER_SIZE bytes to receive the + * header. + * @return An error if the header is missing or invalid. + */ +errorret_t settingsStreamReadHeaderImpl( + settingsstream_t *stream, char_t header[SETTINGS_FILE_HEADER_SIZE] +); + +/** + * Writes the magic header and a zero CRC32 placeholder, then resets the + * running accumulator. + * + * @param stream Active write stream. + * @param header Buffer of SETTINGS_FILE_HEADER_SIZE bytes to write. + * @return An error if the write fails. + */ +errorret_t settingsStreamWriteHeaderImpl( + settingsstream_t *stream, + const char_t header[SETTINGS_FILE_HEADER_SIZE] +); + +/** + * Reads a little-endian uint32 version field from the stream. + * + * @param stream Active read stream. + * @param out Receives the host-order value. + * @return An error if the read fails. + */ +errorret_t settingsStreamReadVersionImpl(settingsstream_t *stream, uint32_t *out); + +/** + * Writes a uint32 version field to the stream in little-endian order. + * + * @param stream Active write stream. + * @param input Value to write. + * @return An error if the write fails. + */ +errorret_t settingsStreamWriteVersionImpl( + settingsstream_t *stream, const uint32_t *input +); + +/** + * Reads a single byte as a boolean (0 = false, non-zero = true). + * + * @param stream Active read stream. + * @param out Receives the boolean value. + * @return An error if the read fails. + */ +errorret_t settingsStreamReadBoolImpl(settingsstream_t *stream, bool_t *out); + +/** + * Writes a boolean as a single byte (true = 1, false = 0). + * + * @param stream Active write stream. + * @param input Value to write. + * @return An error if the write fails. + */ +errorret_t settingsStreamWriteBoolImpl( + settingsstream_t *stream, const bool_t *input +); + +/** + * Reads a little-endian float from the stream. + * + * @param stream Active read stream. + * @param out Receives the host-order value. + * @return An error if the read fails. + */ +errorret_t settingsStreamReadFloatImpl(settingsstream_t *stream, float_t *out); + +/** + * Writes a float to the stream in little-endian order. + * + * @param stream Active write stream. + * @param input Value to write. + * @return An error if the write fails. + */ +errorret_t settingsStreamWriteFloatImpl( + settingsstream_t *stream, const float_t *input +); + +/** + * Reads the contents of the settings file from the stream. + * + * @param stream Active read stream. + * @param file Settings file struct to populate. + * @return An error code if loading fails. + */ +errorret_t settingsFileLoad(settingsstream_t *stream, settingsfile_t *file); + +/** + * Writes the contents of the settings file struct into the stream. + * + * @param stream Active write stream. + * @param file Settings file struct to serialize. + * @return An error code if writing fails. + */ +errorret_t settingsFileWrite(settingsstream_t *stream, settingsfile_t *file); + +#define settingsFileReadHeader(stream, header) \ + errorChain(settingsStreamReadHeaderImpl(stream, header)) +#define settingsFileWriteHeader(stream, header) \ + errorChain(settingsStreamWriteHeaderImpl(stream, header)) + +#define settingsFileReadVersion(stream, out) \ + errorChain(settingsStreamReadVersionImpl(stream, out)) +#define settingsFileWriteVersion(stream, input) \ + errorChain(settingsStreamWriteVersionImpl(stream, input)) + +#define settingsFileReadBool(stream, out) \ + errorChain(settingsStreamReadBoolImpl(stream, out)) +#define settingsFileWriteBool(stream, input) \ + errorChain(settingsStreamWriteBoolImpl(stream, input)) + +#define settingsFileReadFloat(stream, out) \ + errorChain(settingsStreamReadFloatImpl(stream, out)) +#define settingsFileWriteFloat(stream, input) \ + errorChain(settingsStreamWriteFloatImpl(stream, input)) diff --git a/src/dusk/ui/frame/settings/uisettingsinput.c b/src/dusk/ui/frame/settings/uisettingsinput.c index fbf05b3e..7a9edcd4 100644 --- a/src/dusk/ui/frame/settings/uisettingsinput.c +++ b/src/dusk/ui/frame/settings/uisettingsinput.c @@ -11,7 +11,7 @@ #include "util/memory.h" #include "locale/localemanager.h" #include "asset/loader/locale/assetlocaleloader.h" -#include "save/save.h" +#include "save/settings.h" void uiSettingsInputSelected( const uimenu_t *menu, @@ -39,7 +39,7 @@ errorret_t uiSettingsInputInit(uisettingsdata_t *data) { UI_SETTINGS_INPUT_LABEL_MAX )); MENU_SLIDER_FLOAT( - input->deadzoneLabel, SAVE_DEADZONE_DEFAULT, 0.0f, 1.0f, 0.05f + input->deadzoneLabel, SETTINGS_DEADZONE_DEFAULT, 0.0f, 1.0f, 0.05f ); #else MENU_LABEL("No input settings yet"); @@ -55,16 +55,17 @@ void uiSettingsInputLoad(void) { #ifdef DUSK_INPUT_GAMEPAD uiSliderSetFloat( &UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider, - saveGet(SAVE_ACTIVE_SLOT)->deadzone + settingsGet()->deadzone ); #endif } void uiSettingsInputApply(void) { #ifdef DUSK_INPUT_GAMEPAD - saveGet(SAVE_ACTIVE_SLOT)->deadzone = uiSliderGetFloat( + settingsGet()->deadzone = uiSliderGetFloat( &UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider ); + errorCatch(errorPrint(settingsSave())); #endif uiMenuClose(&UI_SETTINGS.data.input.menu); } @@ -73,7 +74,7 @@ bool_t uiSettingsInputHasChanges(void) { #ifdef DUSK_INPUT_GAMEPAD if(uiSliderGetFloat( &UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider - ) != saveGet(SAVE_ACTIVE_SLOT)->deadzone) return true; + ) != settingsGet()->deadzone) return true; #endif return false; diff --git a/src/duskdolphin/input/inputdolphin.c b/src/duskdolphin/input/inputdolphin.c index 1e2f623b..c1f6d436 100644 --- a/src/duskdolphin/input/inputdolphin.c +++ b/src/duskdolphin/input/inputdolphin.c @@ -9,7 +9,7 @@ #include "assert/assert.h" #include "log/log.h" #include "util/string.h" -#include "save/save.h" +#include "save/settings.h" inputbuttondata_t INPUT_BUTTON_DATA[] = { #ifdef DUSK_INPUT_GAMEPAD @@ -188,5 +188,5 @@ float_t inputButtonGetValueDolphin(const inputbutton_t button) { } float_t inputGetDeadzoneDolphin(const inputbutton_t button) { - return saveGet(SAVE_ACTIVE_SLOT)->deadzone; + return settingsGet()->deadzone; } \ No newline at end of file diff --git a/src/duskdolphin/save/CMakeLists.txt b/src/duskdolphin/save/CMakeLists.txt index 95b853df..46683ef0 100644 --- a/src/duskdolphin/save/CMakeLists.txt +++ b/src/duskdolphin/save/CMakeLists.txt @@ -8,4 +8,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC savedolphin.c savestreamdolphin.c + settingsdolphin.c + settingsstreamdolphin.c ) diff --git a/src/duskdolphin/save/settingsdolphin.c b/src/duskdolphin/save/settingsdolphin.c new file mode 100644 index 00000000..14a505f0 --- /dev/null +++ b/src/duskdolphin/save/settingsdolphin.c @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "save/save.h" +#include "save/settings.h" + +errorret_t settingsInitDolphin(void) { + if(!SAVE.platform.mounted) { + errorThrow("No memory card mounted"); + } + errorOk(); +} + +errorret_t settingsDisposeDolphin(void) { + errorOk(); +} diff --git a/src/duskdolphin/save/settingsdolphin.h b/src/duskdolphin/save/settingsdolphin.h new file mode 100644 index 00000000..c95ed07f --- /dev/null +++ b/src/duskdolphin/save/settingsdolphin.h @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "save/settingsfile.h" + +#ifndef SETTINGS_DOLPHIN_FILE_NAME + #define SETTINGS_DOLPHIN_FILE_NAME "DUSK_CFG" +#endif + +typedef struct { + /** No dedicated state - settings reuse the memory card mount that the + * game-save system (see savedolphin.h) already establishes. */ + uint8_t reserved; +} settingsdolphin_t; + +/** + * Initializes the settings system on GameCube. Does not mount the memory + * card itself - reuses the mount already established by the game-save + * system (see saveInitDolphin()), since there's only one memory card + * subsystem to go around. settingsInit() must therefore run after + * saveInit() on this platform. + * + * @return An error code if no memory card is currently mounted. + */ +errorret_t settingsInitDolphin(void); + +/** + * Disposes of the settings system on GameCube. Does not unmount the + * memory card - that's owned by the game-save system. + * + * @return An error code if disposal fails. + */ +errorret_t settingsDisposeDolphin(void); diff --git a/src/duskdolphin/save/settingsplatform.h b/src/duskdolphin/save/settingsplatform.h new file mode 100644 index 00000000..2216a5f5 --- /dev/null +++ b/src/duskdolphin/save/settingsplatform.h @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "save/settingsdolphin.h" +#include "save/settingsstreamdolphin.h" + +typedef settingsdolphin_t settingsplatform_t; +typedef settingsstreamdolphin_t settingsplatformstream_t; + +#define settingsInitPlatform settingsInitDolphin +#define settingsDisposePlatform settingsDisposeDolphin + +#define settingsStreamOpenReadPlatform(stream) \ + settingsStreamOpenReadDolphin(&(stream)->platform, &(stream)->found) +#define settingsStreamOpenWritePlatform(stream) \ + settingsStreamOpenWriteDolphin(&(stream)->platform) +#define settingsStreamClosePlatform(stream) \ + settingsStreamCloseDolphin(&(stream)->platform) +#define settingsStreamReadBytesPlatform(stream, buf, len) \ + settingsStreamReadBytesDolphin(&(stream)->platform, buf, len) +#define settingsStreamWriteBytesPlatform(stream, buf, len) \ + settingsStreamWriteBytesDolphin(&(stream)->platform, buf, len) +#define settingsStreamSeekPlatform(stream, pos) \ + settingsStreamSeekDolphin(&(stream)->platform, pos) diff --git a/src/duskdolphin/save/settingsstreamdolphin.c b/src/duskdolphin/save/settingsstreamdolphin.c new file mode 100644 index 00000000..95848c97 --- /dev/null +++ b/src/duskdolphin/save/settingsstreamdolphin.c @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "save/save.h" +#include "save/settingsdolphin.h" +#include "save/settingsstreamdolphin.h" +#include "util/memory.h" + +errorret_t settingsStreamOpenReadDolphin( + settingsstreamdolphin_t *p, bool_t *found +) { + if(!SAVE.platform.mounted) { + *found = false; + errorThrow("No memory card mounted"); + } + + int32_t result; + do { + result = CARD_Open( + SAVE_DOLPHIN_CHANNEL, SETTINGS_DOLPHIN_FILE_NAME, &p->cardFile + ); + } while(result == CARD_ERROR_BUSY); + if(result == CARD_ERROR_NOFILE) { + *found = false; + p->position = 0; + p->writing = false; + errorOk(); + } + if(result < 0) { + *found = false; + errorThrow("Failed to open memory card settings file: %s (%d)", + saveCardErrorStringDolphin(result), result + ); + } + + do { + result = CARD_Read(&p->cardFile, p->buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0); + } while(result == CARD_ERROR_BUSY); + CARD_Close(&p->cardFile); + if(result < 0) { + *found = false; + errorThrow("Failed to read memory card settings data: %s (%d)", + saveCardErrorStringDolphin(result), result + ); + } + + *found = true; + p->position = 0; + p->writing = false; + errorOk(); +} + +errorret_t settingsStreamOpenWriteDolphin(settingsstreamdolphin_t *p) { + if(!SAVE.platform.mounted) errorThrow("No memory card mounted"); + + memoryZero(p->buffer, SAVE_DOLPHIN_SECTOR_SIZE); + p->position = 0; + p->writing = true; + errorOk(); +} + +void settingsStreamCloseDolphin(settingsstreamdolphin_t *p) { + if(!p->writing) return; + + int32_t result; + do { + result = CARD_Open( + SAVE_DOLPHIN_CHANNEL, SETTINGS_DOLPHIN_FILE_NAME, &p->cardFile + ); + } while(result == CARD_ERROR_BUSY); + if(result == CARD_ERROR_NOFILE) { + do { + result = CARD_Create( + SAVE_DOLPHIN_CHANNEL, SETTINGS_DOLPHIN_FILE_NAME, + SAVE_DOLPHIN_SECTOR_SIZE, &p->cardFile + ); + } while(result == CARD_ERROR_BUSY); + } + + do { + result = CARD_Write(&p->cardFile, p->buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0); + } while(result == CARD_ERROR_BUSY); + CARD_Close(&p->cardFile); +} + +errorret_t settingsStreamReadBytesDolphin( + settingsstreamdolphin_t *p, void *buf, const size_t len +) { + if(p->position + len > SAVE_DOLPHIN_SECTOR_SIZE) { + errorThrow("Settings stream read exceeds sector size"); + } + memoryCopy(buf, p->buffer + p->position, len); + p->position += len; + errorOk(); +} + +errorret_t settingsStreamWriteBytesDolphin( + settingsstreamdolphin_t *p, const void *buf, const size_t len +) { + if(p->position + len > SAVE_DOLPHIN_SECTOR_SIZE) { + errorThrow("Settings stream write exceeds sector size"); + } + memoryCopy(p->buffer + p->position, buf, len); + p->position += len; + errorOk(); +} + +errorret_t settingsStreamSeekDolphin( + settingsstreamdolphin_t *p, const size_t pos +) { + if(pos >= SAVE_DOLPHIN_SECTOR_SIZE) { + errorThrow("Settings stream seek out of range"); + } + p->position = pos; + errorOk(); +} diff --git a/src/duskdolphin/save/settingsstreamdolphin.h b/src/duskdolphin/save/settingsstreamdolphin.h new file mode 100644 index 00000000..59dbbd8b --- /dev/null +++ b/src/duskdolphin/save/settingsstreamdolphin.h @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "save/savedolphin.h" +#include +#include + +typedef struct { + /** libogc memory card file handle. */ + card_file cardFile; + /** In-memory sector buffer; all reads and writes operate on this. */ + uint8_t buffer[SAVE_DOLPHIN_SECTOR_SIZE] __attribute__((aligned(32))); + /** Current read/write position within buffer. */ + size_t position; + /** True when opened for writing; flushes buffer to card on close. */ + bool_t writing; +} settingsstreamdolphin_t; + +/** + * Opens the settings memory card file for reading by loading its sector + * into buffer. + * + * @param p Stream to initialize. + * @param found Set to true if the file exists, false if it does not. + * @return An error if reading the card fails for a reason other than + * missing file. + */ +errorret_t settingsStreamOpenReadDolphin( + settingsstreamdolphin_t *p, bool_t *found +); + +/** + * Opens the settings memory card file for writing by zeroing the sector + * buffer. The buffer is flushed to the card when + * settingsStreamCloseDolphin is called. + * + * @param p Stream to initialize. + * @return An error if initialization fails. + */ +errorret_t settingsStreamOpenWriteDolphin(settingsstreamdolphin_t *p); + +/** + * Flushes the sector buffer to the memory card (write mode only) and + * releases the card file handle. + * + * @param p Stream to close. + */ +void settingsStreamCloseDolphin(settingsstreamdolphin_t *p); + +/** + * Copies len bytes from the sector buffer at the current position into buf. + * + * @param p Active stream. + * @param buf Destination buffer. + * @param len Number of bytes to read. + * @return An error if the read would exceed the sector size. + */ +errorret_t settingsStreamReadBytesDolphin( + settingsstreamdolphin_t *p, void *buf, const size_t len +); + +/** + * Copies len bytes from buf into the sector buffer at the current position. + * + * @param p Active stream. + * @param buf Source buffer. + * @param len Number of bytes to write. + * @return An error if the write would exceed the sector size. + */ +errorret_t settingsStreamWriteBytesDolphin( + settingsstreamdolphin_t *p, const void *buf, const size_t len +); + +/** + * Sets the current read/write position within the sector buffer. + * + * @param p Active stream. + * @param pos Target byte offset from the start of the sector. + * @return An error if pos is out of range. + */ +errorret_t settingsStreamSeekDolphin( + settingsstreamdolphin_t *p, const size_t pos +); diff --git a/src/dusklinux/input/inputlinux.c b/src/dusklinux/input/inputlinux.c index f9cf744e..0455ebd0 100644 --- a/src/dusklinux/input/inputlinux.c +++ b/src/dusklinux/input/inputlinux.c @@ -6,7 +6,7 @@ */ #include "input/input.h" -#include "save/save.h" +#include "save/settings.h" inputbuttondata_t INPUT_BUTTON_DATA[] = { #ifdef DUSK_INPUT_GAMEPAD @@ -548,5 +548,5 @@ errorret_t inputInitLinux(void) { } float_t inputGetDeadzoneSDL2(const inputbutton_t button) { - return saveGet(SAVE_ACTIVE_SLOT)->deadzone; + return settingsGet()->deadzone; } \ No newline at end of file diff --git a/src/dusklinux/save/CMakeLists.txt b/src/dusklinux/save/CMakeLists.txt index 74b1dcc7..99edc030 100644 --- a/src/dusklinux/save/CMakeLists.txt +++ b/src/dusklinux/save/CMakeLists.txt @@ -8,4 +8,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC savelinux.c savestreamlinux.c + settingslinux.c + settingsstreamlinux.c ) diff --git a/src/dusklinux/save/settingslinux.c b/src/dusklinux/save/settingslinux.c new file mode 100644 index 00000000..0200378a --- /dev/null +++ b/src/dusklinux/save/settingslinux.c @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "save/settings.h" +#include "util/string.h" +#include +#include + +errorret_t settingsInitLinux(void) { + stringCopy( + SETTINGS.platform.path, SETTINGS_LINUX_PATH, SETTINGS_LINUX_PATH_MAX + ); + + if(mkdir(SETTINGS.platform.path, 0755) != 0 && errno != EEXIST) { + errorThrow( + "Failed to create settings directory: %s", SETTINGS.platform.path + ); + } + + errorOk(); +} + +errorret_t settingsDisposeLinux(void) { + errorOk(); +} diff --git a/src/dusklinux/save/settingslinux.h b/src/dusklinux/save/settingslinux.h new file mode 100644 index 00000000..da1655c3 --- /dev/null +++ b/src/dusklinux/save/settingslinux.h @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "save/settingsfile.h" + +#define SETTINGS_LINUX_PATH_MAX FILENAME_MAX +#define SETTINGS_LINUX_FILE_FORMAT "%s/settings.dat" + +#ifndef SETTINGS_LINUX_PATH + #define SETTINGS_LINUX_PATH "./saves" +#endif + +typedef struct { + char_t path[SETTINGS_LINUX_PATH_MAX]; +} settingslinux_t; + +/** + * Initializes the settings system on Linux - ensures the settings + * directory exists, independent of whether the game-save system (see + * savelinux.h) has been initialized. + * + * @return An error code if initialization fails. + */ +errorret_t settingsInitLinux(void); + +/** + * Disposes of the settings system on Linux. + * + * @return An error code if disposal fails. + */ +errorret_t settingsDisposeLinux(void); diff --git a/src/dusklinux/save/settingsplatform.h b/src/dusklinux/save/settingsplatform.h new file mode 100644 index 00000000..1e1ab163 --- /dev/null +++ b/src/dusklinux/save/settingsplatform.h @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "save/settingslinux.h" +#include "save/settingsstreamlinux.h" + +typedef settingslinux_t settingsplatform_t; +typedef settingsstreamlinux_t settingsplatformstream_t; + +#define settingsInitPlatform settingsInitLinux +#define settingsDisposePlatform settingsDisposeLinux + +#define settingsStreamOpenReadPlatform(stream) \ + settingsStreamOpenReadLinux(&(stream)->platform, &(stream)->found) +#define settingsStreamOpenWritePlatform(stream) \ + settingsStreamOpenWriteLinux(&(stream)->platform) +#define settingsStreamClosePlatform(stream) \ + settingsStreamCloseLinux(&(stream)->platform) +#define settingsStreamReadBytesPlatform(stream, buf, len) \ + settingsStreamReadBytesLinux(&(stream)->platform, buf, len) +#define settingsStreamWriteBytesPlatform(stream, buf, len) \ + settingsStreamWriteBytesLinux(&(stream)->platform, buf, len) +#define settingsStreamSeekPlatform(stream, pos) \ + settingsStreamSeekLinux(&(stream)->platform, pos) diff --git a/src/dusklinux/save/settingsstreamlinux.c b/src/dusklinux/save/settingsstreamlinux.c new file mode 100644 index 00000000..702c53d1 --- /dev/null +++ b/src/dusklinux/save/settingsstreamlinux.c @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "save/settings.h" +#include "save/settingsstreamlinux.h" +#include "util/string.h" + +static void _settingsStreamGetPath(char_t *out, const size_t max) { + snprintf(out, max, SETTINGS_LINUX_FILE_FORMAT, SETTINGS.platform.path); +} + +errorret_t settingsStreamOpenReadLinux( + settingsstreamlinux_t *p, bool_t *found +) { + char_t path[SETTINGS_LINUX_PATH_MAX]; + _settingsStreamGetPath(path, SETTINGS_LINUX_PATH_MAX); + + p->file = fopen(path, "rb"); + *found = (p->file != NULL); + errorOk(); +} + +errorret_t settingsStreamOpenWriteLinux(settingsstreamlinux_t *p) { + char_t path[SETTINGS_LINUX_PATH_MAX]; + _settingsStreamGetPath(path, SETTINGS_LINUX_PATH_MAX); + + p->file = fopen(path, "wb"); + if(!p->file) { + errorThrow("Failed to open settings file for writing"); + } + errorOk(); +} + +void settingsStreamCloseLinux(settingsstreamlinux_t *p) { + if(p->file) { + fclose(p->file); + p->file = NULL; + } +} + +errorret_t settingsStreamReadBytesLinux( + settingsstreamlinux_t *p, void *buf, const size_t len +) { + if(fread(buf, 1, len, p->file) != len) { + errorThrow("Unexpected end of settings file"); + } + errorOk(); +} + +errorret_t settingsStreamWriteBytesLinux( + settingsstreamlinux_t *p, const void *buf, const size_t len +) { + if(fwrite(buf, 1, len, p->file) != len) { + errorThrow("Failed to write settings data"); + } + errorOk(); +} + +errorret_t settingsStreamSeekLinux(settingsstreamlinux_t *p, const size_t pos) { + if(fseek(p->file, (long)pos, SEEK_SET) != 0) { + errorThrow("Failed to seek in settings file"); + } + errorOk(); +} diff --git a/src/dusklinux/save/settingsstreamlinux.h b/src/dusklinux/save/settingsstreamlinux.h new file mode 100644 index 00000000..2110d170 --- /dev/null +++ b/src/dusklinux/save/settingsstreamlinux.h @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include +#include + +typedef struct { + FILE *file; +} settingsstreamlinux_t; + +/** + * Opens the settings file for reading. + * + * @param p Stream to initialize. + * @param found Set to true if the file exists, false if it does not. + * @return An error if the open fails for a reason other than missing file. + */ +errorret_t settingsStreamOpenReadLinux( + settingsstreamlinux_t *p, bool_t *found +); + +/** + * Opens the settings file for writing, creating or truncating it. + * + * @param p Stream to initialize. + * @return An error if the file cannot be opened for writing. + */ +errorret_t settingsStreamOpenWriteLinux(settingsstreamlinux_t *p); + +/** + * Closes the file handle held by the stream. + * + * @param p Stream to close. + */ +void settingsStreamCloseLinux(settingsstreamlinux_t *p); + +/** + * Reads len bytes from the stream into buf. + * + * @param p Active stream. + * @param buf Destination buffer. + * @param len Number of bytes to read. + * @return An error if fewer than len bytes are available. + */ +errorret_t settingsStreamReadBytesLinux( + settingsstreamlinux_t *p, void *buf, const size_t len +); + +/** + * Writes len bytes from buf into the stream. + * + * @param p Active stream. + * @param buf Source buffer. + * @param len Number of bytes to write. + * @return An error if the write fails. + */ +errorret_t settingsStreamWriteBytesLinux( + settingsstreamlinux_t *p, const void *buf, const size_t len +); + +/** + * Seeks to an absolute byte position within the stream. + * + * @param p Active stream. + * @param pos Target byte offset from the start of the file. + * @return An error if the seek fails. + */ +errorret_t settingsStreamSeekLinux(settingsstreamlinux_t *p, const size_t pos); diff --git a/src/duskpsp/input/inputpsp.c b/src/duskpsp/input/inputpsp.c index 0b61cf24..4a01f8b7 100644 --- a/src/duskpsp/input/inputpsp.c +++ b/src/duskpsp/input/inputpsp.c @@ -6,7 +6,7 @@ */ #include "input/input.h" -#include "save/save.h" +#include "save/settings.h" // #define INPUT_PSP_GAMEPAD_BUTTON_ACCEPT INPUT_SDL2_GAMEPAD_BUTTON_CUSTOM // #define INPUT_PSP_GAMEPAD_BUTTON_CANCEL INPUT_SDL2_GAMEPAD_BUTTON_CUSTOM @@ -95,5 +95,5 @@ errorret_t inputInitPSP(void) { } float_t inputGetDeadzoneSDL2(const inputbutton_t button) { - return saveGet(SAVE_ACTIVE_SLOT)->deadzone; + return settingsGet()->deadzone; } \ No newline at end of file diff --git a/src/duskpsp/save/CMakeLists.txt b/src/duskpsp/save/CMakeLists.txt index ee6cc44a..6708114a 100644 --- a/src/duskpsp/save/CMakeLists.txt +++ b/src/duskpsp/save/CMakeLists.txt @@ -8,4 +8,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC savepsp.c savestreampsp.c + settingspsp.c + settingsstreampsp.c ) diff --git a/src/duskpsp/save/settingsplatform.h b/src/duskpsp/save/settingsplatform.h new file mode 100644 index 00000000..3bf3999d --- /dev/null +++ b/src/duskpsp/save/settingsplatform.h @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "save/settingspsp.h" +#include "save/settingsstreampsp.h" + +typedef settingspsp_t settingsplatform_t; +typedef settingsstreampsp_t settingsplatformstream_t; + +#define settingsInitPlatform settingsInitPSP +#define settingsDisposePlatform settingsDisposePSP + +#define settingsStreamOpenReadPlatform(stream) \ + settingsStreamOpenReadPSP(&(stream)->platform, &(stream)->found) +#define settingsStreamOpenWritePlatform(stream) \ + settingsStreamOpenWritePSP(&(stream)->platform) +#define settingsStreamClosePlatform(stream) \ + settingsStreamClosePSP(&(stream)->platform) +#define settingsStreamReadBytesPlatform(stream, buf, len) \ + settingsStreamReadBytesPSP(&(stream)->platform, buf, len) +#define settingsStreamWriteBytesPlatform(stream, buf, len) \ + settingsStreamWriteBytesPSP(&(stream)->platform, buf, len) +#define settingsStreamSeekPlatform(stream, pos) \ + settingsStreamSeekPSP(&(stream)->platform, pos) diff --git a/src/duskpsp/save/settingspsp.c b/src/duskpsp/save/settingspsp.c new file mode 100644 index 00000000..501fc4c7 --- /dev/null +++ b/src/duskpsp/save/settingspsp.c @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "save/settingspsp.h" + +errorret_t settingsInitPSP(void) { + SceIoStat stat; + if(sceIoGetstat(SAVE_PSP_ROOT, &stat) < 0) { + errorThrow("No memory stick detected"); + } + errorOk(); +} + +errorret_t settingsDisposePSP(void) { + errorOk(); +} diff --git a/src/duskpsp/save/settingspsp.h b/src/duskpsp/save/settingspsp.h new file mode 100644 index 00000000..7ec8af97 --- /dev/null +++ b/src/duskpsp/save/settingspsp.h @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "save/settingsfile.h" +#include "save/savepsp.h" +#include + +#define SETTINGS_PSP_DIR_FORMAT "ms0:/PSP/SAVEDATA/%sCFG" +#define SETTINGS_PSP_FILE_FORMAT SETTINGS_PSP_DIR_FORMAT "/settings.bin" +#define SETTINGS_PSP_PATH_MAX 256 + +typedef struct { + uint8_t reserved; +} settingspsp_t; + +/** + * Initializes the settings system on PSP. Confirms the memory stick is + * reachable (sceIoGetstat on SAVE_PSP_ROOT), same check as saveInitPSP(). + * Deliberately does not go through sceUtilitySavedata (see savepsp.h) - + * that dialog is built for the "game save" browser/PARAM.SFO experience + * and shows a visible native icon flash on every write, which is fine for + * an explicit menu Save but not for a settings file that can be written + * every time the player nudges a slider. Plain sceIo file I/O, in its own + * directory outside the per-slot savedata folders, avoids that entirely. + * + * @return An error code if no memory stick is reachable. + */ +errorret_t settingsInitPSP(void); + +/** + * Disposes of the settings system on PSP. + * + * @return An error code if disposal fails. + */ +errorret_t settingsDisposePSP(void); diff --git a/src/duskpsp/save/settingsstreampsp.c b/src/duskpsp/save/settingsstreampsp.c new file mode 100644 index 00000000..809d4ffa --- /dev/null +++ b/src/duskpsp/save/settingsstreampsp.c @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "save/settingspsp.h" +#include "save/settingsstreampsp.h" +#include "util/string.h" + +errorret_t settingsStreamOpenReadPSP(settingsstreampsp_t *p, bool_t *found) { + char_t path[SETTINGS_PSP_PATH_MAX]; + stringFormat( + path, sizeof(path), SETTINGS_PSP_FILE_FORMAT, SAVE_PSP_GAME_NAME + ); + + p->fd = sceIoOpen(path, PSP_O_RDONLY, 0); + *found = (p->fd >= 0); + errorOk(); +} + +errorret_t settingsStreamOpenWritePSP(settingsstreampsp_t *p) { + char_t dir[SETTINGS_PSP_PATH_MAX]; + stringFormat(dir, sizeof(dir), SETTINGS_PSP_DIR_FORMAT, SAVE_PSP_GAME_NAME); + sceIoMkdir(dir, 0777); + + char_t path[SETTINGS_PSP_PATH_MAX]; + stringFormat( + path, sizeof(path), SETTINGS_PSP_FILE_FORMAT, SAVE_PSP_GAME_NAME + ); + + p->fd = sceIoOpen(path, PSP_O_WRONLY | PSP_O_CREAT | PSP_O_TRUNC, 0777); + if(p->fd < 0) { + errorThrow("Failed to open settings file for writing"); + } + errorOk(); +} + +void settingsStreamClosePSP(settingsstreampsp_t *p) { + if(p->fd >= 0) { + sceIoClose(p->fd); + p->fd = -1; + } +} + +errorret_t settingsStreamReadBytesPSP( + settingsstreampsp_t *p, void *buf, const size_t len +) { + int32_t read = sceIoRead(p->fd, buf, len); + if(read != (int32_t)len) { + errorThrow("Unexpected end of settings file"); + } + errorOk(); +} + +errorret_t settingsStreamWriteBytesPSP( + settingsstreampsp_t *p, const void *buf, const size_t len +) { + int32_t written = sceIoWrite(p->fd, buf, len); + if(written != (int32_t)len) { + errorThrow("Failed to write settings data"); + } + errorOk(); +} + +errorret_t settingsStreamSeekPSP(settingsstreampsp_t *p, const size_t pos) { + if(sceIoLseek(p->fd, (SceOff)pos, PSP_SEEK_SET) < 0) { + errorThrow("Failed to seek in settings file"); + } + errorOk(); +} diff --git a/src/duskpsp/save/settingsstreampsp.h b/src/duskpsp/save/settingsstreampsp.h new file mode 100644 index 00000000..2922349e --- /dev/null +++ b/src/duskpsp/save/settingsstreampsp.h @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include +#include + +typedef struct { + SceUID fd; +} settingsstreampsp_t; + +/** + * Opens the settings file for reading via plain sceIo (not the + * sceUtilitySavedata dialog - see settingspsp.h). + * + * @param p Stream to initialize. + * @param found Set to true if the file exists, false if it does not. + * @return An error if the open fails for a reason other than missing file. + */ +errorret_t settingsStreamOpenReadPSP(settingsstreampsp_t *p, bool_t *found); + +/** + * Opens the settings file for writing, creating its directory and + * truncating the file if needed. + * + * @param p Stream to initialize. + * @return An error if the file cannot be opened for writing. + */ +errorret_t settingsStreamOpenWritePSP(settingsstreampsp_t *p); + +/** + * Closes the file descriptor held by the stream. + * + * @param p Stream to close. + */ +void settingsStreamClosePSP(settingsstreampsp_t *p); + +/** + * Reads len bytes from the stream into buf. + * + * @param p Active stream. + * @param buf Destination buffer. + * @param len Number of bytes to read. + * @return An error if fewer than len bytes are available. + */ +errorret_t settingsStreamReadBytesPSP( + settingsstreampsp_t *p, void *buf, const size_t len +); + +/** + * Writes len bytes from buf into the stream. + * + * @param p Active stream. + * @param buf Source buffer. + * @param len Number of bytes to write. + * @return An error if the write fails. + */ +errorret_t settingsStreamWriteBytesPSP( + settingsstreampsp_t *p, const void *buf, const size_t len +); + +/** + * Seeks to an absolute byte position within the stream. + * + * @param p Active stream. + * @param pos Target byte offset from the start of the file. + * @return An error if the seek fails. + */ +errorret_t settingsStreamSeekPSP(settingsstreampsp_t *p, const size_t pos); diff --git a/src/duskvita/input/inputvita.c b/src/duskvita/input/inputvita.c index 550985e2..15a6461e 100644 --- a/src/duskvita/input/inputvita.c +++ b/src/duskvita/input/inputvita.c @@ -6,7 +6,7 @@ */ #include "input/input.h" -#include "save/save.h" +#include "save/settings.h" inputbuttondata_t INPUT_BUTTON_DATA[] = { { .name = "triangle", { @@ -84,5 +84,5 @@ inputbuttondata_t INPUT_BUTTON_DATA[] = { }; float_t inputGetDeadzoneSDL2(const inputbutton_t button) { - return saveGet(SAVE_ACTIVE_SLOT)->deadzone; + return settingsGet()->deadzone; }