Compare commits
14 Commits
we-ball
...
9aaffff7a8
| Author | SHA1 | Date | |
|---|---|---|---|
| 9aaffff7a8 | |||
| 7357b4a5df | |||
| 1ddc298a74 | |||
| e2a9442aa6 | |||
| aa0180571e | |||
| 7f7be39230 | |||
| 4d95415232 | |||
| 2cbd80a004 | |||
| 9abf8101da | |||
| 24badd06a5 | |||
| 7a03ef8eaf | |||
| f3ea507313 | |||
| 4b0388a0e1 | |||
| a84137b5ff |
@@ -1,455 +0,0 @@
|
|||||||
# Dusk — Claude Code rules
|
|
||||||
|
|
||||||
## File headers
|
|
||||||
Every C, H, and JS file starts with:
|
|
||||||
|
|
||||||
```c
|
|
||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
```
|
|
||||||
|
|
||||||
JS files use `//` comment style instead.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## C conventions
|
|
||||||
|
|
||||||
### Types
|
|
||||||
Always use the project-defined aliases instead of bare C primitives:
|
|
||||||
|
|
||||||
| Use | Not |
|
|
||||||
|-----------|--------------|
|
|
||||||
| `bool_t` | `bool` |
|
|
||||||
| `int_t` | `int` |
|
|
||||||
| `float_t` | `float` |
|
|
||||||
| `char_t` | `char` |
|
|
||||||
|
|
||||||
Use `uint8_t`, `uint16_t`, `int32_t`, etc. for fixed-width integers.
|
|
||||||
All struct and enum types end in `_t` (`animation_t`, `errorret_t`, …).
|
|
||||||
|
|
||||||
### Naming
|
|
||||||
- **Functions** — snake_case, prefixed with their module:
|
|
||||||
`assetLock()`, `entityPositionInit()`, `moduleAssetBatchCtor()`
|
|
||||||
- **Struct fields** — camelCase: `keyframeCount`, `localPosition`
|
|
||||||
- **Macros / constants** — UPPER_SNAKE_CASE:
|
|
||||||
`ENTITY_ID_INVALID`, `ERROR_OK`, `COMPONENT_TYPE_COUNT`
|
|
||||||
- **Files** — snake_case matching the primary type: `entityposition.c`,
|
|
||||||
`moduleassetbatch.c`
|
|
||||||
|
|
||||||
### Header files (`.h`)
|
|
||||||
- Use `#pragma once` — no include guards.
|
|
||||||
- Declare every public function, `#define`, and `extern` global.
|
|
||||||
- Write a JSDoc block (`/** … */`) above every declaration explaining
|
|
||||||
purpose, `@param`s, and `@returns`.
|
|
||||||
- Only include headers that the `.h` file itself strictly requires for
|
|
||||||
the types it exposes. Move everything else to the `.c` file.
|
|
||||||
Do not use forward declarations as a workaround — use the real
|
|
||||||
include in the `.c` file instead.
|
|
||||||
|
|
||||||
### Implementation files (`.c`)
|
|
||||||
- Contain function bodies only; no declarations.
|
|
||||||
- Pull in whatever additional includes the implementation needs.
|
|
||||||
- Do not use `static` or `inline` on **functions**. Every function,
|
|
||||||
including internal helpers, must be declared in the matching `.h` and
|
|
||||||
defined in the `.c` file. Internal helpers belong near the bottom of
|
|
||||||
the `.c` file, not at the top with a `static` qualifier.
|
|
||||||
`static` and `inline` on functions are only appropriate when the
|
|
||||||
function body is written directly inside a `.h` file.
|
|
||||||
`static` on **variables** (file-scope state) is fine and expected.
|
|
||||||
|
|
||||||
### Formatting
|
|
||||||
- Hard-wrap all lines at **80 characters**.
|
|
||||||
|
|
||||||
### Error handling
|
|
||||||
Return `errorret_t` from fallible functions. Use these macros:
|
|
||||||
|
|
||||||
```c
|
|
||||||
errorOk(); // return success
|
|
||||||
errorThrow("msg %d", val); // return failure with message
|
|
||||||
errorChain(someCall()); // propagate failure, continue on success
|
|
||||||
errorIsOk(ret) / errorIsNotOk(ret) // test a result
|
|
||||||
errorCatch(ret); // handle + free an error
|
|
||||||
```
|
|
||||||
|
|
||||||
Never return raw error codes or use `errno` for in-engine errors.
|
|
||||||
|
|
||||||
### Memory
|
|
||||||
Use the project allocator — never raw `malloc`/`free`:
|
|
||||||
|
|
||||||
```c
|
|
||||||
memoryAllocate(size) // allocate
|
|
||||||
memoryFree(ptr) // free
|
|
||||||
memoryZero(dest, size) // zero a block
|
|
||||||
memoryCopy(dest, src, size) // copy
|
|
||||||
```
|
|
||||||
|
|
||||||
### Asserts
|
|
||||||
Prefer specific assert macros over bare `assert()`:
|
|
||||||
|
|
||||||
```c
|
|
||||||
assertNotNull(ptr, "msg");
|
|
||||||
assertTrue(cond, "msg");
|
|
||||||
assertFalse(cond, "msg");
|
|
||||||
assertUnreachable("msg");
|
|
||||||
assertIsMainThread("msg");
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Build system
|
|
||||||
Each subdirectory has its own `CMakeLists.txt` that adds sources with:
|
|
||||||
|
|
||||||
```cmake
|
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|
||||||
PUBLIC
|
|
||||||
myfile.c
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Never add source files to the root `CMakeLists.txt` directly.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Platform support
|
|
||||||
|
|
||||||
### Targets
|
|
||||||
Set `DUSK_TARGET_SYSTEM` at CMake configure time to select a platform:
|
|
||||||
|
|
||||||
| `DUSK_TARGET_SYSTEM` | Macro defined | Platform |
|
|
||||||
|----------------------|-------------------|------------------|
|
|
||||||
| `linux` | `DUSK_LINUX` | Linux desktop |
|
|
||||||
| `knulli` | `DUSK_KNULLI` | Knulli (handheld)|
|
|
||||||
| `psp` | `DUSK_PSP` | Sony PSP |
|
|
||||||
| `vita` | `DUSK_VITA` | PlayStation Vita |
|
|
||||||
| `gamecube` | `DUSK_GAMECUBE` | Nintendo GameCube|
|
|
||||||
| `wii` | `DUSK_WII` | Nintendo Wii |
|
|
||||||
|
|
||||||
### Layer structure
|
|
||||||
```
|
|
||||||
src/dusk/ core, platform-agnostic game logic
|
|
||||||
src/duskgl/ OpenGL abstraction (Linux, Knulli, PSP, Vita)
|
|
||||||
src/dusksdl2/ SDL2 window + input (Linux, Knulli, PSP, Vita)
|
|
||||||
src/dusklinux/ Linux + Knulli platform impl
|
|
||||||
src/duskpsp/ PSP platform impl
|
|
||||||
src/duskvita/ Vita platform impl
|
|
||||||
src/duskdolphin/ GameCube / Wii platform impl (no SDL2/OpenGL)
|
|
||||||
```
|
|
||||||
|
|
||||||
Dolphin is the only target that bypasses SDL2 and OpenGL entirely —
|
|
||||||
it uses native GameCube/Wii rendering and input APIs.
|
|
||||||
|
|
||||||
### Platform guards
|
|
||||||
Use the compile-time macros for platform-specific code:
|
|
||||||
|
|
||||||
```c
|
|
||||||
#ifdef DUSK_PSP
|
|
||||||
// PSP-only path
|
|
||||||
#elif defined(DUSK_GAMECUBE) || defined(DUSK_WII)
|
|
||||||
// GameCube / Wii path
|
|
||||||
#else
|
|
||||||
// Generic / Linux fallback
|
|
||||||
#endif
|
|
||||||
```
|
|
||||||
|
|
||||||
Additional capability macros set per-target:
|
|
||||||
`DUSK_SDL2`, `DUSK_OPENGL`, `DUSK_OPENGL_ES`, `DUSK_OPENGL_LEGACY`,
|
|
||||||
`DUSK_INPUT_GAMEPAD`, `DUSK_INPUT_KEYBOARD`, `DUSK_INPUT_POINTER`,
|
|
||||||
`DUSK_PLATFORM_ENDIAN_BIG` / `DUSK_PLATFORM_ENDIAN_LITTLE`.
|
|
||||||
|
|
||||||
### Abstraction pattern
|
|
||||||
Platform-specific implementations are wired in via `#define` macros in
|
|
||||||
each platform's `displayplatform.h` / `inputplatform.h` etc., which
|
|
||||||
the core calls through. Functions that a platform does not support are
|
|
||||||
simply left undefined — the core guards calls with `#ifdef`.
|
|
||||||
|
|
||||||
### Adding platform-specific code
|
|
||||||
- Put it under `src/dusk<platform>/` in the matching subsystem folder.
|
|
||||||
- Gate any core call-site with the appropriate `#ifdef DUSK_<PLATFORM>`
|
|
||||||
or capability macro.
|
|
||||||
- Keep the `src/dusk/` core free of platform ifdefs — delegate through
|
|
||||||
the platform header macros instead.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Adding a new asset loader type
|
|
||||||
1. Add an enum value to `assetloadertype_t` (before `_COUNT`) in
|
|
||||||
`src/dusk/asset/loader/assetloader.h`.
|
|
||||||
2. Add fields to the input/loading/output unions in `assetloader.h`.
|
|
||||||
3. Implement `assetXxxLoaderSync`, `assetXxxLoaderAsync`, and
|
|
||||||
`assetXxxDispose` in a new `src/dusk/asset/loader/xxx/` directory.
|
|
||||||
4. Register the three callbacks in `ASSET_LOADER_CALLBACKS[]` in
|
|
||||||
`src/dusk/asset/loader/assetloader.c`.
|
|
||||||
5. If user-facing, create a JS module (see below) and a `.d.ts` file.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Adding a new entity component
|
|
||||||
1. Create `src/dusk/entity/component/<category>/entityMyComp.h/.c` with
|
|
||||||
struct `entityMyComp_t`, `entityMyCompInit()`, and optionally
|
|
||||||
`entityMyCompDispose()`.
|
|
||||||
2. Add the include to `src/dusk/entity/componentlist.h` header block.
|
|
||||||
3. Add a row to `src/dusk/entity/componentlist.h`:
|
|
||||||
```c
|
|
||||||
X(MYCOMP, entityMyComp_t, myComp, entityMyCompInit, NULL, NULL)
|
|
||||||
```
|
|
||||||
This auto-generates the enum, union field, and definition entry.
|
|
||||||
4. If JS-facing, create the script module and `.d.ts` (see below).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Adding a new script (JS) module
|
|
||||||
1. Create `src/dusk/script/module/<category>/moduleMyMod.h/.c`.
|
|
||||||
- Declare `extern scriptproto_t MODULE_MYMOD_PROTO;` in the header.
|
|
||||||
- Use `moduleBaseFunction(name)` to define JS-callable functions.
|
|
||||||
- Register props/funcs in `moduleMyModInit()` with
|
|
||||||
`scriptProtoDefineProp` / `scriptProtoDefineFunc` /
|
|
||||||
`scriptProtoDefineStaticFunc`.
|
|
||||||
2. `#include` the header in
|
|
||||||
`src/dusk/script/module/modulelist.c` and call
|
|
||||||
`moduleMyModInit()` in `moduleListInit()` (and `Dispose` in
|
|
||||||
`moduleListDispose()`).
|
|
||||||
3. For component modules also register in
|
|
||||||
`src/dusk/script/module/entity/component/modulecomponentlist.c`
|
|
||||||
so `entity.add()` returns the typed wrapper.
|
|
||||||
4. Create `types/<category>/mymod.d.ts` and add a
|
|
||||||
`/// <reference path="..." />` line to `types/index.d.ts`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Script module type declarations
|
|
||||||
Whenever a `src/dusk/script/module/**/*.c` file is created or modified,
|
|
||||||
check whether the corresponding `types/**/*.d.ts` needs updating and
|
|
||||||
apply any changes before finishing the task.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## JavaScript (asset scripts)
|
|
||||||
- Use `var` for module-level state; `const` for values that never
|
|
||||||
change.
|
|
||||||
- Always use semicolons.
|
|
||||||
- Scene objects are plain objects (`var scene = {}`) with assigned
|
|
||||||
methods.
|
|
||||||
- Export via `module.exports = scene`.
|
|
||||||
- Async scene init should use `async function` and `await`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Coding style
|
|
||||||
|
|
||||||
### ASCII only
|
|
||||||
Source files (`.c`, `.h`, `.js`) must contain only ASCII characters (U+0000–U+007F).
|
|
||||||
Non-ASCII characters are banned even in comments and string literals.
|
|
||||||
Use ASCII-only substitutes instead:
|
|
||||||
- `--` or `-` instead of `—` (em dash)
|
|
||||||
- `->` instead of `→` (arrow)
|
|
||||||
- `x` or `*` instead of `×` (multiplication)
|
|
||||||
|
|
||||||
Only non-script asset files (e.g. `.po` locale files) may contain non-ASCII text.
|
|
||||||
|
|
||||||
### Indentation
|
|
||||||
2 spaces. No tabs.
|
|
||||||
|
|
||||||
### Keyword and operator spacing
|
|
||||||
No space between a keyword or function name and its opening parenthesis:
|
|
||||||
|
|
||||||
```c
|
|
||||||
if(!ptr) return;
|
|
||||||
for(uint8_t i = 0; i < count; i++) {
|
|
||||||
while(entry->state != DONE) {
|
|
||||||
switch(type) {
|
|
||||||
sizeof(assetbatch_t)
|
|
||||||
memoryZero(ptr, size)
|
|
||||||
```
|
|
||||||
|
|
||||||
Spaces around all binary operators and after every comma:
|
|
||||||
|
|
||||||
```c
|
|
||||||
pos->flags |= ENTITY_POSITION_FLAG_WORLD_DIRTY;
|
|
||||||
(size_t)end - (size_t)start
|
|
||||||
foo(a, b, c)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Braces
|
|
||||||
Opening brace on the **same line** as the statement (K&R style) for all
|
|
||||||
constructs — functions, `if`, `else`, `for`, `while`, `switch`:
|
|
||||||
|
|
||||||
```c
|
|
||||||
void assetEntryLock(assetentry_t *entry) {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
|
|
||||||
if(dirty) {
|
|
||||||
...
|
|
||||||
} else {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Guard returns
|
|
||||||
Short guards go on one line with no braces:
|
|
||||||
|
|
||||||
```c
|
|
||||||
if(!ptr) return;
|
|
||||||
if(!b || !b->batch) return jerry_undefined();
|
|
||||||
if(!(flags & DIRTY)) return;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Blank lines
|
|
||||||
- One blank line between functions; no blank line at the start or end of
|
|
||||||
a function body.
|
|
||||||
- One blank line between logical blocks inside a function body.
|
|
||||||
- No trailing blank lines at the end of a file.
|
|
||||||
|
|
||||||
### Pointer placement
|
|
||||||
`*` is attached to the variable name, not the type:
|
|
||||||
|
|
||||||
```c
|
|
||||||
assetentry_t *entry
|
|
||||||
const char_t *name
|
|
||||||
void *ptr
|
|
||||||
uint8_t *d = (uint8_t *)dest;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Casts
|
|
||||||
Space between cast and operand:
|
|
||||||
|
|
||||||
```c
|
|
||||||
(assetbatch_t *)user
|
|
||||||
(uint8_t *)dest
|
|
||||||
(textureformat_t)v
|
|
||||||
```
|
|
||||||
|
|
||||||
### Return
|
|
||||||
No parentheses around the return value:
|
|
||||||
|
|
||||||
```c
|
|
||||||
return ptr;
|
|
||||||
return MEMORY_POINTERS_IN_USE;
|
|
||||||
```
|
|
||||||
|
|
||||||
### switch / case
|
|
||||||
`case` indented 2 spaces from `switch`; body indented 2 more from `case`:
|
|
||||||
|
|
||||||
```c
|
|
||||||
switch(type) {
|
|
||||||
case ASSET_LOADER_TYPE_TEXTURE:
|
|
||||||
descs[i].input.texture = (textureformat_t)v;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Multi-line function signatures
|
|
||||||
When parameters don't fit on one line, put each on its own line indented
|
|
||||||
2 spaces; the closing `) {` (definition) or `);` (declaration) goes on
|
|
||||||
its own line at column 0:
|
|
||||||
|
|
||||||
```c
|
|
||||||
void assetEntryInit(
|
|
||||||
assetentry_t *entry,
|
|
||||||
const char_t *name,
|
|
||||||
const assetloadertype_t type,
|
|
||||||
assetloaderinput_t *input
|
|
||||||
) {
|
|
||||||
|
|
||||||
errorret_t memoryCompare(
|
|
||||||
const void *a,
|
|
||||||
const void *b,
|
|
||||||
const size_t size
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Structs and enums
|
|
||||||
Anonymous inner struct or enum with a `typedef`, `_t` suffix, closing
|
|
||||||
brace and name on the same line:
|
|
||||||
|
|
||||||
```c
|
|
||||||
typedef struct {
|
|
||||||
errorcode_t code;
|
|
||||||
char_t *message;
|
|
||||||
} errorstate_t;
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
ASSET_LOADER_TYPE_NULL,
|
|
||||||
ASSET_LOADER_TYPE_COUNT
|
|
||||||
} assetloadertype_t;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Designated initialisers
|
|
||||||
Spaces inside braces; `.field = value`:
|
|
||||||
|
|
||||||
```c
|
|
||||||
jsassetentry_t e = { .entry = entry };
|
|
||||||
assetbatchloadedpend_t init = { .batch = batch };
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ternary operator
|
|
||||||
Spaces around `?` and `:`:
|
|
||||||
|
|
||||||
```c
|
|
||||||
const float val = psx > 0.0f ? pt[0][0] / psx : 0.0f;
|
|
||||||
```
|
|
||||||
|
|
||||||
### const placement
|
|
||||||
`const` before the type, `*` attached to the variable:
|
|
||||||
|
|
||||||
```c
|
|
||||||
const char_t *name
|
|
||||||
const void *src
|
|
||||||
const size_t size
|
|
||||||
```
|
|
||||||
|
|
||||||
### Comments in `.c` files
|
|
||||||
- Do not use section dividers (`/* ---- ... ---- */`). Just let the
|
|
||||||
functions follow one another with a single blank line between them.
|
|
||||||
- Multi-line explanatory comments inside function bodies use `//` lines:
|
|
||||||
```c
|
|
||||||
// Script modules are freed; orphaned JS wrapper objects now get GC'd
|
|
||||||
// so their finalizers fire before assetDispose() checks ref counts.
|
|
||||||
jerry_heap_gc(JERRY_GC_PRESSURE_HIGH);
|
|
||||||
```
|
|
||||||
- Do not use `/* */` for inline or inline-block comments inside `.c`
|
|
||||||
function bodies.
|
|
||||||
|
|
||||||
### Comments in `.h` files
|
|
||||||
Every public declaration gets a Javadoc block (`/** … */`) with
|
|
||||||
`@param` and `@returns` where relevant. Keep it on the lines immediately
|
|
||||||
above the declaration with no blank line in between.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Color system
|
|
||||||
|
|
||||||
Colors are defined in `src/dusk/display/color.csv` and code-generated
|
|
||||||
into a `color.h` header by `tools/color/csv/__main__.py`.
|
|
||||||
|
|
||||||
Each row in the CSV has `name,r,g,b,a` with channel values in `[0.0, 1.0]`.
|
|
||||||
The script emits four `#define` variants per color plus a bare alias:
|
|
||||||
|
|
||||||
```
|
|
||||||
COLOR_<NAME>_4B color4b(r8, g8, b8, a8) // default alias target
|
|
||||||
COLOR_<NAME>_3B color3b(r8, g8, b8)
|
|
||||||
COLOR_<NAME>_3F color3f(rf, gf, bf)
|
|
||||||
COLOR_<NAME>_4F color4f(rf, gf, bf, af)
|
|
||||||
COLOR_<NAME> COLOR_<NAME>_4B
|
|
||||||
```
|
|
||||||
|
|
||||||
`color_t` is `color4b_t` (four `uint8_t` channels).
|
|
||||||
|
|
||||||
To add a new color, append a row to `color.csv` and rebuild — do not
|
|
||||||
hand-edit the generated header.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
- Tests live in `test/` mirroring `src/dusk/` structure.
|
|
||||||
- Use cmocka; include `dusktest.h`.
|
|
||||||
- Test functions: `static void test_something(void **state)`.
|
|
||||||
- After each test, assert `memoryGetAllocatedCount() == 0` to catch
|
|
||||||
leaks.
|
|
||||||
- Build with `-DDUSK_BUILD_TESTS=ON`.
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -56,6 +56,62 @@ msgstr "Items"
|
|||||||
msgid "ui.game_menu.settings"
|
msgid "ui.game_menu.settings"
|
||||||
msgstr "Settings"
|
msgstr "Settings"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.save"
|
||||||
|
msgstr "Save"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.save_success"
|
||||||
|
msgstr "Game saved."
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.save_cancelled"
|
||||||
|
msgstr "Save cancelled."
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.save_unavailable"
|
||||||
|
msgstr "Can't save - no save device found."
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.save_temporary"
|
||||||
|
msgstr "This session is temporary - no save device was found, so saving is disabled."
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.save_create_confirm"
|
||||||
|
msgstr "No save data found. Create a new save?"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.save_failed_format"
|
||||||
|
msgstr "Save failed: %s"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.save_check_failed_format"
|
||||||
|
msgstr "Can't save: %s"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
||||||
|
msgid "ui.initial.no_card.message"
|
||||||
|
msgstr "No save device found. You can continue, but\nprogress will not be saved."
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
||||||
|
msgid "ui.initial.no_card.retry"
|
||||||
|
msgstr "Retry"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
||||||
|
msgid "ui.initial.no_card.continue"
|
||||||
|
msgstr "Continue Anyway"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
||||||
|
msgid "ui.initial.create_save.message"
|
||||||
|
msgstr "No save data found. Create a new save?"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
||||||
|
msgid "ui.initial.create_save.yes"
|
||||||
|
msgstr "Yes"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
||||||
|
msgid "ui.initial.create_save.no"
|
||||||
|
msgstr "No"
|
||||||
|
|
||||||
msgid "item.potion.name"
|
msgid "item.potion.name"
|
||||||
msgstr "Potion"
|
msgstr "Potion"
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,62 @@ msgstr "Objetos"
|
|||||||
msgid "ui.game_menu.settings"
|
msgid "ui.game_menu.settings"
|
||||||
msgstr "Configuración"
|
msgstr "Configuración"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.save"
|
||||||
|
msgstr "Guardar"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.save_success"
|
||||||
|
msgstr "Partida guardada."
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.save_cancelled"
|
||||||
|
msgstr "Guardado cancelado."
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.save_unavailable"
|
||||||
|
msgstr "No se puede guardar: no se encontró ningún dispositivo de guardado."
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.save_temporary"
|
||||||
|
msgstr "Esta sesión es temporal - no se encontró ningún dispositivo de guardado, por lo que guardar está deshabilitado."
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.save_create_confirm"
|
||||||
|
msgstr "No se encontraron datos guardados. ¿Crear una partida nueva?"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.save_failed_format"
|
||||||
|
msgstr "Error al guardar: %s"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.save_check_failed_format"
|
||||||
|
msgstr "No se puede guardar: %s"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
||||||
|
msgid "ui.initial.no_card.message"
|
||||||
|
msgstr "No se encontró ningún dispositivo de guardado. Puedes continuar, pero\nel progreso no se guardará."
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
||||||
|
msgid "ui.initial.no_card.retry"
|
||||||
|
msgstr "Reintentar"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
||||||
|
msgid "ui.initial.no_card.continue"
|
||||||
|
msgstr "Continuar de todos modos"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
||||||
|
msgid "ui.initial.create_save.message"
|
||||||
|
msgstr "No se encontraron datos guardados. ¿Crear una partida nueva?"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
||||||
|
msgid "ui.initial.create_save.yes"
|
||||||
|
msgstr "Sí"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
||||||
|
msgid "ui.initial.create_save.no"
|
||||||
|
msgstr "No"
|
||||||
|
|
||||||
#: src/dusk/rpg/item/item.json
|
#: src/dusk/rpg/item/item.json
|
||||||
msgid "item.potion.name"
|
msgid "item.potion.name"
|
||||||
msgstr "Poción"
|
msgstr "Poción"
|
||||||
|
|||||||
@@ -57,6 +57,62 @@ msgstr "アイテム"
|
|||||||
msgid "ui.game_menu.settings"
|
msgid "ui.game_menu.settings"
|
||||||
msgstr "設定"
|
msgstr "設定"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.save"
|
||||||
|
msgstr "セーブ"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.save_success"
|
||||||
|
msgstr "セーブしました。"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.save_cancelled"
|
||||||
|
msgstr "セーブをキャンセルしました。"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.save_unavailable"
|
||||||
|
msgstr "セーブできません - セーブデバイスが見つかりません。"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.save_temporary"
|
||||||
|
msgstr "このセッションは一時的です - セーブデバイスが見つからなかったため、セーブは無効になっています。"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.save_create_confirm"
|
||||||
|
msgstr "セーブデータが見つかりません。新しいセーブを作成しますか?"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.save_failed_format"
|
||||||
|
msgstr "セーブに失敗しました: %s"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.save_check_failed_format"
|
||||||
|
msgstr "セーブできません: %s"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
||||||
|
msgid "ui.initial.no_card.message"
|
||||||
|
msgstr "セーブデバイスが見つかりません。続行できますが、\n進行状況は保存されません。"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
||||||
|
msgid "ui.initial.no_card.retry"
|
||||||
|
msgstr "再試行"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
||||||
|
msgid "ui.initial.no_card.continue"
|
||||||
|
msgstr "続行する"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
||||||
|
msgid "ui.initial.create_save.message"
|
||||||
|
msgstr "セーブデータが見つかりません。新しいセーブを作成しますか?"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
||||||
|
msgid "ui.initial.create_save.yes"
|
||||||
|
msgstr "はい"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
||||||
|
msgid "ui.initial.create_save.no"
|
||||||
|
msgstr "いいえ"
|
||||||
|
|
||||||
#: src/dusk/rpg/item/item.json
|
#: src/dusk/rpg/item/item.json
|
||||||
msgid "item.potion.name"
|
msgid "item.potion.name"
|
||||||
msgstr "ポーション"
|
msgstr "ポーション"
|
||||||
|
|||||||
@@ -2179,5 +2179,27 @@
|
|||||||
0
|
0
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"type": "global",
|
||||||
|
"globalId": 3,
|
||||||
|
"pos": [8, 8, 1]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "item",
|
||||||
|
"itemId": 1,
|
||||||
|
"quantity": 1,
|
||||||
|
"pos": [12, 2, 0]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"areas": [
|
||||||
|
{
|
||||||
|
"min": [11, 3, 0],
|
||||||
|
"max": [16, 9, 10],
|
||||||
|
"callbackId": 1,
|
||||||
|
"notify": 3,
|
||||||
|
"trigger": 6
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,26 @@
|
|||||||
#include "asset/loader/assetloader.h"
|
#include "asset/loader/assetloader.h"
|
||||||
#include "asset/asset.h"
|
#include "asset/asset.h"
|
||||||
|
|
||||||
|
// Reads a little-endian int16 from a potentially-unaligned offset into a
|
||||||
|
// worldunit_t, advancing *offset past it.
|
||||||
|
static worldunit_t assetChunkReadWorldUnit(
|
||||||
|
const uint8_t *data,
|
||||||
|
size_t *offset
|
||||||
|
) {
|
||||||
|
int16_t value;
|
||||||
|
memoryCopy(&value, data + *offset, sizeof(int16_t));
|
||||||
|
*offset += sizeof(int16_t);
|
||||||
|
return (worldunit_t)endianLittleToHost16((uint16_t)value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static worldpos_t assetChunkReadWorldPos(const uint8_t *data, size_t *offset) {
|
||||||
|
worldpos_t pos;
|
||||||
|
pos.x = assetChunkReadWorldUnit(data, offset);
|
||||||
|
pos.y = assetChunkReadWorldUnit(data, offset);
|
||||||
|
pos.z = assetChunkReadWorldUnit(data, offset);
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
|
errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
|
||||||
assertNotNull(loading, "Loading cannot be NULL");
|
assertNotNull(loading, "Loading cannot be NULL");
|
||||||
assertNotMainThread("Should be called from an async thread.");
|
assertNotMainThread("Should be called from an async thread.");
|
||||||
@@ -146,6 +166,62 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
|||||||
out->meshOffsets[m][2] = endianLittleToHostFloat(out->meshOffsets[m][2]);
|
out->meshOffsets[m][2] = endianLittleToHostFloat(out->meshOffsets[m][2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
out->entitySpawnCount = data[offset];
|
||||||
|
offset += sizeof(uint8_t);
|
||||||
|
assertTrue(
|
||||||
|
out->entitySpawnCount <= CHUNK_ENTITY_SPAWN_COUNT_MAX,
|
||||||
|
"Chunk entity spawn count exceeds maximum."
|
||||||
|
);
|
||||||
|
|
||||||
|
for(uint8_t s = 0; s < out->entitySpawnCount; s++) {
|
||||||
|
chunkentityspawn_t *spawn = &out->entitySpawns[s];
|
||||||
|
spawn->kind = (chunkentityspawnkind_t)data[offset];
|
||||||
|
offset += sizeof(uint8_t);
|
||||||
|
|
||||||
|
uint16_t a;
|
||||||
|
memoryCopy(&a, data + offset, sizeof(uint16_t));
|
||||||
|
a = endianLittleToHost16(a);
|
||||||
|
offset += sizeof(uint16_t);
|
||||||
|
|
||||||
|
uint8_t b = data[offset];
|
||||||
|
offset += sizeof(uint8_t);
|
||||||
|
|
||||||
|
if(spawn->kind == CHUNK_ENTITY_SPAWN_KIND_ITEM) {
|
||||||
|
spawn->globalId = 0;
|
||||||
|
spawn->itemId = a;
|
||||||
|
spawn->itemQuantity = b;
|
||||||
|
} else {
|
||||||
|
spawn->globalId = a;
|
||||||
|
spawn->itemId = 0;
|
||||||
|
spawn->itemQuantity = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
spawn->position = assetChunkReadWorldPos(data, &offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
out->areaSpawnCount = data[offset];
|
||||||
|
offset += sizeof(uint8_t);
|
||||||
|
assertTrue(
|
||||||
|
out->areaSpawnCount <= CHUNK_AREA_COUNT_MAX,
|
||||||
|
"Chunk area spawn count exceeds maximum."
|
||||||
|
);
|
||||||
|
|
||||||
|
for(uint8_t s = 0; s < out->areaSpawnCount; s++) {
|
||||||
|
chunkareaspawn_t *area = &out->areaSpawns[s];
|
||||||
|
area->min = assetChunkReadWorldPos(data, &offset);
|
||||||
|
area->max = assetChunkReadWorldPos(data, &offset);
|
||||||
|
|
||||||
|
uint16_t callbackId;
|
||||||
|
memoryCopy(&callbackId, data + offset, sizeof(uint16_t));
|
||||||
|
area->callbackId = endianLittleToHost16(callbackId);
|
||||||
|
offset += sizeof(uint16_t);
|
||||||
|
|
||||||
|
area->notify = data[offset];
|
||||||
|
offset += sizeof(uint8_t);
|
||||||
|
area->trigger = data[offset];
|
||||||
|
offset += sizeof(uint8_t);
|
||||||
|
}
|
||||||
|
|
||||||
memoryFree(data);
|
memoryFree(data);
|
||||||
loading->loading.chunk.data = NULL;
|
loading->loading.chunk.data = NULL;
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
#include "asset/assetfile.h"
|
#include "asset/assetfile.h"
|
||||||
#include "rpg/overworld/chunk.h"
|
#include "rpg/overworld/chunk.h"
|
||||||
|
|
||||||
#define ASSET_CHUNK_FILE_VERSION 4
|
#define ASSET_CHUNK_FILE_VERSION 5
|
||||||
|
|
||||||
typedef struct assetloading_s assetloading_t;
|
typedef struct assetloading_s assetloading_t;
|
||||||
typedef struct assetentry_s assetentry_t;
|
typedef struct assetentry_s assetentry_t;
|
||||||
@@ -33,12 +33,39 @@ typedef struct {
|
|||||||
uint8_t modelIndex;
|
uint8_t modelIndex;
|
||||||
} assetchunkloaderloading_t;
|
} assetchunkloaderloading_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CHUNK_ENTITY_SPAWN_KIND_GLOBAL,
|
||||||
|
CHUNK_ENTITY_SPAWN_KIND_ITEM
|
||||||
|
} chunkentityspawnkind_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
chunkentityspawnkind_t kind;
|
||||||
|
uint16_t globalId; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_GLOBAL.
|
||||||
|
uint16_t itemId; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_ITEM.
|
||||||
|
uint8_t itemQuantity; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_ITEM.
|
||||||
|
worldpos_t position;
|
||||||
|
} chunkentityspawn_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
worldpos_t min;
|
||||||
|
worldpos_t max;
|
||||||
|
uint16_t callbackId; // Index into MAP_AREA_CALLBACK_LIST.
|
||||||
|
uint8_t notify;
|
||||||
|
uint8_t trigger;
|
||||||
|
} chunkareaspawn_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
tile_t *tiles;
|
tile_t *tiles;
|
||||||
uint8_t meshCount;
|
uint8_t meshCount;
|
||||||
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
|
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
|
||||||
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
|
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
|
||||||
assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX];
|
assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX];
|
||||||
|
|
||||||
|
uint8_t entitySpawnCount;
|
||||||
|
chunkentityspawn_t entitySpawns[CHUNK_ENTITY_SPAWN_COUNT_MAX];
|
||||||
|
|
||||||
|
uint8_t areaSpawnCount;
|
||||||
|
chunkareaspawn_t areaSpawns[CHUNK_AREA_COUNT_MAX];
|
||||||
} assetchunkoutput_t;
|
} assetchunkoutput_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#include "system/system.h"
|
#include "system/system.h"
|
||||||
#include "console/console.h"
|
#include "console/console.h"
|
||||||
#include "save/save.h"
|
#include "save/save.h"
|
||||||
|
#include "save/autosave.h"
|
||||||
|
|
||||||
engine_t ENGINE;
|
engine_t ENGINE;
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
|||||||
errorChain(systemInit());
|
errorChain(systemInit());
|
||||||
errorChain(inputInit());
|
errorChain(inputInit());
|
||||||
errorChain(assetInit());
|
errorChain(assetInit());
|
||||||
// errorChain(saveInit());
|
errorChain(saveInit());
|
||||||
errorChain(localeManagerInit());
|
errorChain(localeManagerInit());
|
||||||
errorChain(displayInit());
|
errorChain(displayInit());
|
||||||
errorChain(uiInit());
|
errorChain(uiInit());
|
||||||
@@ -53,8 +54,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
|||||||
consolePrint("Assertions real");
|
consolePrint("Assertions real");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
sceneSet(SCENE_TYPE_OVERWORLD);
|
sceneSet(SCENE_TYPE_INITIAL);
|
||||||
|
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -62,6 +62,8 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
|||||||
errorret_t engineUpdate(void) {
|
errorret_t engineUpdate(void) {
|
||||||
// Order here is important.
|
// Order here is important.
|
||||||
errorChain(networkUpdate());
|
errorChain(networkUpdate());
|
||||||
|
errorChain(saveUpdate());
|
||||||
|
autoSaveUpdate();
|
||||||
timeUpdate();
|
timeUpdate();
|
||||||
inputUpdate();
|
inputUpdate();
|
||||||
consoleUpdate();
|
consoleUpdate();
|
||||||
@@ -88,7 +90,7 @@ errorret_t engineDispose(void) {
|
|||||||
errorChain(uiDispose());
|
errorChain(uiDispose());
|
||||||
consoleDispose();
|
consoleDispose();
|
||||||
errorChain(displayDispose());
|
errorChain(displayDispose());
|
||||||
// errorChain(saveDispose());
|
errorChain(saveDispose());
|
||||||
errorChain(assetDispose());
|
errorChain(assetDispose());
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ input_t INPUT;
|
|||||||
|
|
||||||
errorret_t inputInit(void) {
|
errorret_t inputInit(void) {
|
||||||
memoryZero(&INPUT, sizeof(input_t));
|
memoryZero(&INPUT, sizeof(input_t));
|
||||||
INPUT.deadzone = INPUT_DEADZONE_DEFAULT;
|
|
||||||
|
|
||||||
for(uint8_t i = 0; i < INPUT_ACTION_COUNT; i++) {
|
for(uint8_t i = 0; i < INPUT_ACTION_COUNT; i++) {
|
||||||
INPUT.actions[i].action = (inputaction_t)i;
|
INPUT.actions[i].action = (inputaction_t)i;
|
||||||
|
|||||||
@@ -12,15 +12,11 @@
|
|||||||
|
|
||||||
#define INPUT_LISTENER_PRESSED_MAX 16
|
#define INPUT_LISTENER_PRESSED_MAX 16
|
||||||
#define INPUT_LISTENER_RELEASED_MAX INPUT_LISTENER_PRESSED_MAX
|
#define INPUT_LISTENER_RELEASED_MAX INPUT_LISTENER_PRESSED_MAX
|
||||||
#define INPUT_DEADZONE_DEFAULT 0.1f
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
inputactiondata_t actions[INPUT_ACTION_COUNT];
|
inputactiondata_t actions[INPUT_ACTION_COUNT];
|
||||||
|
|
||||||
inputplatform_t platform;
|
inputplatform_t platform;
|
||||||
|
|
||||||
/** User-configured gamepad axis deadzone (0.0f to 1.0f). */
|
|
||||||
float_t deadzone;
|
|
||||||
} input_t;
|
} input_t;
|
||||||
|
|
||||||
extern input_t INPUT;
|
extern input_t INPUT;
|
||||||
|
|||||||
@@ -7,16 +7,42 @@
|
|||||||
|
|
||||||
#include "localemanager.h"
|
#include "localemanager.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
|
#include "save/save.h"
|
||||||
|
|
||||||
localemanager_t LOCALE;
|
localemanager_t LOCALE;
|
||||||
|
|
||||||
|
const localeinfo_t * const LOCALE_LIST[LOCALE_LIST_COUNT] = {
|
||||||
|
&LOCALE_EN_US,
|
||||||
|
&LOCALE_JP_JP,
|
||||||
|
&LOCALE_ES_MX
|
||||||
|
};
|
||||||
|
|
||||||
errorret_t localeManagerInit() {
|
errorret_t localeManagerInit() {
|
||||||
memoryZero(&LOCALE, sizeof(localemanager_t));
|
memoryZero(&LOCALE, sizeof(localemanager_t));
|
||||||
errorChain(localeManagerSetLocale(&LOCALE_EN_US));
|
// saveInit() runs before this (see engine.c) and, on most platforms,
|
||||||
|
// has already loaded a persisted language choice into savemeta_t by
|
||||||
|
// this point - see localeManagerGetByIndex().
|
||||||
|
errorChain(localeManagerSetLocale(
|
||||||
|
localeManagerGetByIndex(saveGetMeta()->language)
|
||||||
|
));
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint8_t localeManagerGetIndex(const localeinfo_t *locale) {
|
||||||
|
assertNotNull(locale, "Locale cannot be NULL");
|
||||||
|
for(uint8_t i = 0; i < LOCALE_LIST_COUNT; i++) {
|
||||||
|
if(stringCompare(LOCALE_LIST[i]->file, locale->file) == 0) return i;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localeinfo_t * localeManagerGetByIndex(const uint8_t index) {
|
||||||
|
if(index >= LOCALE_LIST_COUNT) return LOCALE_LIST[0];
|
||||||
|
return LOCALE_LIST[index];
|
||||||
|
}
|
||||||
|
|
||||||
errorret_t localeManagerSetLocale(const localeinfo_t *locale) {
|
errorret_t localeManagerSetLocale(const localeinfo_t *locale) {
|
||||||
assertNotNull(locale, "Locale cannot be NULL");
|
assertNotNull(locale, "Locale cannot be NULL");
|
||||||
|
|
||||||
|
|||||||
@@ -18,21 +18,51 @@ typedef struct {
|
|||||||
|
|
||||||
extern localemanager_t LOCALE;
|
extern localemanager_t LOCALE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every locale the game supports, in a fixed, stable order - index into
|
||||||
|
* this array is what gets persisted as savemeta_t.language (see
|
||||||
|
* save/savemeta.h), so the order here must never change once shipped
|
||||||
|
* (only append new locales at the end).
|
||||||
|
*/
|
||||||
|
#define LOCALE_LIST_COUNT 3
|
||||||
|
extern const localeinfo_t * const LOCALE_LIST[LOCALE_LIST_COUNT];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the locale system.
|
* Initialize the locale system.
|
||||||
*
|
*
|
||||||
* @return An error code if a failure occurs.
|
* @return An error code if a failure occurs.
|
||||||
*/
|
*/
|
||||||
errorret_t localeManagerInit();
|
errorret_t localeManagerInit();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the current locale.
|
* Set the current locale.
|
||||||
*
|
*
|
||||||
* @param locale The locale to set.
|
* @param locale The locale to set.
|
||||||
* @return An error code if a failure occurs.
|
* @return An error code if a failure occurs.
|
||||||
*/
|
*/
|
||||||
errorret_t localeManagerSetLocale(const localeinfo_t *locale);
|
errorret_t localeManagerSetLocale(const localeinfo_t *locale);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the LOCALE_LIST index matching the given locale's file, by
|
||||||
|
* content rather than pointer identity (localeinfo_t instances are
|
||||||
|
* declared `static const` in a header, so the same locale gets a
|
||||||
|
* distinct pointer in every translation unit that references it).
|
||||||
|
*
|
||||||
|
* @param locale The locale to find.
|
||||||
|
* @return The matching index in LOCALE_LIST, or 0 if not found.
|
||||||
|
*/
|
||||||
|
uint8_t localeManagerGetIndex(const localeinfo_t *locale);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the locale at the given LOCALE_LIST index, clamping to index 0
|
||||||
|
* (LOCALE_EN_US) if out of range - e.g. a save file's persisted language
|
||||||
|
* index from a future build with more locales than this one supports.
|
||||||
|
*
|
||||||
|
* @param index The index to look up.
|
||||||
|
* @return The locale at that index, or LOCALE_LIST[0] if out of range.
|
||||||
|
*/
|
||||||
|
const localeinfo_t * localeManagerGetByIndex(const uint8_t index);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a localized string for the given message ID.
|
* Get a localized string for the given message ID.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "time/time.h"
|
#include "time/time.h"
|
||||||
#include "util/math.h"
|
#include "util/math.h"
|
||||||
|
#include "console/console.h"
|
||||||
#include "rpg/overworld/map.h"
|
#include "rpg/overworld/map.h"
|
||||||
#include "rpg/overworld/maparea.h"
|
#include "rpg/overworld/maparea.h"
|
||||||
#include "rpg/overworld/chunk.h"
|
#include "rpg/overworld/chunk.h"
|
||||||
@@ -292,7 +293,10 @@ void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
entity->chunkIndex = chunkIndex;
|
// Only claim the new chunk once actually inserted into one of its slots -
|
||||||
|
// otherwise entity->chunkIndex would point at a chunk that doesn't know
|
||||||
|
// about this entity, so it would never be torn down on unload.
|
||||||
|
entity->chunkIndex = 0xFF;
|
||||||
|
|
||||||
if(chunkIndex != 0xFF) {
|
if(chunkIndex != 0xFF) {
|
||||||
chunk_t *next = mapGetChunk(chunkIndex);
|
chunk_t *next = mapGetChunk(chunkIndex);
|
||||||
@@ -300,8 +304,16 @@ void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
|
|||||||
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
|
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
|
||||||
if(next->entities[i] != 0xFF) continue;
|
if(next->entities[i] != 0xFF) continue;
|
||||||
next->entities[i] = entity->id;
|
next->entities[i] = entity->id;
|
||||||
|
entity->chunkIndex = chunkIndex;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if(entity->chunkIndex != chunkIndex) {
|
||||||
|
consolePrint(
|
||||||
|
"entitySetChunk: chunk %u has no free entity slots, entity %u "
|
||||||
|
"left untracked",
|
||||||
|
chunkIndex, entity->id
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,7 +142,10 @@ uint8_t entityGetAvailable();
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Assigns an entity to a chunk, removing it from its current chunk first.
|
* Assigns an entity to a chunk, removing it from its current chunk first.
|
||||||
* Pass 0xFF as chunkIndex to detach the entity from any chunk.
|
* Pass 0xFF as chunkIndex to detach the entity from any chunk. If the
|
||||||
|
* target chunk has no free entity slots, the entity is left detached
|
||||||
|
* (chunkIndex 0xFF) rather than assigned to a chunk that isn't actually
|
||||||
|
* tracking it - entityUpdateChunk will keep retrying on subsequent moves.
|
||||||
*
|
*
|
||||||
* @param entity Pointer to the entity.
|
* @param entity Pointer to the entity.
|
||||||
* @param chunkIndex Index of the chunk to assign to, or 0xFF for none.
|
* @param chunkIndex Index of the chunk to assign to, or 0xFF for none.
|
||||||
|
|||||||
@@ -6,4 +6,5 @@
|
|||||||
# Sources
|
# Sources
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
|
globalitemstore.c
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "globalitemstore.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
|
||||||
|
bool_t globalItemStoreIsCollected(
|
||||||
|
const saveslot_t *file, const entityglobalid_t id
|
||||||
|
) {
|
||||||
|
assertNotNull(file, "Save slot cannot be NULL");
|
||||||
|
assertTrue(id < SAVE_GLOBAL_ITEM_COUNT_MAX, "Global item ID out of range");
|
||||||
|
return file->globalItemCollected[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
void globalItemStoreSetCollected(
|
||||||
|
saveslot_t *file, const entityglobalid_t id, const bool_t collected
|
||||||
|
) {
|
||||||
|
assertNotNull(file, "Save slot cannot be NULL");
|
||||||
|
assertTrue(id < SAVE_GLOBAL_ITEM_COUNT_MAX, "Global item ID out of range");
|
||||||
|
file->globalItemCollected[id] = collected;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* 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/saveslot.h"
|
||||||
|
#include "rpg/entity/entity.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the global entity with the given ID has already been
|
||||||
|
* marked collected in the given save slot's data - e.g. so a global item
|
||||||
|
* entity's init callback (see rpg/entity/global/entitygloballist.h) can
|
||||||
|
* skip spawning itself if the player already picked it up in a prior
|
||||||
|
* session, without needing to keep the entity itself alive to remember
|
||||||
|
* that (which would need render/collision special-casing - this doesn't).
|
||||||
|
*
|
||||||
|
* @param file The save slot to check.
|
||||||
|
* @param id The global entity ID to check.
|
||||||
|
* @return True if already marked collected.
|
||||||
|
*/
|
||||||
|
bool_t globalItemStoreIsCollected(
|
||||||
|
const saveslot_t *file, const entityglobalid_t id
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the global entity with the given ID as collected (or not) in the
|
||||||
|
* given save slot's data. Does not itself write the save to disk - call
|
||||||
|
* saveWriteSlot() separately once ready to persist it.
|
||||||
|
*
|
||||||
|
* @param file The save slot to write into.
|
||||||
|
* @param id The global entity ID to mark.
|
||||||
|
* @param collected The new collected state.
|
||||||
|
*/
|
||||||
|
void globalItemStoreSetCollected(
|
||||||
|
saveslot_t *file, const entityglobalid_t id, const bool_t collected
|
||||||
|
);
|
||||||
@@ -14,3 +14,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
tileshape.c
|
tileshape.c
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_subdirectory(global)
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
#define CHUNK_MESH_COUNT_MAX 10
|
#define CHUNK_MESH_COUNT_MAX 10
|
||||||
#define CHUNK_MESH_NAME_MAX 64
|
#define CHUNK_MESH_NAME_MAX 64
|
||||||
#define CHUNK_ENTITY_COUNT_MAX 10
|
#define CHUNK_ENTITY_COUNT_MAX 10
|
||||||
|
#define CHUNK_ENTITY_SPAWN_COUNT_MAX 8
|
||||||
|
#define CHUNK_AREA_COUNT_MAX 4
|
||||||
|
|
||||||
typedef struct assetentry_s assetentry_t;
|
typedef struct assetentry_s assetentry_t;
|
||||||
|
|
||||||
@@ -28,6 +30,13 @@ typedef struct chunk_s {
|
|||||||
assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX];
|
assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX];
|
||||||
|
|
||||||
uint8_t entities[CHUNK_ENTITY_COUNT_MAX];
|
uint8_t entities[CHUNK_ENTITY_COUNT_MAX];
|
||||||
|
|
||||||
|
// Map area IDs (into MAP_AREAS) spawned from this chunk's file data.
|
||||||
|
// Removed via mapAreaRemove when this chunk unloads, and re-added if it
|
||||||
|
// streams back in - unlike entities (tracked by current position via
|
||||||
|
// entities[] above), areas have no position-based ownership mechanism of
|
||||||
|
// their own, so the owning chunk must track and tear them down directly.
|
||||||
|
uint8_t areas[CHUNK_AREA_COUNT_MAX];
|
||||||
} chunk_t;
|
} chunk_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
# Sources
|
||||||
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
|
PUBLIC
|
||||||
|
)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "mapareaglobaldefs.h"
|
||||||
|
#include "mapareagloballist.h"
|
||||||
|
|
||||||
|
#define MAP_AREA_CALLBACK_LIST_COUNT ( \
|
||||||
|
sizeof(MAP_AREA_CALLBACK_LIST) / \
|
||||||
|
sizeof(MAP_AREA_CALLBACK_LIST[0]) \
|
||||||
|
)
|
||||||
|
|
||||||
|
//EOF
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "rpg/overworld/maparea.h"
|
||||||
|
|
||||||
|
#define MAP_AREA_CALLBACK(id) \
|
||||||
|
static void MAP_AREA_CALLBACK_##id(entity_t *entity, const uint8_t trigger)
|
||||||
|
|
||||||
|
#define MAP_AREA_CALLBACK_REF(id) \
|
||||||
|
MAP_AREA_CALLBACK_##id
|
||||||
|
|
||||||
|
//EOF
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "mapareaglobaldefs.h"
|
||||||
|
#include "console/console.h"
|
||||||
|
|
||||||
|
MAP_AREA_CALLBACK(1) {
|
||||||
|
consolePrint("mapAreaGlobalCallback 1: trigger=%u", trigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index 0 is reserved (not a valid callback ID) - see mapAreaAddGlobal.
|
||||||
|
static const mapareacallback_t MAP_AREA_CALLBACK_LIST[] = {
|
||||||
|
NULL,
|
||||||
|
MAP_AREA_CALLBACK_REF(1),
|
||||||
|
};
|
||||||
|
|
||||||
|
//EOF
|
||||||
+112
-40
@@ -14,9 +14,20 @@
|
|||||||
#include "event/event.h"
|
#include "event/event.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
#include "rpg/entity/global/entityglobal.h"
|
#include "rpg/entity/global/entityglobal.h"
|
||||||
|
#include "rpg/entity/item/entityitem.h"
|
||||||
|
#include "rpg/overworld/maparea.h"
|
||||||
|
|
||||||
map_t MAP;
|
map_t MAP;
|
||||||
|
|
||||||
|
// Clears chunk's mid-load slot, if it currently holds one.
|
||||||
|
static void mapChunkLoadingSlotClear(chunk_t *chunk) {
|
||||||
|
for(uint32_t i = 0; i < MAP_CHUNK_LOAD_CONCURRENCY; i++) {
|
||||||
|
if(MAP.loadingChunks[i] != chunk) continue;
|
||||||
|
MAP.loadingChunks[i] = NULL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
errorret_t mapInit() {
|
errorret_t mapInit() {
|
||||||
memoryZero(&MAP, sizeof(map_t));
|
memoryZero(&MAP, sizeof(map_t));
|
||||||
MAP.loaded = true;
|
MAP.loaded = true;
|
||||||
@@ -105,7 +116,7 @@ errorret_t mapDispose() {
|
|||||||
|
|
||||||
void mapChunkUnload(chunk_t *chunk) {
|
void mapChunkUnload(chunk_t *chunk) {
|
||||||
mapChunkLoadQueueRemove(chunk);
|
mapChunkLoadQueueRemove(chunk);
|
||||||
if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL;
|
mapChunkLoadingSlotClear(chunk);
|
||||||
|
|
||||||
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
|
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
|
||||||
if(chunk->entities[i] == 0xFF) continue;
|
if(chunk->entities[i] == 0xFF) continue;
|
||||||
@@ -119,6 +130,12 @@ void mapChunkUnload(chunk_t *chunk) {
|
|||||||
|
|
||||||
memorySet(chunk->entities, 0xFF, sizeof(chunk->entities));
|
memorySet(chunk->entities, 0xFF, sizeof(chunk->entities));
|
||||||
|
|
||||||
|
for(uint8_t i = 0; i < CHUNK_AREA_COUNT_MAX; i++) {
|
||||||
|
if(chunk->areas[i] == 0xFF) continue;
|
||||||
|
mapAreaRemove(chunk->areas[i]);
|
||||||
|
}
|
||||||
|
memorySet(chunk->areas, 0xFF, sizeof(chunk->areas));
|
||||||
|
|
||||||
if(chunk->dcfEntry != NULL) {
|
if(chunk->dcfEntry != NULL) {
|
||||||
eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded);
|
eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded);
|
||||||
eventUnsubscribe(&chunk->dcfEntry->onError, mapChunkLoadError);
|
eventUnsubscribe(&chunk->dcfEntry->onError, mapChunkLoadError);
|
||||||
@@ -139,7 +156,7 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
|
|||||||
if(!mapIsLoaded()) errorThrow("No map loaded");
|
if(!mapIsLoaded()) errorThrow("No map loaded");
|
||||||
|
|
||||||
mapChunkLoadQueueRemove(chunk);
|
mapChunkLoadQueueRemove(chunk);
|
||||||
if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL;
|
mapChunkLoadingSlotClear(chunk);
|
||||||
|
|
||||||
if(chunk->dcfEntry != NULL) {
|
if(chunk->dcfEntry != NULL) {
|
||||||
eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded);
|
eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded);
|
||||||
@@ -149,6 +166,16 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
memorySet(chunk->entities, 0xFF, sizeof(chunk->entities));
|
memorySet(chunk->entities, 0xFF, sizeof(chunk->entities));
|
||||||
|
|
||||||
|
// Normally already empty (mapChunkUnload clears these before a chunk is
|
||||||
|
// handed back for reuse), but cleared defensively here too so a reload
|
||||||
|
// never leaks a MAP_AREAS slot referenced by a stale owned area ID.
|
||||||
|
for(uint8_t i = 0; i < CHUNK_AREA_COUNT_MAX; i++) {
|
||||||
|
if(chunk->areas[i] == 0xFF) continue;
|
||||||
|
mapAreaRemove(chunk->areas[i]);
|
||||||
|
}
|
||||||
|
memorySet(chunk->areas, 0xFF, sizeof(chunk->areas));
|
||||||
|
|
||||||
chunk->meshCount = 0;
|
chunk->meshCount = 0;
|
||||||
|
|
||||||
char_t name[64];
|
char_t name[64];
|
||||||
@@ -178,44 +205,48 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void mapChunkLoadNext() {
|
void mapChunkLoadNext() {
|
||||||
if(MAP.loadingChunk != NULL) return;
|
for(uint32_t slot = 0; slot < MAP_CHUNK_LOAD_CONCURRENCY; slot++) {
|
||||||
if(MAP.loadQueueCount == 0) return;
|
if(MAP.loadingChunks[slot] != NULL) continue;
|
||||||
|
if(MAP.loadQueueCount == 0) return;
|
||||||
|
|
||||||
chunk_t *chunk = MAP.loadQueue[0];
|
chunk_t *chunk = MAP.loadQueue[0];
|
||||||
for(uint32_t i = 1; i < MAP.loadQueueCount; i++) {
|
for(uint32_t i = 1; i < MAP.loadQueueCount; i++) {
|
||||||
MAP.loadQueue[i - 1] = MAP.loadQueue[i];
|
MAP.loadQueue[i - 1] = MAP.loadQueue[i];
|
||||||
|
}
|
||||||
|
MAP.loadQueueCount--;
|
||||||
|
MAP.loadingChunks[slot] = chunk;
|
||||||
|
|
||||||
|
char_t name[64];
|
||||||
|
stringFormat(
|
||||||
|
name, sizeof(name),
|
||||||
|
"chunks/%d_%d_%d.dcf",
|
||||||
|
(int32_t)chunk->position.x,
|
||||||
|
(int32_t)chunk->position.y,
|
||||||
|
(int32_t)chunk->position.z
|
||||||
|
);
|
||||||
|
|
||||||
|
assetentry_t *entry = assetLock(name, ASSET_LOADER_TYPE_CHUNK, NULL);
|
||||||
|
assertNotNull(entry, "Failed to get chunk asset entry");
|
||||||
|
chunk->dcfEntry = entry;
|
||||||
|
|
||||||
|
// The entry may already be resident from an earlier load that hasn't
|
||||||
|
// been reaped yet - in that case onLoaded/onError already fired once
|
||||||
|
// and never will again, so handle the terminal state directly instead
|
||||||
|
// of waiting on a subscription that would never trigger. Both of these
|
||||||
|
// recurse back into mapChunkLoadNext once they clear this slot, so the
|
||||||
|
// outer loop just continues on to try filling the next one.
|
||||||
|
if(entry->state == ASSET_ENTRY_STATE_LOADED) {
|
||||||
|
mapChunkLoaded(entry, chunk);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(entry->state == ASSET_ENTRY_STATE_ERROR) {
|
||||||
|
mapChunkLoadError(entry, chunk);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
eventSubscribe(&entry->onLoaded, mapChunkLoaded, chunk);
|
||||||
|
eventSubscribe(&entry->onError, mapChunkLoadError, chunk);
|
||||||
}
|
}
|
||||||
MAP.loadQueueCount--;
|
|
||||||
MAP.loadingChunk = chunk;
|
|
||||||
|
|
||||||
char_t name[64];
|
|
||||||
stringFormat(
|
|
||||||
name, sizeof(name),
|
|
||||||
"chunks/%d_%d_%d.dcf",
|
|
||||||
(int32_t)chunk->position.x,
|
|
||||||
(int32_t)chunk->position.y,
|
|
||||||
(int32_t)chunk->position.z
|
|
||||||
);
|
|
||||||
|
|
||||||
assetentry_t *entry = assetLock(name, ASSET_LOADER_TYPE_CHUNK, NULL);
|
|
||||||
assertNotNull(entry, "Failed to get chunk asset entry");
|
|
||||||
chunk->dcfEntry = entry;
|
|
||||||
|
|
||||||
// The entry may already be resident from an earlier load that hasn't been
|
|
||||||
// reaped yet - in that case onLoaded/onError already fired once and never
|
|
||||||
// will again, so handle the terminal state directly instead of waiting on
|
|
||||||
// a subscription that would never trigger.
|
|
||||||
if(entry->state == ASSET_ENTRY_STATE_LOADED) {
|
|
||||||
mapChunkLoaded(entry, chunk);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(entry->state == ASSET_ENTRY_STATE_ERROR) {
|
|
||||||
mapChunkLoadError(entry, chunk);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
eventSubscribe(&entry->onLoaded, mapChunkLoaded, chunk);
|
|
||||||
eventSubscribe(&entry->onError, mapChunkLoadError, chunk);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void mapChunkLoadQueueRemove(chunk_t *chunk) {
|
void mapChunkLoadQueueRemove(chunk_t *chunk) {
|
||||||
@@ -372,7 +403,7 @@ void mapChunkLoadError(void *params, void *user) {
|
|||||||
chunk->dcfEntry = NULL;
|
chunk->dcfEntry = NULL;
|
||||||
memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles));
|
memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles));
|
||||||
|
|
||||||
if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL;
|
mapChunkLoadingSlotClear(chunk);
|
||||||
mapChunkLoadNext();
|
mapChunkLoadNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,6 +464,47 @@ void mapChunkLoaded(void *params, void *user) {
|
|||||||
// this chunk_t is displaying it. Released in mapChunkUnload instead.
|
// this chunk_t is displaying it. Released in mapChunkUnload instead.
|
||||||
chunk->meshCount = meshCount;
|
chunk->meshCount = meshCount;
|
||||||
|
|
||||||
if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL;
|
// Spawn entities declared by this chunk's file. Global entities are
|
||||||
|
// deduped by mapSpawnEntity itself (a persistent NPC that streams back
|
||||||
|
// in won't be duplicated); item entities have no persistent identity, so
|
||||||
|
// each reload spawns a fresh one - picking an item up and then leaving
|
||||||
|
// and re-entering its chunk will currently respawn it, since nothing
|
||||||
|
// tracks "already collected" across a chunk unload/reload yet.
|
||||||
|
for(uint8_t s = 0; s < entry->data.chunk.entitySpawnCount; s++) {
|
||||||
|
chunkentityspawn_t *spawn = &entry->data.chunk.entitySpawns[s];
|
||||||
|
if(spawn->kind == CHUNK_ENTITY_SPAWN_KIND_GLOBAL) {
|
||||||
|
mapSpawnEntity((entityglobalid_t)spawn->globalId, spawn->position);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t index = entityGetAvailable();
|
||||||
|
assertTrue(index != 0xFF, "No available entity slots for chunk spawn");
|
||||||
|
entity_t *itemEntity = &ENTITIES[index];
|
||||||
|
entityInit(itemEntity, ENTITY_TYPE_ITEM);
|
||||||
|
entityItemSet(
|
||||||
|
itemEntity, (itemid_t)spawn->itemId, spawn->itemQuantity
|
||||||
|
);
|
||||||
|
entityPositionSet(itemEntity, spawn->position);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn map areas declared by this chunk's file, tracked as owned by
|
||||||
|
// this chunk so mapChunkUnload can tear them down again.
|
||||||
|
for(uint8_t s = 0; s < entry->data.chunk.areaSpawnCount; s++) {
|
||||||
|
chunkareaspawn_t *area = &entry->data.chunk.areaSpawns[s];
|
||||||
|
uint8_t areaId = mapAreaAddGlobal(
|
||||||
|
area->min, area->max, area->callbackId, area->notify, area->trigger
|
||||||
|
);
|
||||||
|
|
||||||
|
uint8_t slot = 0xFF;
|
||||||
|
for(uint8_t i = 0; i < CHUNK_AREA_COUNT_MAX; i++) {
|
||||||
|
if(chunk->areas[i] != 0xFF) continue;
|
||||||
|
slot = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
assertTrue(slot != 0xFF, "Chunk has no free owned-area slots");
|
||||||
|
chunk->areas[slot] = areaId;
|
||||||
|
}
|
||||||
|
|
||||||
|
mapChunkLoadingSlotClear(chunk);
|
||||||
mapChunkLoadNext();
|
mapChunkLoadNext();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
|
|
||||||
#define MAP_FILE_PATH_MAX 128
|
#define MAP_FILE_PATH_MAX 128
|
||||||
|
|
||||||
|
// Number of chunks that may be mid-load (asset locked & awaiting onLoaded/
|
||||||
|
// onError) at the same time - everything past this waits in loadQueue.
|
||||||
|
#define MAP_CHUNK_LOAD_CONCURRENCY 2
|
||||||
|
|
||||||
typedef struct map_s {
|
typedef struct map_s {
|
||||||
bool_t loaded;
|
bool_t loaded;
|
||||||
|
|
||||||
@@ -19,11 +23,9 @@ typedef struct map_s {
|
|||||||
chunk_t *chunkOrder[MAP_CHUNK_COUNT];
|
chunk_t *chunkOrder[MAP_CHUNK_COUNT];
|
||||||
chunkpos_t chunkPosition;
|
chunkpos_t chunkPosition;
|
||||||
|
|
||||||
// Only one chunk may be mid-load (asset locked & awaiting onLoaded/
|
|
||||||
// onError) at any given time - everything else waits here in FIFO order.
|
|
||||||
chunk_t *loadQueue[MAP_CHUNK_COUNT];
|
chunk_t *loadQueue[MAP_CHUNK_COUNT];
|
||||||
uint32_t loadQueueCount;
|
uint32_t loadQueueCount;
|
||||||
chunk_t *loadingChunk;
|
chunk_t *loadingChunks[MAP_CHUNK_LOAD_CONCURRENCY];
|
||||||
} map_t;
|
} map_t;
|
||||||
|
|
||||||
extern map_t MAP;
|
extern map_t MAP;
|
||||||
@@ -80,9 +82,9 @@ void mapChunkUnload(chunk_t* chunk);
|
|||||||
errorret_t mapChunkLoad(chunk_t* chunk);
|
errorret_t mapChunkLoad(chunk_t* chunk);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts loading the next queued chunk, if no chunk is currently mid-load.
|
* Starts loading queued chunks until MAP_CHUNK_LOAD_CONCURRENCY chunks are
|
||||||
* Called after mapChunkLoad enqueues a chunk, and again after the
|
* mid-load. Called after mapChunkLoad enqueues a chunk, and again after a
|
||||||
* currently-loading chunk finishes (or is unloaded) to advance the queue.
|
* mid-load chunk finishes (or is unloaded) to advance the queue.
|
||||||
*/
|
*/
|
||||||
void mapChunkLoadNext();
|
void mapChunkLoadNext();
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "util/math.h"
|
#include "util/math.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "rpg/overworld/map.h"
|
#include "rpg/overworld/map.h"
|
||||||
|
#include "rpg/overworld/global/mapareaglobal.h"
|
||||||
|
|
||||||
maparea_t MAP_AREAS[MAP_AREA_COUNT_MAX];
|
maparea_t MAP_AREAS[MAP_AREA_COUNT_MAX];
|
||||||
|
|
||||||
@@ -148,3 +149,20 @@ void mapAreaCheckEntity(entity_t *entity) {
|
|||||||
|
|
||||||
void mapAreaNoopCallback(entity_t *entity, const uint8_t trigger) {
|
void mapAreaNoopCallback(entity_t *entity, const uint8_t trigger) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint8_t mapAreaAddGlobal(
|
||||||
|
const worldpos_t min,
|
||||||
|
const worldpos_t max,
|
||||||
|
const uint16_t callbackId,
|
||||||
|
const uint8_t notify,
|
||||||
|
const uint8_t trigger
|
||||||
|
) {
|
||||||
|
assertTrue(callbackId > 0, "Map area callback ID 0 is reserved");
|
||||||
|
assertTrue(
|
||||||
|
callbackId < MAP_AREA_CALLBACK_LIST_COUNT,
|
||||||
|
"Map area callback ID is out of range"
|
||||||
|
);
|
||||||
|
return mapAreaAdd(
|
||||||
|
min, max, MAP_AREA_CALLBACK_LIST[callbackId], notify, trigger
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -158,4 +158,29 @@ void mapAreaCheckEntity(entity_t *entity);
|
|||||||
* @param entity Pointer to the entity associated with the callback.
|
* @param entity Pointer to the entity associated with the callback.
|
||||||
* @param trigger Which MAP_TRIGGER_* condition invoked the callback.
|
* @param trigger Which MAP_TRIGGER_* condition invoked the callback.
|
||||||
*/
|
*/
|
||||||
void mapAreaNoopCallback(entity_t *entity, const uint8_t trigger);
|
void mapAreaNoopCallback(entity_t *entity, const uint8_t trigger);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a map area using a compiled-in callback referenced by ID (see
|
||||||
|
* MAP_AREA_CALLBACK_LIST in rpg/overworld/global/mapareagloballist.h),
|
||||||
|
* rather than a direct function pointer. This is what lets chunk file
|
||||||
|
* data - which can only reference compiled code by a small integer ID,
|
||||||
|
* not a function pointer - declare map areas.
|
||||||
|
*
|
||||||
|
* @param min The minimum world position of the area.
|
||||||
|
* @param max The maximum world position of the area.
|
||||||
|
* @param callbackId Index into MAP_AREA_CALLBACK_LIST. Must be greater
|
||||||
|
* than 0 (0 is reserved) and within range.
|
||||||
|
* @param notify Bitwise MAP_AREA_NOTIFY_* flags for which entity types
|
||||||
|
* should trigger the callback.
|
||||||
|
* @param trigger Bitwise MAP_TRIGGER_* flags for which conditions should
|
||||||
|
* invoke the callback.
|
||||||
|
* @returns The ID of the newly added map area.
|
||||||
|
*/
|
||||||
|
uint8_t mapAreaAddGlobal(
|
||||||
|
const worldpos_t min,
|
||||||
|
const worldpos_t max,
|
||||||
|
const uint16_t callbackId,
|
||||||
|
const uint8_t notify,
|
||||||
|
const uint8_t trigger
|
||||||
|
);
|
||||||
+39
-37
@@ -7,32 +7,41 @@
|
|||||||
|
|
||||||
#include "rpg.h"
|
#include "rpg.h"
|
||||||
#include "entity/entity.h"
|
#include "entity/entity.h"
|
||||||
#include "rpg/entity/npc/npcpath.h"
|
|
||||||
#include "rpg/entity/item/entityitem.h"
|
|
||||||
#include "rpg/overworld/map.h"
|
#include "rpg/overworld/map.h"
|
||||||
#include "rpg/overworld/maparea.h"
|
#include "rpg/overworld/maparea.h"
|
||||||
#include "rpg/cutscene/cutscenesystem.h"
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
#include "rpg/cutscene/scene/testcutscene.h"
|
|
||||||
#include "rpg/item/backpack.h"
|
#include "rpg/item/backpack.h"
|
||||||
#include "rpg/battle/party.h"
|
#include "rpg/battle/party.h"
|
||||||
#include "ui/rpg/textbox/uitextboxminilist.h"
|
#include "ui/rpg/textbox/uitextboxminilist.h"
|
||||||
#include "time/time.h"
|
#include "time/time.h"
|
||||||
#include "rpgcamera.h"
|
#include "rpgcamera.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "util/string.h"
|
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "console/console.h"
|
#include "save/save.h"
|
||||||
|
#include "save/autosave.h"
|
||||||
|
#include "error/error.h"
|
||||||
|
#include "scene/scene.h"
|
||||||
|
|
||||||
#include "ui/rpg/uiemoji.h"
|
#include "ui/rpg/uiemoji.h"
|
||||||
|
#include "rpg/story/storyflag.h"
|
||||||
void rpgTestAreaCallback(entity_t *entity, const uint8_t trigger) {
|
|
||||||
consolePrint("rpgTestAreaCallback: trigger=%u", trigger);
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t rpgInit(void) {
|
errorret_t rpgInit(void) {
|
||||||
memoryZero(ENTITIES, sizeof(ENTITIES));
|
memoryZero(ENTITIES, sizeof(ENTITIES));
|
||||||
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
|
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
|
||||||
|
|
||||||
|
saveslot_t *saveSlot = saveGetSlot(SAVE_ACTIVE_SLOT);
|
||||||
|
// saveInit() eagerly loads every slot from disk, but there's no
|
||||||
|
// continue-game flow yet - every boot is a fresh game regardless of
|
||||||
|
// what was found on disk, so force this false rather than let a stale
|
||||||
|
// "exists" from a real save skip storyFlagInitDefaults() below while
|
||||||
|
// everything else here still hardcodes new-game state.
|
||||||
|
saveSlot->exists = false;
|
||||||
|
|
||||||
|
// Must run before any code reads a story flag - stamps CSV-defined
|
||||||
|
// defaults onto the active save slot since it's now forced to look
|
||||||
|
// unloaded.
|
||||||
|
storyFlagInitDefaults(saveSlot);
|
||||||
|
|
||||||
backpackInit();
|
backpackInit();
|
||||||
partyInit();
|
partyInit();
|
||||||
cutsceneSystemInit();
|
cutsceneSystemInit();
|
||||||
@@ -43,7 +52,9 @@ errorret_t rpgInit(void) {
|
|||||||
// Init world
|
// Init world
|
||||||
errorChain(mapPositionSet((chunkpos_t){ 0, 0, 0 }));
|
errorChain(mapPositionSet((chunkpos_t){ 0, 0, 0 }));
|
||||||
|
|
||||||
// TEST: Create some entities.
|
// The player is the one entity that isn't sourced from map/chunk data -
|
||||||
|
// every other entity (NPCs, items) and map area comes from the loaded
|
||||||
|
// chunks' own spawn data (see rpg/overworld/map.c mapChunkLoaded).
|
||||||
uint8_t entIndex = entityGetAvailable();
|
uint8_t entIndex = entityGetAvailable();
|
||||||
assertTrue(entIndex != 0xFF, "No available entity slots!.");
|
assertTrue(entIndex != 0xFF, "No available entity slots!.");
|
||||||
entity_t *ent = &ENTITIES[entIndex];
|
entity_t *ent = &ENTITIES[entIndex];
|
||||||
@@ -52,31 +63,11 @@ errorret_t rpgInit(void) {
|
|||||||
RPG_CAMERA.mode = RPG_CAMERA_MODE_FOLLOW_ENTITY;
|
RPG_CAMERA.mode = RPG_CAMERA_MODE_FOLLOW_ENTITY;
|
||||||
RPG_CAMERA.followEntity.followEntityId = ent->id;
|
RPG_CAMERA.followEntity.followEntityId = ent->id;
|
||||||
|
|
||||||
mapSpawnEntity(3, (worldpos_t){ 8, 8, 1 });
|
// Starting inventory.
|
||||||
|
|
||||||
// TEST: Place an item entity.
|
|
||||||
uint8_t itemEntIndex = entityGetAvailable();
|
|
||||||
assertTrue(itemEntIndex != 0xFF, "No available entity slots!.");
|
|
||||||
entity_t *itemEnt = &ENTITIES[itemEntIndex];
|
|
||||||
entityInit(itemEnt, ENTITY_TYPE_ITEM);
|
|
||||||
entityItemSet(itemEnt, ITEM_ID_POTION, 1);
|
|
||||||
entityPositionSet(itemEnt, (worldpos_t){ 12, 2, 0 });
|
|
||||||
|
|
||||||
// TEST: Give the player a starting assortment of items.
|
|
||||||
backpackAdd(ITEM_ID_POTION, 5);
|
backpackAdd(ITEM_ID_POTION, 5);
|
||||||
backpackAdd(ITEM_ID_POTATO, 3);
|
backpackAdd(ITEM_ID_POTATO, 3);
|
||||||
backpackAdd(ITEM_ID_APPLE, 8);
|
backpackAdd(ITEM_ID_APPLE, 8);
|
||||||
|
|
||||||
// TEST: Create a test map area.
|
|
||||||
uint8_t areaIndex = mapAreaAdd(
|
|
||||||
(worldpos_t){ 11, 3, 0 },
|
|
||||||
(worldpos_t){ 16, 9, 10 },
|
|
||||||
rpgTestAreaCallback,
|
|
||||||
MAP_AREA_NOTIFY_ALL,
|
|
||||||
MAP_TRIGGER_ENTER | MAP_TRIGGER_EXIT
|
|
||||||
);
|
|
||||||
assertTrue(areaIndex != 0xFF, "No available map area slots!.");
|
|
||||||
|
|
||||||
// All Good!
|
// All Good!
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -88,15 +79,26 @@ errorret_t rpgUpdate(void) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// A failed autosave forces a no-save-device prompt (see autosave.h) -
|
||||||
|
// freeze the world entirely until the player resolves it, rather than
|
||||||
|
// letting NPCs/cutscenes/camera keep running behind a modal that's
|
||||||
|
// supposed to be blocking.
|
||||||
|
if(autoSaveIsBlocking()) errorOk();
|
||||||
|
|
||||||
// TODO: Do not update if the scene is not the map scene?
|
// TODO: Do not update if the scene is not the map scene?
|
||||||
errorChain(mapUpdate());
|
errorChain(mapUpdate());
|
||||||
|
|
||||||
// Update overworld ents.
|
// Update overworld ents - only while actually in the overworld. Entities
|
||||||
entity_t *ent = &ENTITIES[0];
|
// (the player among them) keep existing across scene changes, but their
|
||||||
do {
|
// input/movement/animation logic doesn't make sense to run mid-battle or
|
||||||
if(ent->type == ENTITY_TYPE_NULL) continue;
|
// before the initial scene has handed off to the overworld.
|
||||||
entityUpdate(ent);
|
if(SCENE.current == SCENE_TYPE_OVERWORLD) {
|
||||||
} while(++ent < &ENTITIES[ENTITY_COUNT]);
|
entity_t *ent = &ENTITIES[0];
|
||||||
|
do {
|
||||||
|
if(ent->type == ENTITY_TYPE_NULL) continue;
|
||||||
|
entityUpdate(ent);
|
||||||
|
} while(++ent < &ENTITIES[ENTITY_COUNT]);
|
||||||
|
}
|
||||||
|
|
||||||
cutsceneSystemUpdate();
|
cutsceneSystemUpdate();
|
||||||
errorChain(rpgCameraUpdate());
|
errorChain(rpgCameraUpdate());
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2026 Dominic Masters
|
* Copyright (c) 2026 Dominic Masters
|
||||||
*
|
*
|
||||||
* This software is released under the MIT License.
|
* This software is released under the MIT License.
|
||||||
* https://opensource.org/licenses/MIT
|
* https://opensource.org/licenses/MIT
|
||||||
*/
|
*/
|
||||||
@@ -10,5 +10,17 @@
|
|||||||
|
|
||||||
void storyFlagSet(const storyflag_t flag, const storyflagvalue_t value) {
|
void storyFlagSet(const storyflag_t flag, const storyflagvalue_t value) {
|
||||||
assertTrue(flag > STORY_FLAG_NULL && flag < STORY_FLAG_COUNT, "Bad Flag");
|
assertTrue(flag > STORY_FLAG_NULL && flag < STORY_FLAG_COUNT, "Bad Flag");
|
||||||
STORY_FLAG_VALUES[flag] = value;
|
saveGetSlot(SAVE_ACTIVE_SLOT)->storyFlags[flag] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void storyFlagInitDefaults(saveslot_t *file) {
|
||||||
|
assertNotNull(file, "Save slot cannot be NULL");
|
||||||
|
if(file->exists) return;
|
||||||
|
assertTrue(
|
||||||
|
STORY_FLAG_COUNT <= SAVE_STORY_FLAG_COUNT_MAX,
|
||||||
|
"Too many story flags for the save format - bump SAVE_STORY_FLAG_COUNT_MAX"
|
||||||
|
);
|
||||||
|
for(storyflag_t i = 0; i < STORY_FLAG_COUNT; i++) {
|
||||||
|
file->storyFlags[i] = STORY_FLAG_DEFAULTS[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,25 +1,40 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2026 Dominic Masters
|
* Copyright (c) 2026 Dominic Masters
|
||||||
*
|
*
|
||||||
* This software is released under the MIT License.
|
* This software is released under the MIT License.
|
||||||
* https://opensource.org/licenses/MIT
|
* https://opensource.org/licenses/MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "rpg/story/storyflagvalue.h"
|
#include "rpg/story/storyflagvalue.h"
|
||||||
|
#include "save/save.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the value of a story flag.
|
* Gets the value of a story flag. Reads directly from the active save
|
||||||
*
|
* slot (see SAVE_ACTIVE_SLOT) - flag values have no separate live copy.
|
||||||
|
*
|
||||||
* @param flag The story flag to get.
|
* @param flag The story flag to get.
|
||||||
* @return The value of the story flag.
|
* @return The value of the story flag.
|
||||||
*/
|
*/
|
||||||
#define storyFlagGet(flag) (STORY_FLAG_VALUES[(flag)])
|
#define storyFlagGet(flag) (saveGetSlot(SAVE_ACTIVE_SLOT)->storyFlags[(flag)])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the value of a story flag.
|
* Sets the value of a story flag, directly in the active save slot (see
|
||||||
*
|
* SAVE_ACTIVE_SLOT). Does not itself write the save to disk - call
|
||||||
|
* saveWriteSlot() separately once ready to persist it.
|
||||||
|
*
|
||||||
* @param flag The story flag to set.
|
* @param flag The story flag to set.
|
||||||
* @param value The value to set the story flag to.
|
* @param value The value to set the story flag to.
|
||||||
*/
|
*/
|
||||||
void storyFlagSet(const storyflag_t flag, const storyflagvalue_t value);
|
void storyFlagSet(const storyflag_t flag, const storyflagvalue_t value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stamps each story flag's CSV-defined default (STORY_FLAG_DEFAULTS) onto
|
||||||
|
* the given save slot, but only if it hasn't actually been loaded from
|
||||||
|
* disk yet (file->exists is false) - otherwise leaves already-played
|
||||||
|
* progress alone. Call once, e.g. during rpgInit(), before any gameplay
|
||||||
|
* code reads a story flag.
|
||||||
|
*
|
||||||
|
* @param file The save slot to stamp defaults onto.
|
||||||
|
*/
|
||||||
|
void storyFlagInitDefaults(saveslot_t *file);
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
PUBLIC
|
PUBLIC
|
||||||
save.c
|
save.c
|
||||||
savestream.c
|
savestream.c
|
||||||
|
autosave.c
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "autosave.h"
|
||||||
|
#include "save.h"
|
||||||
|
#include "error/error.h"
|
||||||
|
#include "ui/frame/initial/uiinitialnocard.h"
|
||||||
|
|
||||||
|
autosave_t AUTO_SAVE;
|
||||||
|
|
||||||
|
static void autoSaveNoCardResult(const bool_t retry, void *user) {
|
||||||
|
AUTO_SAVE.blocking = false;
|
||||||
|
|
||||||
|
// "Retry" just re-queues the write for the next tick - there's no way
|
||||||
|
// to force a re-probe of the hardware mid-session (see saveInit()'s doc
|
||||||
|
// comment), so this only actually helps for a transient failure.
|
||||||
|
if(retry) {
|
||||||
|
AUTO_SAVE.pending = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The player explicitly acknowledged there's no save device and chose
|
||||||
|
// to proceed anyway - sticks for the rest of the session, so future
|
||||||
|
// autoSaveUpdate() calls silently skip instead of prompting again.
|
||||||
|
saveMarkTemporary();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void autoSaveWriteComplete(errorret_t result, void *user) {
|
||||||
|
AUTO_SAVE.saving = false;
|
||||||
|
if(errorIsOk(result)) return;
|
||||||
|
|
||||||
|
errorCatch(errorPrint(result));
|
||||||
|
|
||||||
|
// The write failed - most likely no save medium is present right now
|
||||||
|
// (a GameCube with no memory card inserted, for example). Pause world
|
||||||
|
// simulation and force the same prompt the initial boot scene uses so
|
||||||
|
// the player can insert a card and retry, or explicitly accept a
|
||||||
|
// temporary, unsaved session.
|
||||||
|
AUTO_SAVE.blocking = true;
|
||||||
|
uiInitialNoCardOpen(autoSaveNoCardResult, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void autoSaveQueue(void) {
|
||||||
|
AUTO_SAVE.pending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void autoSaveUpdate(void) {
|
||||||
|
if(AUTO_SAVE.blocking) return;
|
||||||
|
if(!AUTO_SAVE.pending) return;
|
||||||
|
if(saveIsBusy()) return;
|
||||||
|
|
||||||
|
AUTO_SAVE.pending = false;
|
||||||
|
|
||||||
|
// Already opted out of saving for this session - nothing to do, and
|
||||||
|
// definitely don't reprompt every time an autosave is queued.
|
||||||
|
if(saveIsTemporary()) return;
|
||||||
|
|
||||||
|
AUTO_SAVE.saving = true;
|
||||||
|
saveWriteSlot(SAVE_ACTIVE_SLOT, autoSaveWriteComplete, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t autoSaveIsSaving(void) {
|
||||||
|
return AUTO_SAVE.saving;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t autoSaveIsBlocking(void) {
|
||||||
|
return AUTO_SAVE.blocking;
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
/** True from autoSaveQueue() until the queued write starts. */
|
||||||
|
bool_t pending;
|
||||||
|
/** True while the queued write is actually in flight. */
|
||||||
|
bool_t saving;
|
||||||
|
/**
|
||||||
|
* True while the forced no-save-device prompt is up after a queued
|
||||||
|
* write failed - see autoSaveUpdate(). World simulation must pause
|
||||||
|
* while this is true (see rpgUpdate()).
|
||||||
|
*/
|
||||||
|
bool_t blocking;
|
||||||
|
} autosave_t;
|
||||||
|
|
||||||
|
extern autosave_t AUTO_SAVE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queues an autosave to run on a future engine tick. Safe to call as
|
||||||
|
* often as needed (e.g. after every map transition or story event) -
|
||||||
|
* repeated calls before the queued save starts just collapse into one.
|
||||||
|
*/
|
||||||
|
void autoSaveQueue(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pumps the queued autosave: starts the write once nothing else is
|
||||||
|
* using the save system, and if that write fails (e.g. a GameCube with
|
||||||
|
* no memory card inserted), forces the same no-save-device prompt the
|
||||||
|
* initial boot scene uses and pauses world simulation until the player
|
||||||
|
* resolves it. Must be called every engine frame, after saveUpdate().
|
||||||
|
*/
|
||||||
|
void autoSaveUpdate(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True while a queued autosave's write is actually in progress. Intended
|
||||||
|
* for UI to show a "Saving" indicator.
|
||||||
|
*
|
||||||
|
* @return true if an autosave write is currently in flight.
|
||||||
|
*/
|
||||||
|
bool_t autoSaveIsSaving(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True while a failed autosave is forcing the no-save-device prompt and
|
||||||
|
* waiting on the player's decision. Intended for the world simulation to
|
||||||
|
* pause while this is true.
|
||||||
|
*
|
||||||
|
* @return true if an autosave is currently blocking on player input.
|
||||||
|
*/
|
||||||
|
bool_t autoSaveIsBlocking(void);
|
||||||
+124
-59
@@ -9,19 +9,60 @@
|
|||||||
#include "save/savestream.h"
|
#include "save/savestream.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
save_t SAVE;
|
save_t SAVE;
|
||||||
|
|
||||||
|
static void _saveEagerLoadComplete(errorret_t result, void *user) {
|
||||||
|
if(errorIsNotOk(result)) errorCatch(errorPrint(result));
|
||||||
|
}
|
||||||
|
|
||||||
errorret_t saveInit(void) {
|
errorret_t saveInit(void) {
|
||||||
memoryZero(&SAVE, sizeof(save_t));
|
memoryZero(&SAVE, sizeof(save_t));
|
||||||
|
SAVE.meta.deadzone = SAVE_META_DEADZONE_DEFAULT;
|
||||||
|
SAVE.meta.language = SAVE_META_LANGUAGE_DEFAULT;
|
||||||
|
|
||||||
#ifdef saveInitPlatform
|
#ifdef saveInitPlatform
|
||||||
errorChain(saveInitPlatform());
|
// A missing/unreachable save medium is expected, recoverable state,
|
||||||
|
// not a reason to fail booting the whole game - log it and carry on
|
||||||
|
// with SAVE.available false instead of chaining the error upward.
|
||||||
|
errorret_t result = saveInitPlatform();
|
||||||
|
SAVE.available = errorIsOk(result);
|
||||||
|
if(!SAVE.available) errorCatch(errorPrint(result));
|
||||||
|
#else
|
||||||
|
SAVE.available = false;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Eagerly pull meta + every slot into memory up front - cheap and
|
||||||
|
// harmless (nothing consumes loaded slot data automatically; there's no
|
||||||
|
// continue-game flow yet). PSP opts out entirely (see
|
||||||
|
// saveSkipEagerLoadPlatform) since its only read path is now the native
|
||||||
|
// savedata dialog, and running that on every boot would defeat the whole
|
||||||
|
// point of folding meta into it instead of a separate instant-write file.
|
||||||
|
#ifndef saveSkipEagerLoadPlatform
|
||||||
|
if(SAVE.available) {
|
||||||
|
saveLoadMeta(_saveEagerLoadComplete, NULL);
|
||||||
|
for(uint8_t i = 0; i < SAVE_SLOT_COUNT_MAX; i++) {
|
||||||
|
saveLoadSlot(i, _saveEagerLoadComplete, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool_t saveIsAvailable(void) {
|
||||||
|
return SAVE.available && !SAVE.temporary;
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveMarkTemporary(void) {
|
||||||
|
SAVE.temporary = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t saveIsTemporary(void) {
|
||||||
|
return SAVE.temporary;
|
||||||
|
}
|
||||||
|
|
||||||
errorret_t saveDispose(void) {
|
errorret_t saveDispose(void) {
|
||||||
#ifdef saveDisposePlatform
|
#ifdef saveDisposePlatform
|
||||||
errorChain(saveDisposePlatform());
|
errorChain(saveDisposePlatform());
|
||||||
@@ -29,80 +70,104 @@ errorret_t saveDispose(void) {
|
|||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveLoad(const uint8_t slot) {
|
errorret_t saveUpdate(void) {
|
||||||
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
|
#ifdef savePlatformUpdate
|
||||||
|
errorChain(savePlatformUpdate());
|
||||||
savefile_t *file = &SAVE.files[slot];
|
|
||||||
file->exists = false;
|
|
||||||
|
|
||||||
savestream_t stream;
|
|
||||||
memoryZero(&stream, sizeof(savestream_t));
|
|
||||||
|
|
||||||
#ifdef saveStreamOpenReadPlatform
|
|
||||||
errorChain(saveStreamOpenReadPlatform(&stream, slot));
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if(!stream.found) errorOk();
|
|
||||||
|
|
||||||
errorret_t ret = saveFileLoad(&stream, file);
|
|
||||||
|
|
||||||
#ifdef saveStreamClosePlatform
|
|
||||||
saveStreamClosePlatform(&stream);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if(errorIsNotOk(ret)) return ret;
|
|
||||||
|
|
||||||
errorChain(saveStreamVerifyChecksumImpl(&stream, slot));
|
|
||||||
|
|
||||||
file->exists = true;
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveWrite(const uint8_t slot) {
|
bool_t saveIsBusy(void) {
|
||||||
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
|
#ifdef saveIsBusyPlatform
|
||||||
|
return saveIsBusyPlatform();
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
savefile_t *file = &SAVE.files[slot];
|
void saveLoadSlot(const uint8_t slot, savecallback_t onComplete, void *user) {
|
||||||
|
assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX");
|
||||||
|
assertNotNull(onComplete, "onComplete cannot be NULL");
|
||||||
|
|
||||||
savestream_t stream;
|
SAVE.slots[slot].exists = false;
|
||||||
memoryZero(&stream, sizeof(savestream_t));
|
|
||||||
|
|
||||||
#ifdef saveStreamOpenWritePlatform
|
// Some platforms (PSP's native save dialog) can't complete within this
|
||||||
errorChain(saveStreamOpenWritePlatform(&stream, slot));
|
// call - they take over entirely and invoke onComplete later, from
|
||||||
|
// saveUpdate(), once their own multi-frame flow finishes. Those
|
||||||
|
// platforms never define the sync saveSlotLoadPlatform() at all, so the
|
||||||
|
// fallback below must live in the #else, not just after an early return.
|
||||||
|
#ifdef saveSlotAsyncLoadPlatform
|
||||||
|
saveSlotAsyncLoadPlatform(slot, onComplete, user);
|
||||||
|
#else
|
||||||
|
errorret_t ret = saveSlotLoadPlatform(slot, &SAVE.slots[slot]);
|
||||||
|
SAVE.available = errorIsOk(ret);
|
||||||
|
onComplete(ret, user);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveWriteSlot(const uint8_t slot, savecallback_t onComplete, void *user) {
|
||||||
|
assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX");
|
||||||
|
assertNotNull(onComplete, "onComplete cannot be NULL");
|
||||||
|
|
||||||
|
// See saveLoadSlot() - some platforms take over and complete later.
|
||||||
|
#ifdef saveSlotAsyncWritePlatform
|
||||||
|
saveSlotAsyncWritePlatform(slot, onComplete, user);
|
||||||
|
#else
|
||||||
|
errorret_t ret = saveSlotWritePlatform(slot, &SAVE.slots[slot]);
|
||||||
|
SAVE.available = errorIsOk(ret);
|
||||||
|
onComplete(ret, user);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t saveDeleteSlot(const uint8_t slot) {
|
||||||
|
assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX");
|
||||||
|
|
||||||
|
#ifdef saveSlotDeletePlatform
|
||||||
|
errorret_t deleteRet = saveSlotDeletePlatform(slot);
|
||||||
|
SAVE.available = errorIsOk(deleteRet);
|
||||||
|
errorChain(deleteRet);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
errorret_t ret = saveFileWrite(&stream, file);
|
SAVE.slots[slot].exists = false;
|
||||||
|
|
||||||
if(errorIsOk(ret)) {
|
|
||||||
ret = saveStreamFinalizeWriteImpl(&stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef saveStreamClosePlatform
|
|
||||||
saveStreamClosePlatform(&stream);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if(errorIsNotOk(ret)) return ret;
|
|
||||||
|
|
||||||
file->exists = true;
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveDelete(const uint8_t slot) {
|
bool_t saveSlotExists(const uint8_t slot) {
|
||||||
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
|
assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX");
|
||||||
|
return SAVE.slots[slot].exists;
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef saveDeletePlatform
|
saveslot_t * saveGetSlot(const uint8_t slot) {
|
||||||
errorChain(saveDeletePlatform(slot));
|
assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX");
|
||||||
|
return &SAVE.slots[slot];
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveLoadMeta(savecallback_t onComplete, void *user) {
|
||||||
|
assertNotNull(onComplete, "onComplete cannot be NULL");
|
||||||
|
|
||||||
|
SAVE.meta.exists = false;
|
||||||
|
|
||||||
|
#ifdef saveMetaAsyncLoadPlatform
|
||||||
|
saveMetaAsyncLoadPlatform(onComplete, user);
|
||||||
|
#else
|
||||||
|
errorret_t ret = saveMetaLoadPlatform(&SAVE.meta);
|
||||||
|
SAVE.available = errorIsOk(ret);
|
||||||
|
onComplete(ret, user);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
SAVE.files[slot].exists = false;
|
|
||||||
errorOk();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool_t saveExists(const uint8_t slot) {
|
void saveWriteMeta(savecallback_t onComplete, void *user) {
|
||||||
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
|
assertNotNull(onComplete, "onComplete cannot be NULL");
|
||||||
return SAVE.files[slot].exists;
|
|
||||||
|
#ifdef saveMetaAsyncWritePlatform
|
||||||
|
saveMetaAsyncWritePlatform(onComplete, user);
|
||||||
|
#else
|
||||||
|
errorret_t ret = saveMetaWritePlatform(&SAVE.meta);
|
||||||
|
SAVE.available = errorIsOk(ret);
|
||||||
|
onComplete(ret, user);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
savefile_t * saveGet(const uint8_t slot) {
|
savemeta_t * saveGetMeta(void) {
|
||||||
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
|
return &SAVE.meta;
|
||||||
return &SAVE.files[slot];
|
|
||||||
}
|
}
|
||||||
|
|||||||
+151
-24
@@ -7,25 +7,97 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "savefile.h"
|
#include "saveslot.h"
|
||||||
|
#include "savemeta.h"
|
||||||
#include "save/saveplatform.h"
|
#include "save/saveplatform.h"
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
/** Per-slot save file data; indexed 0 to SAVE_FILE_COUNT_MAX - 1. */
|
/** Per-slot save data; indexed 0 to SAVE_SLOT_COUNT_MAX - 1. */
|
||||||
savefile_t files[SAVE_FILE_COUNT_MAX];
|
saveslot_t slots[SAVE_SLOT_COUNT_MAX];
|
||||||
|
/** Device-wide preferences - see savemeta.h. */
|
||||||
|
savemeta_t meta;
|
||||||
/** Platform-specific save system state (paths, card handles, etc.). */
|
/** Platform-specific save system state (paths, card handles, etc.). */
|
||||||
saveplatform_t platform;
|
saveplatform_t platform;
|
||||||
|
/**
|
||||||
|
* True if the save medium (memory card/stick/disk) was reachable the
|
||||||
|
* last time it was checked - at saveInit(), and refreshed by every
|
||||||
|
* subsequent load/write attempt. Starting the game with no card/stick
|
||||||
|
* inserted, or one being removed mid-session, are both expected
|
||||||
|
* conditions here, not fatal errors - see saveIsAvailable().
|
||||||
|
*/
|
||||||
|
bool_t available;
|
||||||
|
/**
|
||||||
|
* True once the player has explicitly acknowledged there's no save
|
||||||
|
* device and chosen to continue anyway (see the initial scene's "no
|
||||||
|
* card" prompt) - sticky for the rest of the session, folded into
|
||||||
|
* saveIsAvailable() so every existing/future call site that already
|
||||||
|
* gates on that automatically refuses to save/load from here on. See
|
||||||
|
* saveMarkTemporary()/saveIsTemporary().
|
||||||
|
*/
|
||||||
|
bool_t temporary;
|
||||||
|
/**
|
||||||
|
* Scratch error state used by platforms whose save/load completes
|
||||||
|
* asynchronously (see saveIsBusy()) to construct a result to hand to a
|
||||||
|
* savecallback_t from inside saveUpdate(), rather than from a direct
|
||||||
|
* errorThrow() return - mirrors network_t.errorState for the same reason.
|
||||||
|
*/
|
||||||
|
errorstate_t errorState;
|
||||||
} save_t;
|
} save_t;
|
||||||
|
|
||||||
extern save_t SAVE;
|
extern save_t SAVE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the save system.
|
* Initializes the save system. Never fails the way saveWriteSlot()/
|
||||||
|
* saveWriteMeta() can - if the platform's save medium isn't reachable
|
||||||
|
* (e.g. no memory card/stick inserted), that's logged and reflected in
|
||||||
|
* saveIsAvailable() rather than treated as fatal, since the game should
|
||||||
|
* still be playable without save support.
|
||||||
*
|
*
|
||||||
* @return An error code if initialization fails.
|
* On most platforms, this also eagerly loads meta and every slot from
|
||||||
|
* disk immediately - cheap and harmless, since nothing consumes the
|
||||||
|
* loaded slot data automatically (there's no "continue game" flow yet;
|
||||||
|
* rpgInit() always hardcodes a fresh game regardless of what's loaded).
|
||||||
|
* PSP skips this (see saveSkipEagerLoadPlatform) - its only read path is
|
||||||
|
* the native sceUtilitySavedata dialog now that meta lives inside the
|
||||||
|
* same payload as the save slot, and running that multi-frame dialog on
|
||||||
|
* every single boot would reintroduce the exact UX problem a lightweight
|
||||||
|
* settings-only file used to avoid.
|
||||||
|
*
|
||||||
|
* @return An error code only for unexpected platform failures.
|
||||||
*/
|
*/
|
||||||
errorret_t saveInit(void);
|
errorret_t saveInit(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the save medium was reachable as of the last load/write
|
||||||
|
* attempt (or saveInit(), if none has been attempted yet), AND the
|
||||||
|
* session hasn't been marked temporary (see saveMarkTemporary()).
|
||||||
|
* Intended for UI to decide whether to offer saving/loading at all, or to
|
||||||
|
* explain why it isn't available right now - e.g. "No memory card
|
||||||
|
* inserted".
|
||||||
|
*
|
||||||
|
* @return true if saving/loading can be attempted right now.
|
||||||
|
*/
|
||||||
|
bool_t saveIsAvailable(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks this session as temporary - the player explicitly acknowledged
|
||||||
|
* there's no save device and chose to continue anyway. One-way: nothing
|
||||||
|
* currently re-probes the save medium mid-session (see saveInit()'s doc
|
||||||
|
* comment), so there's no legitimate way to un-stick this once set short
|
||||||
|
* of restarting the game.
|
||||||
|
*/
|
||||||
|
void saveMarkTemporary(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True once saveMarkTemporary() has been called this session. Distinct
|
||||||
|
* from saveIsAvailable() so UI can give a more specific message (e.g.
|
||||||
|
* "this session is temporary" rather than the generic "no save device
|
||||||
|
* found") once the player has already made that choice.
|
||||||
|
*
|
||||||
|
* @return true if this session has been marked temporary.
|
||||||
|
*/
|
||||||
|
bool_t saveIsTemporary(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disposes of the save system.
|
* Disposes of the save system.
|
||||||
*
|
*
|
||||||
@@ -34,41 +106,96 @@ errorret_t saveInit(void);
|
|||||||
errorret_t saveDispose(void);
|
errorret_t saveDispose(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the save file for a given slot from persistent storage.
|
* Updates the save manager, pumping any in-progress async save/load and
|
||||||
|
* dispatching its callback once complete. No-op on platforms where every
|
||||||
|
* operation always completes synchronously (see saveIsBusy()). Must be
|
||||||
|
* called every engine frame for platforms that need it (PSP's native save
|
||||||
|
* dialog spans multiple frames).
|
||||||
*
|
*
|
||||||
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1).
|
* @return An error code indicating success or failure.
|
||||||
* @return An error code if the load fails.
|
|
||||||
*/
|
*/
|
||||||
errorret_t saveLoad(const uint8_t slot);
|
errorret_t saveUpdate(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes the save file for a given slot to persistent storage.
|
* True while an async save/load is in progress (e.g. PSP's native save
|
||||||
|
* dialog is open). Calling any save/load function again while this is true
|
||||||
|
* is undefined behavior - wait for the previous call's callback first.
|
||||||
*
|
*
|
||||||
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1).
|
* @return True if a save/load request is currently in progress.
|
||||||
* @return An error code if the write fails.
|
|
||||||
*/
|
*/
|
||||||
errorret_t saveWrite(const uint8_t slot);
|
bool_t saveIsBusy(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the save file for a given slot from persistent storage.
|
* Loads the save slot for a given index from persistent storage. Slow/
|
||||||
|
* async on some platforms (PSP's native save dialog spans multiple
|
||||||
|
* frames) - on others (Linux, Dolphin) onComplete is invoked before this
|
||||||
|
* call returns. See saveIsBusy().
|
||||||
*
|
*
|
||||||
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1).
|
* @param slot The save slot index (0 to SAVE_SLOT_COUNT_MAX - 1).
|
||||||
|
* @param onComplete Callback invoked with the result once loading finishes.
|
||||||
|
* @param user User data passed through to onComplete.
|
||||||
|
*/
|
||||||
|
void saveLoadSlot(const uint8_t slot, savecallback_t onComplete, void *user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the save slot for a given index to persistent storage. Slow/
|
||||||
|
* async on some platforms (PSP's native save dialog spans multiple
|
||||||
|
* frames) - on others (Linux, Dolphin) onComplete is invoked before this
|
||||||
|
* call returns. See saveIsBusy().
|
||||||
|
*
|
||||||
|
* @param slot The save slot index (0 to SAVE_SLOT_COUNT_MAX - 1).
|
||||||
|
* @param onComplete Callback invoked with the result once writing finishes.
|
||||||
|
* @param user User data passed through to onComplete.
|
||||||
|
*/
|
||||||
|
void saveWriteSlot(const uint8_t slot, savecallback_t onComplete, void *user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the save slot for a given index from persistent storage.
|
||||||
|
*
|
||||||
|
* @param slot The save slot index (0 to SAVE_SLOT_COUNT_MAX - 1).
|
||||||
* @return An error code if the delete fails.
|
* @return An error code if the delete fails.
|
||||||
*/
|
*/
|
||||||
errorret_t saveDelete(const uint8_t slot);
|
errorret_t saveDeleteSlot(const uint8_t slot);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether a save file exists for a given slot.
|
* Checks whether a save slot has data for a given index.
|
||||||
*
|
*
|
||||||
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1).
|
* @param slot The save slot index (0 to SAVE_SLOT_COUNT_MAX - 1).
|
||||||
* @return true if a save file exists for the slot, false otherwise.
|
* @return true if the slot has data, false otherwise.
|
||||||
*/
|
*/
|
||||||
bool_t saveExists(const uint8_t slot);
|
bool_t saveSlotExists(const uint8_t slot);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a pointer to the save file data for a given slot.
|
* Gets a pointer to the save slot data for a given index.
|
||||||
*
|
*
|
||||||
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1).
|
* @param slot The save slot index (0 to SAVE_SLOT_COUNT_MAX - 1).
|
||||||
* @return A pointer to the savefile_t for the given slot.
|
* @return A pointer to the saveslot_t for the given slot.
|
||||||
*/
|
*/
|
||||||
savefile_t * saveGet(const uint8_t slot);
|
saveslot_t * saveGetSlot(const uint8_t slot);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads device-wide meta (preferences) from persistent storage. Callback-
|
||||||
|
* based on every platform, same as saveLoadSlot() - on PSP specifically,
|
||||||
|
* loading meta means loading the same combined savedata payload as the
|
||||||
|
* active slot, which is unavoidably async there.
|
||||||
|
*
|
||||||
|
* @param onComplete Callback invoked with the result once loading finishes.
|
||||||
|
* @param user User data passed through to onComplete.
|
||||||
|
*/
|
||||||
|
void saveLoadMeta(savecallback_t onComplete, void *user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes device-wide meta (preferences) to persistent storage.
|
||||||
|
*
|
||||||
|
* @param onComplete Callback invoked with the result once writing finishes.
|
||||||
|
* @param user User data passed through to onComplete.
|
||||||
|
*/
|
||||||
|
void saveWriteMeta(savecallback_t onComplete, void *user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a pointer to the live meta data. Modify fields directly, then call
|
||||||
|
* saveWriteMeta() to persist them.
|
||||||
|
*
|
||||||
|
* @return A pointer to the meta.
|
||||||
|
*/
|
||||||
|
savemeta_t * saveGetMeta(void);
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "dusk.h"
|
|
||||||
|
|
||||||
/** Save file format version. Increment on breaking change. */
|
|
||||||
#define SAVE_FILE_VERSION 1
|
|
||||||
|
|
||||||
/** Magic bytes that identify a Dusk save file. */
|
|
||||||
#define SAVE_FILE_HEADER "DSK"
|
|
||||||
|
|
||||||
/** Byte length of the magic header (excludes the null terminator). */
|
|
||||||
#define SAVE_FILE_HEADER_SIZE (sizeof(SAVE_FILE_HEADER) - 1)
|
|
||||||
|
|
||||||
/** Maximum number of independent save slots supported. */
|
|
||||||
#define SAVE_FILE_COUNT_MAX 3
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
/** Magic header bytes read from the file; must equal SAVE_FILE_HEADER. */
|
|
||||||
char_t header[SAVE_FILE_HEADER_SIZE];
|
|
||||||
/** Format version read from the file; used to branch on older layouts. */
|
|
||||||
uint32_t version;
|
|
||||||
/** Runtime flag - true if this slot was successfully loaded or written. */
|
|
||||||
bool_t exists;
|
|
||||||
} savefile_t;
|
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "dusk.h"
|
||||||
|
|
||||||
|
/** Save meta format version. Increment on breaking change. */
|
||||||
|
#define SAVE_META_VERSION 1
|
||||||
|
|
||||||
|
/** Magic bytes that identify a Dusk save meta blob. */
|
||||||
|
#define SAVE_META_HEADER "DSM"
|
||||||
|
|
||||||
|
/** Byte length of the magic header (excludes the null terminator). */
|
||||||
|
#define SAVE_META_HEADER_SIZE (sizeof(SAVE_META_HEADER) - 1)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default gamepad deadzone for meta that's never actually been loaded from
|
||||||
|
* disk yet (see saveInit(), which stamps this on first boot) - the save
|
||||||
|
* meta is the single source of truth for this value (see
|
||||||
|
* savemeta_t.deadzone); nothing else stores or defaults it.
|
||||||
|
*/
|
||||||
|
#define SAVE_META_DEADZONE_DEFAULT 0.1f
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default language index for meta that's never actually been loaded from
|
||||||
|
* disk yet - index 0 into LOCALE_LIST (see locale/localemanager.h), i.e.
|
||||||
|
* LOCALE_EN_US.
|
||||||
|
*/
|
||||||
|
#define SAVE_META_LANGUAGE_DEFAULT 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device/user-wide preferences, independent of any individual game save
|
||||||
|
* slot (see saveslot.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 blob; must equal SAVE_META_HEADER. */
|
||||||
|
char_t header[SAVE_META_HEADER_SIZE];
|
||||||
|
/** Format version read from the blob; used to branch on older layouts. */
|
||||||
|
uint32_t version;
|
||||||
|
/** Runtime flag - true if meta was successfully loaded or written. */
|
||||||
|
bool_t exists;
|
||||||
|
/**
|
||||||
|
* User-configured gamepad deadzone (0.0f-1.0f) - the save meta is the
|
||||||
|
* only place this lives; read it directly via saveGetMeta()->deadzone
|
||||||
|
* rather than caching it anywhere else.
|
||||||
|
*/
|
||||||
|
float_t deadzone;
|
||||||
|
/**
|
||||||
|
* Index into LOCALE_LIST (see locale/localemanager.h) for the player's
|
||||||
|
* preferred UI language - the save meta is the only place this lives;
|
||||||
|
* read it directly via saveGetMeta()->language rather than caching it
|
||||||
|
* anywhere else. Never reorder/remove entries from LOCALE_LIST, only
|
||||||
|
* append, since this index must keep meaning the same locale across
|
||||||
|
* versions.
|
||||||
|
*/
|
||||||
|
uint8_t language;
|
||||||
|
} savemeta_t;
|
||||||
@@ -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 "dusk.h"
|
||||||
|
|
||||||
|
/** Save slot format version. Increment on breaking change. */
|
||||||
|
#define SAVE_SLOT_VERSION 1
|
||||||
|
|
||||||
|
/** Magic bytes that identify a Dusk save slot. */
|
||||||
|
#define SAVE_SLOT_HEADER "DSK"
|
||||||
|
|
||||||
|
/** Byte length of the magic header (excludes the null terminator). */
|
||||||
|
#define SAVE_SLOT_HEADER_SIZE (sizeof(SAVE_SLOT_HEADER) - 1)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of independent save slots supported. Platform-overridable
|
||||||
|
* via a compiler define (not a header #ifndef alone, since this header is
|
||||||
|
* included before any platform header gets a chance to react) - see PSP's
|
||||||
|
* CMakeLists.txt, which overrides this to 1.
|
||||||
|
*/
|
||||||
|
#ifndef SAVE_SLOT_COUNT_MAX
|
||||||
|
#define SAVE_SLOT_COUNT_MAX 3
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The save slot actually used for gameplay right now - there's no slot
|
||||||
|
* select/multi-save UX yet (SAVE_SLOT_COUNT_MAX > 1 exists for later), so
|
||||||
|
* every part of the game that needs "the" save slot (the game menu's Save
|
||||||
|
* button, etc.) reads/writes this one slot.
|
||||||
|
*/
|
||||||
|
#define SAVE_ACTIVE_SLOT 0
|
||||||
|
|
||||||
|
/** Maximum length of a saved player name, including the null terminator. */
|
||||||
|
#define SAVE_PLAYER_NAME_MAX 32
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of global entities whose "collected" state can be
|
||||||
|
* tracked - see rpg/entity/global/globalitemstore.h. Bounded/fixed here
|
||||||
|
* rather than tied to ENTITY_GLOBAL_LIST_COUNT, since saveslot.h is a
|
||||||
|
* leaf header with no dependency on the entity system (and no reason to
|
||||||
|
* take one just for a size constant).
|
||||||
|
*/
|
||||||
|
#define SAVE_GLOBAL_ITEM_COUNT_MAX 64
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of story flags the save format can hold - see
|
||||||
|
* rpg/story/storyflag.h. Bounded/fixed here (with real headroom over the
|
||||||
|
* current flag count) rather than tied to STORY_FLAG_COUNT, since
|
||||||
|
* saveslot.h is a leaf header with no dependency on generated story
|
||||||
|
* content, matching SAVE_GLOBAL_ITEM_COUNT_MAX's reasoning.
|
||||||
|
*/
|
||||||
|
#define SAVE_STORY_FLAG_COUNT_MAX 128
|
||||||
|
|
||||||
|
/** Per-slot game progress - the state a "save file" traditionally means. */
|
||||||
|
typedef struct {
|
||||||
|
/** Magic header bytes read from the slot; must equal SAVE_SLOT_HEADER. */
|
||||||
|
char_t header[SAVE_SLOT_HEADER_SIZE];
|
||||||
|
/** Format version read from the slot; used to branch on older layouts. */
|
||||||
|
uint32_t version;
|
||||||
|
/** Runtime flag - true if this slot was successfully loaded or written. */
|
||||||
|
bool_t exists;
|
||||||
|
/** The player's saved name. */
|
||||||
|
char_t playerName[SAVE_PLAYER_NAME_MAX];
|
||||||
|
/** Per-global-ID "already collected" flags - see globalitemstore.h. */
|
||||||
|
bool_t globalItemCollected[SAVE_GLOBAL_ITEM_COUNT_MAX];
|
||||||
|
/**
|
||||||
|
* Story flag values, indexed by storyflag_t - the save slot is the only
|
||||||
|
* place these live; read/write via storyFlagGet()/storyFlagSet() (see
|
||||||
|
* rpg/story/storyflag.h), not directly.
|
||||||
|
*/
|
||||||
|
uint8_t storyFlags[SAVE_STORY_FLAG_COUNT_MAX];
|
||||||
|
} saveslot_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback invoked when an async saveWriteSlot()/saveLoadSlot()/
|
||||||
|
* saveWriteMeta()/saveLoadMeta() request completes. Declared here (rather
|
||||||
|
* than save.h) so platform save headers - which save.h's platform
|
||||||
|
* indirection pulls in before save.h finishes defining anything else - can
|
||||||
|
* reference it without a circular include.
|
||||||
|
*
|
||||||
|
* @param result Whether the request succeeded.
|
||||||
|
* @param user User data passed through from the original call.
|
||||||
|
*/
|
||||||
|
typedef void (*savecallback_t)(errorret_t result, void *user);
|
||||||
+86
-23
@@ -45,12 +45,23 @@ errorret_t saveStreamWriteBytesImpl(
|
|||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveStreamFinalizeWriteImpl(savestream_t *stream) {
|
errorret_t saveStreamTellImpl(savestream_t *stream, size_t *out) {
|
||||||
|
#ifdef saveStreamTellPlatform
|
||||||
|
errorChain(saveStreamTellPlatform(stream, out));
|
||||||
|
#else
|
||||||
|
*out = 0;
|
||||||
|
#endif
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t saveStreamFinalizeWriteImpl(
|
||||||
|
savestream_t *stream, const size_t headerPosition, const size_t headerSize
|
||||||
|
) {
|
||||||
uint32_t finalCRC = cryptCRC32End(stream->checksum);
|
uint32_t finalCRC = cryptCRC32End(stream->checksum);
|
||||||
uint32_t leChecksum = endianLittleToHost32(finalCRC);
|
uint32_t leChecksum = endianLittleToHost32(finalCRC);
|
||||||
|
|
||||||
#ifdef saveStreamSeekPlatform
|
#ifdef saveStreamSeekPlatform
|
||||||
errorChain(saveStreamSeekPlatform(stream, SAVE_FILE_HEADER_SIZE));
|
errorChain(saveStreamSeekPlatform(stream, headerPosition + headerSize));
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
errorChain(saveStreamWriteBytesRawImpl(
|
errorChain(saveStreamWriteBytesRawImpl(
|
||||||
@@ -60,27 +71,25 @@ errorret_t saveStreamFinalizeWriteImpl(savestream_t *stream) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveStreamVerifyChecksumImpl(
|
errorret_t saveStreamVerifyChecksumImpl(
|
||||||
savestream_t *stream, const uint8_t slot
|
savestream_t *stream, const char_t *sectionLabel
|
||||||
) {
|
) {
|
||||||
uint32_t computed = cryptCRC32End(stream->checksum);
|
uint32_t computed = cryptCRC32End(stream->checksum);
|
||||||
if(computed != stream->expectedChecksum) {
|
if(computed != stream->expectedChecksum) {
|
||||||
errorThrow("Save slot %u has invalid checksum", (uint32_t)slot);
|
errorThrow("%s has invalid checksum", sectionLabel);
|
||||||
}
|
}
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
errorret_t saveStreamReadHeaderImpl(
|
errorret_t saveStreamReadHeaderImpl(
|
||||||
savestream_t *stream, char_t header[SAVE_FILE_HEADER_SIZE]
|
savestream_t *stream, char_t *header, const char_t *expectedHeader,
|
||||||
|
const size_t headerSize
|
||||||
) {
|
) {
|
||||||
errorChain(saveStreamReadBytesRawImpl(stream, header, SAVE_FILE_HEADER_SIZE));
|
errorChain(saveStreamReadBytesRawImpl(stream, header, headerSize));
|
||||||
|
|
||||||
if(
|
for(size_t i = 0; i < headerSize; i++) {
|
||||||
header[0] != SAVE_FILE_HEADER[0] ||
|
if(header[i] != expectedHeader[i]) {
|
||||||
header[1] != SAVE_FILE_HEADER[1] ||
|
errorThrow("Save data has invalid header");
|
||||||
header[2] != SAVE_FILE_HEADER[2]
|
}
|
||||||
) {
|
|
||||||
errorThrow("Save file has invalid header");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t leChecksum;
|
uint32_t leChecksum;
|
||||||
@@ -91,11 +100,9 @@ errorret_t saveStreamReadHeaderImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveStreamWriteHeaderImpl(
|
errorret_t saveStreamWriteHeaderImpl(
|
||||||
savestream_t *stream, const char_t header[SAVE_FILE_HEADER_SIZE]
|
savestream_t *stream, const char_t *header, const size_t headerSize
|
||||||
) {
|
) {
|
||||||
errorChain(saveStreamWriteBytesRawImpl(
|
errorChain(saveStreamWriteBytesRawImpl(stream, header, headerSize));
|
||||||
stream, header, SAVE_FILE_HEADER_SIZE
|
|
||||||
));
|
|
||||||
|
|
||||||
uint32_t placeholder = 0;
|
uint32_t placeholder = 0;
|
||||||
errorChain(saveStreamWriteBytesRawImpl(
|
errorChain(saveStreamWriteBytesRawImpl(
|
||||||
@@ -327,14 +334,70 @@ errorret_t saveStreamWriteDateImpl(
|
|||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveFileLoad(savestream_t *stream, savefile_t *file) {
|
errorret_t saveMetaSerializeRead(savestream_t *stream, savemeta_t *meta) {
|
||||||
saveFileReadHeader(stream, file->header);
|
saveFileReadHeader(
|
||||||
saveFileReadVersion(stream, &file->version);
|
stream, meta->header, SAVE_META_HEADER, SAVE_META_HEADER_SIZE
|
||||||
|
);
|
||||||
|
saveFileReadVersion(stream, &meta->version);
|
||||||
|
saveFileReadFloat(stream, &meta->deadzone);
|
||||||
|
saveFileReadUInt8(stream, &meta->language);
|
||||||
|
errorChain(saveStreamVerifyChecksumImpl(stream, "Save meta"));
|
||||||
|
meta->exists = true;
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveFileWrite(savestream_t *stream, savefile_t *file) {
|
errorret_t saveMetaSerializeWrite(savestream_t *stream, savemeta_t *meta) {
|
||||||
saveFileWriteHeader(stream, file->header);
|
memoryCopy(meta->header, SAVE_META_HEADER, SAVE_META_HEADER_SIZE);
|
||||||
saveFileWriteVersion(stream, &file->version);
|
meta->version = SAVE_META_VERSION;
|
||||||
|
|
||||||
|
size_t headerPosition;
|
||||||
|
errorChain(saveStreamTellImpl(stream, &headerPosition));
|
||||||
|
saveFileWriteHeader(stream, meta->header, SAVE_META_HEADER_SIZE);
|
||||||
|
saveFileWriteVersion(stream, &meta->version);
|
||||||
|
saveFileWriteFloat(stream, &meta->deadzone);
|
||||||
|
saveFileWriteUInt8(stream, &meta->language);
|
||||||
|
errorChain(saveStreamFinalizeWriteImpl(
|
||||||
|
stream, headerPosition, SAVE_META_HEADER_SIZE
|
||||||
|
));
|
||||||
|
meta->exists = true;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t saveSlotSerializeRead(savestream_t *stream, saveslot_t *slot) {
|
||||||
|
saveFileReadHeader(
|
||||||
|
stream, slot->header, SAVE_SLOT_HEADER, SAVE_SLOT_HEADER_SIZE
|
||||||
|
);
|
||||||
|
saveFileReadVersion(stream, &slot->version);
|
||||||
|
saveFileReadString(stream, slot->playerName, SAVE_PLAYER_NAME_MAX);
|
||||||
|
for(size_t i = 0; i < SAVE_GLOBAL_ITEM_COUNT_MAX; i++) {
|
||||||
|
saveFileReadBool(stream, &slot->globalItemCollected[i]);
|
||||||
|
}
|
||||||
|
for(size_t i = 0; i < SAVE_STORY_FLAG_COUNT_MAX; i++) {
|
||||||
|
saveFileReadUInt8(stream, &slot->storyFlags[i]);
|
||||||
|
}
|
||||||
|
errorChain(saveStreamVerifyChecksumImpl(stream, "Save slot"));
|
||||||
|
slot->exists = true;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t saveSlotSerializeWrite(savestream_t *stream, saveslot_t *slot) {
|
||||||
|
memoryCopy(slot->header, SAVE_SLOT_HEADER, SAVE_SLOT_HEADER_SIZE);
|
||||||
|
slot->version = SAVE_SLOT_VERSION;
|
||||||
|
|
||||||
|
size_t headerPosition;
|
||||||
|
errorChain(saveStreamTellImpl(stream, &headerPosition));
|
||||||
|
saveFileWriteHeader(stream, slot->header, SAVE_SLOT_HEADER_SIZE);
|
||||||
|
saveFileWriteVersion(stream, &slot->version);
|
||||||
|
saveFileWriteString(stream, slot->playerName, SAVE_PLAYER_NAME_MAX);
|
||||||
|
for(size_t i = 0; i < SAVE_GLOBAL_ITEM_COUNT_MAX; i++) {
|
||||||
|
saveFileWriteBool(stream, &slot->globalItemCollected[i]);
|
||||||
|
}
|
||||||
|
for(size_t i = 0; i < SAVE_STORY_FLAG_COUNT_MAX; i++) {
|
||||||
|
saveFileWriteUInt8(stream, &slot->storyFlags[i]);
|
||||||
|
}
|
||||||
|
errorChain(saveStreamFinalizeWriteImpl(
|
||||||
|
stream, headerPosition, SAVE_SLOT_HEADER_SIZE
|
||||||
|
));
|
||||||
|
slot->exists = true;
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|||||||
+74
-31
@@ -7,7 +7,8 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "savefile.h"
|
#include "saveslot.h"
|
||||||
|
#include "savemeta.h"
|
||||||
#include "save/saveplatform.h"
|
#include "save/saveplatform.h"
|
||||||
#include "time/timeepoch.h"
|
#include "time/timeepoch.h"
|
||||||
|
|
||||||
@@ -67,49 +68,72 @@ errorret_t saveStreamWriteBytesImpl(
|
|||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finalizes a write stream: computes the final CRC32, seeks to the
|
* Gets the current read/write position within the stream. Used to capture
|
||||||
* checksum field in the header, and writes it in little-endian order.
|
* a section's start position before writing its header, so its checksum
|
||||||
|
* can be backfilled at the right offset once the section's body is known -
|
||||||
|
* required now that a single stream can hold multiple self-contained
|
||||||
|
* sections back-to-back (meta + N save slots), not just one.
|
||||||
|
*
|
||||||
|
* @param stream Active stream.
|
||||||
|
* @param out Receives the current position.
|
||||||
|
* @return An error if the platform can't report a position.
|
||||||
|
*/
|
||||||
|
errorret_t saveStreamTellImpl(savestream_t *stream, size_t *out);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finalizes a write stream: computes the final CRC32, seeks back to the
|
||||||
|
* checksum field just after this section's header, and writes it in
|
||||||
|
* little-endian order.
|
||||||
*
|
*
|
||||||
* @param stream Active write stream.
|
* @param stream Active write stream.
|
||||||
|
* @param headerPosition Byte offset where this section's header started
|
||||||
|
* (see saveStreamTellImpl), captured before writing the header.
|
||||||
|
* @param headerSize Byte length of this section's magic header.
|
||||||
* @return An error if the seek or write fails.
|
* @return An error if the seek or write fails.
|
||||||
*/
|
*/
|
||||||
errorret_t saveStreamFinalizeWriteImpl(savestream_t *stream);
|
errorret_t saveStreamFinalizeWriteImpl(
|
||||||
|
savestream_t *stream, const size_t headerPosition, const size_t headerSize
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies that the CRC32 accumulated during loading matches the value
|
* Verifies that the CRC32 accumulated during loading matches the value
|
||||||
* stored in the file header.
|
* stored in this section's header.
|
||||||
*
|
*
|
||||||
* @param stream Active read stream (loading must be complete).
|
* @param stream Active read stream (loading must be complete).
|
||||||
* @param slot Slot index used in the error message on mismatch.
|
* @param sectionLabel Human-readable label used in the error message on
|
||||||
|
* mismatch (e.g. "save meta", "save slot").
|
||||||
* @return An error if the checksum does not match.
|
* @return An error if the checksum does not match.
|
||||||
*/
|
*/
|
||||||
errorret_t saveStreamVerifyChecksumImpl(
|
errorret_t saveStreamVerifyChecksumImpl(
|
||||||
savestream_t *stream, const uint8_t slot
|
savestream_t *stream, const char_t *sectionLabel
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads and validates the magic header, then reads the stored CRC32 and
|
* Reads and validates a section's magic header, then reads its stored
|
||||||
* resets the running accumulator.
|
* CRC32 and resets the running accumulator.
|
||||||
*
|
*
|
||||||
* @param stream Active read stream.
|
* @param stream Active read stream.
|
||||||
* @param header Buffer of SAVE_FILE_HEADER_SIZE bytes to receive the header.
|
* @param header Buffer of headerSize bytes to receive the header.
|
||||||
|
* @param expectedHeader The magic bytes this section must match.
|
||||||
|
* @param headerSize Byte length of the magic header.
|
||||||
* @return An error if the header is missing or invalid.
|
* @return An error if the header is missing or invalid.
|
||||||
*/
|
*/
|
||||||
errorret_t saveStreamReadHeaderImpl(
|
errorret_t saveStreamReadHeaderImpl(
|
||||||
savestream_t *stream, char_t header[SAVE_FILE_HEADER_SIZE]
|
savestream_t *stream, char_t *header, const char_t *expectedHeader,
|
||||||
|
const size_t headerSize
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes the magic header and a zero CRC32 placeholder, then resets the
|
* Writes a section's magic header and a zero CRC32 placeholder, then
|
||||||
* running accumulator.
|
* resets the running accumulator.
|
||||||
*
|
*
|
||||||
* @param stream Active write stream.
|
* @param stream Active write stream.
|
||||||
* @param header Buffer of SAVE_FILE_HEADER_SIZE bytes to write.
|
* @param header Buffer of headerSize bytes to write.
|
||||||
|
* @param headerSize Byte length of the magic header.
|
||||||
* @return An error if the write fails.
|
* @return An error if the write fails.
|
||||||
*/
|
*/
|
||||||
errorret_t saveStreamWriteHeaderImpl(
|
errorret_t saveStreamWriteHeaderImpl(
|
||||||
savestream_t *stream,
|
savestream_t *stream, const char_t *header, const size_t headerSize
|
||||||
const char_t header[SAVE_FILE_HEADER_SIZE]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -377,29 +401,49 @@ errorret_t saveStreamWriteDateImpl(
|
|||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the contents of a save slot from the stream into the save file
|
* Reads a self-contained save meta section (header, version, fields,
|
||||||
* struct. Use saveFileRead* macros to deserialize fields one at a time.
|
* checksum verification) from the stream.
|
||||||
*
|
*
|
||||||
* @param stream Active read stream for this slot.
|
* @param stream Active read stream, positioned at the section's start.
|
||||||
* @param file Save file struct to populate.
|
* @param meta Meta struct to populate.
|
||||||
* @return An error code if loading fails.
|
* @return An error code if loading fails.
|
||||||
*/
|
*/
|
||||||
errorret_t saveFileLoad(savestream_t *stream, savefile_t *file);
|
errorret_t saveMetaSerializeRead(savestream_t *stream, savemeta_t *meta);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes the contents of the save file struct into the stream.
|
* Writes a self-contained save meta section (header, version, fields,
|
||||||
* Use saveFileWrite* macros to serialize fields one at a time.
|
* checksum) to the stream.
|
||||||
*
|
*
|
||||||
* @param stream Active write stream for this slot.
|
* @param stream Active write stream, positioned at the section's start.
|
||||||
* @param file Save file struct to serialize.
|
* @param meta Meta struct to serialize.
|
||||||
* @return An error code if writing fails.
|
* @return An error code if writing fails.
|
||||||
*/
|
*/
|
||||||
errorret_t saveFileWrite(savestream_t *stream, savefile_t *file);
|
errorret_t saveMetaSerializeWrite(savestream_t *stream, savemeta_t *meta);
|
||||||
|
|
||||||
#define saveFileReadHeader(stream, header) \
|
/**
|
||||||
errorChain(saveStreamReadHeaderImpl(stream, header))
|
* Reads a self-contained save slot section (header, version, fields,
|
||||||
#define saveFileWriteHeader(stream, header) \
|
* checksum verification) from the stream.
|
||||||
errorChain(saveStreamWriteHeaderImpl(stream, header))
|
*
|
||||||
|
* @param stream Active read stream, positioned at the section's start.
|
||||||
|
* @param slot Slot struct to populate.
|
||||||
|
* @return An error code if loading fails.
|
||||||
|
*/
|
||||||
|
errorret_t saveSlotSerializeRead(savestream_t *stream, saveslot_t *slot);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a self-contained save slot section (header, version, fields,
|
||||||
|
* checksum) to the stream.
|
||||||
|
*
|
||||||
|
* @param stream Active write stream, positioned at the section's start.
|
||||||
|
* @param slot Slot struct to serialize.
|
||||||
|
* @return An error code if writing fails.
|
||||||
|
*/
|
||||||
|
errorret_t saveSlotSerializeWrite(savestream_t *stream, saveslot_t *slot);
|
||||||
|
|
||||||
|
#define saveFileReadHeader(stream, header, expected, size) \
|
||||||
|
errorChain(saveStreamReadHeaderImpl(stream, header, expected, size))
|
||||||
|
#define saveFileWriteHeader(stream, header, size) \
|
||||||
|
errorChain(saveStreamWriteHeaderImpl(stream, header, size))
|
||||||
|
|
||||||
#define saveFileReadVersion(stream, out) \
|
#define saveFileReadVersion(stream, out) \
|
||||||
errorChain(saveStreamReadVersionImpl(stream, out))
|
errorChain(saveStreamReadVersionImpl(stream, out))
|
||||||
@@ -465,4 +509,3 @@ errorret_t saveFileWrite(savestream_t *stream, savefile_t *file);
|
|||||||
errorChain(saveStreamReadDateImpl(stream, out))
|
errorChain(saveStreamReadDateImpl(stream, out))
|
||||||
#define saveFileWriteDate(stream, input) \
|
#define saveFileWriteDate(stream, input) \
|
||||||
errorChain(saveStreamWriteDateImpl(stream, input))
|
errorChain(saveStreamWriteDateImpl(stream, input))
|
||||||
|
|
||||||
|
|||||||
@@ -10,5 +10,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Subdirs
|
# Subdirs
|
||||||
|
add_subdirectory(initial)
|
||||||
add_subdirectory(overworld)
|
add_subdirectory(overworld)
|
||||||
add_subdirectory(battle)
|
add_subdirectory(battle)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
|
PUBLIC
|
||||||
|
sceneinitial.c
|
||||||
|
)
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "scene/scene.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "error/error.h"
|
||||||
|
#include "save/save.h"
|
||||||
|
#include "ui/frame/initial/uiinitialnocard.h"
|
||||||
|
#include "ui/frame/initial/uiinitialcreatesave.h"
|
||||||
|
|
||||||
|
static void sceneInitialCheckSave(void);
|
||||||
|
|
||||||
|
static void sceneInitialCreateSaveWriteComplete(errorret_t result, void *user) {
|
||||||
|
if(errorIsNotOk(result)) errorCatch(errorPrint(result));
|
||||||
|
sceneSet(SCENE_TYPE_OVERWORLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sceneInitialCreateSaveResult(const bool_t create, void *user) {
|
||||||
|
if(!create) {
|
||||||
|
sceneSet(SCENE_TYPE_OVERWORLD);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveWriteSlot(SAVE_ACTIVE_SLOT, sceneInitialCreateSaveWriteComplete, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sceneInitialNoCardResult(const bool_t retry, void *user) {
|
||||||
|
if(retry) {
|
||||||
|
sceneInitialCheckSave();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The player explicitly acknowledged there's no save device and chose
|
||||||
|
// to proceed anyway - stick with that for the rest of the session (see
|
||||||
|
// saveMarkTemporary()'s doc comment for why this can't be un-set later).
|
||||||
|
saveMarkTemporary();
|
||||||
|
sceneSet(SCENE_TYPE_OVERWORLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sceneInitialLoadComplete(errorret_t result, void *user) {
|
||||||
|
if(errorIsNotOk(result) || !saveIsAvailable()) {
|
||||||
|
errorCatch(result);
|
||||||
|
uiInitialNoCardOpen(sceneInitialNoCardResult, NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(saveSlotExists(SAVE_ACTIVE_SLOT)) {
|
||||||
|
sceneSet(SCENE_TYPE_OVERWORLD);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uiInitialCreateSaveOpen(sceneInitialCreateSaveResult, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sceneInitialCheckSave(void) {
|
||||||
|
if(!saveIsAvailable()) {
|
||||||
|
uiInitialNoCardOpen(sceneInitialNoCardResult, NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(saveIsBusy()) return;
|
||||||
|
|
||||||
|
saveLoadSlot(SAVE_ACTIVE_SLOT, sceneInitialLoadComplete, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t sceneInitialInit(scenedata_t *sceneData) {
|
||||||
|
assertNotNull(sceneData, "Scene data cannot be null");
|
||||||
|
memoryZero(&sceneData->initial, sizeof(sceneinitial_t));
|
||||||
|
|
||||||
|
sceneInitialCheckSave();
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t sceneInitialUpdate(scenedata_t *sceneData) {
|
||||||
|
assertNotNull(sceneData, "Scene data cannot be null");
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t sceneInitialRender(scenedata_t *sceneData) {
|
||||||
|
assertNotNull(sceneData, "Scene data cannot be null");
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t sceneInitialDispose(scenedata_t *sceneData) {
|
||||||
|
assertNotNull(sceneData, "Scene data cannot be null");
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "scene/scenebase.h"
|
||||||
|
|
||||||
|
// No per-scene state needed - the save globals and the two modal UI
|
||||||
|
// elements (see ui/frame/initial/) carry everything this scene cares
|
||||||
|
// about. A byte placeholder keeps the struct non-empty for portability.
|
||||||
|
typedef struct {
|
||||||
|
uint8_t reserved;
|
||||||
|
} sceneinitial_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialises the initial (boot) scene - kicks off the save
|
||||||
|
* availability/existence check that decides which prompt, if any, to
|
||||||
|
* show before proceeding to the overworld.
|
||||||
|
*
|
||||||
|
* @param sceneData The scene data used for this scene.
|
||||||
|
* @return An error if the init failed, or errorOk() if it succeeded.
|
||||||
|
*/
|
||||||
|
errorret_t sceneInitialInit(scenedata_t *sceneData);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the initial scene. Currently a no-op - all the work happens in
|
||||||
|
* save callbacks and the two modal UI elements' own button handling.
|
||||||
|
*
|
||||||
|
* @param sceneData The scene data used for this scene.
|
||||||
|
* @return An error if the update failed, or errorOk() if it succeeded.
|
||||||
|
*/
|
||||||
|
errorret_t sceneInitialUpdate(scenedata_t *sceneData);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the initial scene. Currently a no-op - the modals draw
|
||||||
|
* themselves via the global UI element pipeline.
|
||||||
|
*
|
||||||
|
* @param sceneData The scene data used for this scene.
|
||||||
|
* @return An error if the render failed, or errorOk() if it succeeded.
|
||||||
|
*/
|
||||||
|
errorret_t sceneInitialRender(scenedata_t *sceneData);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes the initial scene.
|
||||||
|
*
|
||||||
|
* @param sceneData The scene data used for this scene.
|
||||||
|
* @return An error if the dispose failed, or errorOk() if it succeeded.
|
||||||
|
*/
|
||||||
|
errorret_t sceneInitialDispose(scenedata_t *sceneData);
|
||||||
@@ -10,6 +10,13 @@
|
|||||||
scenecallbacks_t SCENE_TYPES[SCENE_TYPE_COUNT] = {
|
scenecallbacks_t SCENE_TYPES[SCENE_TYPE_COUNT] = {
|
||||||
[SCENE_TYPE_NULL] = { 0 },
|
[SCENE_TYPE_NULL] = { 0 },
|
||||||
|
|
||||||
|
[SCENE_TYPE_INITIAL] = {
|
||||||
|
.init = sceneInitialInit,
|
||||||
|
.update = sceneInitialUpdate,
|
||||||
|
.render = sceneInitialRender,
|
||||||
|
.dispose = sceneInitialDispose
|
||||||
|
},
|
||||||
|
|
||||||
[SCENE_TYPE_OVERWORLD] = {
|
[SCENE_TYPE_OVERWORLD] = {
|
||||||
.init = sceneOverworldInit,
|
.init = sceneOverworldInit,
|
||||||
.update = sceneOverworldUpdate,
|
.update = sceneOverworldUpdate,
|
||||||
|
|||||||
@@ -7,10 +7,12 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "scene/scenebase.h"
|
#include "scene/scenebase.h"
|
||||||
|
#include "scene/initial/sceneinitial.h"
|
||||||
#include "scene/overworld/sceneoverworld.h"
|
#include "scene/overworld/sceneoverworld.h"
|
||||||
#include "scene/battle/scenebattle.h"
|
#include "scene/battle/scenebattle.h"
|
||||||
|
|
||||||
typedef union scenedata_u {
|
typedef union scenedata_u {
|
||||||
|
sceneinitial_t initial;
|
||||||
sceneoverworld_t overworld;
|
sceneoverworld_t overworld;
|
||||||
scenebattle_t battle;
|
scenebattle_t battle;
|
||||||
} scenedata_t;
|
} scenedata_t;
|
||||||
@@ -27,6 +29,7 @@ typedef struct {
|
|||||||
typedef enum {
|
typedef enum {
|
||||||
SCENE_TYPE_NULL,
|
SCENE_TYPE_NULL,
|
||||||
|
|
||||||
|
SCENE_TYPE_INITIAL,
|
||||||
SCENE_TYPE_OVERWORLD,
|
SCENE_TYPE_OVERWORLD,
|
||||||
SCENE_TYPE_BATTLE,
|
SCENE_TYPE_BATTLE,
|
||||||
|
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ add_subdirectory(game)
|
|||||||
add_subdirectory(settings)
|
add_subdirectory(settings)
|
||||||
add_subdirectory(battle)
|
add_subdirectory(battle)
|
||||||
add_subdirectory(backpack)
|
add_subdirectory(backpack)
|
||||||
|
add_subdirectory(initial)
|
||||||
|
|||||||
@@ -7,18 +7,95 @@
|
|||||||
|
|
||||||
#include "uigamemenu.h"
|
#include "uigamemenu.h"
|
||||||
#include "ui/frame/uiframe.h"
|
#include "ui/frame/uiframe.h"
|
||||||
|
#include "ui/frame/uiconfirm.h"
|
||||||
#include "ui/frame/settings/uisettings.h"
|
#include "ui/frame/settings/uisettings.h"
|
||||||
#include "ui/frame/backpack/uibackpack.h"
|
#include "ui/frame/backpack/uibackpack.h"
|
||||||
|
#include "ui/rpg/textbox/uitextboxmain.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "display/spritebatch/spritebatch.h"
|
#include "display/spritebatch/spritebatch.h"
|
||||||
#include "display/screen/screen.h"
|
#include "display/screen/screen.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "locale/localemanager.h"
|
#include "locale/localemanager.h"
|
||||||
#include "asset/loader/locale/assetlocaleloader.h"
|
#include "asset/loader/locale/assetlocaleloader.h"
|
||||||
|
#include "save/save.h"
|
||||||
|
#include "error/error.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
|
||||||
#define UI_GAME_MENU_INDEX_CHARACTERS 0
|
#define UI_GAME_MENU_INDEX_CHARACTERS 0
|
||||||
#define UI_GAME_MENU_INDEX_ITEMS 1
|
#define UI_GAME_MENU_INDEX_ITEMS 1
|
||||||
#define UI_GAME_MENU_INDEX_SETTINGS 2
|
#define UI_GAME_MENU_INDEX_SETTINGS 2
|
||||||
|
#define UI_GAME_MENU_INDEX_SAVE 3
|
||||||
|
|
||||||
|
static void uiGameMenuSaveWriteComplete(errorret_t result, void *user) {
|
||||||
|
if(errorIsNotOk(result)) {
|
||||||
|
// Generously sized - stringFormat asserts (crashes) rather than
|
||||||
|
// truncating if the message doesn't fit, so this must comfortably fit
|
||||||
|
// the longest platform save-error message plus this prefix.
|
||||||
|
char_t msg[256];
|
||||||
|
stringFormat(
|
||||||
|
msg, sizeof(msg), UI_GAME_MENU.saveFailedFormat, result.state->message
|
||||||
|
);
|
||||||
|
errorCatch(result);
|
||||||
|
uiTextboxMainSetText(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uiTextboxMainSetText(UI_GAME_MENU.saveSuccessText);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void uiGameMenuSaveCreateConfirmed(const bool_t confirmed, void *user) {
|
||||||
|
if(!confirmed) {
|
||||||
|
uiTextboxMainSetText(UI_GAME_MENU.saveCancelledText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveWriteSlot(SAVE_ACTIVE_SLOT, uiGameMenuSaveWriteComplete, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determines whether there's actually save data to overwrite (not just
|
||||||
|
// whether the medium is present) by attempting a real load first - this is
|
||||||
|
// what lets a fresh memory card/stick, with no prior save on it yet, be
|
||||||
|
// told apart from one that already has our data on it. Cheap either way
|
||||||
|
// (a single sector/file read), and correct on every platform without any
|
||||||
|
// platform-specific UI code - saveSlotExists() already reflects each
|
||||||
|
// platform's own notion of "found something."
|
||||||
|
static void uiGameMenuSaveCheckComplete(errorret_t result, void *user) {
|
||||||
|
if(errorIsNotOk(result)) {
|
||||||
|
char_t msg[256];
|
||||||
|
stringFormat(
|
||||||
|
msg, sizeof(msg), UI_GAME_MENU.saveCheckFailedFormat,
|
||||||
|
result.state->message
|
||||||
|
);
|
||||||
|
errorCatch(result);
|
||||||
|
uiTextboxMainSetText(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(saveSlotExists(SAVE_ACTIVE_SLOT)) {
|
||||||
|
saveWriteSlot(SAVE_ACTIVE_SLOT, uiGameMenuSaveWriteComplete, NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uiConfirmOpen(
|
||||||
|
UI_GAME_MENU.saveCreateConfirmText,
|
||||||
|
uiGameMenuSaveCreateConfirmed,
|
||||||
|
NULL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void uiGameMenuSave(void) {
|
||||||
|
if(saveIsTemporary()) {
|
||||||
|
uiTextboxMainSetText(UI_GAME_MENU.saveTemporaryText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!saveIsAvailable()) {
|
||||||
|
uiTextboxMainSetText(UI_GAME_MENU.saveUnavailableText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(saveIsBusy()) return;// A save/load dialog (e.g. on PSP) is already up.
|
||||||
|
|
||||||
|
saveLoadSlot(SAVE_ACTIVE_SLOT, uiGameMenuSaveCheckComplete, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
uigamemenu_t UI_GAME_MENU;
|
uigamemenu_t UI_GAME_MENU;
|
||||||
|
|
||||||
@@ -29,6 +106,7 @@ void uiGameMenuSelected(
|
|||||||
) {
|
) {
|
||||||
if(index == UI_GAME_MENU_INDEX_ITEMS) uiBackpackOpen();
|
if(index == UI_GAME_MENU_INDEX_ITEMS) uiBackpackOpen();
|
||||||
if(index == UI_GAME_MENU_INDEX_SETTINGS) uiSettingsOpen();
|
if(index == UI_GAME_MENU_INDEX_SETTINGS) uiSettingsOpen();
|
||||||
|
if(index == UI_GAME_MENU_INDEX_SAVE) uiGameMenuSave();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t uiGameMenuInit(void) {
|
errorret_t uiGameMenuInit(void) {
|
||||||
@@ -55,6 +133,62 @@ errorret_t uiGameMenuInit(void) {
|
|||||||
UI_GAME_MENU.settingsLabel,
|
UI_GAME_MENU.settingsLabel,
|
||||||
UI_GAME_MENU_LABEL_MAX
|
UI_GAME_MENU_LABEL_MAX
|
||||||
));
|
));
|
||||||
|
errorChain(assetLocaleGetString(
|
||||||
|
&LOCALE.entry->data.locale,
|
||||||
|
"ui.game_menu.save",
|
||||||
|
0,
|
||||||
|
UI_GAME_MENU.saveLabel,
|
||||||
|
UI_GAME_MENU_LABEL_MAX
|
||||||
|
));
|
||||||
|
errorChain(assetLocaleGetString(
|
||||||
|
&LOCALE.entry->data.locale,
|
||||||
|
"ui.game_menu.save_success",
|
||||||
|
0,
|
||||||
|
UI_GAME_MENU.saveSuccessText,
|
||||||
|
UI_GAME_MENU_MESSAGE_MAX
|
||||||
|
));
|
||||||
|
errorChain(assetLocaleGetString(
|
||||||
|
&LOCALE.entry->data.locale,
|
||||||
|
"ui.game_menu.save_cancelled",
|
||||||
|
0,
|
||||||
|
UI_GAME_MENU.saveCancelledText,
|
||||||
|
UI_GAME_MENU_MESSAGE_MAX
|
||||||
|
));
|
||||||
|
errorChain(assetLocaleGetString(
|
||||||
|
&LOCALE.entry->data.locale,
|
||||||
|
"ui.game_menu.save_unavailable",
|
||||||
|
0,
|
||||||
|
UI_GAME_MENU.saveUnavailableText,
|
||||||
|
UI_GAME_MENU_MESSAGE_MAX
|
||||||
|
));
|
||||||
|
errorChain(assetLocaleGetString(
|
||||||
|
&LOCALE.entry->data.locale,
|
||||||
|
"ui.game_menu.save_temporary",
|
||||||
|
0,
|
||||||
|
UI_GAME_MENU.saveTemporaryText,
|
||||||
|
UI_GAME_MENU_MESSAGE_MAX
|
||||||
|
));
|
||||||
|
errorChain(assetLocaleGetString(
|
||||||
|
&LOCALE.entry->data.locale,
|
||||||
|
"ui.game_menu.save_create_confirm",
|
||||||
|
0,
|
||||||
|
UI_GAME_MENU.saveCreateConfirmText,
|
||||||
|
UI_GAME_MENU_MESSAGE_MAX
|
||||||
|
));
|
||||||
|
errorChain(assetLocaleGetString(
|
||||||
|
&LOCALE.entry->data.locale,
|
||||||
|
"ui.game_menu.save_failed_format",
|
||||||
|
0,
|
||||||
|
UI_GAME_MENU.saveFailedFormat,
|
||||||
|
UI_GAME_MENU_MESSAGE_MAX
|
||||||
|
));
|
||||||
|
errorChain(assetLocaleGetString(
|
||||||
|
&LOCALE.entry->data.locale,
|
||||||
|
"ui.game_menu.save_check_failed_format",
|
||||||
|
0,
|
||||||
|
UI_GAME_MENU.saveCheckFailedFormat,
|
||||||
|
UI_GAME_MENU_MESSAGE_MAX
|
||||||
|
));
|
||||||
|
|
||||||
MENU_BEGIN(
|
MENU_BEGIN(
|
||||||
&UI_GAME_MENU.menu, UI_GAME_MENU.items, uiGameMenuSelected, NULL, NULL
|
&UI_GAME_MENU.menu, UI_GAME_MENU.items, uiGameMenuSelected, NULL, NULL
|
||||||
@@ -62,6 +196,7 @@ errorret_t uiGameMenuInit(void) {
|
|||||||
MENU_BUTTON(UI_GAME_MENU.charactersLabel);
|
MENU_BUTTON(UI_GAME_MENU.charactersLabel);
|
||||||
MENU_BUTTON(UI_GAME_MENU.itemsLabel);
|
MENU_BUTTON(UI_GAME_MENU.itemsLabel);
|
||||||
MENU_BUTTON(UI_GAME_MENU.settingsLabel);
|
MENU_BUTTON(UI_GAME_MENU.settingsLabel);
|
||||||
|
MENU_BUTTON(UI_GAME_MENU.saveLabel);
|
||||||
|
|
||||||
MENU_END(UI_GAME_MENU.items, 1);
|
MENU_END(UI_GAME_MENU.items, 1);
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,10 @@
|
|||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "ui/widget/uimenu.h"
|
#include "ui/widget/uimenu.h"
|
||||||
|
|
||||||
#define UI_GAME_MENU_ITEM_COUNT 3
|
#define UI_GAME_MENU_ITEM_COUNT 4
|
||||||
#define UI_GAME_MENU_WIDTH 150.0f
|
#define UI_GAME_MENU_WIDTH 150.0f
|
||||||
#define UI_GAME_MENU_LABEL_MAX 32
|
#define UI_GAME_MENU_LABEL_MAX 32
|
||||||
|
#define UI_GAME_MENU_MESSAGE_MAX 192
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uimenu_t menu;
|
uimenu_t menu;
|
||||||
@@ -19,6 +20,14 @@ typedef struct {
|
|||||||
char_t charactersLabel[UI_GAME_MENU_LABEL_MAX];
|
char_t charactersLabel[UI_GAME_MENU_LABEL_MAX];
|
||||||
char_t itemsLabel[UI_GAME_MENU_LABEL_MAX];
|
char_t itemsLabel[UI_GAME_MENU_LABEL_MAX];
|
||||||
char_t settingsLabel[UI_GAME_MENU_LABEL_MAX];
|
char_t settingsLabel[UI_GAME_MENU_LABEL_MAX];
|
||||||
|
char_t saveLabel[UI_GAME_MENU_LABEL_MAX];
|
||||||
|
char_t saveSuccessText[UI_GAME_MENU_MESSAGE_MAX];
|
||||||
|
char_t saveCancelledText[UI_GAME_MENU_MESSAGE_MAX];
|
||||||
|
char_t saveUnavailableText[UI_GAME_MENU_MESSAGE_MAX];
|
||||||
|
char_t saveTemporaryText[UI_GAME_MENU_MESSAGE_MAX];
|
||||||
|
char_t saveCreateConfirmText[UI_GAME_MENU_MESSAGE_MAX];
|
||||||
|
char_t saveFailedFormat[UI_GAME_MENU_MESSAGE_MAX];
|
||||||
|
char_t saveCheckFailedFormat[UI_GAME_MENU_MESSAGE_MAX];
|
||||||
} uigamemenu_t;
|
} uigamemenu_t;
|
||||||
|
|
||||||
extern uigamemenu_t UI_GAME_MENU;
|
extern uigamemenu_t UI_GAME_MENU;
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
|
PUBLIC
|
||||||
|
uiinitialnocard.c
|
||||||
|
uiinitialcreatesave.c
|
||||||
|
)
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "uiinitialcreatesave.h"
|
||||||
|
#include "ui/frame/uiframe.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/math.h"
|
||||||
|
#include "display/screen/screen.h"
|
||||||
|
#include "display/text/text.h"
|
||||||
|
#include "display/color.h"
|
||||||
|
#include "display/spritebatch/spritebatch.h"
|
||||||
|
#include "display/texture/texture.h"
|
||||||
|
#include "display/shader/shaderunlit.h"
|
||||||
|
#include "locale/localemanager.h"
|
||||||
|
#include "asset/loader/locale/assetlocaleloader.h"
|
||||||
|
|
||||||
|
#define UI_INITIAL_CREATE_SAVE_BACKDROP_COLOR color4b(0, 0, 0, 160)
|
||||||
|
|
||||||
|
uiinitialcreatesave_t UI_INITIAL_CREATE_SAVE;
|
||||||
|
|
||||||
|
static void uiInitialCreateSaveSelected(
|
||||||
|
const uimenu_t *menu,
|
||||||
|
const uint8_t index,
|
||||||
|
const uimenuitem_t *item
|
||||||
|
) {
|
||||||
|
UI_INITIAL_CREATE_SAVE.create = index == UI_INITIAL_CREATE_SAVE_INDEX_YES;
|
||||||
|
uiInitialCreateSaveClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void uiInitialCreateSaveClosed(const uimenu_t *menu) {
|
||||||
|
if(UI_INITIAL_CREATE_SAVE.callback != NULL) {
|
||||||
|
UI_INITIAL_CREATE_SAVE.callback(
|
||||||
|
UI_INITIAL_CREATE_SAVE.create, UI_INITIAL_CREATE_SAVE.user
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiInitialCreateSaveInit(void) {
|
||||||
|
memoryZero(&UI_INITIAL_CREATE_SAVE, sizeof(uiinitialcreatesave_t));
|
||||||
|
|
||||||
|
errorChain(assetLocaleGetString(
|
||||||
|
&LOCALE.entry->data.locale,
|
||||||
|
"ui.initial.create_save.message",
|
||||||
|
0,
|
||||||
|
UI_INITIAL_CREATE_SAVE.text,
|
||||||
|
UI_INITIAL_CREATE_SAVE_TEXT_MAX
|
||||||
|
));
|
||||||
|
errorChain(assetLocaleGetString(
|
||||||
|
&LOCALE.entry->data.locale,
|
||||||
|
"ui.initial.create_save.yes",
|
||||||
|
0,
|
||||||
|
UI_INITIAL_CREATE_SAVE.yesLabel,
|
||||||
|
UI_INITIAL_CREATE_SAVE_LABEL_MAX
|
||||||
|
));
|
||||||
|
errorChain(assetLocaleGetString(
|
||||||
|
&LOCALE.entry->data.locale,
|
||||||
|
"ui.initial.create_save.no",
|
||||||
|
0,
|
||||||
|
UI_INITIAL_CREATE_SAVE.noLabel,
|
||||||
|
UI_INITIAL_CREATE_SAVE_LABEL_MAX
|
||||||
|
));
|
||||||
|
|
||||||
|
MENU_BEGIN(
|
||||||
|
&UI_INITIAL_CREATE_SAVE.menu, UI_INITIAL_CREATE_SAVE.items,
|
||||||
|
uiInitialCreateSaveSelected, uiInitialCreateSaveClosed, NULL
|
||||||
|
);
|
||||||
|
MENU_BUTTON(UI_INITIAL_CREATE_SAVE.yesLabel);
|
||||||
|
MENU_BUTTON(UI_INITIAL_CREATE_SAVE.noLabel);
|
||||||
|
|
||||||
|
MENU_END(UI_INITIAL_CREATE_SAVE.items, menuIndex);
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiInitialCreateSaveDraw(void) {
|
||||||
|
if(!uiMenuIsActive(&UI_INITIAL_CREATE_SAVE.menu)) errorOk();
|
||||||
|
|
||||||
|
spritebatchsprite_t backdropSprite = {
|
||||||
|
.min = { 0.0f, 0.0f, 0.0f },
|
||||||
|
.max = { (float_t)SCREEN.width, (float_t)SCREEN.height, 0.0f },
|
||||||
|
.uvMin = { 0.0f, 0.0f },
|
||||||
|
.uvMax = { 1.0f, 1.0f }
|
||||||
|
};
|
||||||
|
shadermaterial_t backdropMaterial = {
|
||||||
|
.unlit = {
|
||||||
|
.color = UI_INITIAL_CREATE_SAVE_BACKDROP_COLOR,
|
||||||
|
.texture = &TEXTURE_WHITE
|
||||||
|
}
|
||||||
|
};
|
||||||
|
errorChain(
|
||||||
|
spriteBatchBuffer(&backdropSprite, 1, &SHADER_UNLIT, backdropMaterial)
|
||||||
|
);
|
||||||
|
errorChain(spriteBatchFlush());
|
||||||
|
|
||||||
|
int32_t textW, textH;
|
||||||
|
textMeasure(UI_INITIAL_CREATE_SAVE.text, &FONT_DEFAULT, &textW, &textH);
|
||||||
|
|
||||||
|
float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||||
|
float_t width = mathMax(
|
||||||
|
(float_t)textW + (UI_FRAME_START_X * 2), UI_INITIAL_CREATE_SAVE_MIN_WIDTH
|
||||||
|
);
|
||||||
|
float_t height = (UI_FRAME_START_Y * 2) + rowHeight + UI_FRAME_PADDING_Y +
|
||||||
|
rowHeight;
|
||||||
|
float_t x = (float_t)SCREEN.scanX + ((float_t)SCREEN.scanWidth - width) * 0.5f;
|
||||||
|
float_t y = (float_t)SCREEN.scanY + ((float_t)SCREEN.scanHeight - height) * 0.5f;
|
||||||
|
|
||||||
|
errorChain(uiFrameDraw(x, y, width, height));
|
||||||
|
|
||||||
|
float_t contentX = x + UI_FRAME_START_X;
|
||||||
|
float_t contentY = y + UI_FRAME_START_Y;
|
||||||
|
float_t contentWidth = width - (UI_FRAME_START_X * 2);
|
||||||
|
|
||||||
|
errorChain(textDraw(
|
||||||
|
contentX, contentY, UI_INITIAL_CREATE_SAVE.text, COLOR_WHITE, &FONT_DEFAULT
|
||||||
|
));
|
||||||
|
|
||||||
|
float_t buttonsY = contentY + rowHeight + UI_FRAME_PADDING_Y;
|
||||||
|
errorChain(uiMenuDraw(
|
||||||
|
&UI_INITIAL_CREATE_SAVE.menu, contentX, buttonsY, contentWidth, rowHeight
|
||||||
|
));
|
||||||
|
|
||||||
|
errorChain(spriteBatchFlush());
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t uiInitialCreateSaveIsOpen(void) {
|
||||||
|
return uiMenuIsActive(&UI_INITIAL_CREATE_SAVE.menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiInitialCreateSaveOpen(
|
||||||
|
uiinitialcreatesavecallback_t callback, void *user
|
||||||
|
) {
|
||||||
|
UI_INITIAL_CREATE_SAVE.callback = callback;
|
||||||
|
UI_INITIAL_CREATE_SAVE.user = user;
|
||||||
|
UI_INITIAL_CREATE_SAVE.create = false;
|
||||||
|
uiMenuOpen(&UI_INITIAL_CREATE_SAVE.menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiInitialCreateSaveClose(void) {
|
||||||
|
uiMenuClose(&UI_INITIAL_CREATE_SAVE.menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiInitialCreateSaveDispose(void) {
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* 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 "ui/widget/uimenu.h"
|
||||||
|
|
||||||
|
#define UI_INITIAL_CREATE_SAVE_INDEX_YES 0
|
||||||
|
#define UI_INITIAL_CREATE_SAVE_INDEX_NO 1
|
||||||
|
#define UI_INITIAL_CREATE_SAVE_ITEM_COUNT 2
|
||||||
|
#define UI_INITIAL_CREATE_SAVE_MIN_WIDTH 160.0f
|
||||||
|
#define UI_INITIAL_CREATE_SAVE_TEXT_MAX 256
|
||||||
|
#define UI_INITIAL_CREATE_SAVE_LABEL_MAX 32
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback invoked once the create-save modal is dismissed.
|
||||||
|
*
|
||||||
|
* @param create True if Yes was selected, false if No was selected or
|
||||||
|
* the dialog was backed out of.
|
||||||
|
* @param user Arbitrary pointer passed to uiInitialCreateSaveOpen.
|
||||||
|
*/
|
||||||
|
typedef void (*uiinitialcreatesavecallback_t)(const bool_t create, void *user);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uimenu_t menu;
|
||||||
|
uimenuitem_t items[UI_INITIAL_CREATE_SAVE_ITEM_COUNT];
|
||||||
|
uiinitialcreatesavecallback_t callback;
|
||||||
|
void *user;
|
||||||
|
bool_t create;
|
||||||
|
char_t text[UI_INITIAL_CREATE_SAVE_TEXT_MAX];
|
||||||
|
char_t yesLabel[UI_INITIAL_CREATE_SAVE_LABEL_MAX];
|
||||||
|
char_t noLabel[UI_INITIAL_CREATE_SAVE_LABEL_MAX];
|
||||||
|
} uiinitialcreatesave_t;
|
||||||
|
|
||||||
|
extern uiinitialcreatesave_t UI_INITIAL_CREATE_SAVE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the create-save modal.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiInitialCreateSaveInit(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the create-save modal: a semi-transparent black backdrop covering
|
||||||
|
* the whole screen, then its own centered frame with a fixed message and
|
||||||
|
* Yes/No buttons. No-op when not open.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiInitialCreateSaveDraw(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when the modal is currently open.
|
||||||
|
*
|
||||||
|
* @return True if open.
|
||||||
|
*/
|
||||||
|
bool_t uiInitialCreateSaveIsOpen(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the create-save modal. callback is invoked exactly once with the
|
||||||
|
* result, whether dismissed by selecting a button or backing out.
|
||||||
|
*
|
||||||
|
* @param callback Called with the result once the modal closes. May be
|
||||||
|
* NULL.
|
||||||
|
* @param user Arbitrary pointer passed through to callback.
|
||||||
|
*/
|
||||||
|
void uiInitialCreateSaveOpen(uiinitialcreatesavecallback_t callback, void *user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the modal. No-op when already closed.
|
||||||
|
*/
|
||||||
|
void uiInitialCreateSaveClose(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of the modal.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiInitialCreateSaveDispose(void);
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "uiinitialnocard.h"
|
||||||
|
#include "ui/frame/uiframe.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/math.h"
|
||||||
|
#include "display/screen/screen.h"
|
||||||
|
#include "display/text/text.h"
|
||||||
|
#include "display/color.h"
|
||||||
|
#include "display/spritebatch/spritebatch.h"
|
||||||
|
#include "display/texture/texture.h"
|
||||||
|
#include "display/shader/shaderunlit.h"
|
||||||
|
#include "locale/localemanager.h"
|
||||||
|
#include "asset/loader/locale/assetlocaleloader.h"
|
||||||
|
|
||||||
|
#define UI_INITIAL_NO_CARD_BACKDROP_COLOR color4b(0, 0, 0, 160)
|
||||||
|
|
||||||
|
uiinitialnocard_t UI_INITIAL_NO_CARD;
|
||||||
|
|
||||||
|
static void uiInitialNoCardSelected(
|
||||||
|
const uimenu_t *menu,
|
||||||
|
const uint8_t index,
|
||||||
|
const uimenuitem_t *item
|
||||||
|
) {
|
||||||
|
UI_INITIAL_NO_CARD.retry = index == UI_INITIAL_NO_CARD_INDEX_RETRY;
|
||||||
|
uiInitialNoCardClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void uiInitialNoCardClosed(const uimenu_t *menu) {
|
||||||
|
if(UI_INITIAL_NO_CARD.callback != NULL) {
|
||||||
|
UI_INITIAL_NO_CARD.callback(UI_INITIAL_NO_CARD.retry, UI_INITIAL_NO_CARD.user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiInitialNoCardInit(void) {
|
||||||
|
memoryZero(&UI_INITIAL_NO_CARD, sizeof(uiinitialnocard_t));
|
||||||
|
|
||||||
|
errorChain(assetLocaleGetString(
|
||||||
|
&LOCALE.entry->data.locale,
|
||||||
|
"ui.initial.no_card.message",
|
||||||
|
0,
|
||||||
|
UI_INITIAL_NO_CARD.text,
|
||||||
|
UI_INITIAL_NO_CARD_TEXT_MAX
|
||||||
|
));
|
||||||
|
errorChain(assetLocaleGetString(
|
||||||
|
&LOCALE.entry->data.locale,
|
||||||
|
"ui.initial.no_card.retry",
|
||||||
|
0,
|
||||||
|
UI_INITIAL_NO_CARD.retryLabel,
|
||||||
|
UI_INITIAL_NO_CARD_LABEL_MAX
|
||||||
|
));
|
||||||
|
errorChain(assetLocaleGetString(
|
||||||
|
&LOCALE.entry->data.locale,
|
||||||
|
"ui.initial.no_card.continue",
|
||||||
|
0,
|
||||||
|
UI_INITIAL_NO_CARD.continueLabel,
|
||||||
|
UI_INITIAL_NO_CARD_LABEL_MAX
|
||||||
|
));
|
||||||
|
|
||||||
|
MENU_BEGIN(
|
||||||
|
&UI_INITIAL_NO_CARD.menu, UI_INITIAL_NO_CARD.items,
|
||||||
|
uiInitialNoCardSelected, uiInitialNoCardClosed, NULL
|
||||||
|
);
|
||||||
|
MENU_BUTTON(UI_INITIAL_NO_CARD.retryLabel);
|
||||||
|
MENU_BUTTON(UI_INITIAL_NO_CARD.continueLabel);
|
||||||
|
|
||||||
|
MENU_END(UI_INITIAL_NO_CARD.items, menuIndex);
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiInitialNoCardDraw(void) {
|
||||||
|
if(!uiMenuIsActive(&UI_INITIAL_NO_CARD.menu)) errorOk();
|
||||||
|
|
||||||
|
spritebatchsprite_t backdropSprite = {
|
||||||
|
.min = { 0.0f, 0.0f, 0.0f },
|
||||||
|
.max = { (float_t)SCREEN.width, (float_t)SCREEN.height, 0.0f },
|
||||||
|
.uvMin = { 0.0f, 0.0f },
|
||||||
|
.uvMax = { 1.0f, 1.0f }
|
||||||
|
};
|
||||||
|
shadermaterial_t backdropMaterial = {
|
||||||
|
.unlit = {
|
||||||
|
.color = UI_INITIAL_NO_CARD_BACKDROP_COLOR,
|
||||||
|
.texture = &TEXTURE_WHITE
|
||||||
|
}
|
||||||
|
};
|
||||||
|
errorChain(
|
||||||
|
spriteBatchBuffer(&backdropSprite, 1, &SHADER_UNLIT, backdropMaterial)
|
||||||
|
);
|
||||||
|
errorChain(spriteBatchFlush());
|
||||||
|
|
||||||
|
int32_t textW, textH;
|
||||||
|
textMeasure(UI_INITIAL_NO_CARD.text, &FONT_DEFAULT, &textW, &textH);
|
||||||
|
|
||||||
|
float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||||
|
float_t width = mathMax(
|
||||||
|
(float_t)textW + (UI_FRAME_START_X * 2), UI_INITIAL_NO_CARD_MIN_WIDTH
|
||||||
|
);
|
||||||
|
float_t height = (UI_FRAME_START_Y * 2) + (float_t)textH +
|
||||||
|
UI_FRAME_PADDING_Y + rowHeight;
|
||||||
|
float_t x = (float_t)SCREEN.scanX + ((float_t)SCREEN.scanWidth - width) * 0.5f;
|
||||||
|
float_t y = (float_t)SCREEN.scanY + ((float_t)SCREEN.scanHeight - height) * 0.5f;
|
||||||
|
|
||||||
|
errorChain(uiFrameDraw(x, y, width, height));
|
||||||
|
|
||||||
|
float_t contentX = x + UI_FRAME_START_X;
|
||||||
|
float_t contentY = y + UI_FRAME_START_Y;
|
||||||
|
float_t contentWidth = width - (UI_FRAME_START_X * 2);
|
||||||
|
|
||||||
|
errorChain(textDraw(
|
||||||
|
contentX, contentY, UI_INITIAL_NO_CARD.text, COLOR_WHITE, &FONT_DEFAULT
|
||||||
|
));
|
||||||
|
|
||||||
|
float_t buttonsY = contentY + (float_t)textH + UI_FRAME_PADDING_Y;
|
||||||
|
errorChain(uiMenuDraw(
|
||||||
|
&UI_INITIAL_NO_CARD.menu, contentX, buttonsY, contentWidth, rowHeight
|
||||||
|
));
|
||||||
|
|
||||||
|
errorChain(spriteBatchFlush());
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t uiInitialNoCardIsOpen(void) {
|
||||||
|
return uiMenuIsActive(&UI_INITIAL_NO_CARD.menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiInitialNoCardOpen(uiinitialnocardcallback_t callback, void *user) {
|
||||||
|
UI_INITIAL_NO_CARD.callback = callback;
|
||||||
|
UI_INITIAL_NO_CARD.user = user;
|
||||||
|
UI_INITIAL_NO_CARD.retry = false;
|
||||||
|
uiMenuOpen(&UI_INITIAL_NO_CARD.menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiInitialNoCardClose(void) {
|
||||||
|
uiMenuClose(&UI_INITIAL_NO_CARD.menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiInitialNoCardDispose(void) {
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* 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 "ui/widget/uimenu.h"
|
||||||
|
|
||||||
|
#define UI_INITIAL_NO_CARD_INDEX_RETRY 0
|
||||||
|
#define UI_INITIAL_NO_CARD_INDEX_CONTINUE 1
|
||||||
|
#define UI_INITIAL_NO_CARD_ITEM_COUNT 2
|
||||||
|
#define UI_INITIAL_NO_CARD_MIN_WIDTH 220.0f
|
||||||
|
#define UI_INITIAL_NO_CARD_TEXT_MAX 256
|
||||||
|
#define UI_INITIAL_NO_CARD_LABEL_MAX 32
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback invoked once the no-save-device modal is dismissed.
|
||||||
|
*
|
||||||
|
* @param retry True if Retry was selected, false if Continue Anyway was.
|
||||||
|
* @param user Arbitrary pointer passed to uiInitialNoCardOpen.
|
||||||
|
*/
|
||||||
|
typedef void (*uiinitialnocardcallback_t)(const bool_t retry, void *user);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uimenu_t menu;
|
||||||
|
uimenuitem_t items[UI_INITIAL_NO_CARD_ITEM_COUNT];
|
||||||
|
uiinitialnocardcallback_t callback;
|
||||||
|
void *user;
|
||||||
|
bool_t retry;
|
||||||
|
char_t text[UI_INITIAL_NO_CARD_TEXT_MAX];
|
||||||
|
char_t retryLabel[UI_INITIAL_NO_CARD_LABEL_MAX];
|
||||||
|
char_t continueLabel[UI_INITIAL_NO_CARD_LABEL_MAX];
|
||||||
|
} uiinitialnocard_t;
|
||||||
|
|
||||||
|
extern uiinitialnocard_t UI_INITIAL_NO_CARD;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the no-save-device modal.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiInitialNoCardInit(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the no-save-device modal: a semi-transparent black backdrop
|
||||||
|
* covering the whole screen, then its own centered frame with a fixed
|
||||||
|
* message and Retry/Continue Anyway buttons. No-op when not open.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiInitialNoCardDraw(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when the modal is currently open.
|
||||||
|
*
|
||||||
|
* @return True if open.
|
||||||
|
*/
|
||||||
|
bool_t uiInitialNoCardIsOpen(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the no-save-device modal. callback is invoked exactly once with
|
||||||
|
* the result, whether dismissed by selecting a button or backing out.
|
||||||
|
*
|
||||||
|
* @param callback Called with the result once the modal closes. May be
|
||||||
|
* NULL.
|
||||||
|
* @param user Arbitrary pointer passed through to callback.
|
||||||
|
*/
|
||||||
|
void uiInitialNoCardOpen(uiinitialnocardcallback_t callback, void *user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the modal. No-op when already closed.
|
||||||
|
*/
|
||||||
|
void uiInitialNoCardClose(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of the modal.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiInitialNoCardDispose(void);
|
||||||
@@ -8,19 +8,19 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "ui/widget/uimenu.h"
|
#include "ui/widget/uimenu.h"
|
||||||
|
#include "locale/localemanager.h"
|
||||||
|
|
||||||
#define UI_SETTINGS_GENERAL_ITEM_COUNT 3
|
#define UI_SETTINGS_GENERAL_ITEM_COUNT 3
|
||||||
#define UI_SETTINGS_GENERAL_INDEX_LANGUAGE 0
|
#define UI_SETTINGS_GENERAL_INDEX_LANGUAGE 0
|
||||||
#define UI_SETTINGS_GENERAL_INDEX_LANGUAGE_DETAIL 1
|
#define UI_SETTINGS_GENERAL_INDEX_LANGUAGE_DETAIL 1
|
||||||
#define UI_SETTINGS_GENERAL_INDEX_APPLY 2
|
#define UI_SETTINGS_GENERAL_INDEX_APPLY 2
|
||||||
#define UI_SETTINGS_GENERAL_LOCALE_COUNT 3
|
|
||||||
#define UI_SETTINGS_GENERAL_LABEL_MAX 32
|
#define UI_SETTINGS_GENERAL_LABEL_MAX 32
|
||||||
#define UI_SETTINGS_GENERAL_INFO_MAX 64
|
#define UI_SETTINGS_GENERAL_INFO_MAX 96
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uimenu_t menu;
|
uimenu_t menu;
|
||||||
uimenuitem_t items[UI_SETTINGS_GENERAL_ITEM_COUNT];
|
uimenuitem_t items[UI_SETTINGS_GENERAL_ITEM_COUNT];
|
||||||
const char_t *localeNames[UI_SETTINGS_GENERAL_LOCALE_COUNT];
|
const char_t *localeNames[LOCALE_LIST_COUNT];
|
||||||
char_t languageLabel[UI_SETTINGS_GENERAL_LABEL_MAX];
|
char_t languageLabel[UI_SETTINGS_GENERAL_LABEL_MAX];
|
||||||
char_t labelInfo[UI_SETTINGS_GENERAL_INFO_MAX];
|
char_t labelInfo[UI_SETTINGS_GENERAL_INFO_MAX];
|
||||||
} uisettingsgeneral_t;
|
} uisettingsgeneral_t;
|
||||||
|
|||||||
@@ -9,18 +9,15 @@
|
|||||||
#include "uisettings.h"
|
#include "uisettings.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "util/string.h"
|
|
||||||
#include "locale/localemanager.h"
|
#include "locale/localemanager.h"
|
||||||
#include "locale/localeinfo.h"
|
#include "locale/localeinfo.h"
|
||||||
#include "asset/loader/locale/assetlocaleloader.h"
|
#include "asset/loader/locale/assetlocaleloader.h"
|
||||||
|
#include "save/save.h"
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
static const localeinfo_t *const UI_SETTINGS_GENERAL_LOCALES[
|
static void uiSettingsGeneralSaveComplete(errorret_t result, void *user) {
|
||||||
UI_SETTINGS_GENERAL_LOCALE_COUNT
|
if(errorIsNotOk(result)) errorCatch(errorPrint(result));
|
||||||
] = {
|
}
|
||||||
&LOCALE_EN_US,
|
|
||||||
&LOCALE_JP_JP,
|
|
||||||
&LOCALE_ES_MX
|
|
||||||
};
|
|
||||||
|
|
||||||
void uiSettingsGeneralSelected(
|
void uiSettingsGeneralSelected(
|
||||||
const uimenu_t *menu,
|
const uimenu_t *menu,
|
||||||
@@ -41,8 +38,8 @@ errorret_t uiSettingsGeneralInit(uisettingsdata_t *data) {
|
|||||||
uisettingsgeneral_t *general = &data->general;
|
uisettingsgeneral_t *general = &data->general;
|
||||||
memoryZero(general, sizeof(uisettingsgeneral_t));
|
memoryZero(general, sizeof(uisettingsgeneral_t));
|
||||||
|
|
||||||
for(uint8_t i = 0; i < UI_SETTINGS_GENERAL_LOCALE_COUNT; i++) {
|
for(uint8_t i = 0; i < LOCALE_LIST_COUNT; i++) {
|
||||||
general->localeNames[i] = UI_SETTINGS_GENERAL_LOCALES[i]->name;
|
general->localeNames[i] = LOCALE_LIST[i]->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
errorChain(assetLocaleGetString(
|
errorChain(assetLocaleGetString(
|
||||||
@@ -67,7 +64,7 @@ errorret_t uiSettingsGeneralInit(uisettingsdata_t *data) {
|
|||||||
|
|
||||||
MENU_DROPDOWN(
|
MENU_DROPDOWN(
|
||||||
general->languageLabel, general->localeNames,
|
general->languageLabel, general->localeNames,
|
||||||
UI_SETTINGS_GENERAL_LOCALE_COUNT, 0
|
LOCALE_LIST_COUNT, 0
|
||||||
);
|
);
|
||||||
MENU_LABEL(general->labelInfo);
|
MENU_LABEL(general->labelInfo);
|
||||||
|
|
||||||
@@ -80,14 +77,7 @@ errorret_t uiSettingsGeneralInit(uisettingsdata_t *data) {
|
|||||||
void uiSettingsGeneralLoad(void) {
|
void uiSettingsGeneralLoad(void) {
|
||||||
uisettingsgeneral_t *general = &UI_SETTINGS.data.general;
|
uisettingsgeneral_t *general = &UI_SETTINGS.data.general;
|
||||||
|
|
||||||
uint8_t index = 0;
|
uint8_t index = localeManagerGetIndex(LOCALE.locale);
|
||||||
for(uint8_t i = 0; i < UI_SETTINGS_GENERAL_LOCALE_COUNT; i++) {
|
|
||||||
if(stringCompare(LOCALE.locale->file, UI_SETTINGS_GENERAL_LOCALES[i]->file) != 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
index = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
uiDropdownSetSelectedIndex(
|
uiDropdownSetSelectedIndex(
|
||||||
&general->items[UI_SETTINGS_GENERAL_INDEX_LANGUAGE].dropdown, index
|
&general->items[UI_SETTINGS_GENERAL_INDEX_LANGUAGE].dropdown, index
|
||||||
@@ -100,7 +90,10 @@ void uiSettingsGeneralApply(void) {
|
|||||||
uint8_t index = uiDropdownGetSelectedIndex(
|
uint8_t index = uiDropdownGetSelectedIndex(
|
||||||
&general->items[UI_SETTINGS_GENERAL_INDEX_LANGUAGE].dropdown
|
&general->items[UI_SETTINGS_GENERAL_INDEX_LANGUAGE].dropdown
|
||||||
);
|
);
|
||||||
errorCatch(localeManagerSetLocale(UI_SETTINGS_GENERAL_LOCALES[index]));
|
errorCatch(localeManagerSetLocale(LOCALE_LIST[index]));
|
||||||
|
|
||||||
|
saveGetMeta()->language = index;
|
||||||
|
saveWriteMeta(uiSettingsGeneralSaveComplete, NULL);
|
||||||
|
|
||||||
uiMenuClose(&general->menu);
|
uiMenuClose(&general->menu);
|
||||||
}
|
}
|
||||||
@@ -111,7 +104,5 @@ bool_t uiSettingsGeneralHasChanges(void) {
|
|||||||
uint8_t index = uiDropdownGetSelectedIndex(
|
uint8_t index = uiDropdownGetSelectedIndex(
|
||||||
&general->items[UI_SETTINGS_GENERAL_INDEX_LANGUAGE].dropdown
|
&general->items[UI_SETTINGS_GENERAL_INDEX_LANGUAGE].dropdown
|
||||||
);
|
);
|
||||||
return stringCompare(
|
return localeManagerGetIndex(LOCALE.locale) != index;
|
||||||
LOCALE.locale->file, UI_SETTINGS_GENERAL_LOCALES[index]->file
|
|
||||||
) != 0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,11 @@
|
|||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "locale/localemanager.h"
|
#include "locale/localemanager.h"
|
||||||
#include "asset/loader/locale/assetlocaleloader.h"
|
#include "asset/loader/locale/assetlocaleloader.h"
|
||||||
#include "input/input.h"
|
#include "save/save.h"
|
||||||
|
|
||||||
|
static void uiSettingsInputSaveComplete(errorret_t result, void *user) {
|
||||||
|
if(errorIsNotOk(result)) errorCatch(errorPrint(result));
|
||||||
|
}
|
||||||
|
|
||||||
void uiSettingsInputSelected(
|
void uiSettingsInputSelected(
|
||||||
const uimenu_t *menu,
|
const uimenu_t *menu,
|
||||||
@@ -39,7 +43,7 @@ errorret_t uiSettingsInputInit(uisettingsdata_t *data) {
|
|||||||
UI_SETTINGS_INPUT_LABEL_MAX
|
UI_SETTINGS_INPUT_LABEL_MAX
|
||||||
));
|
));
|
||||||
MENU_SLIDER_FLOAT(
|
MENU_SLIDER_FLOAT(
|
||||||
input->deadzoneLabel, INPUT_DEADZONE_DEFAULT, 0.0f, 1.0f, 0.05f
|
input->deadzoneLabel, SAVE_META_DEADZONE_DEFAULT, 0.0f, 1.0f, 0.05f
|
||||||
);
|
);
|
||||||
#else
|
#else
|
||||||
MENU_LABEL("No input settings yet");
|
MENU_LABEL("No input settings yet");
|
||||||
@@ -55,16 +59,17 @@ void uiSettingsInputLoad(void) {
|
|||||||
#ifdef DUSK_INPUT_GAMEPAD
|
#ifdef DUSK_INPUT_GAMEPAD
|
||||||
uiSliderSetFloat(
|
uiSliderSetFloat(
|
||||||
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider,
|
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider,
|
||||||
INPUT.deadzone
|
saveGetMeta()->deadzone
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiSettingsInputApply(void) {
|
void uiSettingsInputApply(void) {
|
||||||
#ifdef DUSK_INPUT_GAMEPAD
|
#ifdef DUSK_INPUT_GAMEPAD
|
||||||
INPUT.deadzone = uiSliderGetFloat(
|
saveGetMeta()->deadzone = uiSliderGetFloat(
|
||||||
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider
|
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider
|
||||||
);
|
);
|
||||||
|
saveWriteMeta(uiSettingsInputSaveComplete, NULL);
|
||||||
#endif
|
#endif
|
||||||
uiMenuClose(&UI_SETTINGS.data.input.menu);
|
uiMenuClose(&UI_SETTINGS.data.input.menu);
|
||||||
}
|
}
|
||||||
@@ -73,7 +78,7 @@ bool_t uiSettingsInputHasChanges(void) {
|
|||||||
#ifdef DUSK_INPUT_GAMEPAD
|
#ifdef DUSK_INPUT_GAMEPAD
|
||||||
if(uiSliderGetFloat(
|
if(uiSliderGetFloat(
|
||||||
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider
|
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider
|
||||||
) != INPUT.deadzone) return true;
|
) != saveGetMeta()->deadzone) return true;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
uicrop.c
|
uicrop.c
|
||||||
uifullbox.c
|
uifullbox.c
|
||||||
uiloading.c
|
uiloading.c
|
||||||
|
uiautosave.c
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "uiautosave.h"
|
||||||
|
#include "save/autosave.h"
|
||||||
|
#include "display/text/text.h"
|
||||||
|
#include "display/screen/screen.h"
|
||||||
|
#include "display/spritebatch/spritebatch.h"
|
||||||
|
|
||||||
|
// TODO: Localize once this grows beyond a placeholder indicator.
|
||||||
|
#define UI_AUTOSAVE_TEXT "SAVING"
|
||||||
|
|
||||||
|
errorret_t uiAutoSaveDraw(void) {
|
||||||
|
if(!autoSaveIsSaving()) errorOk();
|
||||||
|
|
||||||
|
int32_t textW, textH;
|
||||||
|
textMeasure(UI_AUTOSAVE_TEXT, &FONT_DEFAULT, &textW, &textH);
|
||||||
|
|
||||||
|
float_t x = (float_t)SCREEN.scanX + UI_AUTOSAVE_MARGIN;
|
||||||
|
float_t y = (float_t)(SCREEN.scanY + SCREEN.scanHeight) -
|
||||||
|
(float_t)textH - UI_AUTOSAVE_MARGIN;
|
||||||
|
|
||||||
|
errorChain(textDraw(x, y, UI_AUTOSAVE_TEXT, COLOR_WHITE, &FONT_DEFAULT));
|
||||||
|
return spriteBatchFlush();
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
#define UI_AUTOSAVE_MARGIN 8.0f
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a "Saving" indicator in the bottom-left corner of the screen
|
||||||
|
* while an autosave write is in flight (see save/autosave.h). No-op
|
||||||
|
* otherwise.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiAutoSaveDraw(void);
|
||||||
+18
-1
@@ -12,6 +12,7 @@
|
|||||||
#include "engine/engine.h"
|
#include "engine/engine.h"
|
||||||
#include "ui/overlay/uifullbox.h"
|
#include "ui/overlay/uifullbox.h"
|
||||||
#include "ui/overlay/uiloading.h"
|
#include "ui/overlay/uiloading.h"
|
||||||
|
#include "ui/overlay/uiautosave.h"
|
||||||
#include "ui/debug/uiplayerpos.h"
|
#include "ui/debug/uiplayerpos.h"
|
||||||
#include "ui/overlay/uicrop.h"
|
#include "ui/overlay/uicrop.h"
|
||||||
#include "ui/transition/uitransition.h"
|
#include "ui/transition/uitransition.h"
|
||||||
@@ -21,6 +22,8 @@
|
|||||||
#include "ui/frame/battle/uibattlemenu.h"
|
#include "ui/frame/battle/uibattlemenu.h"
|
||||||
#include "ui/frame/backpack/uibackpack.h"
|
#include "ui/frame/backpack/uibackpack.h"
|
||||||
#include "ui/frame/uiconfirm.h"
|
#include "ui/frame/uiconfirm.h"
|
||||||
|
#include "ui/frame/initial/uiinitialnocard.h"
|
||||||
|
#include "ui/frame/initial/uiinitialcreatesave.h"
|
||||||
#include "ui/rpg/textbox/uitextboxmain.h"
|
#include "ui/rpg/textbox/uitextboxmain.h"
|
||||||
#include "ui/rpg/textbox/uitextboxminilist.h"
|
#include "ui/rpg/textbox/uitextboxminilist.h"
|
||||||
#include "ui/rpg/uiemoji.h"
|
#include "ui/rpg/uiemoji.h"
|
||||||
@@ -79,6 +82,18 @@ uielement_t UI_ELEMENTS[] = {
|
|||||||
.dispose = uiConfirmDispose
|
.dispose = uiConfirmDispose
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
.init = uiInitialNoCardInit,
|
||||||
|
.draw = uiInitialNoCardDraw,
|
||||||
|
.dispose = uiInitialNoCardDispose
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
.init = uiInitialCreateSaveInit,
|
||||||
|
.draw = uiInitialCreateSaveDraw,
|
||||||
|
.dispose = uiInitialCreateSaveDispose
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
.init = uiTextboxMainInit,
|
.init = uiTextboxMainInit,
|
||||||
.update = uiTextboxMainUpdate,
|
.update = uiTextboxMainUpdate,
|
||||||
@@ -115,11 +130,13 @@ uielement_t UI_ELEMENTS[] = {
|
|||||||
.draw = uiCropDraw
|
.draw = uiCropDraw
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{ .draw = uiAutoSaveDraw },
|
||||||
|
|
||||||
// Debug items
|
// Debug items
|
||||||
{ .draw = uiConsoleDraw },
|
{ .draw = uiConsoleDraw },
|
||||||
{ .draw = uiFPSDraw },
|
{ .draw = uiFPSDraw },
|
||||||
{ .draw = uiPlayerPosDraw },
|
{ .draw = uiPlayerPosDraw },
|
||||||
|
|
||||||
{ 0 } // Null terminator
|
{ 0 } // Null terminator
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "log/log.h"
|
#include "log/log.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
|
#include "save/save.h"
|
||||||
|
|
||||||
inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
||||||
#ifdef DUSK_INPUT_GAMEPAD
|
#ifdef DUSK_INPUT_GAMEPAD
|
||||||
@@ -187,5 +188,5 @@ float_t inputButtonGetValueDolphin(const inputbutton_t button) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
float_t inputGetDeadzoneDolphin(const inputbutton_t button) {
|
float_t inputGetDeadzoneDolphin(const inputbutton_t button) {
|
||||||
return 0.2f;
|
return saveGetMeta()->deadzone;
|
||||||
}
|
}
|
||||||
@@ -6,26 +6,52 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "save/save.h"
|
#include "save/save.h"
|
||||||
|
#include "save/savestream.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
|
|
||||||
static void _saveGetFileName(
|
|
||||||
const uint8_t slot, char_t *out, const size_t max
|
|
||||||
) {
|
|
||||||
snprintf(out, max, "%s_%u", SAVE_DOLPHIN_GAME_CODE, (uint32_t)slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t saveInitDolphin(void) {
|
errorret_t saveInitDolphin(void) {
|
||||||
SAVE.platform.mounted = false;
|
SAVE.platform.mounted = false;
|
||||||
|
|
||||||
int32_t result = CARD_Mount(
|
// Must run once before any other CARD_* call: sets up card_inited,
|
||||||
SAVE_DOLPHIN_CHANNEL,
|
// the per-channel control blocks (wait queues, alarms) CARD_Mount reads,
|
||||||
SAVE.platform.cardBuffer,
|
// and initializes the DSP (needed for the card unlock sequence).
|
||||||
NULL
|
// Skipping this leaves those structures unset, so CARD_Mount ends up
|
||||||
);
|
// touching hardware state that was never brought up -- e.g. Dolphin's
|
||||||
|
// "Trying to read 32 bits from an invalid MMIO" error -- rather than
|
||||||
|
// failing cleanly with a CARD_ERROR_* code.
|
||||||
|
int32_t result = CARD_Init(SAVE_DOLPHIN_GAME_CODE, NULL);
|
||||||
|
if(result < 0) {
|
||||||
|
errorThrow("Failed to initialize memory card subsystem: %s (%d)",
|
||||||
|
saveCardErrorStringDolphin(result), result
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
result = CARD_Mount(
|
||||||
|
SAVE_DOLPHIN_CHANNEL,
|
||||||
|
SAVE.platform.cardBuffer,
|
||||||
|
NULL
|
||||||
|
);
|
||||||
|
} while(result == CARD_ERROR_BUSY);
|
||||||
|
|
||||||
|
// Special-case the failures a player can actually act on; everything
|
||||||
|
// else falls through to the generic, fully-enumerated message below.
|
||||||
|
switch(result) {
|
||||||
|
case CARD_ERROR_NOCARD:
|
||||||
|
errorThrow("No memory card inserted in the slot");
|
||||||
|
case CARD_ERROR_WRONGDEVICE:
|
||||||
|
errorThrow("Unsupported device inserted in the memory card slot");
|
||||||
|
case CARD_ERROR_BROKEN:
|
||||||
|
errorThrow("Memory card is damaged or unformatted");
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if(result < 0) {
|
if(result < 0) {
|
||||||
errorThrow("Failed to mount memory card (error %d)", result);
|
errorThrow("Failed to mount memory card: %s (%d)",
|
||||||
|
saveCardErrorStringDolphin(result), result
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SAVE.platform.mounted = true;
|
SAVE.platform.mounted = true;
|
||||||
@@ -40,106 +66,103 @@ errorret_t saveDisposeDolphin(void) {
|
|||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveLoadDolphin(const uint8_t slot, savefile_t *file) {
|
errorret_t saveCombinedLoadDolphin(void) {
|
||||||
char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX];
|
savestream_t stream;
|
||||||
_saveGetFileName(slot, fileName, SAVE_DOLPHIN_FILE_NAME_MAX);
|
memoryZero(&stream, sizeof(savestream_t));
|
||||||
|
|
||||||
int32_t result = CARD_Open(
|
errorret_t openRet = saveStreamOpenReadPlatform(&stream);
|
||||||
SAVE_DOLPHIN_CHANNEL, fileName, &SAVE.platform.cardFile
|
SAVE.available = errorIsOk(openRet);
|
||||||
);
|
errorChain(openRet);
|
||||||
if(result == CARD_ERROR_NOFILE) {
|
|
||||||
file->exists = false;
|
if(!stream.found) errorOk();
|
||||||
errorOk();
|
|
||||||
}
|
errorret_t ret = saveMetaSerializeRead(&stream, &SAVE.meta);
|
||||||
if(result < 0) {
|
for(uint8_t i = 0; errorIsOk(ret) && i < SAVE_SLOT_COUNT_MAX; i++) {
|
||||||
file->exists = false;
|
ret = saveSlotSerializeRead(&stream, &SAVE.slots[i]);
|
||||||
errorThrow("Failed to open memory card file for slot %u (error %d)",
|
|
||||||
(uint32_t)slot, result
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void *buffer = memoryAlign(32, SAVE_DOLPHIN_SECTOR_SIZE);
|
#ifdef saveStreamClosePlatform
|
||||||
if(!buffer) {
|
saveStreamClosePlatform(&stream);
|
||||||
CARD_Close(&SAVE.platform.cardFile);
|
#endif
|
||||||
errorThrow("Failed to allocate memory card read buffer");
|
|
||||||
}
|
|
||||||
|
|
||||||
result = CARD_Read(
|
errorChain(ret);
|
||||||
&SAVE.platform.cardFile, buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0
|
|
||||||
);
|
|
||||||
CARD_Close(&SAVE.platform.cardFile);
|
|
||||||
|
|
||||||
if(result < 0) {
|
|
||||||
memoryFree(buffer);
|
|
||||||
file->exists = false;
|
|
||||||
errorThrow("Failed to read memory card data for slot %u (error %d)",
|
|
||||||
(uint32_t)slot, result
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
memoryCopy(file, buffer, sizeof(savefile_t));
|
|
||||||
memoryFree(buffer);
|
|
||||||
|
|
||||||
file->exists = true;
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveWriteDolphin(const uint8_t slot, const savefile_t *file) {
|
errorret_t saveCombinedWriteDolphin(void) {
|
||||||
char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX];
|
savestream_t stream;
|
||||||
_saveGetFileName(slot, fileName, SAVE_DOLPHIN_FILE_NAME_MAX);
|
memoryZero(&stream, sizeof(savestream_t));
|
||||||
|
|
||||||
void *buffer = memoryAlign(32, SAVE_DOLPHIN_SECTOR_SIZE);
|
errorret_t openRet = saveStreamOpenWritePlatform(&stream);
|
||||||
if(!buffer) {
|
SAVE.available = errorIsOk(openRet);
|
||||||
errorThrow("Failed to allocate memory card write buffer");
|
errorChain(openRet);
|
||||||
}
|
|
||||||
memoryZero(buffer, SAVE_DOLPHIN_SECTOR_SIZE);
|
|
||||||
memoryCopy(buffer, file, sizeof(savefile_t));
|
|
||||||
|
|
||||||
// Try open existing file first; create if absent.
|
errorret_t ret = saveMetaSerializeWrite(&stream, &SAVE.meta);
|
||||||
int32_t result = CARD_Open(
|
for(uint8_t i = 0; errorIsOk(ret) && i < SAVE_SLOT_COUNT_MAX; i++) {
|
||||||
SAVE_DOLPHIN_CHANNEL, fileName, &SAVE.platform.cardFile
|
ret = saveSlotSerializeWrite(&stream, &SAVE.slots[i]);
|
||||||
);
|
|
||||||
if(result == CARD_ERROR_NOFILE) {
|
|
||||||
result = CARD_Create(
|
|
||||||
SAVE_DOLPHIN_CHANNEL,
|
|
||||||
fileName,
|
|
||||||
SAVE_DOLPHIN_SECTOR_SIZE,
|
|
||||||
&SAVE.platform.cardFile
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(result < 0) {
|
#ifdef saveStreamClosePlatform
|
||||||
memoryFree(buffer);
|
saveStreamClosePlatform(&stream);
|
||||||
errorThrow("Failed to open/create memory card file for slot %u (error %d)",
|
#endif
|
||||||
(uint32_t)slot, result
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
result = CARD_Write(
|
|
||||||
&SAVE.platform.cardFile, buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0
|
|
||||||
);
|
|
||||||
CARD_Close(&SAVE.platform.cardFile);
|
|
||||||
memoryFree(buffer);
|
|
||||||
|
|
||||||
if(result < 0) {
|
|
||||||
errorThrow("Failed to write memory card data for slot %u (error %d)",
|
|
||||||
(uint32_t)slot, result
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
errorChain(ret);
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveDeleteDolphin(const uint8_t slot) {
|
errorret_t saveSlotLoadDolphin(const uint8_t slot, saveslot_t *out) {
|
||||||
char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX];
|
(void)slot;
|
||||||
_saveGetFileName(slot, fileName, SAVE_DOLPHIN_FILE_NAME_MAX);
|
(void)out;
|
||||||
|
return saveCombinedLoadDolphin();
|
||||||
int32_t result = CARD_Delete(SAVE_DOLPHIN_CHANNEL, fileName);
|
}
|
||||||
if(result < 0 && result != CARD_ERROR_NOFILE) {
|
|
||||||
errorThrow("Failed to delete memory card file for slot %u (error %d)",
|
errorret_t saveSlotWriteDolphin(const uint8_t slot, saveslot_t *slotData) {
|
||||||
(uint32_t)slot, result
|
(void)slot;
|
||||||
);
|
(void)slotData;
|
||||||
}
|
return saveCombinedWriteDolphin();
|
||||||
|
}
|
||||||
errorOk();
|
|
||||||
|
errorret_t saveSlotDeleteDolphin(const uint8_t slot) {
|
||||||
|
memoryZero(&SAVE.slots[slot], sizeof(saveslot_t));
|
||||||
|
SAVE.slots[slot].exists = false;
|
||||||
|
return saveCombinedWriteDolphin();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t saveMetaLoadDolphin(savemeta_t *out) {
|
||||||
|
(void)out;
|
||||||
|
return saveCombinedLoadDolphin();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t saveMetaWriteDolphin(savemeta_t *meta) {
|
||||||
|
(void)meta;
|
||||||
|
return saveCombinedWriteDolphin();
|
||||||
|
}
|
||||||
|
|
||||||
|
const char_t *saveCardErrorStringDolphin(const int32_t result) {
|
||||||
|
switch(result) {
|
||||||
|
case CARD_ERROR_READY: return "card is ready";
|
||||||
|
case CARD_ERROR_UNLOCKED:
|
||||||
|
return "card is being unlocked or already unlocked";
|
||||||
|
case CARD_ERROR_BUSY: return "card is busy";
|
||||||
|
case CARD_ERROR_WRONGDEVICE: return "wrong device connected in slot";
|
||||||
|
case CARD_ERROR_NOCARD: return "no memory card in slot";
|
||||||
|
case CARD_ERROR_NOFILE: return "specified file not found";
|
||||||
|
case CARD_ERROR_IOERROR: return "internal EXI I/O error";
|
||||||
|
case CARD_ERROR_BROKEN:
|
||||||
|
return "directory structure or file entry broken";
|
||||||
|
case CARD_ERROR_EXIST:
|
||||||
|
return "file already exists with the specified parameters";
|
||||||
|
case CARD_ERROR_NOENT:
|
||||||
|
return "no empty block available to create the file";
|
||||||
|
case CARD_ERROR_INSSPACE:
|
||||||
|
return "not enough space to write file to memory card";
|
||||||
|
case CARD_ERROR_NOPERM:
|
||||||
|
return "not enough permissions to operate on the file";
|
||||||
|
case CARD_ERROR_LIMIT: return "card size limit reached";
|
||||||
|
case CARD_ERROR_NAMETOOLONG: return "filename too long";
|
||||||
|
case CARD_ERROR_ENCODING: return "font encoding PAL/SJIS mismatch";
|
||||||
|
case CARD_ERROR_CANCELED: return "card operation canceled";
|
||||||
|
case CARD_ERROR_FATAL_ERROR: return "fatal error, non-recoverable";
|
||||||
|
default: return "unknown card error";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "save/savefile.h"
|
#include "save/saveslot.h"
|
||||||
|
#include "save/savemeta.h"
|
||||||
#include <gccore.h>
|
#include <gccore.h>
|
||||||
|
|
||||||
#define SAVE_DOLPHIN_FILE_NAME_MAX 32
|
|
||||||
#define SAVE_DOLPHIN_SECTOR_SIZE 8192
|
#define SAVE_DOLPHIN_SECTOR_SIZE 8192
|
||||||
|
|
||||||
#ifndef SAVE_DOLPHIN_GAME_CODE
|
#ifndef SAVE_DOLPHIN_GAME_CODE
|
||||||
@@ -21,6 +21,17 @@
|
|||||||
#define SAVE_DOLPHIN_CHANNEL CARD_SLOTA
|
#define SAVE_DOLPHIN_CHANNEL CARD_SLOTA
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixed memory card file name holding meta + every save slot, back to
|
||||||
|
* back, in one file - GameCube memory cards are small enough that one
|
||||||
|
* consolidated file (rather than one per slot, plus a separate one for
|
||||||
|
* meta) meaningfully saves card space, and nothing needs true random
|
||||||
|
* access into just one section (see saveCombinedLoadDolphin()).
|
||||||
|
*/
|
||||||
|
#ifndef SAVE_DOLPHIN_FILE_NAME
|
||||||
|
#define SAVE_DOLPHIN_FILE_NAME "DUSK_SAVE"
|
||||||
|
#endif
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
card_file cardFile;
|
card_file cardFile;
|
||||||
uint8_t cardBuffer[CARD_WORKAREA] __attribute__((aligned(32)));
|
uint8_t cardBuffer[CARD_WORKAREA] __attribute__((aligned(32)));
|
||||||
@@ -42,27 +53,68 @@ errorret_t saveInitDolphin(void);
|
|||||||
errorret_t saveDisposeDolphin(void);
|
errorret_t saveDisposeDolphin(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads a save file from the memory card for the given slot.
|
* Reads the one consolidated card file (SAVE_DOLPHIN_FILE_NAME) into
|
||||||
|
* SAVE.meta and every SAVE.slots[i], in order. Not finding the file is
|
||||||
|
* not an error - SAVE.meta/SAVE.slots simply keep their compiled
|
||||||
|
* defaults.
|
||||||
*
|
*
|
||||||
* @param slot The save slot index.
|
* @return An error code if the card is mounted but the read/parse fails.
|
||||||
* @param file Output save file data.
|
|
||||||
* @return An error code if the load fails.
|
|
||||||
*/
|
*/
|
||||||
errorret_t saveLoadDolphin(const uint8_t slot, savefile_t *file);
|
errorret_t saveCombinedLoadDolphin(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes a save file to the memory card for the given slot.
|
* Writes SAVE.meta and every SAVE.slots[i], in order, into the one
|
||||||
|
* consolidated card file (SAVE_DOLPHIN_FILE_NAME), creating it if needed.
|
||||||
*
|
*
|
||||||
* @param slot The save slot index.
|
|
||||||
* @param file Save file data to write.
|
|
||||||
* @return An error code if the write fails.
|
* @return An error code if the write fails.
|
||||||
*/
|
*/
|
||||||
errorret_t saveWriteDolphin(const uint8_t slot, const savefile_t *file);
|
errorret_t saveCombinedWriteDolphin(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the save file for the given slot from the memory card.
|
* Save-slot platform entry point - always (re)reads the whole
|
||||||
|
* consolidated file (see saveCombinedLoadDolphin()); slot/out are unused
|
||||||
|
* since every slot is populated in the same pass.
|
||||||
|
*/
|
||||||
|
errorret_t saveSlotLoadDolphin(const uint8_t slot, saveslot_t *out);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save-slot platform entry point - always (re)writes the whole
|
||||||
|
* consolidated file (see saveCombinedWriteDolphin()); slot/slotData are
|
||||||
|
* unused since every slot is written in the same pass.
|
||||||
|
*/
|
||||||
|
errorret_t saveSlotWriteDolphin(const uint8_t slot, saveslot_t *slotData);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a single save slot's data (zeroes it in memory) and re-writes
|
||||||
|
* the consolidated file - the file itself always exists as long as any
|
||||||
|
* slot or meta does, so "delete" can't remove the file wholesale.
|
||||||
*
|
*
|
||||||
* @param slot The save slot index.
|
* @param slot The save slot index.
|
||||||
* @return An error code if the delete fails.
|
* @return An error code if the delete fails.
|
||||||
*/
|
*/
|
||||||
errorret_t saveDeleteDolphin(const uint8_t slot);
|
errorret_t saveSlotDeleteDolphin(const uint8_t slot);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meta platform entry point - always (re)reads the whole consolidated
|
||||||
|
* file (see saveCombinedLoadDolphin()); out is unused since meta is
|
||||||
|
* populated in the same pass.
|
||||||
|
*/
|
||||||
|
errorret_t saveMetaLoadDolphin(savemeta_t *out);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meta platform entry point - always (re)writes the whole consolidated
|
||||||
|
* file (see saveCombinedWriteDolphin()); meta is unused since it's
|
||||||
|
* written in the same pass.
|
||||||
|
*/
|
||||||
|
errorret_t saveMetaWriteDolphin(savemeta_t *meta);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes a libogc CARD_ERROR_* result code (see
|
||||||
|
* https://libogc.devkitpro.org/group__card__errors.html), for logging
|
||||||
|
* alongside the raw numeric code.
|
||||||
|
*
|
||||||
|
* @param result The result code returned by a CARD_* libogc call.
|
||||||
|
* @return A human-readable description of the result code, or
|
||||||
|
* "unknown card error" if result doesn't match a known CARD_ERROR_* code.
|
||||||
|
*/
|
||||||
|
const char_t *saveCardErrorStringDolphin(const int32_t result);
|
||||||
|
|||||||
@@ -14,12 +14,17 @@ typedef savestreamdolphin_t saveplatformstream_t;
|
|||||||
|
|
||||||
#define saveInitPlatform saveInitDolphin
|
#define saveInitPlatform saveInitDolphin
|
||||||
#define saveDisposePlatform saveDisposeDolphin
|
#define saveDisposePlatform saveDisposeDolphin
|
||||||
#define saveDeletePlatform saveDeleteDolphin
|
|
||||||
|
|
||||||
#define saveStreamOpenReadPlatform(stream, slot) \
|
#define saveSlotDeletePlatform saveSlotDeleteDolphin
|
||||||
saveStreamOpenReadDolphin(&(stream)->platform, &(stream)->found, slot)
|
#define saveSlotLoadPlatform saveSlotLoadDolphin
|
||||||
#define saveStreamOpenWritePlatform(stream, slot) \
|
#define saveSlotWritePlatform saveSlotWriteDolphin
|
||||||
saveStreamOpenWriteDolphin(&(stream)->platform, slot)
|
#define saveMetaLoadPlatform saveMetaLoadDolphin
|
||||||
|
#define saveMetaWritePlatform saveMetaWriteDolphin
|
||||||
|
|
||||||
|
#define saveStreamOpenReadPlatform(stream) \
|
||||||
|
saveStreamOpenReadDolphin(&(stream)->platform, &(stream)->found)
|
||||||
|
#define saveStreamOpenWritePlatform(stream) \
|
||||||
|
saveStreamOpenWriteDolphin(&(stream)->platform)
|
||||||
#define saveStreamClosePlatform(stream) \
|
#define saveStreamClosePlatform(stream) \
|
||||||
saveStreamCloseDolphin(&(stream)->platform)
|
saveStreamCloseDolphin(&(stream)->platform)
|
||||||
#define saveStreamReadBytesPlatform(stream, buf, len) \
|
#define saveStreamReadBytesPlatform(stream, buf, len) \
|
||||||
@@ -28,3 +33,5 @@ typedef savestreamdolphin_t saveplatformstream_t;
|
|||||||
saveStreamWriteBytesDolphin(&(stream)->platform, buf, len)
|
saveStreamWriteBytesDolphin(&(stream)->platform, buf, len)
|
||||||
#define saveStreamSeekPlatform(stream, pos) \
|
#define saveStreamSeekPlatform(stream, pos) \
|
||||||
saveStreamSeekDolphin(&(stream)->platform, pos)
|
saveStreamSeekDolphin(&(stream)->platform, pos)
|
||||||
|
#define saveStreamTellPlatform(stream, out) \
|
||||||
|
saveStreamTellDolphin(&(stream)->platform, out)
|
||||||
|
|||||||
@@ -8,23 +8,19 @@
|
|||||||
#include "save/save.h"
|
#include "save/save.h"
|
||||||
#include "save/savestreamdolphin.h"
|
#include "save/savestreamdolphin.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "util/string.h"
|
|
||||||
|
|
||||||
static void _saveStreamGetFileName(
|
errorret_t saveStreamOpenReadDolphin(savestreamdolphin_t *p, bool_t *found) {
|
||||||
char_t *out, const size_t max, const uint8_t slot
|
if(!SAVE.platform.mounted) {
|
||||||
) {
|
*found = false;
|
||||||
snprintf(out, max, "%s_%u", SAVE_DOLPHIN_GAME_CODE, (uint32_t)slot);
|
errorThrow("No memory card mounted");
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveStreamOpenReadDolphin(
|
int32_t result;
|
||||||
savestreamdolphin_t *p, bool_t *found, const uint8_t slot
|
do {
|
||||||
) {
|
result = CARD_Open(
|
||||||
char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX];
|
SAVE_DOLPHIN_CHANNEL, SAVE_DOLPHIN_FILE_NAME, &p->cardFile
|
||||||
_saveStreamGetFileName(fileName, SAVE_DOLPHIN_FILE_NAME_MAX, slot);
|
);
|
||||||
|
} while(result == CARD_ERROR_BUSY);
|
||||||
int32_t result = CARD_Open(
|
|
||||||
SAVE_DOLPHIN_CHANNEL, fileName, &p->cardFile
|
|
||||||
);
|
|
||||||
if(result == CARD_ERROR_NOFILE) {
|
if(result == CARD_ERROR_NOFILE) {
|
||||||
*found = false;
|
*found = false;
|
||||||
p->position = 0;
|
p->position = 0;
|
||||||
@@ -33,51 +29,58 @@ errorret_t saveStreamOpenReadDolphin(
|
|||||||
}
|
}
|
||||||
if(result < 0) {
|
if(result < 0) {
|
||||||
*found = false;
|
*found = false;
|
||||||
errorThrow("Failed to open memory card file for slot %u (error %d)",
|
errorThrow("Failed to open memory card file: %s (%d)",
|
||||||
(uint32_t)slot, result
|
saveCardErrorStringDolphin(result), result
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
result = CARD_Read(&p->cardFile, p->buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0);
|
do {
|
||||||
|
result = CARD_Read(&p->cardFile, p->buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0);
|
||||||
|
} while(result == CARD_ERROR_BUSY);
|
||||||
CARD_Close(&p->cardFile);
|
CARD_Close(&p->cardFile);
|
||||||
if(result < 0) {
|
if(result < 0) {
|
||||||
*found = false;
|
*found = false;
|
||||||
errorThrow("Failed to read memory card data for slot %u (error %d)",
|
errorThrow("Failed to read memory card data: %s (%d)",
|
||||||
(uint32_t)slot, result
|
saveCardErrorStringDolphin(result), result
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
*found = true;
|
*found = true;
|
||||||
p->position = 0;
|
p->position = 0;
|
||||||
p->writing = false;
|
p->writing = false;
|
||||||
p->slot = slot;
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveStreamOpenWriteDolphin(
|
errorret_t saveStreamOpenWriteDolphin(savestreamdolphin_t *p) {
|
||||||
savestreamdolphin_t *p, const uint8_t slot
|
if(!SAVE.platform.mounted) errorThrow("No memory card mounted");
|
||||||
) {
|
|
||||||
memoryZero(p->buffer, SAVE_DOLPHIN_SECTOR_SIZE);
|
memoryZero(p->buffer, SAVE_DOLPHIN_SECTOR_SIZE);
|
||||||
p->position = 0;
|
p->position = 0;
|
||||||
p->writing = true;
|
p->writing = true;
|
||||||
p->slot = slot;
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
void saveStreamCloseDolphin(savestreamdolphin_t *p) {
|
void saveStreamCloseDolphin(savestreamdolphin_t *p) {
|
||||||
if(!p->writing) return;
|
if(!p->writing) return;
|
||||||
|
|
||||||
char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX];
|
int32_t result;
|
||||||
_saveStreamGetFileName(fileName, SAVE_DOLPHIN_FILE_NAME_MAX, p->slot);
|
do {
|
||||||
|
result = CARD_Open(
|
||||||
int32_t result = CARD_Open(SAVE_DOLPHIN_CHANNEL, fileName, &p->cardFile);
|
SAVE_DOLPHIN_CHANNEL, SAVE_DOLPHIN_FILE_NAME, &p->cardFile
|
||||||
if(result == CARD_ERROR_NOFILE) {
|
|
||||||
CARD_Create(
|
|
||||||
SAVE_DOLPHIN_CHANNEL, fileName, SAVE_DOLPHIN_SECTOR_SIZE, &p->cardFile
|
|
||||||
);
|
);
|
||||||
|
} while(result == CARD_ERROR_BUSY);
|
||||||
|
if(result == CARD_ERROR_NOFILE) {
|
||||||
|
do {
|
||||||
|
result = CARD_Create(
|
||||||
|
SAVE_DOLPHIN_CHANNEL, SAVE_DOLPHIN_FILE_NAME,
|
||||||
|
SAVE_DOLPHIN_SECTOR_SIZE, &p->cardFile
|
||||||
|
);
|
||||||
|
} while(result == CARD_ERROR_BUSY);
|
||||||
}
|
}
|
||||||
|
|
||||||
CARD_Write(&p->cardFile, p->buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0);
|
do {
|
||||||
|
result = CARD_Write(&p->cardFile, p->buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0);
|
||||||
|
} while(result == CARD_ERROR_BUSY);
|
||||||
CARD_Close(&p->cardFile);
|
CARD_Close(&p->cardFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,3 +113,8 @@ errorret_t saveStreamSeekDolphin(savestreamdolphin_t *p, const size_t pos) {
|
|||||||
p->position = pos;
|
p->position = pos;
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errorret_t saveStreamTellDolphin(savestreamdolphin_t *p, size_t *out) {
|
||||||
|
*out = p->position;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,34 +20,28 @@ typedef struct {
|
|||||||
size_t position;
|
size_t position;
|
||||||
/** True when opened for writing; flushes buffer to card on close. */
|
/** True when opened for writing; flushes buffer to card on close. */
|
||||||
bool_t writing;
|
bool_t writing;
|
||||||
/** Slot index stored at open time so Close can derive the filename. */
|
|
||||||
uint8_t slot;
|
|
||||||
} savestreamdolphin_t;
|
} savestreamdolphin_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a memory card slot for reading by loading its sector into buffer.
|
* Opens the consolidated memory card file for reading by loading its
|
||||||
|
* sector into buffer.
|
||||||
*
|
*
|
||||||
* @param p Stream to initialize.
|
* @param p Stream to initialize.
|
||||||
* @param found Set to true if the file exists, false if it does not.
|
* @param found Set to true if the file exists, false if it does not.
|
||||||
* @param slot Save slot index.
|
|
||||||
* @return An error if reading the card fails for a reason other than
|
* @return An error if reading the card fails for a reason other than
|
||||||
* missing file.
|
* missing file.
|
||||||
*/
|
*/
|
||||||
errorret_t saveStreamOpenReadDolphin(
|
errorret_t saveStreamOpenReadDolphin(savestreamdolphin_t *p, bool_t *found);
|
||||||
savestreamdolphin_t *p, bool_t *found, const uint8_t slot
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a memory card slot for writing by zeroing the sector buffer.
|
* Opens the consolidated memory card file for writing by zeroing the
|
||||||
* The buffer is flushed to the card when savestreamCloseDolphin is called.
|
* sector buffer. The buffer is flushed to the card when
|
||||||
|
* saveStreamCloseDolphin is called.
|
||||||
*
|
*
|
||||||
* @param p Stream to initialize.
|
* @param p Stream to initialize.
|
||||||
* @param slot Save slot index.
|
|
||||||
* @return An error if initialization fails.
|
* @return An error if initialization fails.
|
||||||
*/
|
*/
|
||||||
errorret_t saveStreamOpenWriteDolphin(
|
errorret_t saveStreamOpenWriteDolphin(savestreamdolphin_t *p);
|
||||||
savestreamdolphin_t *p, const uint8_t slot
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flushes the sector buffer to the memory card (write mode only) and
|
* Flushes the sector buffer to the memory card (write mode only) and
|
||||||
@@ -89,3 +83,12 @@ errorret_t saveStreamWriteBytesDolphin(
|
|||||||
* @return An error if pos is out of range.
|
* @return An error if pos is out of range.
|
||||||
*/
|
*/
|
||||||
errorret_t saveStreamSeekDolphin(savestreamdolphin_t *p, const size_t pos);
|
errorret_t saveStreamSeekDolphin(savestreamdolphin_t *p, const size_t pos);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current read/write position within the sector buffer.
|
||||||
|
*
|
||||||
|
* @param p Active stream.
|
||||||
|
* @param out Receives the current position.
|
||||||
|
* @return An error - always succeeds, matches saveStreamTellImpl's shape.
|
||||||
|
*/
|
||||||
|
errorret_t saveStreamTellDolphin(savestreamdolphin_t *p, size_t *out);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "input/input.h"
|
#include "input/input.h"
|
||||||
|
#include "save/save.h"
|
||||||
|
|
||||||
inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
||||||
#ifdef DUSK_INPUT_GAMEPAD
|
#ifdef DUSK_INPUT_GAMEPAD
|
||||||
@@ -547,5 +548,5 @@ errorret_t inputInitLinux(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
float_t inputGetDeadzoneSDL2(const inputbutton_t button) {
|
float_t inputGetDeadzoneSDL2(const inputbutton_t button) {
|
||||||
return 0.17f;
|
return saveGetMeta()->deadzone;
|
||||||
}
|
}
|
||||||
@@ -7,5 +7,5 @@
|
|||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
savelinux.c
|
savelinux.c
|
||||||
savestreamlinux.c
|
savejsonlinux.c
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "save/savejsonlinux.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
errorret_t saveJsonWriterInitLinux(savejsonwriterlinux_t *writer) {
|
||||||
|
writer->doc = yyjson_mut_doc_new(NULL);
|
||||||
|
if(!writer->doc) {
|
||||||
|
errorThrow("Failed to allocate JSON document");
|
||||||
|
}
|
||||||
|
writer->root = yyjson_mut_obj(writer->doc);
|
||||||
|
yyjson_mut_doc_set_root(writer->doc, writer->root);
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveJsonWriterAddUInt32Linux(
|
||||||
|
savejsonwriterlinux_t *writer, const char_t *key, const uint32_t value
|
||||||
|
) {
|
||||||
|
yyjson_mut_obj_add_uint(writer->doc, writer->root, key, (uint64_t)value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveJsonWriterAddFloatLinux(
|
||||||
|
savejsonwriterlinux_t *writer, const char_t *key, const float_t value
|
||||||
|
) {
|
||||||
|
yyjson_mut_obj_add_real(writer->doc, writer->root, key, (double)value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveJsonWriterAddUInt8Linux(
|
||||||
|
savejsonwriterlinux_t *writer, const char_t *key, const uint8_t value
|
||||||
|
) {
|
||||||
|
yyjson_mut_obj_add_uint(writer->doc, writer->root, key, (uint64_t)value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveJsonWriterAddStringLinux(
|
||||||
|
savejsonwriterlinux_t *writer, const char_t *key, const char_t *value
|
||||||
|
) {
|
||||||
|
yyjson_mut_obj_add_strcpy(writer->doc, writer->root, key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveJsonWriterAddBoolArrayLinux(
|
||||||
|
savejsonwriterlinux_t *writer, const char_t *key, const bool_t *values,
|
||||||
|
const size_t count
|
||||||
|
) {
|
||||||
|
yyjson_mut_val *arr = yyjson_mut_arr(writer->doc);
|
||||||
|
for(size_t i = 0; i < count; i++) {
|
||||||
|
yyjson_mut_arr_add_bool(writer->doc, arr, values[i]);
|
||||||
|
}
|
||||||
|
yyjson_mut_obj_add_val(writer->doc, writer->root, key, arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveJsonWriterAddUInt8ArrayLinux(
|
||||||
|
savejsonwriterlinux_t *writer, const char_t *key, const uint8_t *values,
|
||||||
|
const size_t count
|
||||||
|
) {
|
||||||
|
yyjson_mut_val *arr = yyjson_mut_arr(writer->doc);
|
||||||
|
for(size_t i = 0; i < count; i++) {
|
||||||
|
yyjson_mut_arr_add_uint(writer->doc, arr, (uint64_t)values[i]);
|
||||||
|
}
|
||||||
|
yyjson_mut_obj_add_val(writer->doc, writer->root, key, arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t saveJsonWriterSaveLinux(
|
||||||
|
savejsonwriterlinux_t *writer, const char_t *path
|
||||||
|
) {
|
||||||
|
yyjson_write_err err;
|
||||||
|
if(!yyjson_mut_write_file(
|
||||||
|
path, writer->doc, YYJSON_WRITE_PRETTY, NULL, &err
|
||||||
|
)) {
|
||||||
|
errorThrow("Failed to write %s: %s", path, err.msg);
|
||||||
|
}
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveJsonWriterDisposeLinux(savejsonwriterlinux_t *writer) {
|
||||||
|
if(writer->doc) {
|
||||||
|
yyjson_mut_doc_free(writer->doc);
|
||||||
|
writer->doc = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t saveJsonReaderOpenLinux(
|
||||||
|
const char_t *path, yyjson_doc **outDoc, yyjson_val **outRoot,
|
||||||
|
bool_t *found
|
||||||
|
) {
|
||||||
|
*outDoc = NULL;
|
||||||
|
*outRoot = NULL;
|
||||||
|
|
||||||
|
struct stat st;
|
||||||
|
if(stat(path, &st) != 0) {
|
||||||
|
*found = false;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
yyjson_read_err err;
|
||||||
|
*outDoc = yyjson_read_file(
|
||||||
|
path, YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS,
|
||||||
|
NULL, &err
|
||||||
|
);
|
||||||
|
if(!*outDoc) {
|
||||||
|
*found = false;
|
||||||
|
errorThrow("Failed to parse %s: %s", path, err.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
*outRoot = yyjson_doc_get_root(*outDoc);
|
||||||
|
*found = true;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t saveJsonReadUInt32Linux(
|
||||||
|
yyjson_val *root, const char_t *key, const uint32_t defaultValue
|
||||||
|
) {
|
||||||
|
yyjson_val *val = yyjson_obj_get(root, key);
|
||||||
|
if(!val || !yyjson_is_num(val)) return defaultValue;
|
||||||
|
return (uint32_t)yyjson_get_uint(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
float_t saveJsonReadFloatLinux(
|
||||||
|
yyjson_val *root, const char_t *key, const float_t defaultValue
|
||||||
|
) {
|
||||||
|
yyjson_val *val = yyjson_obj_get(root, key);
|
||||||
|
if(!val || !yyjson_is_num(val)) return defaultValue;
|
||||||
|
return (float_t)yyjson_get_num(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t saveJsonReadUInt8Linux(
|
||||||
|
yyjson_val *root, const char_t *key, const uint8_t defaultValue
|
||||||
|
) {
|
||||||
|
yyjson_val *val = yyjson_obj_get(root, key);
|
||||||
|
if(!val || !yyjson_is_num(val)) return defaultValue;
|
||||||
|
return (uint8_t)yyjson_get_uint(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveJsonReadStringLinux(
|
||||||
|
yyjson_val *root, const char_t *key, char_t *out, const size_t maxLen,
|
||||||
|
const char_t *defaultValue
|
||||||
|
) {
|
||||||
|
yyjson_val *val = yyjson_obj_get(root, key);
|
||||||
|
const char_t *src = defaultValue;
|
||||||
|
if(val && yyjson_is_str(val)) src = yyjson_get_str(val);
|
||||||
|
stringCopy(out, src, maxLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveJsonReadBoolArrayLinux(
|
||||||
|
yyjson_val *root, const char_t *key, bool_t *out, const size_t count
|
||||||
|
) {
|
||||||
|
yyjson_val *arr = yyjson_obj_get(root, key);
|
||||||
|
if(!arr || !yyjson_is_arr(arr)) return;
|
||||||
|
|
||||||
|
size_t idx, len;
|
||||||
|
yyjson_val *elem;
|
||||||
|
yyjson_arr_foreach(arr, idx, len, elem) {
|
||||||
|
if(idx >= count) break;
|
||||||
|
if(yyjson_is_bool(elem)) out[idx] = yyjson_get_bool(elem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveJsonReadUInt8ArrayLinux(
|
||||||
|
yyjson_val *root, const char_t *key, uint8_t *out, const size_t count
|
||||||
|
) {
|
||||||
|
yyjson_val *arr = yyjson_obj_get(root, key);
|
||||||
|
if(!arr || !yyjson_is_arr(arr)) return;
|
||||||
|
|
||||||
|
size_t idx, len;
|
||||||
|
yyjson_val *elem;
|
||||||
|
yyjson_arr_foreach(arr, idx, len, elem) {
|
||||||
|
if(idx >= count) break;
|
||||||
|
if(yyjson_is_int(elem)) out[idx] = (uint8_t)yyjson_get_int(elem);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
/**
|
||||||
|
* 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 "yyjson.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Small helper around a yyjson mutable document, used to build up a save
|
||||||
|
* file's fields one at a time before writing it out. Schema-agnostic -
|
||||||
|
* knows nothing about saveslot_t/savemeta_t; the caller supplies field
|
||||||
|
* names and values.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
yyjson_mut_doc *doc;
|
||||||
|
yyjson_mut_val *root;
|
||||||
|
} savejsonwriterlinux_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new mutable JSON document with an empty root object.
|
||||||
|
*
|
||||||
|
* @param writer Writer to initialize.
|
||||||
|
* @return An error if the document can't be allocated.
|
||||||
|
*/
|
||||||
|
errorret_t saveJsonWriterInitLinux(savejsonwriterlinux_t *writer);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a "version": <uint> field to the root object.
|
||||||
|
*/
|
||||||
|
void saveJsonWriterAddUInt32Linux(
|
||||||
|
savejsonwriterlinux_t *writer, const char_t *key, const uint32_t value
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a float field to the root object (written as a JSON number).
|
||||||
|
*/
|
||||||
|
void saveJsonWriterAddFloatLinux(
|
||||||
|
savejsonwriterlinux_t *writer, const char_t *key, const float_t value
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a "key": <uint> field to the root object.
|
||||||
|
*/
|
||||||
|
void saveJsonWriterAddUInt8Linux(
|
||||||
|
savejsonwriterlinux_t *writer, const char_t *key, const uint8_t value
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a string field to the root object. The value is copied into the
|
||||||
|
* document, so the caller's buffer doesn't need to outlive the call.
|
||||||
|
*/
|
||||||
|
void saveJsonWriterAddStringLinux(
|
||||||
|
savejsonwriterlinux_t *writer, const char_t *key, const char_t *value
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a JSON array of booleans as a field on the root object.
|
||||||
|
*/
|
||||||
|
void saveJsonWriterAddBoolArrayLinux(
|
||||||
|
savejsonwriterlinux_t *writer, const char_t *key, const bool_t *values,
|
||||||
|
const size_t count
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a JSON array of unsigned 8-bit integers as a field on the root
|
||||||
|
* object.
|
||||||
|
*/
|
||||||
|
void saveJsonWriterAddUInt8ArrayLinux(
|
||||||
|
savejsonwriterlinux_t *writer, const char_t *key, const uint8_t *values,
|
||||||
|
const size_t count
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pretty-prints the document to the given file path, creating or
|
||||||
|
* truncating it.
|
||||||
|
*
|
||||||
|
* @param writer Writer holding the document to write.
|
||||||
|
* @param path Destination file path.
|
||||||
|
* @return An error if the write fails.
|
||||||
|
*/
|
||||||
|
errorret_t saveJsonWriterSaveLinux(
|
||||||
|
savejsonwriterlinux_t *writer, const char_t *path
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frees the document. Safe to call even if saveJsonWriterInitLinux()
|
||||||
|
* failed partway.
|
||||||
|
*
|
||||||
|
* @param writer Writer to dispose.
|
||||||
|
*/
|
||||||
|
void saveJsonWriterDisposeLinux(savejsonwriterlinux_t *writer);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads and parses a JSON file, returning its root object.
|
||||||
|
*
|
||||||
|
* @param path File path to read.
|
||||||
|
* @param outDoc Receives the parsed document (must be freed via
|
||||||
|
* yyjson_doc_free() once done, regardless of found/error outcome).
|
||||||
|
* @param outRoot Receives the root object, or NULL if not found.
|
||||||
|
* @param found Set to true if the file exists, false if it does not
|
||||||
|
* (not finding the file is not an error).
|
||||||
|
* @return An error if the file exists but fails to parse.
|
||||||
|
*/
|
||||||
|
errorret_t saveJsonReaderOpenLinux(
|
||||||
|
const char_t *path, yyjson_doc **outDoc, yyjson_val **outRoot,
|
||||||
|
bool_t *found
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a uint32 field, falling back to defaultValue if the key is
|
||||||
|
* missing or not a number - a hand-edited file shouldn't hard-fail the
|
||||||
|
* whole load over one bad/missing field.
|
||||||
|
*/
|
||||||
|
uint32_t saveJsonReadUInt32Linux(
|
||||||
|
yyjson_val *root, const char_t *key, const uint32_t defaultValue
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a float field, falling back to defaultValue if the key is
|
||||||
|
* missing or not a number.
|
||||||
|
*/
|
||||||
|
float_t saveJsonReadFloatLinux(
|
||||||
|
yyjson_val *root, const char_t *key, const float_t defaultValue
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a uint8 field, falling back to defaultValue if the key is
|
||||||
|
* missing or not a number.
|
||||||
|
*/
|
||||||
|
uint8_t saveJsonReadUInt8Linux(
|
||||||
|
yyjson_val *root, const char_t *key, const uint8_t defaultValue
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a string field into out, falling back to defaultValue if the key
|
||||||
|
* is missing or not a string. Always null-terminates.
|
||||||
|
*/
|
||||||
|
void saveJsonReadStringLinux(
|
||||||
|
yyjson_val *root, const char_t *key, char_t *out, const size_t maxLen,
|
||||||
|
const char_t *defaultValue
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a JSON array of booleans into out, up to count entries. Missing
|
||||||
|
* key, non-array value, or a shorter array all leave the remaining/all
|
||||||
|
* entries untouched (caller should zero the buffer first).
|
||||||
|
*/
|
||||||
|
void saveJsonReadBoolArrayLinux(
|
||||||
|
yyjson_val *root, const char_t *key, bool_t *out, const size_t count
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a JSON array of unsigned 8-bit integers into out, up to count
|
||||||
|
* entries. Same forgiving semantics as saveJsonReadBoolArrayLinux().
|
||||||
|
*/
|
||||||
|
void saveJsonReadUInt8ArrayLinux(
|
||||||
|
yyjson_val *root, const char_t *key, uint8_t *out, const size_t count
|
||||||
|
);
|
||||||
+102
-36
@@ -6,8 +6,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "save/save.h"
|
#include "save/save.h"
|
||||||
|
#include "save/savejsonlinux.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
#include <stdio.h>
|
#include "util/memory.h"
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
|
||||||
@@ -25,60 +26,125 @@ errorret_t saveDisposeLinux(void) {
|
|||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveLoadLinux(const uint8_t slot, savefile_t *file) {
|
static void _saveSlotPathLinux(
|
||||||
char_t path[SAVE_LINUX_PATH_MAX];
|
char_t *out, const size_t max, const uint8_t slot
|
||||||
snprintf(path, SAVE_LINUX_PATH_MAX, SAVE_LINUX_FILE_FORMAT,
|
) {
|
||||||
SAVE.platform.savePath, (uint32_t)slot
|
snprintf(
|
||||||
|
out, max, SAVE_LINUX_SLOT_FILE_FORMAT, SAVE.platform.savePath,
|
||||||
|
(uint32_t)slot
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
FILE *f = fopen(path, "rb");
|
static void _saveMetaPathLinux(char_t *out, const size_t max) {
|
||||||
if(!f) {
|
snprintf(out, max, SAVE_LINUX_META_FILE_FORMAT, SAVE.platform.savePath);
|
||||||
file->exists = false;
|
}
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t read = fread(file, sizeof(savefile_t), 1, f);
|
errorret_t saveSlotLoadLinux(const uint8_t slot, saveslot_t *out) {
|
||||||
fclose(f);
|
char_t path[SAVE_LINUX_PATH_MAX];
|
||||||
|
_saveSlotPathLinux(path, SAVE_LINUX_PATH_MAX, slot);
|
||||||
|
|
||||||
if(read != 1) {
|
yyjson_doc *doc;
|
||||||
file->exists = false;
|
yyjson_val *root;
|
||||||
errorThrow("Failed to read save data for slot %u", (uint32_t)slot);
|
bool_t found;
|
||||||
}
|
errorret_t ret = saveJsonReaderOpenLinux(path, &doc, &root, &found);
|
||||||
|
if(errorIsNotOk(ret)) { yyjson_doc_free(doc); errorChain(ret); }
|
||||||
|
if(!found) errorOk();
|
||||||
|
|
||||||
file->exists = true;
|
memoryZero(out, sizeof(saveslot_t));
|
||||||
|
out->version = saveJsonReadUInt32Linux(root, "version", SAVE_SLOT_VERSION);
|
||||||
|
saveJsonReadStringLinux(
|
||||||
|
root, "playerName", out->playerName, SAVE_PLAYER_NAME_MAX, ""
|
||||||
|
);
|
||||||
|
saveJsonReadBoolArrayLinux(
|
||||||
|
root, "globalItemCollected", out->globalItemCollected,
|
||||||
|
SAVE_GLOBAL_ITEM_COUNT_MAX
|
||||||
|
);
|
||||||
|
saveJsonReadUInt8ArrayLinux(
|
||||||
|
root, "storyFlags", out->storyFlags, SAVE_STORY_FLAG_COUNT_MAX
|
||||||
|
);
|
||||||
|
out->exists = true;
|
||||||
|
|
||||||
|
yyjson_doc_free(doc);
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveWriteLinux(const uint8_t slot, const savefile_t *file) {
|
errorret_t saveSlotWriteLinux(const uint8_t slot, saveslot_t *slotData) {
|
||||||
char_t path[SAVE_LINUX_PATH_MAX];
|
char_t path[SAVE_LINUX_PATH_MAX];
|
||||||
snprintf(path, SAVE_LINUX_PATH_MAX, SAVE_LINUX_FILE_FORMAT,
|
_saveSlotPathLinux(path, SAVE_LINUX_PATH_MAX, slot);
|
||||||
SAVE.platform.savePath, (uint32_t)slot
|
|
||||||
|
slotData->version = SAVE_SLOT_VERSION;
|
||||||
|
|
||||||
|
savejsonwriterlinux_t writer;
|
||||||
|
errorChain(saveJsonWriterInitLinux(&writer));
|
||||||
|
saveJsonWriterAddUInt32Linux(&writer, "version", slotData->version);
|
||||||
|
saveJsonWriterAddStringLinux(&writer, "playerName", slotData->playerName);
|
||||||
|
saveJsonWriterAddBoolArrayLinux(
|
||||||
|
&writer, "globalItemCollected", slotData->globalItemCollected,
|
||||||
|
SAVE_GLOBAL_ITEM_COUNT_MAX
|
||||||
|
);
|
||||||
|
saveJsonWriterAddUInt8ArrayLinux(
|
||||||
|
&writer, "storyFlags", slotData->storyFlags, SAVE_STORY_FLAG_COUNT_MAX
|
||||||
);
|
);
|
||||||
|
|
||||||
FILE *f = fopen(path, "wb");
|
errorret_t ret = saveJsonWriterSaveLinux(&writer, path);
|
||||||
if(!f) {
|
saveJsonWriterDisposeLinux(&writer);
|
||||||
errorThrow("Failed to open save file for writing: slot %u", (uint32_t)slot);
|
errorChain(ret);
|
||||||
}
|
|
||||||
|
|
||||||
size_t written = fwrite(file, sizeof(savefile_t), 1, f);
|
|
||||||
fclose(f);
|
|
||||||
|
|
||||||
if(written != 1) {
|
|
||||||
errorThrow("Failed to write save data for slot %u", (uint32_t)slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
slotData->exists = true;
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveDeleteLinux(const uint8_t slot) {
|
errorret_t saveDeleteSlotLinux(const uint8_t slot) {
|
||||||
char_t path[SAVE_LINUX_PATH_MAX];
|
char_t path[SAVE_LINUX_PATH_MAX];
|
||||||
snprintf(path, SAVE_LINUX_PATH_MAX, SAVE_LINUX_FILE_FORMAT,
|
_saveSlotPathLinux(path, SAVE_LINUX_PATH_MAX, slot);
|
||||||
SAVE.platform.savePath, (uint32_t)slot
|
|
||||||
);
|
|
||||||
|
|
||||||
if(remove(path) != 0 && errno != ENOENT) {
|
if(remove(path) != 0 && errno != ENOENT) {
|
||||||
errorThrow("Failed to delete save file for slot %u", (uint32_t)slot);
|
errorThrow("Failed to delete save slot %u: %s", (uint32_t)slot, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errorret_t saveMetaLoadLinux(savemeta_t *out) {
|
||||||
|
char_t path[SAVE_LINUX_PATH_MAX];
|
||||||
|
_saveMetaPathLinux(path, SAVE_LINUX_PATH_MAX);
|
||||||
|
|
||||||
|
yyjson_doc *doc;
|
||||||
|
yyjson_val *root;
|
||||||
|
bool_t found;
|
||||||
|
errorret_t ret = saveJsonReaderOpenLinux(path, &doc, &root, &found);
|
||||||
|
if(errorIsNotOk(ret)) { yyjson_doc_free(doc); errorChain(ret); }
|
||||||
|
if(!found) errorOk();
|
||||||
|
|
||||||
|
out->version = saveJsonReadUInt32Linux(root, "version", SAVE_META_VERSION);
|
||||||
|
out->deadzone = saveJsonReadFloatLinux(
|
||||||
|
root, "deadzone", SAVE_META_DEADZONE_DEFAULT
|
||||||
|
);
|
||||||
|
out->language = saveJsonReadUInt8Linux(
|
||||||
|
root, "language", SAVE_META_LANGUAGE_DEFAULT
|
||||||
|
);
|
||||||
|
out->exists = true;
|
||||||
|
|
||||||
|
yyjson_doc_free(doc);
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t saveMetaWriteLinux(savemeta_t *meta) {
|
||||||
|
char_t path[SAVE_LINUX_PATH_MAX];
|
||||||
|
_saveMetaPathLinux(path, SAVE_LINUX_PATH_MAX);
|
||||||
|
|
||||||
|
meta->version = SAVE_META_VERSION;
|
||||||
|
|
||||||
|
savejsonwriterlinux_t writer;
|
||||||
|
errorChain(saveJsonWriterInitLinux(&writer));
|
||||||
|
saveJsonWriterAddUInt32Linux(&writer, "version", meta->version);
|
||||||
|
saveJsonWriterAddFloatLinux(&writer, "deadzone", meta->deadzone);
|
||||||
|
saveJsonWriterAddUInt8Linux(&writer, "language", meta->language);
|
||||||
|
|
||||||
|
errorret_t ret = saveJsonWriterSaveLinux(&writer, path);
|
||||||
|
saveJsonWriterDisposeLinux(&writer);
|
||||||
|
errorChain(ret);
|
||||||
|
|
||||||
|
meta->exists = true;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,10 +7,12 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "save/savefile.h"
|
#include "save/saveslot.h"
|
||||||
|
#include "save/savemeta.h"
|
||||||
|
|
||||||
#define SAVE_LINUX_PATH_MAX FILENAME_MAX
|
#define SAVE_LINUX_PATH_MAX FILENAME_MAX
|
||||||
#define SAVE_LINUX_FILE_FORMAT "%s/save_%u.dat"
|
#define SAVE_LINUX_SLOT_FILE_FORMAT "%s/slot%u.json"
|
||||||
|
#define SAVE_LINUX_META_FILE_FORMAT "%s/settings.json"
|
||||||
|
|
||||||
#ifndef SAVE_LINUX_PATH
|
#ifndef SAVE_LINUX_PATH
|
||||||
#define SAVE_LINUX_PATH "./saves"
|
#define SAVE_LINUX_PATH "./saves"
|
||||||
@@ -21,7 +23,8 @@ typedef struct {
|
|||||||
} savelinux_t;
|
} savelinux_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the save system on Linux.
|
* Initializes the save system on Linux - ensures the save directory
|
||||||
|
* exists (shared by both save slots and meta).
|
||||||
*
|
*
|
||||||
* @return An error code if initialization fails.
|
* @return An error code if initialization fails.
|
||||||
*/
|
*/
|
||||||
@@ -35,27 +38,43 @@ errorret_t saveInitLinux(void);
|
|||||||
errorret_t saveDisposeLinux(void);
|
errorret_t saveDisposeLinux(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads a save file from disk for the given slot.
|
* Loads a save slot as JSON (slotN.json) from disk, if it exists.
|
||||||
*
|
*
|
||||||
* @param slot The save slot index.
|
* @param slot The save slot index.
|
||||||
* @param file Output save file data.
|
* @param out Output slot data.
|
||||||
* @return An error code if the load fails.
|
* @return An error code if the slot exists but fails to parse.
|
||||||
*/
|
*/
|
||||||
errorret_t saveLoadLinux(const uint8_t slot, savefile_t *file);
|
errorret_t saveSlotLoadLinux(const uint8_t slot, saveslot_t *out);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes a save file to disk for the given slot.
|
* Writes a save slot as JSON (slotN.json) to disk.
|
||||||
*
|
*
|
||||||
* @param slot The save slot index.
|
* @param slot The save slot index.
|
||||||
* @param file Save file data to write.
|
* @param slotData Slot data to write.
|
||||||
* @return An error code if the write fails.
|
* @return An error code if the write fails.
|
||||||
*/
|
*/
|
||||||
errorret_t saveWriteLinux(const uint8_t slot, const savefile_t *file);
|
errorret_t saveSlotWriteLinux(const uint8_t slot, saveslot_t *slotData);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the save file for the given slot from disk.
|
* Deletes the save slot JSON file for the given index.
|
||||||
*
|
*
|
||||||
* @param slot The save slot index.
|
* @param slot The save slot index.
|
||||||
* @return An error code if the delete fails.
|
* @return An error code if the delete fails.
|
||||||
*/
|
*/
|
||||||
errorret_t saveDeleteLinux(const uint8_t slot);
|
errorret_t saveDeleteSlotLinux(const uint8_t slot);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads save meta as JSON (settings.json) from disk, if it exists.
|
||||||
|
*
|
||||||
|
* @param out Output meta data.
|
||||||
|
* @return An error code if the file exists but fails to parse.
|
||||||
|
*/
|
||||||
|
errorret_t saveMetaLoadLinux(savemeta_t *out);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes save meta as JSON (settings.json) to disk.
|
||||||
|
*
|
||||||
|
* @param meta Meta data to write.
|
||||||
|
* @return An error code if the write fails.
|
||||||
|
*/
|
||||||
|
errorret_t saveMetaWriteLinux(savemeta_t *meta);
|
||||||
|
|||||||
@@ -7,24 +7,21 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "save/savelinux.h"
|
#include "save/savelinux.h"
|
||||||
#include "save/savestreamlinux.h"
|
|
||||||
|
|
||||||
typedef savelinux_t saveplatform_t;
|
typedef savelinux_t saveplatform_t;
|
||||||
typedef savestreamlinux_t saveplatformstream_t;
|
// Linux fully overrides every save operation with JSON I/O (see
|
||||||
|
// savelinux.c/savejsonlinux.c) - nothing generic ever opens a
|
||||||
|
// savestream_t here, so this only needs to exist for that shared type to
|
||||||
|
// compile.
|
||||||
|
typedef struct {
|
||||||
|
uint8_t reserved;
|
||||||
|
} saveplatformstream_t;
|
||||||
|
|
||||||
#define saveInitPlatform saveInitLinux
|
#define saveInitPlatform saveInitLinux
|
||||||
#define saveDisposePlatform saveDisposeLinux
|
#define saveDisposePlatform saveDisposeLinux
|
||||||
#define saveDeletePlatform saveDeleteLinux
|
|
||||||
|
|
||||||
#define saveStreamOpenReadPlatform(stream, slot) \
|
#define saveSlotDeletePlatform saveDeleteSlotLinux
|
||||||
saveStreamOpenReadLinux(&(stream)->platform, &(stream)->found, slot)
|
#define saveSlotLoadPlatform saveSlotLoadLinux
|
||||||
#define saveStreamOpenWritePlatform(stream, slot) \
|
#define saveSlotWritePlatform saveSlotWriteLinux
|
||||||
saveStreamOpenWriteLinux(&(stream)->platform, slot)
|
#define saveMetaLoadPlatform saveMetaLoadLinux
|
||||||
#define saveStreamClosePlatform(stream) \
|
#define saveMetaWritePlatform saveMetaWriteLinux
|
||||||
saveStreamCloseLinux(&(stream)->platform)
|
|
||||||
#define saveStreamReadBytesPlatform(stream, buf, len) \
|
|
||||||
saveStreamReadBytesLinux(&(stream)->platform, buf, len)
|
|
||||||
#define saveStreamWriteBytesPlatform(stream, buf, len) \
|
|
||||||
saveStreamWriteBytesLinux(&(stream)->platform, buf, len)
|
|
||||||
#define saveStreamSeekPlatform(stream, pos) \
|
|
||||||
saveStreamSeekLinux(&(stream)->platform, pos)
|
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "save/save.h"
|
|
||||||
#include "save/savestreamlinux.h"
|
|
||||||
#include "util/string.h"
|
|
||||||
#include <sys/stat.h>
|
|
||||||
#include <errno.h>
|
|
||||||
|
|
||||||
static void _saveStreamGetPath(
|
|
||||||
char_t *out, const size_t max, const uint8_t slot
|
|
||||||
) {
|
|
||||||
snprintf(
|
|
||||||
out, max, SAVE_LINUX_FILE_FORMAT,
|
|
||||||
SAVE.platform.savePath, (uint32_t)slot
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t saveStreamOpenReadLinux(
|
|
||||||
savestreamlinux_t *p, bool_t *found, const uint8_t slot
|
|
||||||
) {
|
|
||||||
char_t path[SAVE_LINUX_PATH_MAX];
|
|
||||||
_saveStreamGetPath(path, SAVE_LINUX_PATH_MAX, slot);
|
|
||||||
|
|
||||||
p->file = fopen(path, "rb");
|
|
||||||
*found = (p->file != NULL);
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t saveStreamOpenWriteLinux(savestreamlinux_t *p, const uint8_t slot) {
|
|
||||||
char_t path[SAVE_LINUX_PATH_MAX];
|
|
||||||
_saveStreamGetPath(path, SAVE_LINUX_PATH_MAX, slot);
|
|
||||||
|
|
||||||
p->file = fopen(path, "wb");
|
|
||||||
if(!p->file) {
|
|
||||||
errorThrow("Failed to open save file for writing: slot %u", (uint32_t)slot);
|
|
||||||
}
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
void saveStreamCloseLinux(savestreamlinux_t *p) {
|
|
||||||
if(p->file) {
|
|
||||||
fclose(p->file);
|
|
||||||
p->file = NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t saveStreamReadBytesLinux(
|
|
||||||
savestreamlinux_t *p, void *buf, const size_t len
|
|
||||||
) {
|
|
||||||
if(fread(buf, 1, len, p->file) != len) {
|
|
||||||
errorThrow("Unexpected end of save file");
|
|
||||||
}
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t saveStreamWriteBytesLinux(
|
|
||||||
savestreamlinux_t *p, const void *buf, const size_t len
|
|
||||||
) {
|
|
||||||
if(fwrite(buf, 1, len, p->file) != len) {
|
|
||||||
errorThrow("Failed to write save data");
|
|
||||||
}
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t saveStreamSeekLinux(savestreamlinux_t *p, const size_t pos) {
|
|
||||||
if(fseek(p->file, (long)pos, SEEK_SET) != 0) {
|
|
||||||
errorThrow("Failed to seek in save file");
|
|
||||||
}
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 <stdio.h>
|
|
||||||
#include <stddef.h>
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
FILE *file;
|
|
||||||
} savestreamlinux_t;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens a save slot file for reading.
|
|
||||||
*
|
|
||||||
* @param p Stream to initialize.
|
|
||||||
* @param found Set to true if the file exists, false if it does not.
|
|
||||||
* @param slot Save slot index.
|
|
||||||
* @return An error if the open fails for a reason other than missing file.
|
|
||||||
*/
|
|
||||||
errorret_t saveStreamOpenReadLinux(
|
|
||||||
savestreamlinux_t *p, bool_t *found, const uint8_t slot
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens a save slot file for writing, creating or truncating it.
|
|
||||||
*
|
|
||||||
* @param p Stream to initialize.
|
|
||||||
* @param slot Save slot index.
|
|
||||||
* @return An error if the file cannot be opened for writing.
|
|
||||||
*/
|
|
||||||
errorret_t saveStreamOpenWriteLinux(
|
|
||||||
savestreamlinux_t *p, const uint8_t slot
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes the file handle held by the stream.
|
|
||||||
*
|
|
||||||
* @param p Stream to close.
|
|
||||||
*/
|
|
||||||
void saveStreamCloseLinux(savestreamlinux_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 saveStreamReadBytesLinux(
|
|
||||||
savestreamlinux_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 saveStreamWriteBytesLinux(
|
|
||||||
savestreamlinux_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 saveStreamSeekLinux(savestreamlinux_t *p, const size_t pos);
|
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "input/input.h"
|
#include "input/input.h"
|
||||||
|
#include "save/save.h"
|
||||||
|
|
||||||
// #define INPUT_PSP_GAMEPAD_BUTTON_ACCEPT INPUT_SDL2_GAMEPAD_BUTTON_CUSTOM
|
// #define INPUT_PSP_GAMEPAD_BUTTON_ACCEPT INPUT_SDL2_GAMEPAD_BUTTON_CUSTOM
|
||||||
// #define INPUT_PSP_GAMEPAD_BUTTON_CANCEL INPUT_SDL2_GAMEPAD_BUTTON_CUSTOM
|
// #define INPUT_PSP_GAMEPAD_BUTTON_CANCEL INPUT_SDL2_GAMEPAD_BUTTON_CUSTOM
|
||||||
@@ -94,5 +95,5 @@ errorret_t inputInitPSP(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
float_t inputGetDeadzoneSDL2(const inputbutton_t button) {
|
float_t inputGetDeadzoneSDL2(const inputbutton_t button) {
|
||||||
return 0.2f;
|
return saveGetMeta()->deadzone;
|
||||||
}
|
}
|
||||||
@@ -9,3 +9,11 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
savepsp.c
|
savepsp.c
|
||||||
savestreampsp.c
|
savestreampsp.c
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# PSP only needs one Dusk-side save slot - a future main-menu save picker
|
||||||
|
# will let players manage multiple named saves through the OS's own
|
||||||
|
# sceUtilitySavedata browser instead of Dusk maintaining its own numbered
|
||||||
|
# slots (see save/saveslot.h for the default used by every other platform).
|
||||||
|
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||||
|
SAVE_SLOT_COUNT_MAX=1
|
||||||
|
)
|
||||||
|
|||||||
@@ -14,17 +14,40 @@ typedef savestreampsp_t saveplatformstream_t;
|
|||||||
|
|
||||||
#define saveInitPlatform saveInitPSP
|
#define saveInitPlatform saveInitPSP
|
||||||
#define saveDisposePlatform saveDisposePSP
|
#define saveDisposePlatform saveDisposePSP
|
||||||
#define saveDeletePlatform saveDeletePSP
|
#define saveSlotDeletePlatform saveDeleteSlotPSP
|
||||||
|
|
||||||
#define saveStreamOpenReadPlatform(stream, slot) \
|
|
||||||
saveStreamOpenReadPSP(&(stream)->platform, &(stream)->found, slot)
|
|
||||||
#define saveStreamOpenWritePlatform(stream, slot) \
|
|
||||||
saveStreamOpenWritePSP(&(stream)->platform, slot)
|
|
||||||
#define saveStreamClosePlatform(stream) \
|
|
||||||
saveStreamClosePSP(&(stream)->platform)
|
|
||||||
#define saveStreamReadBytesPlatform(stream, buf, len) \
|
#define saveStreamReadBytesPlatform(stream, buf, len) \
|
||||||
saveStreamReadBytesPSP(&(stream)->platform, buf, len)
|
saveStreamReadBytesPSP(&(stream)->platform, buf, len)
|
||||||
#define saveStreamWriteBytesPlatform(stream, buf, len) \
|
#define saveStreamWriteBytesPlatform(stream, buf, len) \
|
||||||
saveStreamWriteBytesPSP(&(stream)->platform, buf, len)
|
saveStreamWriteBytesPSP(&(stream)->platform, buf, len)
|
||||||
#define saveStreamSeekPlatform(stream, pos) \
|
#define saveStreamSeekPlatform(stream, pos) \
|
||||||
saveStreamSeekPSP(&(stream)->platform, pos)
|
saveStreamSeekPSP(&(stream)->platform, pos)
|
||||||
|
#define saveStreamTellPlatform(stream, out) \
|
||||||
|
saveStreamTellPSP(&(stream)->platform, out)
|
||||||
|
|
||||||
|
// Save/load go entirely through the native sceUtilitySavedata dialog
|
||||||
|
// (savePSPBeginSave/Load), which spans multiple frames - these bypass
|
||||||
|
// save.c's normal synchronous open/write-fields/close flow above (that's
|
||||||
|
// still used internally, just against an in-memory buffer, from within
|
||||||
|
// savePSPBeginSave/Load themselves) and are what save.c's saveWriteSlot()/
|
||||||
|
// saveLoadSlot()/saveWriteMeta()/saveLoadMeta() actually call on this
|
||||||
|
// platform. Meta and the (one) slot are serialized together into the same
|
||||||
|
// buffer - there is no separate meta-only path on PSP - so the meta
|
||||||
|
// variants just drive the same dialog against SAVE_ACTIVE_SLOT.
|
||||||
|
#define saveSlotAsyncWritePlatform(slot, onComplete, user) \
|
||||||
|
savePSPBeginSave(slot, onComplete, user)
|
||||||
|
#define saveSlotAsyncLoadPlatform(slot, onComplete, user) \
|
||||||
|
savePSPBeginLoad(slot, onComplete, user)
|
||||||
|
#define saveMetaAsyncWritePlatform(onComplete, user) \
|
||||||
|
savePSPBeginSave(SAVE_ACTIVE_SLOT, onComplete, user)
|
||||||
|
#define saveMetaAsyncLoadPlatform(onComplete, user) \
|
||||||
|
savePSPBeginLoad(SAVE_ACTIVE_SLOT, onComplete, user)
|
||||||
|
#define saveIsBusyPlatform() savePSPIsBusy()
|
||||||
|
#define savePlatformUpdate() savePSPUpdate()
|
||||||
|
|
||||||
|
// Meta only reaches memory via the native dialog now (folded into the same
|
||||||
|
// payload as the save slot) - running that multi-frame dialog on every
|
||||||
|
// single boot just to eagerly populate SAVE.meta would reintroduce the
|
||||||
|
// exact UX problem a lightweight settings-only file used to avoid, so
|
||||||
|
// saveInit() skips eager loading entirely on this platform.
|
||||||
|
#define saveSkipEagerLoadPlatform
|
||||||
|
|||||||
+265
-55
@@ -6,8 +6,38 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "save/save.h"
|
#include "save/save.h"
|
||||||
|
#include "save/savepsp.h"
|
||||||
|
#include "save/savestream.h"
|
||||||
|
#include "system/systempsp.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
|
||||||
|
static void savePSPParamCommonInit(SceUtilitySavedataParam *param) {
|
||||||
|
memoryZero(param, sizeof(SceUtilitySavedataParam));
|
||||||
|
param->base.size = sizeof(SceUtilitySavedataParam);
|
||||||
|
param->base.language = systemPSPGetLanguage();
|
||||||
|
param->base.buttonSwap = systemPSPGetCrossButtonSetting();
|
||||||
|
param->base.graphicsThread = 17;
|
||||||
|
param->base.accessThread = 19;
|
||||||
|
param->base.fontThread = 18;
|
||||||
|
param->base.soundThread = 16;
|
||||||
|
|
||||||
|
stringCopy(param->gameName, SAVE_PSP_GAME_NAME, sizeof(param->gameName));
|
||||||
|
stringCopy(param->fileName, SAVE_PSP_FILE_NAME, sizeof(param->fileName));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void savePSPSaveNameForSlot(
|
||||||
|
char_t *out, const size_t max, const uint8_t slot
|
||||||
|
) {
|
||||||
|
stringFormat(out, max, "%02u", (uint32_t)slot);
|
||||||
|
}
|
||||||
|
|
||||||
errorret_t saveInitPSP(void) {
|
errorret_t saveInitPSP(void) {
|
||||||
|
SceIoStat stat;
|
||||||
|
if(sceIoGetstat(SAVE_PSP_ROOT, &stat) < 0) {
|
||||||
|
errorThrow("No memory stick detected");
|
||||||
|
}
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,66 +45,246 @@ errorret_t saveDisposePSP(void) {
|
|||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveLoadPSP(const uint8_t slot, savefile_t *file) {
|
errorret_t saveDeleteSlotPSP(const uint8_t slot) {
|
||||||
char_t path[SAVE_PSP_PATH_MAX];
|
char_t path[SAVE_PSP_PATH_MAX];
|
||||||
snprintf(path, SAVE_PSP_PATH_MAX, SAVE_PSP_FILE_FORMAT,
|
stringFormat(
|
||||||
SAVE_PSP_TITLE_ID, (uint32_t)slot
|
path, sizeof(path), SAVE_PSP_FILE_FORMAT, SAVE_PSP_GAME_NAME,
|
||||||
);
|
(uint32_t)slot
|
||||||
|
|
||||||
SceUID fd = sceIoOpen(path, PSP_O_RDONLY, 0);
|
|
||||||
if(fd < 0) {
|
|
||||||
file->exists = false;
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t read = sceIoRead(fd, file, sizeof(savefile_t));
|
|
||||||
sceIoClose(fd);
|
|
||||||
|
|
||||||
if(read != (int32_t)sizeof(savefile_t)) {
|
|
||||||
file->exists = false;
|
|
||||||
errorThrow("Failed to read save data for slot %u", (uint32_t)slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
file->exists = true;
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t saveWritePSP(const uint8_t slot, const savefile_t *file) {
|
|
||||||
char_t dir[SAVE_PSP_PATH_MAX];
|
|
||||||
snprintf(dir, SAVE_PSP_PATH_MAX, SAVE_PSP_DIR_FORMAT,
|
|
||||||
SAVE_PSP_TITLE_ID, (uint32_t)slot
|
|
||||||
);
|
|
||||||
sceIoMkdir(dir, 0777);
|
|
||||||
|
|
||||||
char_t path[SAVE_PSP_PATH_MAX];
|
|
||||||
snprintf(path, SAVE_PSP_PATH_MAX, SAVE_PSP_FILE_FORMAT,
|
|
||||||
SAVE_PSP_TITLE_ID, (uint32_t)slot
|
|
||||||
);
|
|
||||||
|
|
||||||
SceUID fd = sceIoOpen(path, PSP_O_WRONLY | PSP_O_CREAT | PSP_O_TRUNC, 0777);
|
|
||||||
if(fd < 0) {
|
|
||||||
errorThrow("Failed to open save file for writing: slot %u", (uint32_t)slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t written = sceIoWrite(fd, file, sizeof(savefile_t));
|
|
||||||
sceIoClose(fd);
|
|
||||||
|
|
||||||
if(written != (int32_t)sizeof(savefile_t)) {
|
|
||||||
errorThrow("Failed to write save data for slot %u", (uint32_t)slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t saveDeletePSP(const uint8_t slot) {
|
|
||||||
char_t path[SAVE_PSP_PATH_MAX];
|
|
||||||
snprintf(path, SAVE_PSP_PATH_MAX, SAVE_PSP_FILE_FORMAT,
|
|
||||||
SAVE_PSP_TITLE_ID, (uint32_t)slot
|
|
||||||
);
|
);
|
||||||
|
|
||||||
int32_t result = sceIoRemove(path);
|
int32_t result = sceIoRemove(path);
|
||||||
if(result < 0 && result != (int32_t)0x80010002) {
|
if(result < 0 && result != (int32_t)0x80010002) {
|
||||||
errorThrow("Failed to delete save file for slot %u", (uint32_t)slot);
|
errorThrow("Failed to delete save data for slot %u", (uint32_t)slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
char_t dir[SAVE_PSP_PATH_MAX];
|
||||||
|
stringFormat(
|
||||||
|
dir, sizeof(dir), "ms0:/PSP/SAVEDATA/%s%02u", SAVE_PSP_GAME_NAME,
|
||||||
|
(uint32_t)slot
|
||||||
|
);
|
||||||
|
char_t sfoPath[SAVE_PSP_PATH_MAX];
|
||||||
|
stringFormat(sfoPath, sizeof(sfoPath), "%s/PARAM.SFO", dir);
|
||||||
|
// Best-effort - PARAM.SFO/the directory itself may not exist (e.g. this
|
||||||
|
// slot was written by the old raw-file format, pre-dating this dialog-
|
||||||
|
// based rewrite) or the directory may still contain other entries.
|
||||||
|
sceIoRemove(sfoPath);
|
||||||
|
sceIoRmdir(dir);
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
void savePSPBeginSave(
|
||||||
|
const uint8_t slot, savecallback_t onComplete, void *user
|
||||||
|
) {
|
||||||
|
assertNotNull(onComplete, "onComplete cannot be NULL");
|
||||||
|
assertTrue(SAVE.platform.op == SAVE_PSP_OP_NONE, "Save already in progress");
|
||||||
|
|
||||||
|
saveslot_t *slotData = &SAVE.slots[slot];
|
||||||
|
|
||||||
|
// Serialize meta then the slot into the buffer synchronously (plain
|
||||||
|
// memory writes, same header/version/CRC framing as every other
|
||||||
|
// platform) before the dialog ever starts - only the actual commit-to-
|
||||||
|
// storage step needs to wait on the dialog.
|
||||||
|
savestream_t stream;
|
||||||
|
memoryZero(&stream, sizeof(savestream_t));
|
||||||
|
stream.platform.buffer = SAVE.platform.dataBuffer;
|
||||||
|
stream.platform.bufferSize = sizeof(SAVE.platform.dataBuffer);
|
||||||
|
|
||||||
|
errorret_t ret = saveMetaSerializeWrite(&stream, &SAVE.meta);
|
||||||
|
if(errorIsOk(ret)) ret = saveSlotSerializeWrite(&stream, slotData);
|
||||||
|
if(errorIsNotOk(ret)) {
|
||||||
|
onComplete(ret, user);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SAVE.platform.dataLength = stream.platform.length;
|
||||||
|
|
||||||
|
SceUtilitySavedataParam *param = &SAVE.platform.param;
|
||||||
|
savePSPParamCommonInit(param);
|
||||||
|
// AUTOSAVE rather than SAVE - SAVE shows a "save to this data?" confirm
|
||||||
|
// screen even for a slot with no existing data, which isn't the UX we
|
||||||
|
// want for a menu-triggered "Save" action (that confirmation already
|
||||||
|
// happened when the player chose to save). AUTOSAVE writes silently
|
||||||
|
// (just a brief "saving" icon flash) while still generating the same
|
||||||
|
// PARAM.SFO/title/description as any other mode.
|
||||||
|
param->mode = PSP_UTILITY_SAVEDATA_AUTOSAVE;
|
||||||
|
param->overwrite = 1;
|
||||||
|
savePSPSaveNameForSlot(param->saveName, sizeof(param->saveName), slot);
|
||||||
|
|
||||||
|
param->dataBuf = SAVE.platform.dataBuffer;
|
||||||
|
param->dataBufSize = sizeof(SAVE.platform.dataBuffer);
|
||||||
|
param->dataSize = SAVE.platform.dataLength;
|
||||||
|
|
||||||
|
// No ICON0/PIC1/SND0 art exists in this project yet, so these are left
|
||||||
|
// zeroed (bufSize 0) - the utility treats that as "no icon/background/
|
||||||
|
// sound" rather than an error. title/savedataTitle/detail are still
|
||||||
|
// fully functional and are what actually populates PARAM.SFO and the
|
||||||
|
// save browser entry.
|
||||||
|
stringCopy(param->sfoParam.title, "Dusk", sizeof(param->sfoParam.title));
|
||||||
|
stringCopy(
|
||||||
|
param->sfoParam.savedataTitle, slotData->playerName,
|
||||||
|
sizeof(param->sfoParam.savedataTitle)
|
||||||
|
);
|
||||||
|
stringCopy(
|
||||||
|
param->sfoParam.detail, "Dusk save file.", sizeof(param->sfoParam.detail)
|
||||||
|
);
|
||||||
|
|
||||||
|
int32_t initRet = sceUtilitySavedataInitStart(param);
|
||||||
|
if(initRet < 0) {
|
||||||
|
onComplete(errorThrowImpl(
|
||||||
|
&SAVE.errorState, ERROR_NOT_OK, __FILE__, __func__, __LINE__,
|
||||||
|
"Failed to start save dialog: 0x%08X", initRet
|
||||||
|
), user);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SAVE.platform.op = SAVE_PSP_OP_SAVE;
|
||||||
|
SAVE.platform.slot = slot;
|
||||||
|
SAVE.platform.onComplete = onComplete;
|
||||||
|
SAVE.platform.onCompleteUser = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
void savePSPBeginLoad(
|
||||||
|
const uint8_t slot, savecallback_t onComplete, void *user
|
||||||
|
) {
|
||||||
|
assertNotNull(onComplete, "onComplete cannot be NULL");
|
||||||
|
assertTrue(SAVE.platform.op == SAVE_PSP_OP_NONE, "Save already in progress");
|
||||||
|
|
||||||
|
char_t path[SAVE_PSP_PATH_MAX];
|
||||||
|
stringFormat(
|
||||||
|
path, sizeof(path), SAVE_PSP_FILE_FORMAT, SAVE_PSP_GAME_NAME,
|
||||||
|
(uint32_t)slot
|
||||||
|
);
|
||||||
|
|
||||||
|
SceIoStat stat;
|
||||||
|
if(sceIoGetstat(path, &stat) < 0) {
|
||||||
|
// No save data yet - not an error (matches every other platform's
|
||||||
|
// "nothing to load yet" behavior), and deliberately skips showing the
|
||||||
|
// dialog at all rather than surfacing an empty "no data" native
|
||||||
|
// screen for data the player has never saved.
|
||||||
|
onComplete(errorOkImpl(), user);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SceUtilitySavedataParam *param = &SAVE.platform.param;
|
||||||
|
savePSPParamCommonInit(param);
|
||||||
|
param->mode = PSP_UTILITY_SAVEDATA_AUTOLOAD;// See savePSPBeginSave().
|
||||||
|
savePSPSaveNameForSlot(param->saveName, sizeof(param->saveName), slot);
|
||||||
|
|
||||||
|
param->dataBuf = SAVE.platform.dataBuffer;
|
||||||
|
param->dataBufSize = sizeof(SAVE.platform.dataBuffer);
|
||||||
|
|
||||||
|
int32_t initRet = sceUtilitySavedataInitStart(param);
|
||||||
|
if(initRet < 0) {
|
||||||
|
onComplete(errorThrowImpl(
|
||||||
|
&SAVE.errorState, ERROR_NOT_OK, __FILE__, __func__, __LINE__,
|
||||||
|
"Failed to start load dialog: 0x%08X", initRet
|
||||||
|
), user);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SAVE.platform.op = SAVE_PSP_OP_LOAD;
|
||||||
|
SAVE.platform.slot = slot;
|
||||||
|
SAVE.platform.onComplete = onComplete;
|
||||||
|
SAVE.platform.onCompleteUser = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t savePSPIsBusy(void) {
|
||||||
|
return SAVE.platform.op != SAVE_PSP_OP_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t savePSPUpdate(void) {
|
||||||
|
if(SAVE.platform.op == SAVE_PSP_OP_NONE) errorOk();
|
||||||
|
|
||||||
|
int32_t status = sceUtilitySavedataGetStatus();
|
||||||
|
switch(status) {
|
||||||
|
case PSP_UTILITY_DIALOG_INIT:
|
||||||
|
break;
|
||||||
|
|
||||||
|
// NOTE: unlike the netconf dialog, this does not replicate Dusk's own
|
||||||
|
// GL state (blend/cull/depth + texture/color) before calling Update().
|
||||||
|
// A prior fix for exactly that class of bug was documented for the
|
||||||
|
// network dialog, but no longer exists in the current codebase to
|
||||||
|
// copy from - if the save dialog's own text/icons don't render
|
||||||
|
// correctly on real hardware (PPSSPP won't reproduce this - it doesn't
|
||||||
|
// model pspGL's deferred state application), that state-priming
|
||||||
|
// pattern is the fix to reach for. See the network dialog's git
|
||||||
|
// history / the project's PSP dialog memory notes for the exact
|
||||||
|
// technique (state flags + a forced flush via a degenerate triangle
|
||||||
|
// draw).
|
||||||
|
case PSP_UTILITY_DIALOG_VISIBLE:
|
||||||
|
// sceUtilitySavedataUpdate() is void, unlike sceUtilityNetconfUpdate()
|
||||||
|
// - nothing to check here, GetStatus() next frame reflects any
|
||||||
|
// resulting state change.
|
||||||
|
sceUtilitySavedataUpdate(1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PSP_UTILITY_DIALOG_QUIT:
|
||||||
|
// The save/load operation itself has already finished (successfully
|
||||||
|
// or not) - this just starts tearing the dialog down. The actual
|
||||||
|
// result is read once that teardown settles, below - don't call
|
||||||
|
// ShutdownStart more than once while waiting for it to.
|
||||||
|
if(!SAVE.platform.shuttingDown) {
|
||||||
|
SAVE.platform.shuttingDown = true;
|
||||||
|
sceUtilitySavedataShutdownStart();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Confirmed under PPSSPP: status settles straight from QUIT to NONE,
|
||||||
|
// without FINISHED ever being separately observed in between - so
|
||||||
|
// both are treated identically here as "torn down, read the result",
|
||||||
|
// and it's shuttingDown (not which of these two codes we saw) that
|
||||||
|
// distinguishes that from a genuine disappearance.
|
||||||
|
case PSP_UTILITY_DIALOG_FINISHED:
|
||||||
|
case PSP_UTILITY_DIALOG_NONE: {
|
||||||
|
savepspop_t op = SAVE.platform.op;
|
||||||
|
uint8_t slot = SAVE.platform.slot;
|
||||||
|
savecallback_t cb = SAVE.platform.onComplete;
|
||||||
|
void *user = SAVE.platform.onCompleteUser;
|
||||||
|
bool_t reachedQuit = SAVE.platform.shuttingDown;
|
||||||
|
SAVE.platform.op = SAVE_PSP_OP_NONE;
|
||||||
|
SAVE.platform.shuttingDown = false;
|
||||||
|
|
||||||
|
if(!reachedQuit) {
|
||||||
|
cb(errorThrowImpl(
|
||||||
|
&SAVE.errorState, ERROR_NOT_OK, __FILE__, __func__, __LINE__,
|
||||||
|
"Save dialog disappeared without a result"
|
||||||
|
), user);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t result = SAVE.platform.param.base.result;
|
||||||
|
if(result != 0) {
|
||||||
|
SAVE.available = false;
|
||||||
|
cb(errorThrowImpl(
|
||||||
|
&SAVE.errorState, ERROR_NOT_OK, __FILE__, __func__, __LINE__,
|
||||||
|
"Save dialog failed: 0x%08X", result
|
||||||
|
), user);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
SAVE.available = true;
|
||||||
|
|
||||||
|
if(op == SAVE_PSP_OP_LOAD) {
|
||||||
|
savestream_t stream;
|
||||||
|
memoryZero(&stream, sizeof(savestream_t));
|
||||||
|
stream.platform.buffer = SAVE.platform.dataBuffer;
|
||||||
|
stream.platform.bufferSize = sizeof(SAVE.platform.dataBuffer);
|
||||||
|
stream.platform.length = SAVE.platform.param.dataSize;
|
||||||
|
|
||||||
|
errorret_t ret = saveMetaSerializeRead(&stream, &SAVE.meta);
|
||||||
|
if(errorIsOk(ret)) {
|
||||||
|
ret = saveSlotSerializeRead(&stream, &SAVE.slots[slot]);
|
||||||
|
}
|
||||||
|
cb(ret, user);
|
||||||
|
} else {
|
||||||
|
SAVE.meta.exists = true;
|
||||||
|
SAVE.slots[slot].exists = true;
|
||||||
|
cb(errorOkImpl(), user);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
errorThrow("Unknown savedata dialog status: %d", status);
|
||||||
}
|
}
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
|
|||||||
+99
-29
@@ -7,25 +7,60 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "save/savefile.h"
|
#include "save/saveslot.h"
|
||||||
|
#include "save/savemeta.h"
|
||||||
#include <pspiofilemgr.h>
|
#include <pspiofilemgr.h>
|
||||||
|
#include <psputility.h>
|
||||||
|
|
||||||
#define SAVE_PSP_PATH_MAX 256
|
#define SAVE_PSP_PATH_MAX 256
|
||||||
#define SAVE_PSP_FILE_FORMAT "ms0:/PSP/SAVEDATA/%s%02u/save.dat"
|
#define SAVE_PSP_ROOT "ms0:/"
|
||||||
#define SAVE_PSP_DIR_FORMAT "ms0:/PSP/SAVEDATA/%s%02u"
|
#define SAVE_PSP_FILE_NAME "save.bin"
|
||||||
|
#define SAVE_PSP_FILE_FORMAT "ms0:/PSP/SAVEDATA/%s%02u/" SAVE_PSP_FILE_NAME
|
||||||
|
#define SAVE_PSP_DATA_BUFFER_SIZE 4096
|
||||||
|
|
||||||
#ifndef SAVE_PSP_TITLE_ID
|
#ifndef SAVE_PSP_GAME_NAME
|
||||||
#define SAVE_PSP_TITLE_ID "DUSK00001"
|
#define SAVE_PSP_GAME_NAME "DUSK00001"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
SAVE_PSP_OP_NONE,
|
||||||
|
SAVE_PSP_OP_SAVE,
|
||||||
|
SAVE_PSP_OP_LOAD
|
||||||
|
} savepspop_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t unused;
|
SceUtilitySavedataParam param;
|
||||||
|
// Raw buffer sceUtilitySavedata reads/writes the whole save into/from -
|
||||||
|
// holds BOTH save meta and the (single) save slot back-to-back,
|
||||||
|
// populated by our own savestream_t serialization (see savestreampsp.h)
|
||||||
|
// before a save starts, and deserialized from after a load finishes.
|
||||||
|
// Meta lives in here rather than its own lightweight file specifically
|
||||||
|
// because a device-wide preference change is meant to feel like a real
|
||||||
|
// save on this platform (a brief native icon flash), not need its own
|
||||||
|
// separate storage mechanism.
|
||||||
|
uint8_t dataBuffer[SAVE_PSP_DATA_BUFFER_SIZE] __attribute__((aligned(64)));
|
||||||
|
size_t dataLength;
|
||||||
|
|
||||||
|
savepspop_t op;
|
||||||
|
// True once sceUtilitySavedataShutdownStart() has been requested (dialog
|
||||||
|
// status PSP_UTILITY_DIALOG_QUIT seen) - distinguishes a normal "torn
|
||||||
|
// down after finishing" NONE/FINISHED from a genuinely unexpected one
|
||||||
|
// seen before ever reaching QUIT. Some implementations (confirmed on
|
||||||
|
// PPSSPP) settle straight to NONE after shutdown without a separately
|
||||||
|
// observable FINISHED step in between.
|
||||||
|
bool_t shuttingDown;
|
||||||
|
uint8_t slot;
|
||||||
|
savecallback_t onComplete;
|
||||||
|
void *onCompleteUser;
|
||||||
} savepsp_t;
|
} savepsp_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the save system on PSP.
|
* Initializes the save system on PSP. Confirms the memory stick is
|
||||||
|
* actually reachable (sceIoGetstat on SAVE_PSP_ROOT) rather than assuming
|
||||||
|
* so, since the savedata dialog otherwise only reports failure once a
|
||||||
|
* save/load is actually attempted.
|
||||||
*
|
*
|
||||||
* @return An error code if initialization fails.
|
* @return An error code if no memory stick is reachable.
|
||||||
*/
|
*/
|
||||||
errorret_t saveInitPSP(void);
|
errorret_t saveInitPSP(void);
|
||||||
|
|
||||||
@@ -37,27 +72,62 @@ errorret_t saveInitPSP(void);
|
|||||||
errorret_t saveDisposePSP(void);
|
errorret_t saveDisposePSP(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads a save file from PSP save data for the given slot.
|
* Deletes the (one) save data folder from the memory stick.
|
||||||
*
|
*
|
||||||
* @param slot The save slot index.
|
* @param slot The save slot index (always 0 on PSP - see
|
||||||
* @param file Output save file data.
|
* SAVE_SLOT_COUNT_MAX's override in this platform's CMakeLists.txt).
|
||||||
* @return An error code if the load fails.
|
|
||||||
*/
|
|
||||||
errorret_t saveLoadPSP(const uint8_t slot, savefile_t *file);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes a save file to PSP save data for the given slot.
|
|
||||||
*
|
|
||||||
* @param slot The save slot index.
|
|
||||||
* @param file Save file data to write.
|
|
||||||
* @return An error code if the write fails.
|
|
||||||
*/
|
|
||||||
errorret_t saveWritePSP(const uint8_t slot, const savefile_t *file);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the save file for the given slot from PSP save data.
|
|
||||||
*
|
|
||||||
* @param slot The save slot index.
|
|
||||||
* @return An error code if the delete fails.
|
* @return An error code if the delete fails.
|
||||||
*/
|
*/
|
||||||
errorret_t saveDeletePSP(const uint8_t slot);
|
errorret_t saveDeleteSlotPSP(const uint8_t slot);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a save via the native sceUtilitySavedata dialog (mode AUTOSAVE -
|
||||||
|
* writes silently with just a brief icon flash, no confirm screen, since
|
||||||
|
* SAVE mode shows one even for a slot with no existing data - but
|
||||||
|
* PARAM.SFO/title/description are generated identically regardless of
|
||||||
|
* mode, and the OS handles the save browser entry either way). Serializes
|
||||||
|
* SAVE.meta then SAVE.slots[slot] into SAVE.platform.dataBuffer first,
|
||||||
|
* synchronously, then kicks off the dialog and returns - completion is
|
||||||
|
* reported later via onComplete, driven by savePSPUpdate() each frame.
|
||||||
|
* If no save data exists yet, sceUtilitySavedataInitStart() creates it.
|
||||||
|
*
|
||||||
|
* @param slot The save slot index.
|
||||||
|
* @param onComplete Callback invoked once the dialog finishes.
|
||||||
|
* @param user User data passed through to onComplete.
|
||||||
|
*/
|
||||||
|
void savePSPBeginSave(
|
||||||
|
const uint8_t slot, savecallback_t onComplete, void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a load via the native sceUtilitySavedata dialog (mode AUTOLOAD -
|
||||||
|
* see savePSPBeginSave() for why not the plain LOAD mode) unless a quick
|
||||||
|
* sceIoGetstat check finds no save data yet - in which case onComplete is
|
||||||
|
* invoked immediately with SAVE.meta/SAVE.slots[slot].exists left false,
|
||||||
|
* matching the other platforms' "no file yet" semantics, and no dialog is
|
||||||
|
* shown at all.
|
||||||
|
*
|
||||||
|
* @param slot The save slot index.
|
||||||
|
* @param onComplete Callback invoked once the dialog (or immediate
|
||||||
|
* not-found short-circuit) finishes.
|
||||||
|
* @param user User data passed through to onComplete.
|
||||||
|
*/
|
||||||
|
void savePSPBeginLoad(
|
||||||
|
const uint8_t slot, savecallback_t onComplete, void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pumps the in-progress save/load dialog one step, if any - must be called
|
||||||
|
* every engine frame (see saveUpdate()). No-op if no dialog is active.
|
||||||
|
*
|
||||||
|
* @return An error code indicating success or failure.
|
||||||
|
*/
|
||||||
|
errorret_t savePSPUpdate(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True while a save/load dialog is in progress (see savePSPBeginSave()/
|
||||||
|
* savePSPBeginLoad()).
|
||||||
|
*
|
||||||
|
* @return True if a save/load dialog is currently open.
|
||||||
|
*/
|
||||||
|
bool_t savePSPIsBusy(void);
|
||||||
|
|||||||
@@ -7,71 +7,40 @@
|
|||||||
|
|
||||||
#include "save/save.h"
|
#include "save/save.h"
|
||||||
#include "save/savestreampsp.h"
|
#include "save/savestreampsp.h"
|
||||||
|
#include "util/memory.h"
|
||||||
errorret_t saveStreamOpenReadPSP(
|
|
||||||
savestreampsp_t *p, bool_t *found, const uint8_t slot
|
|
||||||
) {
|
|
||||||
char_t path[SAVE_PSP_PATH_MAX];
|
|
||||||
snprintf(path, SAVE_PSP_PATH_MAX, SAVE_PSP_FILE_FORMAT,
|
|
||||||
SAVE_PSP_TITLE_ID, (uint32_t)slot
|
|
||||||
);
|
|
||||||
|
|
||||||
p->fd = sceIoOpen(path, PSP_O_RDONLY, 0);
|
|
||||||
*found = (p->fd >= 0);
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t saveStreamOpenWritePSP(savestreampsp_t *p, const uint8_t slot) {
|
|
||||||
char_t dir[SAVE_PSP_PATH_MAX];
|
|
||||||
snprintf(dir, SAVE_PSP_PATH_MAX, SAVE_PSP_DIR_FORMAT,
|
|
||||||
SAVE_PSP_TITLE_ID, (uint32_t)slot
|
|
||||||
);
|
|
||||||
sceIoMkdir(dir, 0777);
|
|
||||||
|
|
||||||
char_t path[SAVE_PSP_PATH_MAX];
|
|
||||||
snprintf(path, SAVE_PSP_PATH_MAX, SAVE_PSP_FILE_FORMAT,
|
|
||||||
SAVE_PSP_TITLE_ID, (uint32_t)slot
|
|
||||||
);
|
|
||||||
|
|
||||||
p->fd = sceIoOpen(path, PSP_O_WRONLY | PSP_O_CREAT | PSP_O_TRUNC, 0777);
|
|
||||||
if(p->fd < 0) {
|
|
||||||
errorThrow(
|
|
||||||
"Failed to open PSP save file for writing: slot %u", (uint32_t)slot
|
|
||||||
);
|
|
||||||
}
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
void saveStreamClosePSP(savestreampsp_t *p) {
|
|
||||||
if(p->fd >= 0) {
|
|
||||||
sceIoClose(p->fd);
|
|
||||||
p->fd = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t saveStreamReadBytesPSP(
|
errorret_t saveStreamReadBytesPSP(
|
||||||
savestreampsp_t *p, void *buf, const size_t len
|
savestreampsp_t *p, void *buf, const size_t len
|
||||||
) {
|
) {
|
||||||
int32_t read = sceIoRead(p->fd, buf, (SceSize)len);
|
if(p->position + len > p->length) {
|
||||||
if(read != (int32_t)len) {
|
errorThrow("Save stream read exceeds buffer length");
|
||||||
errorThrow("Unexpected end of PSP save file");
|
|
||||||
}
|
}
|
||||||
|
memoryCopy(buf, p->buffer + p->position, len);
|
||||||
|
p->position += len;
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveStreamWriteBytesPSP(
|
errorret_t saveStreamWriteBytesPSP(
|
||||||
savestreampsp_t *p, const void *buf, const size_t len
|
savestreampsp_t *p, const void *buf, const size_t len
|
||||||
) {
|
) {
|
||||||
int32_t written = sceIoWrite(p->fd, buf, (SceSize)len);
|
if(p->position + len > p->bufferSize) {
|
||||||
if(written != (int32_t)len) {
|
errorThrow("Save stream write exceeds buffer size");
|
||||||
errorThrow("Failed to write PSP save data");
|
|
||||||
}
|
}
|
||||||
|
memoryCopy(p->buffer + p->position, buf, len);
|
||||||
|
p->position += len;
|
||||||
|
if(p->position > p->length) p->length = p->position;
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveStreamSeekPSP(savestreampsp_t *p, const size_t pos) {
|
errorret_t saveStreamSeekPSP(savestreampsp_t *p, const size_t pos) {
|
||||||
if(sceIoLseek(p->fd, (SceOff)pos, PSP_SEEK_SET) < 0) {
|
if(pos > p->bufferSize) {
|
||||||
errorThrow("Failed to seek in PSP save file");
|
errorThrow("Save stream seek out of range");
|
||||||
}
|
}
|
||||||
|
p->position = pos;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t saveStreamTellPSP(savestreampsp_t *p, size_t *out) {
|
||||||
|
*out = p->position;
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,71 +7,58 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include <pspiofilemgr.h>
|
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
|
||||||
|
// Backed by SAVE.platform.dataBuffer (see savepsp.h) rather than owning its
|
||||||
|
// own memory - the buffer has to outlive a single saveFileWrite()/Load()
|
||||||
|
// call, since the actual save/load dialog it's handed to only completes
|
||||||
|
// several frames later.
|
||||||
typedef struct {
|
typedef struct {
|
||||||
SceUID fd;
|
uint8_t *buffer;
|
||||||
|
size_t bufferSize;
|
||||||
|
size_t position;
|
||||||
|
size_t length;
|
||||||
} savestreampsp_t;
|
} savestreampsp_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a PSP save data file for reading.
|
* Copies len bytes from the buffer at the current position into buf.
|
||||||
*
|
|
||||||
* @param p Stream to initialize.
|
|
||||||
* @param found Set to true if the file exists, false if it does not.
|
|
||||||
* @param slot Save slot index.
|
|
||||||
* @return An error if the open fails for a reason other than missing file.
|
|
||||||
*/
|
|
||||||
errorret_t saveStreamOpenReadPSP(
|
|
||||||
savestreampsp_t *p, bool_t *found, const uint8_t slot
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens a PSP save data file for writing, creating or truncating it.
|
|
||||||
* Creates the save data directory if it does not already exist.
|
|
||||||
*
|
|
||||||
* @param p Stream to initialize.
|
|
||||||
* @param slot Save slot index.
|
|
||||||
* @return An error if the file cannot be opened for writing.
|
|
||||||
*/
|
|
||||||
errorret_t saveStreamOpenWritePSP(savestreampsp_t *p, const uint8_t slot);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes the file descriptor held by the stream.
|
|
||||||
*
|
|
||||||
* @param p Stream to close.
|
|
||||||
*/
|
|
||||||
void saveStreamClosePSP(savestreampsp_t *p);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads len bytes from the stream into buf.
|
|
||||||
*
|
*
|
||||||
* @param p Active stream.
|
* @param p Active stream.
|
||||||
* @param buf Destination buffer.
|
* @param buf Destination buffer.
|
||||||
* @param len Number of bytes to read.
|
* @param len Number of bytes to read.
|
||||||
* @return An error if fewer than len bytes are available.
|
* @return An error if the read would exceed the populated data length.
|
||||||
*/
|
*/
|
||||||
errorret_t saveStreamReadBytesPSP(
|
errorret_t saveStreamReadBytesPSP(
|
||||||
savestreampsp_t *p, void *buf, const size_t len
|
savestreampsp_t *p, void *buf, const size_t len
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes len bytes from buf into the stream.
|
* Copies len bytes from buf into the buffer at the current position,
|
||||||
|
* growing p->length if this write extends past it.
|
||||||
*
|
*
|
||||||
* @param p Active stream.
|
* @param p Active stream.
|
||||||
* @param buf Source buffer.
|
* @param buf Source buffer.
|
||||||
* @param len Number of bytes to write.
|
* @param len Number of bytes to write.
|
||||||
* @return An error if the write fails.
|
* @return An error if the write would exceed bufferSize.
|
||||||
*/
|
*/
|
||||||
errorret_t saveStreamWriteBytesPSP(
|
errorret_t saveStreamWriteBytesPSP(
|
||||||
savestreampsp_t *p, const void *buf, const size_t len
|
savestreampsp_t *p, const void *buf, const size_t len
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seeks to an absolute byte position within the stream.
|
* Sets the current read/write position within the buffer.
|
||||||
*
|
*
|
||||||
* @param p Active stream.
|
* @param p Active stream.
|
||||||
* @param pos Target byte offset from the start of the file.
|
* @param pos Target byte offset from the start of the buffer.
|
||||||
* @return An error if the seek fails.
|
* @return An error if pos is out of range.
|
||||||
*/
|
*/
|
||||||
errorret_t saveStreamSeekPSP(savestreampsp_t *p, const size_t pos);
|
errorret_t saveStreamSeekPSP(savestreampsp_t *p, const size_t pos);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current read/write position within the buffer.
|
||||||
|
*
|
||||||
|
* @param p Active stream.
|
||||||
|
* @param out Receives the current position.
|
||||||
|
* @return An error - always succeeds, matches saveStreamTellImpl's shape.
|
||||||
|
*/
|
||||||
|
errorret_t saveStreamTellPSP(savestreampsp_t *p, size_t *out);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "input/input.h"
|
#include "input/input.h"
|
||||||
|
#include "save/save.h"
|
||||||
|
|
||||||
inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
||||||
{ .name = "triangle", {
|
{ .name = "triangle", {
|
||||||
@@ -83,5 +84,5 @@ inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
float_t inputGetDeadzoneSDL2(const inputbutton_t button) {
|
float_t inputGetDeadzoneSDL2(const inputbutton_t button) {
|
||||||
return 0.17f;
|
return saveGetMeta()->deadzone;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,16 @@ JSON input (assetsraw/chunks/chunk_X_Y_Z.json):
|
|||||||
],
|
],
|
||||||
"meshes": [
|
"meshes": [
|
||||||
{ "file": "house_5_3.dmf", "pos": [x, y, z] }
|
{ "file": "house_5_3.dmf", "pos": [x, y, z] }
|
||||||
|
],
|
||||||
|
"entities": [
|
||||||
|
{ "type": "global", "globalId": <int>, "pos": [x, y, z] },
|
||||||
|
{ "type": "item", "itemId": <int>, "quantity": <int>, "pos": [x, y, z] }
|
||||||
|
],
|
||||||
|
"areas": [
|
||||||
|
{
|
||||||
|
"min": [x, y, z], "max": [x, y, z],
|
||||||
|
"callbackId": <int>, "notify": <int>, "trigger": <int>
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,10 +35,27 @@ JSON input (assetsraw/chunks/chunk_X_Y_Z.json):
|
|||||||
Mesh files are located by searching under assets/meshes/ and referenced by
|
Mesh files are located by searching under assets/meshes/ and referenced by
|
||||||
path from the assets root in the DCF.
|
path from the assets root in the DCF.
|
||||||
|
|
||||||
|
"entities" spawns things into the world when this chunk loads. A "global"
|
||||||
|
entity is spawned via mapSpawnEntity() - globalId indexes
|
||||||
|
ENTITY_GLOBAL_LIST (src/dusk/rpg/entity/global/entitygloballist.h) and is
|
||||||
|
deduped automatically if already spawned, so it's safe to declare on a
|
||||||
|
chunk that streams in more than once. An "item" entity has no persistent
|
||||||
|
identity - it respawns fresh every time this chunk (re)loads, including
|
||||||
|
after being picked up, since nothing tracks "already collected" yet.
|
||||||
|
itemId is a raw ITEM_ID_* value (see src/dusk/rpg/item/item.json for the
|
||||||
|
name -> id mapping, same convention as the tile "type" ints above).
|
||||||
|
|
||||||
|
"areas" declares map trigger regions (see rpg/overworld/maparea.h) owned
|
||||||
|
by this chunk - they're removed when the chunk unloads and re-added if it
|
||||||
|
streams back in. callbackId indexes MAP_AREA_CALLBACK_LIST
|
||||||
|
(src/dusk/rpg/overworld/global/mapareagloballist.h; 0 is reserved and
|
||||||
|
invalid). notify is bitwise MAP_AREA_NOTIFY_PLAYER(1)|NOTIFY_NPC(2).
|
||||||
|
trigger is bitwise MAP_TRIGGER_STEP(1)|ENTER(2)|EXIT(4).
|
||||||
|
|
||||||
Output DCF is derived automatically:
|
Output DCF is derived automatically:
|
||||||
assetsraw/chunks/chunk_X_Y_Z.json -> assets/chunks/X_Y_Z.dcf
|
assetsraw/chunks/chunk_X_Y_Z.json -> assets/chunks/X_Y_Z.dcf
|
||||||
|
|
||||||
Version 4 DCF format (after 8-byte header):
|
Version 5 DCF format (after 8-byte header):
|
||||||
tile_t tiles[CHUNK_WIDTH * CHUNK_HEIGHT] (one per x/y column)
|
tile_t tiles[CHUNK_WIDTH * CHUNK_HEIGHT] (one per x/y column)
|
||||||
each tile: uint32_t shape, uint8_t z, 3 padding bytes (8 bytes total,
|
each tile: uint32_t shape, uint8_t z, 3 padding bytes (8 bytes total,
|
||||||
matching the C tile_t struct's layout: { tileshape_t shape; uint8_t z; })
|
matching the C tile_t struct's layout: { tileshape_t shape; uint8_t z; })
|
||||||
@@ -36,6 +63,19 @@ Version 4 DCF format (after 8-byte header):
|
|||||||
for each model:
|
for each model:
|
||||||
null-terminated string (relative asset path to .json model)
|
null-terminated string (relative asset path to .json model)
|
||||||
float32[3] (x, y, z offset)
|
float32[3] (x, y, z offset)
|
||||||
|
uint8_t entitySpawnCount
|
||||||
|
for each entity spawn:
|
||||||
|
uint8_t kind (0 = global entity, 1 = item entity)
|
||||||
|
uint16_t a (globalId if kind 0, itemId if kind 1)
|
||||||
|
uint8_t b (unused if kind 0, quantity if kind 1)
|
||||||
|
int16_t x, y, z (world position, 3 fields)
|
||||||
|
uint8_t areaSpawnCount
|
||||||
|
for each area spawn:
|
||||||
|
int16_t minX, minY, minZ (3 fields)
|
||||||
|
int16_t maxX, maxY, maxZ (3 fields)
|
||||||
|
uint16_t callbackId
|
||||||
|
uint8_t notify
|
||||||
|
uint8_t trigger
|
||||||
|
|
||||||
DMF format:
|
DMF format:
|
||||||
Bytes 0-3: DMF\\x00
|
Bytes 0-3: DMF\\x00
|
||||||
@@ -74,6 +114,11 @@ WORLD_LAYER_HEIGHT = 1.0 / math.sqrt(2)
|
|||||||
|
|
||||||
CHUNK_MESH_COUNT_MAX = 10
|
CHUNK_MESH_COUNT_MAX = 10
|
||||||
CHUNK_MESH_NAME_MAX = 64
|
CHUNK_MESH_NAME_MAX = 64
|
||||||
|
CHUNK_ENTITY_SPAWN_COUNT_MAX = 8
|
||||||
|
CHUNK_AREA_COUNT_MAX = 4
|
||||||
|
|
||||||
|
ENTITY_SPAWN_KIND_GLOBAL = 0
|
||||||
|
ENTITY_SPAWN_KIND_ITEM = 1
|
||||||
|
|
||||||
# Matches sizeof(tile_t) on the C side: uint32_t shape + uint8_t z, padded
|
# Matches sizeof(tile_t) on the C side: uint32_t shape + uint8_t z, padded
|
||||||
# to 8 bytes ({ tileshape_t shape; uint8_t z; } with 4-byte enum alignment).
|
# to 8 bytes ({ tileshape_t shape; uint8_t z; } with 4-byte enum alignment).
|
||||||
@@ -106,7 +151,7 @@ TILE_SHAPE_RAMP_SOUTHWEST_INNER = 13
|
|||||||
|
|
||||||
FILE_MAGIC = b'DCF'
|
FILE_MAGIC = b'DCF'
|
||||||
DMF_MAGIC = b'DMF\x00'
|
DMF_MAGIC = b'DMF\x00'
|
||||||
VERSION_OUT = 4
|
VERSION_OUT = 5
|
||||||
DMF_VERSION = 1
|
DMF_VERSION = 1
|
||||||
|
|
||||||
|
|
||||||
@@ -163,11 +208,29 @@ def write_dmf(path, vertex_bytes):
|
|||||||
print(f' Wrote DMF {path}: {vert_count} vertices, {len(buf)} bytes')
|
print(f' Wrote DMF {path}: {vert_count} vertices, {len(buf)} bytes')
|
||||||
|
|
||||||
|
|
||||||
def write_dcf(dcf_path, tiles, mesh_names, mesh_offsets=None):
|
def write_dcf(
|
||||||
|
dcf_path, tiles, mesh_names, mesh_offsets=None,
|
||||||
|
entity_spawns=None, area_spawns=None
|
||||||
|
):
|
||||||
"""Write a current-version DCF referencing the given DMF asset paths."""
|
"""Write a current-version DCF referencing the given DMF asset paths."""
|
||||||
mesh_count = len(mesh_names)
|
mesh_count = len(mesh_names)
|
||||||
if mesh_offsets is None:
|
if mesh_offsets is None:
|
||||||
mesh_offsets = [(0.0, 0.0, 0.0)] * mesh_count
|
mesh_offsets = [(0.0, 0.0, 0.0)] * mesh_count
|
||||||
|
if entity_spawns is None:
|
||||||
|
entity_spawns = []
|
||||||
|
if area_spawns is None:
|
||||||
|
area_spawns = []
|
||||||
|
|
||||||
|
if len(entity_spawns) > CHUNK_ENTITY_SPAWN_COUNT_MAX:
|
||||||
|
raise ValueError(
|
||||||
|
f"Too many entity spawns ({len(entity_spawns)}) - max "
|
||||||
|
f"{CHUNK_ENTITY_SPAWN_COUNT_MAX}"
|
||||||
|
)
|
||||||
|
if len(area_spawns) > CHUNK_AREA_COUNT_MAX:
|
||||||
|
raise ValueError(
|
||||||
|
f"Too many area spawns ({len(area_spawns)}) - max "
|
||||||
|
f"{CHUNK_AREA_COUNT_MAX}"
|
||||||
|
)
|
||||||
|
|
||||||
buf = bytearray()
|
buf = bytearray()
|
||||||
buf += FILE_MAGIC
|
buf += FILE_MAGIC
|
||||||
@@ -183,11 +246,34 @@ def write_dcf(dcf_path, tiles, mesh_names, mesh_offsets=None):
|
|||||||
)
|
)
|
||||||
buf += encoded + b'\x00'
|
buf += encoded + b'\x00'
|
||||||
buf += struct.pack('<3f', offset[0], offset[1], offset[2])
|
buf += struct.pack('<3f', offset[0], offset[1], offset[2])
|
||||||
|
|
||||||
|
buf += struct.pack('<B', len(entity_spawns))
|
||||||
|
for spawn in entity_spawns:
|
||||||
|
kind = spawn['kind']
|
||||||
|
x, y, z = spawn['pos']
|
||||||
|
if kind == ENTITY_SPAWN_KIND_GLOBAL:
|
||||||
|
a, b = spawn['globalId'], 0
|
||||||
|
else:
|
||||||
|
a, b = spawn['itemId'], spawn['quantity']
|
||||||
|
buf += struct.pack('<BHB3h', kind, a, b, x, y, z)
|
||||||
|
|
||||||
|
buf += struct.pack('<B', len(area_spawns))
|
||||||
|
for area in area_spawns:
|
||||||
|
minX, minY, minZ = area['min']
|
||||||
|
maxX, maxY, maxZ = area['max']
|
||||||
|
buf += struct.pack(
|
||||||
|
'<6hHBB',
|
||||||
|
minX, minY, minZ, maxX, maxY, maxZ,
|
||||||
|
area['callbackId'], area['notify'], area['trigger']
|
||||||
|
)
|
||||||
|
|
||||||
with open(dcf_path, 'wb') as f:
|
with open(dcf_path, 'wb') as f:
|
||||||
f.write(buf)
|
f.write(buf)
|
||||||
print(
|
print(
|
||||||
f' Wrote DCF {dcf_path}: '
|
f' Wrote DCF {dcf_path}: '
|
||||||
f'version {VERSION_OUT}, {mesh_count} mesh(es), {len(buf)} bytes'
|
f'version {VERSION_OUT}, {mesh_count} mesh(es), '
|
||||||
|
f'{len(entity_spawns)} entity spawn(s), {len(area_spawns)} '
|
||||||
|
f'area(s), {len(buf)} bytes'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -323,7 +409,39 @@ def from_json(json_path, dcf_path):
|
|||||||
mesh_offsets.append((float(pos[0]), float(pos[1]), float(pos[2])))
|
mesh_offsets.append((float(pos[0]), float(pos[1]), float(pos[2])))
|
||||||
print(f' Resolved {filename} -> {rel}')
|
print(f' Resolved {filename} -> {rel}')
|
||||||
|
|
||||||
write_dcf(dcf_path, bytes(tiles), model_names, mesh_offsets)
|
entity_spawns = []
|
||||||
|
for spawn in data.get('entities', []):
|
||||||
|
pos = tuple(int(v) for v in spawn['pos'])
|
||||||
|
if spawn['type'] == 'global':
|
||||||
|
entity_spawns.append({
|
||||||
|
'kind': ENTITY_SPAWN_KIND_GLOBAL,
|
||||||
|
'globalId': int(spawn['globalId']),
|
||||||
|
'pos': pos,
|
||||||
|
})
|
||||||
|
elif spawn['type'] == 'item':
|
||||||
|
entity_spawns.append({
|
||||||
|
'kind': ENTITY_SPAWN_KIND_ITEM,
|
||||||
|
'itemId': int(spawn['itemId']),
|
||||||
|
'quantity': int(spawn['quantity']),
|
||||||
|
'pos': pos,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown entity spawn type: {spawn['type']}")
|
||||||
|
|
||||||
|
area_spawns = []
|
||||||
|
for area in data.get('areas', []):
|
||||||
|
area_spawns.append({
|
||||||
|
'min': tuple(int(v) for v in area['min']),
|
||||||
|
'max': tuple(int(v) for v in area['max']),
|
||||||
|
'callbackId': int(area['callbackId']),
|
||||||
|
'notify': int(area['notify']),
|
||||||
|
'trigger': int(area['trigger']),
|
||||||
|
})
|
||||||
|
|
||||||
|
write_dcf(
|
||||||
|
dcf_path, bytes(tiles), model_names, mesh_offsets,
|
||||||
|
entity_spawns, area_spawns
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def process_json(json_path):
|
def process_json(json_path):
|
||||||
|
|||||||
+5
-1
@@ -38,7 +38,11 @@ out += [
|
|||||||
" STORY_FLAG_COUNT",
|
" STORY_FLAG_COUNT",
|
||||||
"} storyflag_t;",
|
"} storyflag_t;",
|
||||||
"",
|
"",
|
||||||
"static storyflagvalue_t STORY_FLAG_VALUES[STORY_FLAG_COUNT] = {",
|
"// Stamped onto a save file's storyFlags the first time it's used (see",
|
||||||
|
"// storyFlagInitDefaults()) - not a live value array. Live flag state",
|
||||||
|
"// lives entirely in the save file (savefile_t.storyFlags), read/written",
|
||||||
|
"// via storyFlagGet()/storyFlagSet() - see storyflag.h.",
|
||||||
|
"static const storyflagvalue_t STORY_FLAG_DEFAULTS[STORY_FLAG_COUNT] = {",
|
||||||
]
|
]
|
||||||
for flag in flags:
|
for flag in flags:
|
||||||
out.append(f" [{flag_enum(flag['id'])}] = {flag['initial']},")
|
out.append(f" [{flag_enum(flag['id'])}] = {flag['initial']},")
|
||||||
|
|||||||
Reference in New Issue
Block a user