Compare commits
33 Commits
item-as-file
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e3f10e0926 | |||
| d7223d7387 | |||
| 45331c2a60 | |||
| 33f50a2c69 | |||
| 6c8e4d5cbd | |||
| 3b7215876a | |||
| c74f5890bd | |||
| fbd3c71ba7 | |||
| 128f9ab9d4 | |||
| 36fb359aa2 | |||
| 1bd73d69fe | |||
| fb48285143 | |||
| 3de50b8370 | |||
| e61cbe25b7 | |||
| 8395830be6 | |||
| 6e4ec2b9d8 | |||
| f501bb8e28 | |||
| d07cd3397d | |||
| e008fb108a | |||
| 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`.
|
||||
@@ -13,6 +13,7 @@ cmake_policy(SET CMP0079 NEW)
|
||||
# set(FETCHCONTENT_UPDATES_DISCONNECTED ON)
|
||||
|
||||
option(DUSK_BUILD_TESTS "Enable tests" OFF)
|
||||
option(DUSK_NETWORK "Enable network support" ON)
|
||||
|
||||
set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name")
|
||||
set(DUSK_GAME_AUTHOR "YourWishes" CACHE STRING "Game author / coder")
|
||||
@@ -90,6 +91,12 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
|
||||
DUSK_VERSION="${DUSK_VERSION}"
|
||||
)
|
||||
|
||||
if(DUSK_NETWORK)
|
||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
DUSK_NETWORK
|
||||
)
|
||||
endif()
|
||||
|
||||
# Toolchains
|
||||
include(cmake/targets/${DUSK_TARGET_SYSTEM}.cmake)
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -10,6 +10,9 @@ msgid "ui.title"
|
||||
msgstr ""
|
||||
"Welcome"
|
||||
|
||||
msgid "save.linux.mkdirp_failed"
|
||||
msgstr "Failed to create save directory, check the disk is not full or write-protected."
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.general"
|
||||
msgstr "General"
|
||||
@@ -56,6 +59,130 @@ msgstr "Items"
|
||||
msgid "ui.game_menu.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"
|
||||
|
||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
||||
msgid "ui.main_menu.new_game"
|
||||
msgstr "New Game"
|
||||
|
||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
||||
msgid "ui.main_menu.load_game"
|
||||
msgstr "Load Game"
|
||||
|
||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
||||
msgid "ui.main_menu.options"
|
||||
msgstr "Options"
|
||||
|
||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
||||
msgid "ui.main_menu.quit"
|
||||
msgstr "Quit Game"
|
||||
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.confirm"
|
||||
msgstr "Confirm"
|
||||
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.cancel"
|
||||
msgstr "Cancel"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
||||
msgid "ui.battle.menu.attack"
|
||||
msgstr "Attack"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
||||
msgid "ui.battle.menu.flee"
|
||||
msgstr "Flee"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
||||
msgid "ui.battle.menu.target_format"
|
||||
msgstr "Enemy %u (%u/%u HP)"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlehud.c
|
||||
msgid "ui.battle.hud.hp_format"
|
||||
msgstr "HP %u/%u"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlehud.c
|
||||
msgid "ui.battle.hud.mp_format"
|
||||
msgstr "MP %u/%u"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsaudio.c
|
||||
msgid "ui.settings.audio.placeholder"
|
||||
msgstr "No audio settings yet"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsdisplay.c
|
||||
msgid "ui.settings.display.placeholder"
|
||||
msgstr "No display settings yet"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsinput.c
|
||||
msgid "ui.settings.input.placeholder"
|
||||
msgstr "No input settings yet"
|
||||
|
||||
#: src/dusk/ui/frame/backpack/uibackpack.c
|
||||
msgid "ui.backpack.category_format"
|
||||
msgstr "Category %u"
|
||||
|
||||
#: src/dusk/ui/overlay/uiloading.c
|
||||
msgid "ui.loading.text"
|
||||
msgstr "loading"
|
||||
|
||||
#: src/dusk/ui/overlay/uiautosave.c
|
||||
msgid "ui.autosave.saving"
|
||||
msgstr "SAVING"
|
||||
|
||||
msgid "item.potion.name"
|
||||
msgstr "Potion"
|
||||
|
||||
|
||||
@@ -57,6 +57,130 @@ msgstr "Objetos"
|
||||
msgid "ui.game_menu.settings"
|
||||
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/ui/frame/mainmenu/uimainmenu.c
|
||||
msgid "ui.main_menu.new_game"
|
||||
msgstr "Nueva Partida"
|
||||
|
||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
||||
msgid "ui.main_menu.load_game"
|
||||
msgstr "Cargar Partida"
|
||||
|
||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
||||
msgid "ui.main_menu.options"
|
||||
msgstr "Opciones"
|
||||
|
||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
||||
msgid "ui.main_menu.quit"
|
||||
msgstr "Salir del Juego"
|
||||
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.confirm"
|
||||
msgstr "Confirmar"
|
||||
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
||||
msgid "ui.battle.menu.attack"
|
||||
msgstr "Atacar"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
||||
msgid "ui.battle.menu.flee"
|
||||
msgstr "Huir"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
||||
msgid "ui.battle.menu.target_format"
|
||||
msgstr "Enemigo %u (%u/%u PS)"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlehud.c
|
||||
msgid "ui.battle.hud.hp_format"
|
||||
msgstr "PS %u/%u"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlehud.c
|
||||
msgid "ui.battle.hud.mp_format"
|
||||
msgstr "PM %u/%u"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsaudio.c
|
||||
msgid "ui.settings.audio.placeholder"
|
||||
msgstr "Aún no hay opciones de audio"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsdisplay.c
|
||||
msgid "ui.settings.display.placeholder"
|
||||
msgstr "Aún no hay opciones de pantalla"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsinput.c
|
||||
msgid "ui.settings.input.placeholder"
|
||||
msgstr "Aún no hay opciones de entrada"
|
||||
|
||||
#: src/dusk/ui/frame/backpack/uibackpack.c
|
||||
msgid "ui.backpack.category_format"
|
||||
msgstr "Categoría %u"
|
||||
|
||||
#: src/dusk/ui/overlay/uiloading.c
|
||||
msgid "ui.loading.text"
|
||||
msgstr "cargando"
|
||||
|
||||
#: src/dusk/ui/overlay/uiautosave.c
|
||||
msgid "ui.autosave.saving"
|
||||
msgstr "GUARDANDO"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.potion.name"
|
||||
msgstr "Poción"
|
||||
|
||||
@@ -57,6 +57,130 @@ msgstr "アイテム"
|
||||
msgid "ui.game_menu.settings"
|
||||
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/ui/frame/mainmenu/uimainmenu.c
|
||||
msgid "ui.main_menu.new_game"
|
||||
msgstr "ニューゲーム"
|
||||
|
||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
||||
msgid "ui.main_menu.load_game"
|
||||
msgstr "ロードゲーム"
|
||||
|
||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
||||
msgid "ui.main_menu.options"
|
||||
msgstr "オプション"
|
||||
|
||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
||||
msgid "ui.main_menu.quit"
|
||||
msgstr "ゲームを終了"
|
||||
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.confirm"
|
||||
msgstr "確認"
|
||||
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.cancel"
|
||||
msgstr "キャンセル"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
||||
msgid "ui.battle.menu.attack"
|
||||
msgstr "攻撃"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
||||
msgid "ui.battle.menu.flee"
|
||||
msgstr "逃げる"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
||||
msgid "ui.battle.menu.target_format"
|
||||
msgstr "敵%u (%u/%u HP)"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlehud.c
|
||||
msgid "ui.battle.hud.hp_format"
|
||||
msgstr "HP %u/%u"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlehud.c
|
||||
msgid "ui.battle.hud.mp_format"
|
||||
msgstr "MP %u/%u"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsaudio.c
|
||||
msgid "ui.settings.audio.placeholder"
|
||||
msgstr "オーディオ設定はまだありません"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsdisplay.c
|
||||
msgid "ui.settings.display.placeholder"
|
||||
msgstr "表示設定はまだありません"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsinput.c
|
||||
msgid "ui.settings.input.placeholder"
|
||||
msgstr "入力設定はまだありません"
|
||||
|
||||
#: src/dusk/ui/frame/backpack/uibackpack.c
|
||||
msgid "ui.backpack.category_format"
|
||||
msgstr "カテゴリー%u"
|
||||
|
||||
#: src/dusk/ui/overlay/uiloading.c
|
||||
msgid "ui.loading.text"
|
||||
msgstr "読み込み中"
|
||||
|
||||
#: src/dusk/ui/overlay/uiautosave.c
|
||||
msgid "ui.autosave.saving"
|
||||
msgstr "保存中"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.potion.name"
|
||||
msgstr "ポーション"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -2179,5 +2179,27 @@
|
||||
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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -4,6 +4,17 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
DUSK_WII
|
||||
)
|
||||
|
||||
# Wii save storage method - see src/duskdolphin/save/savedeviceplatform.h.
|
||||
set(DUSK_SAVE_WII_METHOD "NAND" CACHE STRING
|
||||
"Wii save storage: NAND (internal storage via ISFS), CARD (GameCube-\
|
||||
compatible memory card emulation), or SD (SD card via libfat)"
|
||||
)
|
||||
set_property(CACHE DUSK_SAVE_WII_METHOD PROPERTY STRINGS "NAND" "CARD" "SD")
|
||||
|
||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
DUSK_SAVE_WII_METHOD_${DUSK_SAVE_WII_METHOD}
|
||||
)
|
||||
|
||||
# Generate Homebrew Channel meta.xml from project identity variables
|
||||
string(TIMESTAMP DUSK_BUILD_DATE "%Y%m%d000000" UTC)
|
||||
configure_file(
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
|
||||
add_subdirectory(dusk)
|
||||
|
||||
if(DUSK_NETWORK)
|
||||
add_subdirectory(dusknetwork)
|
||||
endif()
|
||||
|
||||
if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "knulli")
|
||||
add_subdirectory(dusklinux)
|
||||
add_subdirectory(dusksdl2)
|
||||
|
||||
@@ -68,7 +68,6 @@ add_subdirectory(scene)
|
||||
add_subdirectory(system)
|
||||
add_subdirectory(time)
|
||||
add_subdirectory(ui)
|
||||
add_subdirectory(network)
|
||||
add_subdirectory(save)
|
||||
add_subdirectory(util)
|
||||
add_subdirectory(thread)
|
||||
@@ -7,4 +7,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
easing.c
|
||||
animation.c
|
||||
keyframe.c
|
||||
)
|
||||
|
||||
+109
-29
@@ -11,42 +11,122 @@
|
||||
void animationInit(
|
||||
animation_t *anim,
|
||||
keyframe_t *keyframes,
|
||||
uint16_t keyframeCount
|
||||
uint16_t *keyframeCounts,
|
||||
const uint16_t layerCount
|
||||
) {
|
||||
assertNotNull(anim, "Animation pointer cannot be null.");
|
||||
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
|
||||
assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
|
||||
assertNotNull(keyframeCounts, "Keyframe counts pointer cannot be null.");
|
||||
assertTrue(layerCount > 0, "Layer count must be greater than zero.");
|
||||
|
||||
memoryZero(anim, sizeof(animation_t));
|
||||
anim->keyframes = keyframes;
|
||||
anim->keyframeCount = keyframeCount;
|
||||
anim->keyframeCounts = keyframeCounts;
|
||||
anim->layerCount = layerCount;
|
||||
|
||||
// Determine duration
|
||||
float_t duration = 0.0f;
|
||||
for(uint16_t layer = 0; layer < layerCount; layer++) {
|
||||
uint16_t keyframeCount = keyframeCounts[layer];
|
||||
assertTrue(keyframeCount > 0, "Keyframe count invalid.");
|
||||
keyframe_t *layerKeyframes = keyframes + layer * keyframeCount;
|
||||
|
||||
#ifdef DUSK_ASSERTIONS
|
||||
// Check that the keyframes are sorted by time.
|
||||
for(uint16_t i = 1; i < keyframeCount; i++) {
|
||||
assertTrue(
|
||||
layerKeyframes[i].time >= layerKeyframes[i - 1].time,
|
||||
"Keyframes must be sorted by time."
|
||||
);
|
||||
}
|
||||
#endif
|
||||
|
||||
keyframe_t *lastKeyframe = layerKeyframes + keyframeCount - 1;
|
||||
duration = mathMax(duration, lastKeyframe->time);
|
||||
}
|
||||
assertTrue(duration > 0, "Animation duration must be greater than 0.");
|
||||
anim->duration = duration;
|
||||
}
|
||||
|
||||
float_t animationGetValue(animation_t *anim, const float_t time) {
|
||||
float_t animationGetLayerValue(const animation_t *anim, const uint16_t layer) {
|
||||
assertNotNull(anim, "Animation pointer cannot be null.");
|
||||
assertNotNull(anim->keyframes, "Keyframes pointer cannot be null.");
|
||||
assertTrue(anim->keyframeCount > 0, "Keyframe count invalid.");
|
||||
assertTrue(time >= 0, "Time must be non-negative.");
|
||||
assertTrue(layer < anim->layerCount, "Layer index out of bounds.");
|
||||
|
||||
keyframe_t *start;
|
||||
keyframe_t *end;
|
||||
keyframe_t *last = anim->keyframes + anim->keyframeCount - 1;
|
||||
keyframe_t *current = anim->keyframes;
|
||||
start = current;
|
||||
|
||||
do {
|
||||
if(current->time > time) {
|
||||
end = current;
|
||||
break;
|
||||
}
|
||||
start = current;
|
||||
current++;
|
||||
|
||||
if(current > last) {
|
||||
end = start;
|
||||
break;
|
||||
}
|
||||
} while(true);
|
||||
|
||||
float_t t = (time - start->time) / (end->time - start->time);
|
||||
return mathLerp(start->value, end->value, easingApply(start->easing, t));
|
||||
uint16_t keyframeCount = anim->keyframeCounts[layer];
|
||||
keyframe_t *layerKeyframes = anim->keyframes + layer * keyframeCount;
|
||||
return keyframeGetValue(layerKeyframes, keyframeCount, anim->time);
|
||||
}
|
||||
|
||||
void animationUpdate(
|
||||
animation_t *anim,
|
||||
const float_t deltaTime
|
||||
) {
|
||||
assertNotNull(anim, "Animation pointer cannot be null.");
|
||||
assertTrue(deltaTime >= 0, "Delta time must be non-negative.");
|
||||
|
||||
bool_t justCompleted = false;
|
||||
|
||||
if(!(anim->flags & ANIMATION_FLAG_INTERNAL_COMPLETED)) {
|
||||
bool_t loop = (anim->flags & ANIMATION_FLAG_LOOP) != 0;
|
||||
bool_t pingpong = (anim->flags & ANIMATION_FLAG_PINGPONG) != 0;
|
||||
assertFalse(
|
||||
loop && pingpong,
|
||||
"Cannot set both ANIMATION_FLAG_LOOP and ANIMATION_FLAG_PINGPONG."
|
||||
);
|
||||
|
||||
bool_t backward;
|
||||
if(pingpong) {
|
||||
backward = (anim->flags & ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD) != 0;
|
||||
} else {
|
||||
backward = (anim->flags & ANIMATION_FLAG_REVERSE) != 0;
|
||||
}
|
||||
|
||||
// Resolve boundary crossings one at a time, so a single large deltaTime
|
||||
// can correctly loop/pingpong across multiple boundaries in one call.
|
||||
float_t remaining = deltaTime;
|
||||
while(remaining > 0.0f) {
|
||||
float_t toBoundary = (
|
||||
backward ? anim->time : (anim->duration - anim->time)
|
||||
);
|
||||
if(remaining < toBoundary) {
|
||||
anim->time += backward ? -remaining : remaining;
|
||||
break;
|
||||
}
|
||||
|
||||
remaining -= toBoundary;
|
||||
anim->time = backward ? 0.0f : anim->duration;
|
||||
|
||||
bool_t stopHere;
|
||||
if(backward) {
|
||||
stopHere = (anim->flags & ANIMATION_FLAG_STOP_BEGINNING) != 0;
|
||||
} else {
|
||||
stopHere = (anim->flags & ANIMATION_FLAG_STOP_END) != 0;
|
||||
}
|
||||
|
||||
if(stopHere) {
|
||||
justCompleted = true;
|
||||
break;
|
||||
} else if(pingpong) {
|
||||
backward = !backward;
|
||||
if(backward) anim->flags |= ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD;
|
||||
else anim->flags &= ~ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD;
|
||||
} else if(loop) {
|
||||
anim->time = backward ? anim->duration : 0.0f;
|
||||
if(anim->onLoop) anim->onLoop(anim->user);
|
||||
} else {
|
||||
justCompleted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(justCompleted) anim->flags |= ANIMATION_FLAG_INTERNAL_COMPLETED;
|
||||
}
|
||||
|
||||
// Call onUpdate for each layer.
|
||||
for(uint16_t layer = 0; layer < anim->layerCount; layer++) {
|
||||
float_t value = animationGetLayerValue(anim, layer);
|
||||
if(anim->onUpdate) anim->onUpdate(layer, value, anim->user);
|
||||
}
|
||||
|
||||
if(justCompleted && anim->onComplete) anim->onComplete(anim->user);
|
||||
}
|
||||
@@ -6,29 +6,89 @@
|
||||
#pragma once
|
||||
#include "keyframe.h"
|
||||
|
||||
#define ANIMATION_FLAG_LOOP (1 << 0)
|
||||
#define ANIMATION_FLAG_REVERSE (1 << 1)
|
||||
#define ANIMATION_FLAG_PINGPONG (1 << 2)
|
||||
#define ANIMATION_FLAG_STOP_BEGINNING (1 << 3)
|
||||
#define ANIMATION_FLAG_STOP_END (1 << 4)
|
||||
|
||||
// Internal - tracks which direction a pingponging animation is currently
|
||||
// travelling. Do not set this manually, it is managed by animationUpdate().
|
||||
#define ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD (1 << 7)
|
||||
|
||||
// Internal - set once the animation has stopped advancing (see
|
||||
// animationUpdate()). Do not set this manually. There is currently no way to
|
||||
// restart a completed animation short of clearing this bit and resetting
|
||||
// anim->time by hand.
|
||||
#define ANIMATION_FLAG_INTERNAL_COMPLETED (1 << 6)
|
||||
|
||||
typedef struct {
|
||||
keyframe_t *keyframes;
|
||||
uint16_t keyframeCount;
|
||||
uint16_t *keyframeCounts;
|
||||
uint16_t layerCount;
|
||||
float_t time;
|
||||
float_t duration;
|
||||
uint8_t flags;
|
||||
|
||||
void *user;
|
||||
void (*onUpdate)(const uint16_t layer, const float_t value, void *user);
|
||||
void (*onComplete)(void *user);
|
||||
void (*onLoop)(void *user);
|
||||
} animation_t;
|
||||
|
||||
/**
|
||||
* Initializes an animation.
|
||||
* Initializes an animation with the given keyframes and layer count.
|
||||
*
|
||||
* @param anim The animation to initialize.
|
||||
* @param keyframes The keyframes to use for the animation.
|
||||
* @param keyframeCount The number of keyframes in the animation.
|
||||
* @param anim Pointer to the animation to initialize.
|
||||
* @param keyframes Pointer to the array of keyframes for each layer.
|
||||
* @param keyframeCount Number of keyframes in each layer.
|
||||
* @param layerCount Number of layers in the animation.
|
||||
*/
|
||||
void animationInit(
|
||||
animation_t *anim,
|
||||
keyframe_t *keyframes,
|
||||
uint16_t keyframeCount
|
||||
uint16_t *keyframeCounts,
|
||||
const uint16_t layerCount
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the value of the animation at a given time.
|
||||
* Sets the current time of the animation, clamping it to the valid range.
|
||||
* This will call the onUpdate callback but none of the other callbacks.
|
||||
*
|
||||
* @param anim The animation to get the value from.
|
||||
* @param time The time at which to get the value, in seconds.
|
||||
* @return The value of the animation at the given time.
|
||||
* @param anim Pointer to the animation to set the time for.
|
||||
* @param time The new time to set for the animation.
|
||||
*/
|
||||
float_t animationGetValue(animation_t *anim, const float_t time);
|
||||
void animationSetTime(animation_t *anim, const float_t time);
|
||||
|
||||
/**
|
||||
* Gets the current value of a specific layer in the animation based on the
|
||||
* current animation time.
|
||||
*/
|
||||
float_t animationGetLayerValue(const animation_t *anim, const uint16_t layer);
|
||||
|
||||
/**
|
||||
* Updates the animation state based on the elapsed time. Advances anim->time
|
||||
* by deltaTime (or against it, if ANIMATION_FLAG_REVERSE is set), then
|
||||
* resolves whatever happens when it reaches the 0 or duration boundary:
|
||||
*
|
||||
* - ANIMATION_FLAG_PINGPONG: reflects off the boundary and continues playing
|
||||
* in the opposite direction, forever, unless stopped (see below).
|
||||
* - ANIMATION_FLAG_LOOP: wraps back around to the other boundary and keeps
|
||||
* playing in the same direction, forever, unless stopped (see below).
|
||||
* - ANIMATION_FLAG_STOP_BEGINNING / ANIMATION_FLAG_STOP_END: when the
|
||||
* animation reaches that specific boundary, it clamps there and stops
|
||||
* (firing onComplete) instead of looping/pingponging past it.
|
||||
* - If none of the above apply at a boundary, the animation clamps there and
|
||||
* stops, firing onComplete.
|
||||
*
|
||||
* onUpdate is called for every layer on every call. onLoop is called each
|
||||
* time a loop wraps around. onComplete is called at most once, the moment
|
||||
* the animation stops advancing.
|
||||
*
|
||||
* @param anim Pointer to the animation to update.
|
||||
* @param deltaTime Time elapsed since the last update (in seconds).
|
||||
*/
|
||||
void animationUpdate(
|
||||
animation_t *anim,
|
||||
const float_t deltaTime
|
||||
);
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "keyframe.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/math.h"
|
||||
|
||||
float_t keyframeGetValue(
|
||||
const keyframe_t *keyframes,
|
||||
const uint32_t keyframeCount,
|
||||
const float_t time
|
||||
) {
|
||||
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
|
||||
assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
|
||||
assertTrue(time >= 0, "Time must be non-negative.");
|
||||
#ifdef DUSK_ASSERTIONS
|
||||
// Checks that the keyframes are sorted by time.
|
||||
for(uint32_t i = 1; i < keyframeCount; i++) {
|
||||
assertTrue(
|
||||
keyframes[i].time >= keyframes[i - 1].time,
|
||||
"Keyframes must be sorted by time."
|
||||
);
|
||||
}
|
||||
#endif
|
||||
|
||||
keyframe_t *last = (keyframe_t *)(keyframes + keyframeCount - 1);
|
||||
if(time >= last->time) return last->value;
|
||||
|
||||
// Since time < last->time (checked above), current is guaranteed to stop
|
||||
// at or before reaching last, so no separate end-of-array check is needed.
|
||||
keyframe_t *current = (keyframe_t *)keyframes;
|
||||
keyframe_t *start = current;
|
||||
while(current->time <= time) {
|
||||
start = current;
|
||||
current++;
|
||||
}
|
||||
keyframe_t *end = current;
|
||||
|
||||
float_t t = (time - start->time) / (end->time - start->time);
|
||||
return mathLerp(start->value, end->value, easingApply(start->easing, t));
|
||||
}
|
||||
@@ -11,3 +11,17 @@ typedef struct {
|
||||
float_t value;
|
||||
easingtype_t easing;
|
||||
} keyframe_t;
|
||||
|
||||
/**
|
||||
* Gets the value of a keyframe at a given time.
|
||||
*
|
||||
* @param keyframes The keyframes to get the value from.
|
||||
* @param keyframeCount The number of keyframes in the array.
|
||||
* @param time The time at which to get the value, in seconds.
|
||||
* @return The value of the keyframe at the given time.
|
||||
*/
|
||||
float_t keyframeGetValue(
|
||||
const keyframe_t *keyframes,
|
||||
const uint32_t keyframeCount,
|
||||
const float_t time
|
||||
);
|
||||
@@ -22,6 +22,8 @@
|
||||
#endif
|
||||
|
||||
#ifndef DUSK_ASSERTIONS_FAKED
|
||||
#define DUSK_ASSERTIONS 1
|
||||
|
||||
/**
|
||||
* Initializes the assert system. Must be the very first call in engine
|
||||
* startup.
|
||||
|
||||
@@ -14,6 +14,26 @@
|
||||
#include "asset/loader/assetloader.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) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
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->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);
|
||||
loading->loading.chunk.data = NULL;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include "asset/assetfile.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 assetentry_s assetentry_t;
|
||||
@@ -33,12 +33,39 @@ typedef struct {
|
||||
uint8_t modelIndex;
|
||||
} 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 {
|
||||
tile_t *tiles;
|
||||
uint8_t meshCount;
|
||||
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
|
||||
vec3 meshOffsets[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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,10 +18,7 @@ console_t CONSOLE;
|
||||
void consoleInit(void) {
|
||||
memoryZero(&CONSOLE, sizeof(console_t));
|
||||
CONSOLE.visible = false;
|
||||
|
||||
#ifdef DUSK_CONSOLE_POSIX
|
||||
threadMutexInit(&CONSOLE.printMutex);
|
||||
#endif
|
||||
}
|
||||
|
||||
void consolePrint(const char_t *message, ...) {
|
||||
@@ -32,20 +29,14 @@ void consolePrint(const char_t *message, ...) {
|
||||
int32_t len = stringFormatVA(buffer, CONSOLE_LINE_MAX, message, args);
|
||||
va_end(args);
|
||||
|
||||
#ifdef DUSK_CONSOLE_POSIX
|
||||
threadMutexLock(&CONSOLE.printMutex);
|
||||
#endif
|
||||
|
||||
memoryMove(
|
||||
CONSOLE.line[0],
|
||||
CONSOLE.line[1],
|
||||
(CONSOLE_HISTORY_MAX - 1) * CONSOLE_LINE_MAX
|
||||
);
|
||||
memoryCopy(CONSOLE.line[CONSOLE_HISTORY_MAX - 1], buffer, len + 1);
|
||||
|
||||
#ifdef DUSK_CONSOLE_POSIX
|
||||
threadMutexUnlock(&CONSOLE.printMutex);
|
||||
#endif
|
||||
|
||||
logDebug("%s\n", buffer);
|
||||
}
|
||||
@@ -61,7 +52,5 @@ void consoleUpdate(void) {
|
||||
}
|
||||
|
||||
void consoleDispose(void) {
|
||||
#ifdef DUSK_CONSOLE_POSIX
|
||||
threadMutexDispose(&CONSOLE.printMutex);
|
||||
#endif
|
||||
}
|
||||
@@ -6,24 +6,18 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "consoledefs.h"
|
||||
#include "error/error.h"
|
||||
#include "dusk.h"
|
||||
#include "thread/thread.h"
|
||||
|
||||
#ifdef DUSK_CONSOLE_POSIX
|
||||
#include "thread/thread.h"
|
||||
#include <poll.h>
|
||||
#include <unistd.h>
|
||||
#define CONSOLE_POSIX_POLL_RATE 75
|
||||
#endif
|
||||
#define CONSOLE_LINE_MAX 512
|
||||
#define CONSOLE_HISTORY_MAX 16
|
||||
#define CONSOLE_EXEC_BUFFER_MAX 32
|
||||
|
||||
typedef struct {
|
||||
char_t line[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX];
|
||||
bool_t visible;
|
||||
|
||||
#ifdef DUSK_CONSOLE_POSIX
|
||||
threadmutex_t printMutex;
|
||||
#endif
|
||||
} console_t;
|
||||
|
||||
extern console_t CONSOLE;
|
||||
|
||||
@@ -33,13 +33,17 @@ errorret_t displayInit(void) {
|
||||
#ifdef displayPlatformInit
|
||||
errorChain(displayPlatformInit());
|
||||
#endif
|
||||
|
||||
// Set initial state
|
||||
errorChain(displaySetState((displaystate_t){ .flags = 0 }));
|
||||
|
||||
// Init the fixed textures
|
||||
errorChain(textureInit(
|
||||
&TEXTURE_WHITE, 4, 4,
|
||||
&TEXTURE_WHITE, TEXTURE_FIXED_WIDTH, TEXTURE_FIXED_HEIGHT,
|
||||
TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_WHITE_PIXELS }
|
||||
));
|
||||
errorChain(textureInit(
|
||||
&TEXTURE_TEST, 4, 4,
|
||||
&TEXTURE_TEST, TEXTURE_FIXED_WIDTH, TEXTURE_FIXED_HEIGHT,
|
||||
TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_TEST_PIXELS }
|
||||
));
|
||||
|
||||
@@ -51,13 +55,13 @@ errorret_t displayInit(void) {
|
||||
errorChain(capsuleInit());
|
||||
errorChain(triPrismInit());
|
||||
|
||||
// Init the subsystems
|
||||
errorChain(frameBufferInitBackBuffer());
|
||||
errorChain(spriteBatchInit());
|
||||
errorChain(textInit());
|
||||
errorChain(screenInit());
|
||||
|
||||
// Setup initial shader with default values
|
||||
|
||||
errorChain(shaderListInit());
|
||||
|
||||
errorOk();
|
||||
|
||||
@@ -7,4 +7,5 @@
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
text.c
|
||||
font.c
|
||||
)
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "font.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/math.h"
|
||||
#include "display/color.h"
|
||||
|
||||
font_t FONT_DEFAULT;
|
||||
static texture_t FONT_DEFAULT_TEXTURE;
|
||||
static tileset_t FONT_DEFAULT_TILESET;
|
||||
|
||||
const uint8_t FONT_DEFAULT_GLYPHS[FONT_DEFAULT_TILE_COUNT][
|
||||
FONT_DEFAULT_TILE_HEIGHT
|
||||
] = {
|
||||
{ 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x10, 0x00, 0x00 }, // !
|
||||
{ 0x00, 0x14, 0x14, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // "
|
||||
{ 0x00, 0x14, 0x14, 0x3E, 0x14, 0x3E, 0x14, 0x14, 0x00, 0x00 }, // #
|
||||
{ 0x00, 0x08, 0x1E, 0x28, 0x1C, 0x0A, 0x3C, 0x08, 0x00, 0x00 }, // $
|
||||
{ 0x00, 0x00, 0x22, 0x24, 0x08, 0x12, 0x22, 0x00, 0x00, 0x00 }, // %
|
||||
{ 0x00, 0x08, 0x14, 0x14, 0x1A, 0x24, 0x24, 0x1A, 0x00, 0x00 }, // &
|
||||
{ 0x00, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // '
|
||||
{ 0x00, 0x04, 0x08, 0x08, 0x08, 0x08, 0x08, 0x04, 0x00, 0x00 }, // (
|
||||
{ 0x00, 0x10, 0x08, 0x08, 0x08, 0x08, 0x08, 0x10, 0x00, 0x00 }, // )
|
||||
{ 0x00, 0x00, 0x08, 0x2A, 0x1C, 0x2A, 0x08, 0x00, 0x00, 0x00 }, // *
|
||||
{ 0x00, 0x00, 0x08, 0x08, 0x3E, 0x08, 0x08, 0x00, 0x00, 0x00 }, // +
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x20, 0x00 }, // ,
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00 }, // -
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00 }, // .
|
||||
{ 0x00, 0x04, 0x04, 0x08, 0x08, 0x08, 0x10, 0x10, 0x00, 0x00 }, // /
|
||||
{ 0x00, 0x1C, 0x22, 0x26, 0x2A, 0x32, 0x22, 0x1C, 0x00, 0x00 }, // 0
|
||||
{ 0x00, 0x08, 0x18, 0x08, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // 1
|
||||
{ 0x00, 0x1C, 0x22, 0x02, 0x04, 0x08, 0x10, 0x3E, 0x00, 0x00 }, // 2
|
||||
{ 0x00, 0x1C, 0x22, 0x02, 0x0C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 3
|
||||
{ 0x00, 0x22, 0x22, 0x22, 0x3E, 0x02, 0x02, 0x02, 0x00, 0x00 }, // 4
|
||||
{ 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 5
|
||||
{ 0x00, 0x1C, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // 6
|
||||
{ 0x00, 0x3E, 0x02, 0x02, 0x04, 0x08, 0x08, 0x08, 0x00, 0x00 }, // 7
|
||||
{ 0x00, 0x1C, 0x22, 0x22, 0x1C, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // 8
|
||||
{ 0x00, 0x1C, 0x22, 0x22, 0x1E, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 9
|
||||
{ 0x00, 0x00, 0x10, 0x10, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00 }, // :
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ;
|
||||
{ 0x00, 0x04, 0x08, 0x10, 0x20, 0x10, 0x08, 0x04, 0x00, 0x00 }, // <
|
||||
{ 0x00, 0x00, 0x00, 0x3E, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00 }, // =
|
||||
{ 0x00, 0x10, 0x08, 0x04, 0x02, 0x04, 0x08, 0x10, 0x00, 0x00 }, // >
|
||||
{ 0x00, 0x1C, 0x22, 0x02, 0x04, 0x08, 0x00, 0x08, 0x00, 0x00 }, // ?
|
||||
{ 0x00, 0x1C, 0x26, 0x2A, 0x2A, 0x26, 0x20, 0x1C, 0x00, 0x00 }, // @
|
||||
{ 0x00, 0x1C, 0x22, 0x22, 0x22, 0x3E, 0x22, 0x22, 0x00, 0x00 }, // A
|
||||
{ 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // B
|
||||
{ 0x00, 0x1C, 0x22, 0x20, 0x20, 0x20, 0x22, 0x1C, 0x00, 0x00 }, // C
|
||||
{ 0x00, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // D
|
||||
{ 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x20, 0x20, 0x3E, 0x00, 0x00 }, // E
|
||||
{ 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x20, 0x20, 0x20, 0x00, 0x00 }, // F
|
||||
{ 0x00, 0x1C, 0x22, 0x20, 0x2E, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // G
|
||||
{ 0x00, 0x22, 0x22, 0x22, 0x3E, 0x22, 0x22, 0x22, 0x00, 0x00 }, // H
|
||||
{ 0x00, 0x3E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // I
|
||||
{ 0x00, 0x02, 0x02, 0x02, 0x02, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // J
|
||||
{ 0x00, 0x22, 0x24, 0x28, 0x30, 0x28, 0x24, 0x22, 0x00, 0x00 }, // K
|
||||
{ 0x00, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3E, 0x00, 0x00 }, // L
|
||||
{ 0x00, 0x22, 0x36, 0x2A, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // M
|
||||
{ 0x00, 0x22, 0x22, 0x32, 0x2A, 0x26, 0x22, 0x22, 0x00, 0x00 }, // N
|
||||
{ 0x00, 0x1C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // O
|
||||
{ 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x20, 0x20, 0x20, 0x00, 0x00 }, // P
|
||||
{ 0x00, 0x1C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x06, 0x00 }, // Q
|
||||
{ 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x22, 0x22, 0x22, 0x00, 0x00 }, // R
|
||||
{ 0x00, 0x1C, 0x22, 0x20, 0x1C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // S
|
||||
{ 0x00, 0x3E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00 }, // T
|
||||
{ 0x00, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // U
|
||||
{ 0x00, 0x22, 0x22, 0x22, 0x22, 0x14, 0x14, 0x08, 0x00, 0x00 }, // V
|
||||
{ 0x00, 0x22, 0x22, 0x22, 0x22, 0x2A, 0x36, 0x22, 0x00, 0x00 }, // W
|
||||
{ 0x00, 0x22, 0x22, 0x14, 0x08, 0x14, 0x22, 0x22, 0x00, 0x00 }, // X
|
||||
{ 0x00, 0x22, 0x22, 0x14, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00 }, // Y
|
||||
{ 0x00, 0x3E, 0x02, 0x04, 0x08, 0x10, 0x20, 0x3E, 0x00, 0x00 }, // Z
|
||||
{ 0x00, 0x0C, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0C, 0x00, 0x00 }, // [
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // backslash (not drawn in source font)
|
||||
{ 0x00, 0x18, 0x08, 0x08, 0x08, 0x08, 0x08, 0x18, 0x00, 0x00 }, // ]
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ^ (not drawn in source font)
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // _ (not drawn in source font)
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ` (not drawn in source font)
|
||||
{ 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // a
|
||||
{ 0x00, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // b
|
||||
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x20, 0x22, 0x1C, 0x00, 0x00 }, // c
|
||||
{ 0x00, 0x02, 0x02, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // d
|
||||
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x3E, 0x20, 0x1C, 0x00, 0x00 }, // e
|
||||
{ 0x00, 0x0C, 0x12, 0x10, 0x3C, 0x10, 0x10, 0x10, 0x00, 0x00 }, // f
|
||||
{ 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x1C }, // g
|
||||
{ 0x00, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // h
|
||||
{ 0x00, 0x08, 0x00, 0x18, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // i
|
||||
{ 0x00, 0x02, 0x00, 0x06, 0x02, 0x02, 0x02, 0x02, 0x22, 0x1C }, // j
|
||||
{ 0x00, 0x20, 0x20, 0x22, 0x24, 0x38, 0x24, 0x22, 0x00, 0x00 }, // k
|
||||
{ 0x00, 0x30, 0x10, 0x10, 0x10, 0x10, 0x10, 0x0E, 0x00, 0x00 }, // l
|
||||
{ 0x00, 0x00, 0x00, 0x3C, 0x2A, 0x2A, 0x2A, 0x2A, 0x00, 0x00 }, // m
|
||||
{ 0x00, 0x00, 0x00, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // n
|
||||
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // o
|
||||
{ 0x00, 0x00, 0x00, 0x3C, 0x22, 0x22, 0x22, 0x3C, 0x20, 0x20 }, // p
|
||||
{ 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x02 }, // q
|
||||
{ 0x00, 0x00, 0x00, 0x2C, 0x32, 0x20, 0x20, 0x20, 0x00, 0x00 }, // r
|
||||
{ 0x00, 0x00, 0x00, 0x1E, 0x20, 0x1C, 0x02, 0x3C, 0x00, 0x00 }, // s
|
||||
{ 0x00, 0x10, 0x10, 0x3C, 0x10, 0x10, 0x10, 0x0E, 0x00, 0x00 }, // t
|
||||
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // u
|
||||
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x14, 0x08, 0x00, 0x00 }, // v
|
||||
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x2A, 0x2A, 0x14, 0x00, 0x00 }, // w
|
||||
{ 0x00, 0x00, 0x00, 0x22, 0x14, 0x08, 0x14, 0x22, 0x00, 0x00 }, // x
|
||||
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x1C }, // y
|
||||
{ 0x00, 0x00, 0x00, 0x3E, 0x04, 0x08, 0x10, 0x3E, 0x00, 0x00 }, // z
|
||||
{ 0x00, 0x04, 0x08, 0x08, 0x10, 0x08, 0x08, 0x04, 0x00, 0x00 }, // {
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // | (not drawn in source font)
|
||||
{ 0x00, 0x10, 0x08, 0x08, 0x04, 0x08, 0x08, 0x10, 0x00, 0x00 }, // }
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ~ (not drawn in source font)
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // unused trailing tile
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // unused trailing tile
|
||||
};
|
||||
|
||||
errorret_t fontDefaultInit(void) {
|
||||
const int32_t width = (int32_t)mathNextPowTwo(
|
||||
FONT_DEFAULT_COLUMNS * FONT_DEFAULT_TILE_WIDTH
|
||||
);
|
||||
const int32_t height = (int32_t)mathNextPowTwo(
|
||||
FONT_DEFAULT_ROWS * FONT_DEFAULT_TILE_HEIGHT
|
||||
);
|
||||
|
||||
color_t *pixels = memoryAllocate(sizeof(color_t) * width * height);
|
||||
memoryZero(pixels, sizeof(color_t) * width * height);
|
||||
|
||||
for(uint16_t i = 0; i < FONT_DEFAULT_TILE_COUNT; i++) {
|
||||
const uint16_t tileX = (i % FONT_DEFAULT_COLUMNS) * FONT_DEFAULT_TILE_WIDTH;
|
||||
const uint16_t tileY = (i / FONT_DEFAULT_COLUMNS) * FONT_DEFAULT_TILE_HEIGHT;
|
||||
|
||||
for(uint8_t row = 0; row < FONT_DEFAULT_TILE_HEIGHT; row++) {
|
||||
const uint8_t bits = FONT_DEFAULT_GLYPHS[i][row];
|
||||
|
||||
for(uint8_t col = 0; col < FONT_DEFAULT_TILE_WIDTH; col++) {
|
||||
if(!((bits >> (FONT_DEFAULT_TILE_WIDTH - 1 - col)) & 1)) continue;
|
||||
pixels[((tileY + row) * width) + (tileX + col)] = COLOR_WHITE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FONT_DEFAULT_TILESET.tileWidth = FONT_DEFAULT_TILE_WIDTH;
|
||||
FONT_DEFAULT_TILESET.tileHeight = FONT_DEFAULT_TILE_HEIGHT;
|
||||
FONT_DEFAULT_TILESET.columns = FONT_DEFAULT_COLUMNS;
|
||||
FONT_DEFAULT_TILESET.rows = FONT_DEFAULT_ROWS;
|
||||
FONT_DEFAULT_TILESET.tileCount = FONT_DEFAULT_TILE_COUNT;
|
||||
FONT_DEFAULT_TILESET.uv[0] = (float_t)FONT_DEFAULT_TILE_WIDTH / (float_t)width;
|
||||
FONT_DEFAULT_TILESET.uv[1] = (float_t)FONT_DEFAULT_TILE_HEIGHT / (float_t)height;
|
||||
|
||||
const texturedata_t data = { .rgbaColors = pixels };
|
||||
errorret_t textureResult = textureInit(
|
||||
&FONT_DEFAULT_TEXTURE, width, height, TEXTURE_FORMAT_RGBA, data
|
||||
);
|
||||
memoryFree(pixels);
|
||||
errorChain(textureResult);
|
||||
|
||||
FONT_DEFAULT.texture = &FONT_DEFAULT_TEXTURE;
|
||||
FONT_DEFAULT.tileset = &FONT_DEFAULT_TILESET;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t fontDefaultDispose(void) {
|
||||
errorChain(textureDispose(&FONT_DEFAULT_TEXTURE));
|
||||
FONT_DEFAULT.texture = NULL;
|
||||
FONT_DEFAULT.tileset = NULL;
|
||||
errorOk();
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "display/texture/texture.h"
|
||||
#include "display/texture/tileset.h"
|
||||
|
||||
@@ -13,3 +14,51 @@ typedef struct {
|
||||
texture_t *texture;
|
||||
tileset_t *tileset;
|
||||
} font_t;
|
||||
|
||||
/**
|
||||
* Pixel width/height of a single default-font glyph tile.
|
||||
*/
|
||||
#define FONT_DEFAULT_TILE_WIDTH 6
|
||||
#define FONT_DEFAULT_TILE_HEIGHT 10
|
||||
|
||||
/** Grid layout of the generated default-font texture, in tiles. */
|
||||
#define FONT_DEFAULT_COLUMNS 16
|
||||
#define FONT_DEFAULT_ROWS 6
|
||||
|
||||
/**
|
||||
* Number of glyphs defined in FONT_DEFAULT_GLYPHS (FONT_DEFAULT_COLUMNS *
|
||||
* FONT_DEFAULT_ROWS), covering the printable ASCII range starting at
|
||||
* TEXT_CHAR_START ('!') plus a couple of unused trailing tiles.
|
||||
*/
|
||||
#define FONT_DEFAULT_TILE_COUNT (FONT_DEFAULT_COLUMNS * FONT_DEFAULT_ROWS)
|
||||
|
||||
extern font_t FONT_DEFAULT;
|
||||
|
||||
/**
|
||||
* Hard coded bitmap data for the built-in default font. Indexed
|
||||
* [glyph][row], where glyph 0 corresponds to TEXT_CHAR_START ('!') and
|
||||
* glyphs run consecutively through the printable ASCII range. Each row
|
||||
* byte holds FONT_DEFAULT_TILE_WIDTH bit flags, one per pixel column:
|
||||
* bit (FONT_DEFAULT_TILE_WIDTH - 1) is the leftmost pixel and bit 0 is
|
||||
* the rightmost; 1 means the pixel is set, 0 means it is not.
|
||||
*/
|
||||
extern const uint8_t FONT_DEFAULT_GLYPHS[FONT_DEFAULT_TILE_COUNT][
|
||||
FONT_DEFAULT_TILE_HEIGHT
|
||||
];
|
||||
|
||||
/**
|
||||
* Builds the default font's texture + tileset directly from
|
||||
* FONT_DEFAULT_GLYPHS, without going through the asset system - so the
|
||||
* engine always has a usable font to render with regardless of whether
|
||||
* asset loading (e.g. the packed .dsk archive) succeeds.
|
||||
*
|
||||
* @return Either an error or success result.
|
||||
*/
|
||||
errorret_t fontDefaultInit(void);
|
||||
|
||||
/**
|
||||
* Disposes of the default font created by fontDefaultInit().
|
||||
*
|
||||
* @return Either an error or success result.
|
||||
*/
|
||||
errorret_t fontDefaultDispose(void);
|
||||
|
||||
@@ -9,34 +9,15 @@
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "asset/asset.h"
|
||||
#include "asset/loader/display/assettextureloader.h"
|
||||
#include "asset/loader/display/assettilesetloader.h"
|
||||
#include "display/shader/shaderunlit.h"
|
||||
|
||||
font_t FONT_DEFAULT;
|
||||
|
||||
errorret_t textInit(void) {
|
||||
assetloaderinput_t input = { .texture = TEXTURE_FORMAT_RGBA };
|
||||
assetentry_t *entryTexture = assetLock(
|
||||
"ui/minogram.png", ASSET_LOADER_TYPE_TEXTURE, &input
|
||||
);
|
||||
assetentry_t *entryTileset = assetLock(
|
||||
"ui/minogram.dtf", ASSET_LOADER_TYPE_TILESET, NULL
|
||||
);
|
||||
errorChain(assetRequireLoaded(entryTexture));
|
||||
errorChain(assetRequireLoaded(entryTileset));
|
||||
|
||||
FONT_DEFAULT.texture = &entryTexture->data.texture;
|
||||
FONT_DEFAULT.tileset = &entryTileset->data.tileset;
|
||||
errorChain(fontDefaultInit());
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t textDispose(void) {
|
||||
FONT_DEFAULT.texture = NULL;
|
||||
FONT_DEFAULT.tileset = NULL;
|
||||
assetUnlock("ui/minogram.png");
|
||||
assetUnlock("ui/minogram.dtf");
|
||||
errorChain(fontDefaultDispose());
|
||||
errorOk();
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
|
||||
#define TEXT_CHAR_START '!'
|
||||
|
||||
extern font_t FONT_DEFAULT;
|
||||
|
||||
/**
|
||||
* Initializes the text system.
|
||||
*
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#include "display/display.h"
|
||||
|
||||
texture_t TEXTURE_WHITE;
|
||||
color_t TEXTURE_WHITE_PIXELS[4*4] = {
|
||||
color_t TEXTURE_WHITE_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT] = {
|
||||
COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE,
|
||||
COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE,
|
||||
COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE,
|
||||
@@ -20,7 +20,7 @@ color_t TEXTURE_WHITE_PIXELS[4*4] = {
|
||||
};
|
||||
|
||||
texture_t TEXTURE_TEST;
|
||||
color_t TEXTURE_TEST_PIXELS[4*4] = {
|
||||
color_t TEXTURE_TEST_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT] = {
|
||||
COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA,
|
||||
COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK,
|
||||
COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA,
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
#error "textureDisposePlatform should not be defined."
|
||||
#endif
|
||||
|
||||
#define TEXTURE_FIXED_WIDTH 4
|
||||
#define TEXTURE_FIXED_HEIGHT 4
|
||||
|
||||
typedef textureformatplatform_t textureformat_t;
|
||||
typedef textureplatform_t texture_t;
|
||||
|
||||
@@ -29,9 +32,9 @@ typedef union texturedata_u {
|
||||
} texturedata_t;
|
||||
|
||||
extern texture_t TEXTURE_WHITE;
|
||||
extern color_t TEXTURE_WHITE_PIXELS[4*4];
|
||||
extern color_t TEXTURE_WHITE_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT];
|
||||
extern texture_t TEXTURE_TEST;
|
||||
extern color_t TEXTURE_TEST_PIXELS[4*4];
|
||||
extern color_t TEXTURE_TEST_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT];
|
||||
|
||||
/**
|
||||
* Initializes a texture.
|
||||
|
||||
@@ -16,10 +16,12 @@
|
||||
#include "asset/asset.h"
|
||||
#include "ui/ui.h"
|
||||
#include "assert/assert.h"
|
||||
#include "network/network.h"
|
||||
#ifdef DUSK_NETWORK
|
||||
#include "network/network.h"
|
||||
#endif
|
||||
#include "system/system.h"
|
||||
#include "console/console.h"
|
||||
#include "save/save.h"
|
||||
#include "save/savemanager.h"\
|
||||
|
||||
engine_t ENGINE;
|
||||
|
||||
@@ -37,12 +39,14 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
||||
errorChain(systemInit());
|
||||
errorChain(inputInit());
|
||||
errorChain(assetInit());
|
||||
// errorChain(saveInit());
|
||||
errorChain(saveManagerInit());
|
||||
errorChain(localeManagerInit());
|
||||
errorChain(displayInit());
|
||||
errorChain(uiInit());
|
||||
errorChain(rpgInit());
|
||||
#ifdef DUSK_NETWORK
|
||||
errorChain(networkInit());
|
||||
#endif
|
||||
errorChain(sceneInit());
|
||||
|
||||
consolePrint("Engine initialized");
|
||||
@@ -53,15 +57,17 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
||||
consolePrint("Assertions real");
|
||||
#endif
|
||||
|
||||
sceneSet(SCENE_TYPE_OVERWORLD);
|
||||
|
||||
sceneSet(SCENE_TYPE_INITIAL);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t engineUpdate(void) {
|
||||
// Order here is important.
|
||||
#ifdef DUSK_NETWORK
|
||||
errorChain(networkUpdate());
|
||||
#endif
|
||||
errorChain(saveManagerUpdate());
|
||||
timeUpdate();
|
||||
inputUpdate();
|
||||
consoleUpdate();
|
||||
@@ -82,13 +88,15 @@ void engineExit(void) {
|
||||
|
||||
errorret_t engineDispose(void) {
|
||||
errorChain(sceneDispose());
|
||||
#ifdef DUSK_NETWORK
|
||||
errorChain(networkDispose());
|
||||
#endif
|
||||
errorChain(rpgDispose());
|
||||
localeManagerDispose();
|
||||
errorChain(uiDispose());
|
||||
consoleDispose();
|
||||
errorChain(displayDispose());
|
||||
// errorChain(saveDispose());
|
||||
errorChain(saveManagerDispose());
|
||||
errorChain(assetDispose());
|
||||
|
||||
errorOk();
|
||||
|
||||
@@ -17,26 +17,11 @@ input_t INPUT;
|
||||
|
||||
errorret_t inputInit(void) {
|
||||
memoryZero(&INPUT, sizeof(input_t));
|
||||
INPUT.deadzone = INPUT_DEADZONE_DEFAULT;
|
||||
|
||||
for(uint8_t i = 0; i < INPUT_ACTION_COUNT; i++) {
|
||||
INPUT.actions[i].action = (inputaction_t)i;
|
||||
INPUT.actions[i].lastValue = 0.0f;
|
||||
INPUT.actions[i].currentValue = 0.0f;
|
||||
|
||||
eventInit(
|
||||
&INPUT.actions[i].onPressed,
|
||||
INPUT.actions[i].onPressedCallbacks,
|
||||
INPUT.actions[i].onPressedUsers,
|
||||
INPUT_ACTION_CALLBACK_COUNT_MAX
|
||||
);
|
||||
|
||||
eventInit(
|
||||
&INPUT.actions[i].onReleased,
|
||||
INPUT.actions[i].onReleasedCallbacks,
|
||||
INPUT.actions[i].onReleasedUsers,
|
||||
INPUT_ACTION_CALLBACK_COUNT_MAX
|
||||
);
|
||||
}
|
||||
|
||||
#ifdef inputInitPlatform
|
||||
@@ -107,8 +92,6 @@ void inputUpdate(void) {
|
||||
inputactiondata_t *act = &INPUT.actions[i];
|
||||
bool_t isDown = act->currentValue > 0.0f;
|
||||
bool_t wasDown = act->lastValue > 0.0f;
|
||||
if(isDown && !wasDown) eventInvoke(&act->onPressed, act);
|
||||
if(!isDown && wasDown) eventInvoke(&act->onReleased, act);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,17 +10,9 @@
|
||||
#include "inputbutton.h"
|
||||
#include "inputaction.h"
|
||||
|
||||
#define INPUT_LISTENER_PRESSED_MAX 16
|
||||
#define INPUT_LISTENER_RELEASED_MAX INPUT_LISTENER_PRESSED_MAX
|
||||
#define INPUT_DEADZONE_DEFAULT 0.1f
|
||||
|
||||
typedef struct {
|
||||
inputactiondata_t actions[INPUT_ACTION_COUNT];
|
||||
|
||||
inputplatform_t platform;
|
||||
|
||||
/** User-configured gamepad axis deadzone (0.0f to 1.0f). */
|
||||
float_t deadzone;
|
||||
} input_t;
|
||||
|
||||
extern input_t INPUT;
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#pragma once
|
||||
#include "time/time.h"
|
||||
#include "input/inputactiondefs.h"
|
||||
#include "event/event.h"
|
||||
|
||||
#define INPUT_ACTION_CALLBACK_COUNT_MAX 4
|
||||
|
||||
@@ -21,13 +20,6 @@ typedef struct {
|
||||
float_t lastDynamicValue;
|
||||
float_t currentDynamicValue;
|
||||
#endif
|
||||
|
||||
eventcallback_t onPressedCallbacks[INPUT_ACTION_CALLBACK_COUNT_MAX];
|
||||
void *onPressedUsers[INPUT_ACTION_CALLBACK_COUNT_MAX];
|
||||
event_t onPressed;
|
||||
eventcallback_t onReleasedCallbacks[INPUT_ACTION_CALLBACK_COUNT_MAX];
|
||||
void *onReleasedUsers[INPUT_ACTION_CALLBACK_COUNT_MAX];
|
||||
event_t onReleased;
|
||||
} inputactiondata_t;
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,17 +13,31 @@ typedef struct {
|
||||
const char_t *file;
|
||||
} localeinfo_t;
|
||||
|
||||
static const localeinfo_t LOCALE_EN_US = {
|
||||
static const localeinfo_t LOCALE_INFO_EN_US = {
|
||||
.name = "en-US",
|
||||
.file = "locale/en_US.po",
|
||||
};
|
||||
|
||||
static const localeinfo_t LOCALE_JP_JP = {
|
||||
static const localeinfo_t LOCALE_INFO_JP_JP = {
|
||||
.name = "ja-JP",
|
||||
.file = "locale/jp_JP.po",
|
||||
};
|
||||
|
||||
static const localeinfo_t LOCALE_ES_MX = {
|
||||
static const localeinfo_t LOCALE_INFO_ES_MX = {
|
||||
.name = "es-MX",
|
||||
.file = "locale/es_MX.po",
|
||||
};
|
||||
|
||||
static const localeinfo_t * const LOCALE_INFO_LIST[] = {
|
||||
&LOCALE_INFO_EN_US,
|
||||
&LOCALE_INFO_JP_JP,
|
||||
&LOCALE_INFO_ES_MX
|
||||
};
|
||||
|
||||
#define LOCALE_INFO_LIST_COUNT ( \
|
||||
sizeof(LOCALE_INFO_LIST) / sizeof(LOCALE_INFO_LIST[0]) \
|
||||
)
|
||||
|
||||
#define LOCALE_DEFAULT LOCALE_INFO_EN_US
|
||||
|
||||
// EOF
|
||||
|
||||
@@ -7,13 +7,22 @@
|
||||
|
||||
#include "localemanager.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
#include "ui/ui.h"
|
||||
#include "system/system.h"
|
||||
#include "console/console.h"
|
||||
|
||||
localemanager_t LOCALE;
|
||||
|
||||
errorret_t localeManagerInit() {
|
||||
memoryZero(&LOCALE, sizeof(localemanager_t));
|
||||
errorChain(localeManagerSetLocale(&LOCALE_EN_US));
|
||||
|
||||
// TODO: Set locale based on system locale.
|
||||
const localeinfo_t *locale = systemGetLocale();
|
||||
errorChain(localeManagerSetLocale(locale));
|
||||
consolePrint("Locale set to: %s", locale->name);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -30,6 +39,9 @@ errorret_t localeManagerSetLocale(const localeinfo_t *locale) {
|
||||
assetEntryLock(LOCALE.entry);
|
||||
errorChain(assetRequireLoaded(LOCALE.entry));
|
||||
|
||||
// TODO : Trigger UI update.
|
||||
errorChain(uiUpdateTranslations());
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "localemanager.h"
|
||||
#include "locale/localemanager.h"
|
||||
#include "locale/localeinfo.h"
|
||||
#include "asset/asset.h"
|
||||
|
||||
|
||||
@@ -10,3 +10,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
battlefighter.c
|
||||
party.c
|
||||
)
|
||||
|
||||
# Subdirs
|
||||
add_subdirectory(testbattle)
|
||||
|
||||
+161
-58
@@ -8,6 +8,7 @@
|
||||
#include "battle.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
|
||||
battle_t BATTLE;
|
||||
|
||||
@@ -51,10 +52,8 @@ void battleStart(
|
||||
BATTLE.fleeAvailable = fleeAvailable;
|
||||
BATTLE.result = BATTLE_RESULT_NONE;
|
||||
BATTLE.round = 1;
|
||||
BATTLE.turnIndex = 0;
|
||||
battleBuildTurnOrder(true);
|
||||
|
||||
BATTLE.active = true;
|
||||
battleSetState(BATTLE_STATE_OPENING);
|
||||
}
|
||||
|
||||
void battleDispose(void) {
|
||||
@@ -62,9 +61,9 @@ void battleDispose(void) {
|
||||
}
|
||||
|
||||
battlefighter_t *battleGetCurrentFighter(void) {
|
||||
if(!BATTLE.active) return NULL;
|
||||
if(BATTLE.turnIndex >= BATTLE.turnCount) return NULL;
|
||||
return &BATTLE.fighters[BATTLE.turnOrder[BATTLE.turnIndex]];
|
||||
if(BATTLE.state != BATTLE_STATE_PLAYER_SELECTION) return NULL;
|
||||
if(BATTLE.selectionIndex >= BATTLE.executionCount) return NULL;
|
||||
return &BATTLE.fighters[BATTLE.executionOrder[BATTLE.selectionIndex]];
|
||||
}
|
||||
|
||||
uint8_t battleGetAliveCount(const battlefighterteam_t team) {
|
||||
@@ -92,91 +91,105 @@ void battleResolveAttack(
|
||||
if(defender->health == 0) defender->status = BATTLE_FIGHTER_STATUS_DEAD;
|
||||
}
|
||||
|
||||
void battleNextTurn(void) {
|
||||
BATTLE.turnIndex++;
|
||||
if(BATTLE.turnIndex < BATTLE.turnCount) return;
|
||||
|
||||
BATTLE.round++;
|
||||
BATTLE.turnIndex = 0;
|
||||
battleBuildTurnOrder(false);
|
||||
}
|
||||
|
||||
battleresult_t battleCheckResult(void) {
|
||||
if(BATTLE.result != BATTLE_RESULT_NONE) return BATTLE.result;
|
||||
|
||||
if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ALLY) == 0) {
|
||||
BATTLE.result = BATTLE_RESULT_LOSS;
|
||||
battleSetResult(BATTLE_RESULT_LOSS);
|
||||
} else if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ENEMY) == 0) {
|
||||
BATTLE.result = BATTLE_RESULT_WIN;
|
||||
battleSetResult(BATTLE_RESULT_WIN);
|
||||
}
|
||||
|
||||
return BATTLE.result;
|
||||
}
|
||||
|
||||
void battleQueueAction(
|
||||
const uint8_t fighterIndex,
|
||||
const battleactiontype_t type,
|
||||
const uint8_t targetIndex
|
||||
) {
|
||||
battleaction_t *action = &BATTLE.actions[fighterIndex];
|
||||
action->type = type;
|
||||
action->targetIndex = targetIndex;
|
||||
|
||||
if(BATTLE.onActionDecided != NULL) {
|
||||
BATTLE.onActionDecided(&BATTLE.fighters[fighterIndex], action);
|
||||
}
|
||||
}
|
||||
|
||||
void battlePlayerAttack(const uint8_t targetIndex) {
|
||||
battlefighter_t *attacker = battleGetCurrentFighter();
|
||||
if(attacker == NULL) return;
|
||||
if(attacker->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) return;
|
||||
battlefighter_t *fighter = battleGetCurrentFighter();
|
||||
if(fighter == NULL) return;
|
||||
if(targetIndex >= BATTLE_FIGHTER_COUNT_MAX) return;
|
||||
if(!battleFighterIsAlive(&BATTLE.fighters[targetIndex])) return;
|
||||
|
||||
battlefighter_t *defender = &BATTLE.fighters[targetIndex];
|
||||
if(!battleFighterIsAlive(defender)) return;
|
||||
|
||||
battleResolveAttack(attacker, defender);
|
||||
battleCheckResult();
|
||||
if(BATTLE.result == BATTLE_RESULT_NONE) battleNextTurn();
|
||||
battleQueueAction(fighter->id, BATTLE_ACTION_ATTACK, targetIndex);
|
||||
BATTLE.selectionIndex++;
|
||||
battleAdvanceSelection();
|
||||
}
|
||||
|
||||
void battlePlayerFlee(void) {
|
||||
battlefighter_t *fighter = battleGetCurrentFighter();
|
||||
if(fighter == NULL) return;
|
||||
if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) return;
|
||||
if(!BATTLE.fleeAvailable) return;
|
||||
|
||||
BATTLE.result = BATTLE_RESULT_FLED;
|
||||
battleSetResult(BATTLE_RESULT_FLED);
|
||||
}
|
||||
|
||||
void battleUpdate(void) {
|
||||
if(!BATTLE.active) return;
|
||||
if(BATTLE.result != BATTLE_RESULT_NONE) return;
|
||||
if(BATTLE.state == BATTLE_STATE_NONE) return;
|
||||
if(BATTLE.state == BATTLE_STATE_ENDED) return;
|
||||
if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_BATTLE) return;
|
||||
|
||||
battlefighter_t *current = battleGetCurrentFighter();
|
||||
if(current == NULL) return;
|
||||
switch(BATTLE.state) {
|
||||
case BATTLE_STATE_OPENING:
|
||||
battleSetState(BATTLE_STATE_PRE_ROUND);
|
||||
break;
|
||||
|
||||
if(!battleFighterIsAlive(current)) {
|
||||
battleNextTurn();
|
||||
return;
|
||||
case BATTLE_STATE_PRE_ROUND:
|
||||
battleUpdatePreRound();
|
||||
break;
|
||||
|
||||
case BATTLE_STATE_AI_SELECTION:
|
||||
battleUpdateAiSelection();
|
||||
break;
|
||||
|
||||
case BATTLE_STATE_MOVES_EXECUTING:
|
||||
battleUpdateMovesExecuting();
|
||||
break;
|
||||
|
||||
case BATTLE_STATE_POST_ROUND:
|
||||
battleUpdatePostRound();
|
||||
break;
|
||||
|
||||
default:
|
||||
// BATTLE_STATE_PLAYER_SELECTION: waits on battlePlayerAttack/Flee.
|
||||
// BATTLE_STATE_NONE/ENDED: handled above.
|
||||
break;
|
||||
}
|
||||
|
||||
if(current->controller != BATTLE_FIGHTER_CONTROLLER_AI) return;
|
||||
|
||||
battlefighter_t *target = battleAIChooseTarget(current);
|
||||
if(target != NULL) battleResolveAttack(current, target);
|
||||
|
||||
battleCheckResult();
|
||||
if(BATTLE.result == BATTLE_RESULT_NONE) battleNextTurn();
|
||||
}
|
||||
|
||||
void battleBuildTurnOrder(const bool_t applyEncounterBias) {
|
||||
BATTLE.turnCount = 0;
|
||||
void battleBuildExecutionOrder(const bool_t applyEncounterBias) {
|
||||
BATTLE.executionCount = 0;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
|
||||
BATTLE.turnOrder[BATTLE.turnCount++] = i;
|
||||
BATTLE.executionOrder[BATTLE.executionCount++] = i;
|
||||
}
|
||||
|
||||
// Insertion sort by speed descending -- fine for BATTLE_FIGHTER_COUNT_MAX.
|
||||
for(uint8_t i = 1; i < BATTLE.turnCount; i++) {
|
||||
const uint8_t key = BATTLE.turnOrder[i];
|
||||
for(uint8_t i = 1; i < BATTLE.executionCount; i++) {
|
||||
const uint8_t key = BATTLE.executionOrder[i];
|
||||
const uint16_t keySpeed = BATTLE.fighters[key].stats.speed;
|
||||
|
||||
int8_t j = (int8_t)i - 1;
|
||||
while(
|
||||
j >= 0 && BATTLE.fighters[BATTLE.turnOrder[j]].stats.speed < keySpeed
|
||||
j >= 0 &&
|
||||
BATTLE.fighters[BATTLE.executionOrder[j]].stats.speed < keySpeed
|
||||
) {
|
||||
BATTLE.turnOrder[j + 1] = BATTLE.turnOrder[j];
|
||||
BATTLE.executionOrder[j + 1] = BATTLE.executionOrder[j];
|
||||
j--;
|
||||
}
|
||||
BATTLE.turnOrder[j + 1] = key;
|
||||
BATTLE.executionOrder[j + 1] = key;
|
||||
}
|
||||
|
||||
if(!applyEncounterBias) return;
|
||||
@@ -192,16 +205,16 @@ void battleMoveTeamFirst(const battlefighterteam_t team) {
|
||||
uint8_t sorted[BATTLE_FIGHTER_COUNT_MAX];
|
||||
uint8_t count = 0;
|
||||
|
||||
for(uint8_t i = 0; i < BATTLE.turnCount; i++) {
|
||||
if(BATTLE.fighters[BATTLE.turnOrder[i]].team != team) continue;
|
||||
sorted[count++] = BATTLE.turnOrder[i];
|
||||
for(uint8_t i = 0; i < BATTLE.executionCount; i++) {
|
||||
if(BATTLE.fighters[BATTLE.executionOrder[i]].team != team) continue;
|
||||
sorted[count++] = BATTLE.executionOrder[i];
|
||||
}
|
||||
for(uint8_t i = 0; i < BATTLE.turnCount; i++) {
|
||||
if(BATTLE.fighters[BATTLE.turnOrder[i]].team == team) continue;
|
||||
sorted[count++] = BATTLE.turnOrder[i];
|
||||
for(uint8_t i = 0; i < BATTLE.executionCount; i++) {
|
||||
if(BATTLE.fighters[BATTLE.executionOrder[i]].team == team) continue;
|
||||
sorted[count++] = BATTLE.executionOrder[i];
|
||||
}
|
||||
|
||||
memoryCopy(BATTLE.turnOrder, sorted, sizeof(uint8_t) * BATTLE.turnCount);
|
||||
memoryCopy(BATTLE.executionOrder, sorted, sizeof(uint8_t) * BATTLE.executionCount);
|
||||
}
|
||||
|
||||
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter) {
|
||||
@@ -221,3 +234,93 @@ battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter) {
|
||||
|
||||
return weakest;
|
||||
}
|
||||
|
||||
void battleSetState(const battlestate_t next) {
|
||||
const battlestate_t previous = BATTLE.state;
|
||||
BATTLE.state = next;
|
||||
if(BATTLE.onStateChanged != NULL) BATTLE.onStateChanged(previous, next);
|
||||
}
|
||||
|
||||
void battleSetResult(const battleresult_t result) {
|
||||
BATTLE.result = result;
|
||||
battleSetState(BATTLE_STATE_ENDED);
|
||||
}
|
||||
|
||||
bool_t battleFighterNeedsDecision(
|
||||
const uint8_t fighterIndex,
|
||||
const battlefightercontroller_t controller
|
||||
) {
|
||||
return battleFighterIsAlive(&BATTLE.fighters[fighterIndex])
|
||||
&& BATTLE.fighters[fighterIndex].controller == controller
|
||||
&& BATTLE.actions[fighterIndex].type == BATTLE_ACTION_NONE;
|
||||
}
|
||||
|
||||
void battleAdvanceSelection(void) {
|
||||
while(BATTLE.selectionIndex < BATTLE.executionCount) {
|
||||
const uint8_t fighterIndex = BATTLE.executionOrder[BATTLE.selectionIndex];
|
||||
if(
|
||||
battleFighterNeedsDecision(fighterIndex, BATTLE_FIGHTER_CONTROLLER_PLAYER)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
BATTLE.selectionIndex++;
|
||||
}
|
||||
|
||||
battleSetState(BATTLE_STATE_AI_SELECTION);
|
||||
}
|
||||
|
||||
void battleUpdatePreRound(void) {
|
||||
// No need to clear BATTLE.actions here: every living fighter's action is
|
||||
// unconditionally reset to BATTLE_ACTION_NONE as it's processed in
|
||||
// battleUpdateMovesExecuting, and round 1 starts pre-zeroed by
|
||||
// battleInit(). Clearing it here would also wipe out any action a
|
||||
// cutscene force-queued while parked at BATTLE_STATE_PRE_ROUND.
|
||||
battleBuildExecutionOrder(BATTLE.round == 1);
|
||||
|
||||
BATTLE.selectionIndex = 0;
|
||||
battleSetState(BATTLE_STATE_PLAYER_SELECTION);
|
||||
battleAdvanceSelection();
|
||||
}
|
||||
|
||||
void battleUpdateAiSelection(void) {
|
||||
for(uint8_t i = 0; i < BATTLE.executionCount; i++) {
|
||||
const uint8_t fighterIndex = BATTLE.executionOrder[i];
|
||||
if(
|
||||
!battleFighterNeedsDecision(fighterIndex, BATTLE_FIGHTER_CONTROLLER_AI)
|
||||
) continue;
|
||||
|
||||
battlefighter_t *target =
|
||||
battleAIChooseTarget(&BATTLE.fighters[fighterIndex]);
|
||||
if(target == NULL) continue;
|
||||
|
||||
battleQueueAction(fighterIndex, BATTLE_ACTION_ATTACK, target->id);
|
||||
}
|
||||
|
||||
BATTLE.executionIndex = 0;
|
||||
battleSetState(BATTLE_STATE_MOVES_EXECUTING);
|
||||
}
|
||||
|
||||
void battleUpdateMovesExecuting(void) {
|
||||
if(BATTLE.executionIndex >= BATTLE.executionCount) {
|
||||
battleSetState(BATTLE_STATE_POST_ROUND);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t fighterIndex = BATTLE.executionOrder[BATTLE.executionIndex++];
|
||||
battlefighter_t *fighter = &BATTLE.fighters[fighterIndex];
|
||||
if(!battleFighterIsAlive(fighter)) return;
|
||||
|
||||
battleaction_t *action = &BATTLE.actions[fighterIndex];
|
||||
if(action->type == BATTLE_ACTION_ATTACK) {
|
||||
battlefighter_t *target = &BATTLE.fighters[action->targetIndex];
|
||||
if(battleFighterIsAlive(target)) battleResolveAttack(fighter, target);
|
||||
}
|
||||
action->type = BATTLE_ACTION_NONE;
|
||||
|
||||
battleCheckResult();
|
||||
}
|
||||
|
||||
void battleUpdatePostRound(void) {
|
||||
BATTLE.round++;
|
||||
battleSetState(BATTLE_STATE_PRE_ROUND);
|
||||
}
|
||||
|
||||
+155
-36
@@ -27,19 +27,68 @@ typedef enum {
|
||||
BATTLE_RESULT_COUNT
|
||||
} battleresult_t;
|
||||
|
||||
// Where BATTLE currently is within a round. A cutscene can pause progression
|
||||
// (CUTSCENE_PAUSE_BATTLE) and use CUTSCENE_BATTLE_WAIT_STATE to synchronize
|
||||
// with any of these, or CUTSCENE_BATTLE_FORCE_ACTION to decide a fighter's
|
||||
// action ahead of PLAYER_SELECTION/AI_SELECTION reaching them.
|
||||
typedef enum {
|
||||
BATTLE_STATE_NONE, // Battle inactive.
|
||||
|
||||
BATTLE_STATE_OPENING, // Entered once by battleStart().
|
||||
BATTLE_STATE_PRE_ROUND, // Rebuilds execution order, clears the action queue.
|
||||
BATTLE_STATE_PLAYER_SELECTION, // Waits on battlePlayerAttack/Flee.
|
||||
BATTLE_STATE_AI_SELECTION, // Auto-queues every undecided AI fighter.
|
||||
BATTLE_STATE_MOVES_EXECUTING, // Resolves one queued action per update.
|
||||
BATTLE_STATE_POST_ROUND, // Round wrap-up; loops back to PRE_ROUND.
|
||||
|
||||
BATTLE_STATE_ENDED, // Terminal for WIN/LOSS/FLED alike -- see BATTLE.result.
|
||||
|
||||
BATTLE_STATE_COUNT
|
||||
} battlestate_t;
|
||||
|
||||
typedef enum {
|
||||
BATTLE_ACTION_NONE, // No action decided yet for this fighter this round.
|
||||
BATTLE_ACTION_ATTACK,
|
||||
|
||||
BATTLE_ACTION_COUNT
|
||||
} battleactiontype_t;
|
||||
|
||||
typedef struct {
|
||||
bool_t active;
|
||||
battleactiontype_t type;
|
||||
uint8_t targetIndex; // Meaningful for BATTLE_ACTION_ATTACK.
|
||||
} battleaction_t;
|
||||
|
||||
typedef void (*battlestatechangedcallback_t)(
|
||||
const battlestate_t previous,
|
||||
const battlestate_t next
|
||||
);
|
||||
|
||||
typedef void (*battleactiondecidedcallback_t)(
|
||||
const battlefighter_t *fighter,
|
||||
const battleaction_t *action
|
||||
);
|
||||
|
||||
typedef struct {
|
||||
battlestate_t state;
|
||||
battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX];
|
||||
battleaction_t actions[BATTLE_FIGHTER_COUNT_MAX];
|
||||
|
||||
battleencountertype_t encounterType;
|
||||
bool_t fleeAvailable;
|
||||
battleresult_t result;
|
||||
|
||||
// Fighter indices (into fighters[]), sorted for the current round.
|
||||
uint8_t turnOrder[BATTLE_FIGHTER_COUNT_MAX];
|
||||
uint8_t turnCount;
|
||||
uint8_t turnIndex;
|
||||
uint8_t executionOrder[BATTLE_FIGHTER_COUNT_MAX];
|
||||
uint8_t executionCount;
|
||||
uint8_t executionIndex;
|
||||
uint16_t round;
|
||||
|
||||
// Cursor into executionOrder used by BATTLE_STATE_PLAYER_SELECTION to find
|
||||
// the next player-controlled fighter that still needs a decision.
|
||||
uint8_t selectionIndex;
|
||||
|
||||
battlestatechangedcallback_t onStateChanged;
|
||||
battleactiondecidedcallback_t onActionDecided;
|
||||
} battle_t;
|
||||
|
||||
extern battle_t BATTLE;
|
||||
@@ -77,11 +126,10 @@ battlefighter_t *battleAddFighter(
|
||||
);
|
||||
|
||||
/**
|
||||
* Starts the battle: builds the opening turn order (biased by
|
||||
* encounterType for the first round only) and marks the battle active.
|
||||
* Call once every fighter has been added via battleAddFighter.
|
||||
* Starts the battle: enters BATTLE_STATE_OPENING and marks the battle
|
||||
* active. Call once every fighter has been added via battleAddFighter.
|
||||
*
|
||||
* @param encounterType Determines the opening round's turn order.
|
||||
* @param encounterType Determines the opening round's execution order.
|
||||
* @param fleeAvailable Whether the party may attempt to flee this battle.
|
||||
*/
|
||||
void battleStart(
|
||||
@@ -95,10 +143,11 @@ void battleStart(
|
||||
void battleDispose(void);
|
||||
|
||||
/**
|
||||
* Returns the fighter whose turn it currently is.
|
||||
* Returns the fighter currently awaiting a player decision.
|
||||
*
|
||||
* @return Pointer to the active fighter, or NULL if the battle isn't
|
||||
* active or has no living fighters left to act.
|
||||
* @return Pointer to the fighter awaiting a decision, or NULL if the battle
|
||||
* isn't in BATTLE_STATE_PLAYER_SELECTION or every player-controlled
|
||||
* fighter has already decided.
|
||||
*/
|
||||
battlefighter_t *battleGetCurrentFighter(void);
|
||||
|
||||
@@ -125,61 +174,68 @@ void battleResolveAttack(
|
||||
);
|
||||
|
||||
/**
|
||||
* Ends the current fighter's turn and advances to the next fighter in
|
||||
* the turn order, starting a new round (rebuilding turn order purely by
|
||||
* speed) once every fighter in the current round has acted.
|
||||
*/
|
||||
void battleNextTurn(void);
|
||||
|
||||
/**
|
||||
* Checks whether the battle has been won or lost, updating and
|
||||
* returning BATTLE.result. Does nothing if a result has already been
|
||||
* set (e.g. by a successful flee).
|
||||
* Checks whether the battle has been won or lost, transitioning to
|
||||
* BATTLE_STATE_ENDED and updating BATTLE.result if so. Does nothing if a
|
||||
* result has already been set (e.g. by a successful flee).
|
||||
*
|
||||
* @return The battle's current result.
|
||||
*/
|
||||
battleresult_t battleCheckResult(void);
|
||||
|
||||
/**
|
||||
* Submits the current fighter's attack against a target, if it is
|
||||
* currently a player-controlled fighter's turn. Resolves the attack,
|
||||
* checks for a battle result, and advances the turn.
|
||||
* Queues an action for a fighter to perform once BATTLE_STATE_MOVES_EXECUTING
|
||||
* reaches them this round, overwriting any action already queued for that
|
||||
* fighter. Fires BATTLE.onActionDecided.
|
||||
*
|
||||
* @param fighterIndex Index into BATTLE.fighters of the deciding fighter.
|
||||
* @param type The type of action to perform.
|
||||
* @param targetIndex Index into BATTLE.fighters of the target, meaningful
|
||||
* for BATTLE_ACTION_ATTACK.
|
||||
*/
|
||||
void battleQueueAction(
|
||||
const uint8_t fighterIndex,
|
||||
const battleactiontype_t type,
|
||||
const uint8_t targetIndex
|
||||
);
|
||||
|
||||
/**
|
||||
* Submits the currently-selecting fighter's attack against a target, if the
|
||||
* battle is in BATTLE_STATE_PLAYER_SELECTION and awaiting a decision.
|
||||
* Queues the action and advances the selection cursor.
|
||||
*
|
||||
* @param targetIndex Index into BATTLE.fighters of the target.
|
||||
*/
|
||||
void battlePlayerAttack(const uint8_t targetIndex);
|
||||
|
||||
/**
|
||||
* Submits a flee attempt for the current fighter's turn, if it is
|
||||
* currently a player-controlled fighter's turn and fleeing is
|
||||
* available for this battle. Always succeeds, ending the battle with
|
||||
* BATTLE_RESULT_FLED.
|
||||
* Submits a flee attempt for the currently-selecting fighter, if the battle
|
||||
* is in BATTLE_STATE_PLAYER_SELECTION and fleeing is available for this
|
||||
* battle. Always succeeds, ending the battle with BATTLE_RESULT_FLED.
|
||||
*/
|
||||
void battlePlayerFlee(void);
|
||||
|
||||
/**
|
||||
* Updates the battle simulation for one frame: resolves the current
|
||||
* fighter's turn automatically if AI-controlled, otherwise waits for a
|
||||
* player action via battlePlayerAttack/battlePlayerFlee. No-op if the
|
||||
* battle isn't active or already has a result.
|
||||
* Updates the battle simulation for one frame, dispatching on BATTLE.state.
|
||||
* No-op if the battle isn't active, has already ended, or
|
||||
* CUTSCENE_PAUSE_BATTLE is set.
|
||||
*/
|
||||
void battleUpdate(void);
|
||||
|
||||
/**
|
||||
* Rebuilds BATTLE.turnOrder/turnCount from every currently living
|
||||
* Rebuilds BATTLE.executionOrder/executionCount from every currently living
|
||||
* fighter, sorted by speed descending.
|
||||
*
|
||||
* @param applyEncounterBias If true, reorders the freshly speed-sorted
|
||||
* queue so BATTLE.encounterType's favoured team goes first (used only
|
||||
* for the opening round).
|
||||
*/
|
||||
void battleBuildTurnOrder(const bool_t applyEncounterBias);
|
||||
void battleBuildExecutionOrder(const bool_t applyEncounterBias);
|
||||
|
||||
/**
|
||||
* Stably partitions BATTLE.turnOrder so every fighter on the given team
|
||||
* Stably partitions BATTLE.executionOrder so every fighter on the given team
|
||||
* comes first, preserving each side's relative (speed-sorted) order.
|
||||
*
|
||||
* @param team The team to move to the front of the turn order.
|
||||
* @param team The team to move to the front of the execution order.
|
||||
*/
|
||||
void battleMoveTeamFirst(const battlefighterteam_t team);
|
||||
|
||||
@@ -192,3 +248,66 @@ void battleMoveTeamFirst(const battlefighterteam_t team);
|
||||
* living fighters.
|
||||
*/
|
||||
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter);
|
||||
|
||||
/**
|
||||
* Sets BATTLE.state and fires BATTLE.onStateChanged with the previous and
|
||||
* new state.
|
||||
*
|
||||
* @param next The state to transition to.
|
||||
*/
|
||||
void battleSetState(const battlestate_t next);
|
||||
|
||||
/**
|
||||
* Sets BATTLE.result and transitions to BATTLE_STATE_ENDED.
|
||||
*
|
||||
* @param result The result to end the battle with.
|
||||
*/
|
||||
void battleSetResult(const battleresult_t result);
|
||||
|
||||
/**
|
||||
* Checks whether a fighter is a live, undecided candidate for the given
|
||||
* controller -- i.e. whether PLAYER_SELECTION or AI_SELECTION should still
|
||||
* be deciding an action for it this round.
|
||||
*
|
||||
* @param fighterIndex Index into BATTLE.fighters to check.
|
||||
* @param controller The controller PLAYER_SELECTION/AI_SELECTION is
|
||||
* currently deciding for.
|
||||
* @return True if the fighter is alive, matches controller, and has no
|
||||
* action queued yet.
|
||||
*/
|
||||
bool_t battleFighterNeedsDecision(
|
||||
const uint8_t fighterIndex,
|
||||
const battlefightercontroller_t controller
|
||||
);
|
||||
|
||||
/**
|
||||
* Advances BATTLE.selectionIndex to the next player-controlled fighter that
|
||||
* still needs a decision, or transitions to BATTLE_STATE_AI_SELECTION once
|
||||
* none remain.
|
||||
*/
|
||||
void battleAdvanceSelection(void);
|
||||
|
||||
/**
|
||||
* Handles BATTLE_STATE_PRE_ROUND: rebuilds the execution order and moves on
|
||||
* to BATTLE_STATE_PLAYER_SELECTION, positioning the selection cursor.
|
||||
*/
|
||||
void battleUpdatePreRound(void);
|
||||
|
||||
/**
|
||||
* Handles BATTLE_STATE_AI_SELECTION: queues an attack for every undecided
|
||||
* AI-controlled fighter, then moves on to BATTLE_STATE_MOVES_EXECUTING.
|
||||
*/
|
||||
void battleUpdateAiSelection(void);
|
||||
|
||||
/**
|
||||
* Handles BATTLE_STATE_MOVES_EXECUTING: resolves one queued action from
|
||||
* BATTLE.executionOrder per call, or transitions to BATTLE_STATE_POST_ROUND
|
||||
* once every fighter this round has been processed.
|
||||
*/
|
||||
void battleUpdateMovesExecuting(void);
|
||||
|
||||
/**
|
||||
* Handles BATTLE_STATE_POST_ROUND: advances BATTLE.round and transitions
|
||||
* back to BATTLE_STATE_PRE_ROUND.
|
||||
*/
|
||||
void battleUpdatePostRound(void);
|
||||
|
||||
@@ -6,6 +6,5 @@
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
savevita.c
|
||||
savestreamvita.c
|
||||
testbattle.c
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "testbattle.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
#include "scene/scene.h"
|
||||
|
||||
void testBattleStart(void) {
|
||||
battleInit();
|
||||
|
||||
const battlefighterstats_t allyOneStats =
|
||||
{ .attack = 10, .defense = 5, .magic = 0, .speed = 10, .luck = 0 };
|
||||
const battlefighterstats_t allyTwoStats =
|
||||
{ .attack = 8, .defense = 4, .magic = 0, .speed = 8, .luck = 0 };
|
||||
const battlefighterstats_t enemyOneStats =
|
||||
{ .attack = 6, .defense = 3, .magic = 0, .speed = 6, .luck = 0 };
|
||||
const battlefighterstats_t enemyTwoStats =
|
||||
{ .attack = 7, .defense = 3, .magic = 0, .speed = 5, .luck = 0 };
|
||||
|
||||
battleAddFighter(
|
||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||
allyOneStats, 30, 10
|
||||
);
|
||||
battleAddFighter(
|
||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||
allyTwoStats, 25, 10
|
||||
);
|
||||
battleAddFighter(
|
||||
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
|
||||
enemyOneStats, 20, 5
|
||||
);
|
||||
battleAddFighter(
|
||||
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
|
||||
enemyTwoStats, 20, 5
|
||||
);
|
||||
|
||||
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
|
||||
sceneSet(SCENE_TYPE_BATTLE);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
|
||||
/**
|
||||
* TEMPORARY test hook: sets up a hardcoded mock battle and switches to the
|
||||
* battle scene, so the battle scene (camera/fighters/HUD) can be seen and
|
||||
* played without a real encounter trigger yet.
|
||||
*/
|
||||
void testBattleStart(void);
|
||||
@@ -162,6 +162,28 @@ typedef struct cutscene_s {
|
||||
.shake = { .amount = AMOUNT, .duration = DURATION } \
|
||||
}
|
||||
|
||||
// Waits until BATTLE.state reaches STATE. Put this BEFORE
|
||||
// CUTSCENE_SET_PAUSE(CUTSCENE_PAUSE_BATTLE), not after -- pausing first
|
||||
// freezes BATTLE.state wherever it already is, so it would never reach
|
||||
// STATE on its own to satisfy the wait. Waiting unpaused, then pausing the
|
||||
// moment it's satisfied, catches the battle right at STATE before it can
|
||||
// advance further.
|
||||
#define CUTSCENE_BATTLE_WAIT_STATE(STATE) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE, \
|
||||
.battleWaitState = { .state = STATE } \
|
||||
}
|
||||
|
||||
// Immediately queues an attack for FIGHTER_INDEX against TARGET_INDEX,
|
||||
// bypassing normal player/AI selection for that fighter this round.
|
||||
#define CUTSCENE_BATTLE_FORCE_ACTION(FIGHTER_INDEX, TARGET_INDEX) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION, \
|
||||
.battleForceAction = { \
|
||||
.fighterIndex = FIGHTER_INDEX, .targetIndex = TARGET_INDEX \
|
||||
} \
|
||||
}
|
||||
|
||||
#define CUTSCENE_SET_PAUSE(FLAGS) \
|
||||
{ .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = (FLAGS) }
|
||||
|
||||
|
||||
@@ -14,11 +14,13 @@ typedef uint8_t cutscenepause_t;
|
||||
#define CUTSCENE_PAUSE_NPC ((cutscenepause_t)(1 << 0))
|
||||
#define CUTSCENE_PAUSE_PLAYER ((cutscenepause_t)(1 << 1))
|
||||
#define CUTSCENE_PAUSE_WORLD ((cutscenepause_t)(1 << 2))
|
||||
#define CUTSCENE_PAUSE_BATTLE ((cutscenepause_t)(1 << 3))
|
||||
|
||||
#define CUTSCENE_PAUSE_DEFAULT ((cutscenepause_t)( \
|
||||
CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER \
|
||||
))
|
||||
|
||||
#define CUTSCENE_PAUSE_ALL ((cutscenepause_t)( \
|
||||
CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER | CUTSCENE_PAUSE_WORLD \
|
||||
CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER | CUTSCENE_PAUSE_WORLD | \
|
||||
CUTSCENE_PAUSE_BATTLE \
|
||||
))
|
||||
|
||||
@@ -6,4 +6,6 @@
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
cutscenestartbattle.c
|
||||
cutscenebattlewaitstate.c
|
||||
cutscenebattleforceaction.c
|
||||
)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
|
||||
void cutsceneBattleForceActionStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
battleQueueAction(
|
||||
item->battleForceAction.fighterIndex, BATTLE_ACTION_ATTACK,
|
||||
item->battleForceAction.targetIndex
|
||||
);
|
||||
}
|
||||
|
||||
bool_t cutsceneBattleForceActionUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "rpg/battle/battle.h"
|
||||
|
||||
typedef struct {
|
||||
uint8_t fighterIndex;
|
||||
uint8_t targetIndex;
|
||||
} cutscenebattleforceaction_t;
|
||||
|
||||
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||
|
||||
/**
|
||||
* Starts a battle force-action step: immediately queues an attack for the
|
||||
* given fighter against the given target, bypassing normal player/AI
|
||||
* selection for that fighter this round.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
*/
|
||||
void cutsceneBattleForceActionStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates a battle force-action step (always completes immediately).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true always.
|
||||
*/
|
||||
bool_t cutsceneBattleForceActionUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
|
||||
bool_t cutsceneBattleWaitStateUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return BATTLE.state == item->battleWaitState.state;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "rpg/battle/battle.h"
|
||||
|
||||
typedef struct {
|
||||
battlestate_t state;
|
||||
} cutscenebattlewaitstate_t;
|
||||
|
||||
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||
|
||||
/**
|
||||
* Updates a battle wait-state step, completing once BATTLE.state reaches
|
||||
* the watched state. Has no Start callback -- there's nothing to do until
|
||||
* the state is actually reached.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true once BATTLE.state equals the watched state.
|
||||
*/
|
||||
bool_t cutsceneBattleWaitStateUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
@@ -118,6 +118,15 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
|
||||
[CUTSCENE_ITEM_TYPE_SHAKE] = {
|
||||
.init = cutsceneShakeStart,
|
||||
.update = cutsceneShakeUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE] = {
|
||||
.update = cutsceneBattleWaitStateUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION] = {
|
||||
.init = cutsceneBattleForceActionStart,
|
||||
.update = cutsceneBattleForceActionUpdate
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
#include "maparea/cutscenemaparearemove.h"
|
||||
#include "maparea/cutscenemapareawait.h"
|
||||
#include "battle/cutscenestartbattle.h"
|
||||
#include "battle/cutscenebattlewaitstate.h"
|
||||
#include "battle/cutscenebattleforceaction.h"
|
||||
|
||||
typedef struct cutscene_s cutscene_t;
|
||||
|
||||
@@ -55,6 +57,8 @@ typedef enum {
|
||||
CUTSCENE_ITEM_TYPE_START_BATTLE,
|
||||
CUTSCENE_ITEM_TYPE_EMOJI,
|
||||
CUTSCENE_ITEM_TYPE_SHAKE,
|
||||
CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE,
|
||||
CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION,
|
||||
|
||||
CUTSCENE_ITEM_TYPE_COUNT
|
||||
} cutsceneitemtype_t;
|
||||
@@ -85,6 +89,8 @@ struct cutsceneitem_s {
|
||||
cutscenestartbattle_t startBattle;
|
||||
cutsceneemoji_t emoji;
|
||||
cutsceneshake_t shake;
|
||||
cutscenebattlewaitstate_t battleWaitState;
|
||||
cutscenebattleforceaction_t battleForceAction;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "util/memory.h"
|
||||
#include "time/time.h"
|
||||
#include "util/math.h"
|
||||
#include "console/console.h"
|
||||
#include "rpg/overworld/map.h"
|
||||
#include "rpg/overworld/maparea.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) {
|
||||
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++) {
|
||||
if(next->entities[i] != 0xFF) continue;
|
||||
next->entities[i] = entity->id;
|
||||
entity->chunkIndex = chunkIndex;
|
||||
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.
|
||||
* 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 chunkIndex Index of the chunk to assign to, or 0xFF for none.
|
||||
|
||||
@@ -14,3 +14,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
tileshape.c
|
||||
)
|
||||
|
||||
add_subdirectory(global)
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#define CHUNK_MESH_COUNT_MAX 10
|
||||
#define CHUNK_MESH_NAME_MAX 64
|
||||
#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;
|
||||
|
||||
@@ -28,6 +30,13 @@ typedef struct chunk_s {
|
||||
assetentry_t *modelEntries[CHUNK_MESH_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;
|
||||
|
||||
/**
|
||||
|
||||
+1
-5
@@ -3,11 +3,7 @@
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
uisettings.c
|
||||
uisettingsgeneral.c
|
||||
uisettingsinput.c
|
||||
uisettingsdisplay.c
|
||||
uisettingsaudio.c
|
||||
)
|
||||
@@ -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
|
||||
@@ -14,9 +14,20 @@
|
||||
#include "event/event.h"
|
||||
#include "util/string.h"
|
||||
#include "rpg/entity/global/entityglobal.h"
|
||||
#include "rpg/entity/item/entityitem.h"
|
||||
#include "rpg/overworld/maparea.h"
|
||||
|
||||
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() {
|
||||
memoryZero(&MAP, sizeof(map_t));
|
||||
MAP.loaded = true;
|
||||
@@ -105,7 +116,7 @@ errorret_t mapDispose() {
|
||||
|
||||
void mapChunkUnload(chunk_t *chunk) {
|
||||
mapChunkLoadQueueRemove(chunk);
|
||||
if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL;
|
||||
mapChunkLoadingSlotClear(chunk);
|
||||
|
||||
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
|
||||
if(chunk->entities[i] == 0xFF) continue;
|
||||
@@ -119,6 +130,12 @@ void mapChunkUnload(chunk_t *chunk) {
|
||||
|
||||
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) {
|
||||
eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded);
|
||||
eventUnsubscribe(&chunk->dcfEntry->onError, mapChunkLoadError);
|
||||
@@ -139,7 +156,7 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
|
||||
if(!mapIsLoaded()) errorThrow("No map loaded");
|
||||
|
||||
mapChunkLoadQueueRemove(chunk);
|
||||
if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL;
|
||||
mapChunkLoadingSlotClear(chunk);
|
||||
|
||||
if(chunk->dcfEntry != NULL) {
|
||||
eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded);
|
||||
@@ -149,6 +166,16 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
char_t name[64];
|
||||
@@ -178,7 +205,8 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
|
||||
}
|
||||
|
||||
void mapChunkLoadNext() {
|
||||
if(MAP.loadingChunk != NULL) return;
|
||||
for(uint32_t slot = 0; slot < MAP_CHUNK_LOAD_CONCURRENCY; slot++) {
|
||||
if(MAP.loadingChunks[slot] != NULL) continue;
|
||||
if(MAP.loadQueueCount == 0) return;
|
||||
|
||||
chunk_t *chunk = MAP.loadQueue[0];
|
||||
@@ -186,7 +214,7 @@ void mapChunkLoadNext() {
|
||||
MAP.loadQueue[i - 1] = MAP.loadQueue[i];
|
||||
}
|
||||
MAP.loadQueueCount--;
|
||||
MAP.loadingChunk = chunk;
|
||||
MAP.loadingChunks[slot] = chunk;
|
||||
|
||||
char_t name[64];
|
||||
stringFormat(
|
||||
@@ -201,21 +229,24 @@ void mapChunkLoadNext() {
|
||||
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.
|
||||
// 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);
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
if(entry->state == ASSET_ENTRY_STATE_ERROR) {
|
||||
mapChunkLoadError(entry, chunk);
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
eventSubscribe(&entry->onLoaded, mapChunkLoaded, chunk);
|
||||
eventSubscribe(&entry->onError, mapChunkLoadError, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
void mapChunkLoadQueueRemove(chunk_t *chunk) {
|
||||
@@ -372,7 +403,7 @@ void mapChunkLoadError(void *params, void *user) {
|
||||
chunk->dcfEntry = NULL;
|
||||
memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles));
|
||||
|
||||
if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL;
|
||||
mapChunkLoadingSlotClear(chunk);
|
||||
mapChunkLoadNext();
|
||||
}
|
||||
|
||||
@@ -433,6 +464,47 @@ void mapChunkLoaded(void *params, void *user) {
|
||||
// this chunk_t is displaying it. Released in mapChunkUnload instead.
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
|
||||
#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 {
|
||||
bool_t loaded;
|
||||
|
||||
@@ -19,11 +23,9 @@ typedef struct map_s {
|
||||
chunk_t *chunkOrder[MAP_CHUNK_COUNT];
|
||||
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];
|
||||
uint32_t loadQueueCount;
|
||||
chunk_t *loadingChunk;
|
||||
chunk_t *loadingChunks[MAP_CHUNK_LOAD_CONCURRENCY];
|
||||
} map_t;
|
||||
|
||||
extern map_t MAP;
|
||||
@@ -80,9 +82,9 @@ void mapChunkUnload(chunk_t* chunk);
|
||||
errorret_t mapChunkLoad(chunk_t* chunk);
|
||||
|
||||
/**
|
||||
* Starts loading the next queued chunk, if no chunk is currently mid-load.
|
||||
* Called after mapChunkLoad enqueues a chunk, and again after the
|
||||
* currently-loading chunk finishes (or is unloaded) to advance the queue.
|
||||
* Starts loading queued chunks until MAP_CHUNK_LOAD_CONCURRENCY chunks are
|
||||
* mid-load. Called after mapChunkLoad enqueues a chunk, and again after a
|
||||
* mid-load chunk finishes (or is unloaded) to advance the queue.
|
||||
*/
|
||||
void mapChunkLoadNext();
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "util/math.h"
|
||||
#include "util/memory.h"
|
||||
#include "rpg/overworld/map.h"
|
||||
#include "rpg/overworld/global/mapareaglobal.h"
|
||||
|
||||
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) {
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -159,3 +159,28 @@ void mapAreaCheckEntity(entity_t *entity);
|
||||
* @param trigger Which MAP_TRIGGER_* condition invoked the callback.
|
||||
*/
|
||||
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
|
||||
);
|
||||
+16
-35
@@ -7,27 +7,20 @@
|
||||
|
||||
#include "rpg.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/maparea.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "rpg/cutscene/scene/testcutscene.h"
|
||||
#include "rpg/item/backpack.h"
|
||||
#include "rpg/battle/party.h"
|
||||
#include "ui/rpg/textbox/uitextboxminilist.h"
|
||||
#include "time/time.h"
|
||||
#include "rpgcamera.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
#include "console/console.h"
|
||||
|
||||
#include "error/error.h"
|
||||
#include "scene/scene.h"
|
||||
#include "ui/rpg/uiemoji.h"
|
||||
|
||||
void rpgTestAreaCallback(entity_t *entity, const uint8_t trigger) {
|
||||
consolePrint("rpgTestAreaCallback: trigger=%u", trigger);
|
||||
}
|
||||
#include "rpg/story/storyflag.h"
|
||||
|
||||
errorret_t rpgInit(void) {
|
||||
memoryZero(ENTITIES, sizeof(ENTITIES));
|
||||
@@ -36,14 +29,16 @@ errorret_t rpgInit(void) {
|
||||
backpackInit();
|
||||
partyInit();
|
||||
cutsceneSystemInit();
|
||||
|
||||
errorChain(mapInit());
|
||||
|
||||
rpgCameraInit();
|
||||
// Init world
|
||||
|
||||
|
||||
// Init test world
|
||||
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();
|
||||
assertTrue(entIndex != 0xFF, "No available entity slots!.");
|
||||
entity_t *ent = &ENTITIES[entIndex];
|
||||
@@ -52,30 +47,11 @@ errorret_t rpgInit(void) {
|
||||
RPG_CAMERA.mode = RPG_CAMERA_MODE_FOLLOW_ENTITY;
|
||||
RPG_CAMERA.followEntity.followEntityId = ent->id;
|
||||
|
||||
mapSpawnEntity(3, (worldpos_t){ 8, 8, 1 });
|
||||
|
||||
// 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.
|
||||
// Starting inventory.
|
||||
backpackAdd(ITEM_ID_POTION, 5);
|
||||
backpackAdd(ITEM_ID_POTATO, 3);
|
||||
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!
|
||||
errorOk();
|
||||
@@ -91,12 +67,17 @@ errorret_t rpgUpdate(void) {
|
||||
// TODO: Do not update if the scene is not the map scene?
|
||||
errorChain(mapUpdate());
|
||||
|
||||
// Update overworld ents.
|
||||
// Update overworld ents - only while actually in the overworld. Entities
|
||||
// (the player among them) keep existing across scene changes, but their
|
||||
// input/movement/animation logic doesn't make sense to run mid-battle or
|
||||
// before the initial scene has handed off to the overworld.
|
||||
if(SCENE.current == SCENE_TYPE_OVERWORLD) {
|
||||
entity_t *ent = &ENTITIES[0];
|
||||
do {
|
||||
if(ent->type == ENTITY_TYPE_NULL) continue;
|
||||
entityUpdate(ent);
|
||||
} while(++ent < &ENTITIES[ENTITY_COUNT]);
|
||||
}
|
||||
|
||||
cutsceneSystemUpdate();
|
||||
errorChain(rpgCameraUpdate());
|
||||
|
||||
@@ -7,8 +7,17 @@
|
||||
|
||||
#include "storyflag.h"
|
||||
#include "assert/assert.h"
|
||||
#include "console/console.h"
|
||||
|
||||
storyflagvalue_t storyFlagGet(const storyflag_t flag) {
|
||||
assertTrue(flag > STORY_FLAG_NULL && flag < STORY_FLAG_COUNT, "Bad Flag");
|
||||
consolePrint("Story flag get without save implementation");
|
||||
return 0;
|
||||
}
|
||||
|
||||
void storyFlagSet(const storyflag_t flag, const storyflagvalue_t value) {
|
||||
assertTrue(flag > STORY_FLAG_NULL && flag < STORY_FLAG_COUNT, "Bad Flag");
|
||||
STORY_FLAG_VALUES[flag] = value;
|
||||
|
||||
// TODO: Dirty savefile
|
||||
consolePrint("Story flag set without save implementation");
|
||||
}
|
||||
@@ -9,15 +9,16 @@
|
||||
#include "rpg/story/storyflagvalue.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.
|
||||
* @return The value of the story flag.
|
||||
*/
|
||||
#define storyFlagGet(flag) (STORY_FLAG_VALUES[(flag)])
|
||||
storyflagvalue_t storyFlagGet(const storyflag_t flag);
|
||||
|
||||
/**
|
||||
* Sets the value of a story flag.
|
||||
* Sets the value of a story flag. Will dirty the savefile.
|
||||
*
|
||||
* @param flag The story flag to set.
|
||||
* @param value The value to set the story flag to.
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
save.c
|
||||
savestream.c
|
||||
savemanager.c
|
||||
savedevice.c
|
||||
)
|
||||
|
||||
@@ -1,108 +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/savestream.h"
|
||||
#include "util/memory.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
save_t SAVE;
|
||||
|
||||
errorret_t saveInit(void) {
|
||||
memoryZero(&SAVE, sizeof(save_t));
|
||||
|
||||
#ifdef saveInitPlatform
|
||||
errorChain(saveInitPlatform());
|
||||
#endif
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveDispose(void) {
|
||||
#ifdef saveDisposePlatform
|
||||
errorChain(saveDisposePlatform());
|
||||
#endif
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveLoad(const uint8_t slot) {
|
||||
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
|
||||
|
||||
savefile_t *file = &SAVE.files[slot];
|
||||
file->exists = false;
|
||||
|
||||
savestream_t stream;
|
||||
memoryZero(&stream, sizeof(savestream_t));
|
||||
|
||||
#ifdef saveStreamOpenReadPlatform
|
||||
errorChain(saveStreamOpenReadPlatform(&stream, slot));
|
||||
#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();
|
||||
}
|
||||
|
||||
errorret_t saveWrite(const uint8_t slot) {
|
||||
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
|
||||
|
||||
savefile_t *file = &SAVE.files[slot];
|
||||
|
||||
savestream_t stream;
|
||||
memoryZero(&stream, sizeof(savestream_t));
|
||||
|
||||
#ifdef saveStreamOpenWritePlatform
|
||||
errorChain(saveStreamOpenWritePlatform(&stream, slot));
|
||||
#endif
|
||||
|
||||
errorret_t ret = saveFileWrite(&stream, file);
|
||||
|
||||
if(errorIsOk(ret)) {
|
||||
ret = saveStreamFinalizeWriteImpl(&stream);
|
||||
}
|
||||
|
||||
#ifdef saveStreamClosePlatform
|
||||
saveStreamClosePlatform(&stream);
|
||||
#endif
|
||||
|
||||
if(errorIsNotOk(ret)) return ret;
|
||||
|
||||
file->exists = true;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveDelete(const uint8_t slot) {
|
||||
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
|
||||
|
||||
#ifdef saveDeletePlatform
|
||||
errorChain(saveDeletePlatform(slot));
|
||||
#endif
|
||||
|
||||
SAVE.files[slot].exists = false;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
bool_t saveExists(const uint8_t slot) {
|
||||
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
|
||||
return SAVE.files[slot].exists;
|
||||
}
|
||||
|
||||
savefile_t * saveGet(const uint8_t slot) {
|
||||
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
|
||||
return &SAVE.files[slot];
|
||||
}
|
||||
@@ -1,74 +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 "savefile.h"
|
||||
#include "save/saveplatform.h"
|
||||
|
||||
typedef struct {
|
||||
/** Per-slot save file data; indexed 0 to SAVE_FILE_COUNT_MAX - 1. */
|
||||
savefile_t files[SAVE_FILE_COUNT_MAX];
|
||||
/** Platform-specific save system state (paths, card handles, etc.). */
|
||||
saveplatform_t platform;
|
||||
} save_t;
|
||||
|
||||
extern save_t SAVE;
|
||||
|
||||
/**
|
||||
* Initializes the save system.
|
||||
*
|
||||
* @return An error code if initialization fails.
|
||||
*/
|
||||
errorret_t saveInit(void);
|
||||
|
||||
/**
|
||||
* Disposes of the save system.
|
||||
*
|
||||
* @return An error code if disposal fails.
|
||||
*/
|
||||
errorret_t saveDispose(void);
|
||||
|
||||
/**
|
||||
* Loads the save file for a given slot from persistent storage.
|
||||
*
|
||||
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1).
|
||||
* @return An error code if the load fails.
|
||||
*/
|
||||
errorret_t saveLoad(const uint8_t slot);
|
||||
|
||||
/**
|
||||
* Writes the save file for a given slot to persistent storage.
|
||||
*
|
||||
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1).
|
||||
* @return An error code if the write fails.
|
||||
*/
|
||||
errorret_t saveWrite(const uint8_t slot);
|
||||
|
||||
/**
|
||||
* Deletes the save file for a given slot from persistent storage.
|
||||
*
|
||||
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1).
|
||||
* @return An error code if the delete fails.
|
||||
*/
|
||||
errorret_t saveDelete(const uint8_t slot);
|
||||
|
||||
/**
|
||||
* Checks whether a save file exists for a given slot.
|
||||
*
|
||||
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1).
|
||||
* @return true if a save file exists for the slot, false otherwise.
|
||||
*/
|
||||
bool_t saveExists(const uint8_t slot);
|
||||
|
||||
/**
|
||||
* Gets a pointer to the save file data for a given slot.
|
||||
*
|
||||
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1).
|
||||
* @return A pointer to the savefile_t for the given slot.
|
||||
*/
|
||||
savefile_t * saveGet(const uint8_t slot);
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "save/savedevice.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
errorret_t saveDeviceInit(savedevice_t *device) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
|
||||
memoryZero(device, sizeof(savedevice_t));
|
||||
device->state = 0xFF;// We set this because the platform must define it.
|
||||
errorChain(saveDevicePlatformInit(device));
|
||||
|
||||
// The platform must put the device into a state we can work with.
|
||||
assertTrue(
|
||||
device->state == SAVE_DEVICE_STATE_UNKNOWN ||
|
||||
device->state == SAVE_DEVICE_STATE_UNAVAILABLE ||
|
||||
device->state == SAVE_DEVICE_STATE_AVAILABLE ||
|
||||
device->state == SAVE_DEVICE_STATE_ERRORED,
|
||||
"Save device must be in a state that allows checking availability"
|
||||
);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveDeviceUpdate(savedevice_t *device) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
assertIsMainThread("Invalid thread");
|
||||
|
||||
// Perform a device update.
|
||||
errorChain(saveDevicePlatformUpdate(device));
|
||||
|
||||
// Fire the callback if desired.
|
||||
if(device->fireCallback) {
|
||||
device->fireCallback = false;
|
||||
if(device->stateCallback) device->stateCallback(device, device->user);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void saveDeviceFireCallback(savedevice_t *device) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
assertIsMainThread("Invalid thread");
|
||||
assertFalse(device->fireCallback, "Device callback already fired?");
|
||||
device->fireCallback = true;
|
||||
}
|
||||
|
||||
void saveDeviceCheckAvailability(
|
||||
savedevice_t *device,
|
||||
savedevicestatecallback_t callback,
|
||||
void *user
|
||||
) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
assertNotNull(callback, "callback cannot be null");
|
||||
assertIsMainThread("Invalid thread");
|
||||
|
||||
// The device must be in a state that we allow checking the availability.
|
||||
assertTrue(
|
||||
device->state == SAVE_DEVICE_STATE_UNKNOWN ||
|
||||
device->state == SAVE_DEVICE_STATE_UNAVAILABLE ||
|
||||
device->state == SAVE_DEVICE_STATE_AVAILABLE ||
|
||||
device->state == SAVE_DEVICE_STATE_ERRORED,
|
||||
"Save device must be in a state that allows checking availability"
|
||||
);
|
||||
|
||||
// Set state data and callback.
|
||||
device->state = SAVE_DEVICE_STATE_CHECKING_AVAILABILITY;
|
||||
device->stateCallback = callback;
|
||||
device->user = user;
|
||||
|
||||
// Handoff to the platform to do its checks, it can opt to do this either
|
||||
// synchronously or asynchronously.
|
||||
saveDeviceCheckAvailabilityPlatform(device);
|
||||
}
|
||||
|
||||
errorret_t saveDeviceDispose(savedevice_t *device) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
errorChain(saveDevicePlatformDispose(device));
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 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/savedeviceplatform.h"
|
||||
|
||||
// Save device count e.g. Memory Cards count.
|
||||
#ifndef SAVE_DEVICE_COUNT
|
||||
#error "SAVE_DEVICE_COUNT must be defined"
|
||||
#endif
|
||||
|
||||
typedef struct savedevice_s savedevice_t;
|
||||
|
||||
typedef void (*savedevicestatecallback_t)(savedevice_t *device, void *user);
|
||||
|
||||
typedef enum {
|
||||
SAVE_DEVICE_STATE_UNKNOWN,
|
||||
SAVE_DEVICE_STATE_CHECKING_AVAILABILITY,
|
||||
SAVE_DEVICE_STATE_UNAVAILABLE,
|
||||
SAVE_DEVICE_STATE_AVAILABLE,
|
||||
SAVE_DEVICE_STATE_ERRORED
|
||||
} savedevicestate_t;
|
||||
|
||||
typedef struct savedevice_s {
|
||||
savedevicestate_t state;
|
||||
savedeviceplatform_t platform;
|
||||
const char_t *reasonKey;
|
||||
bool_t fireCallback;
|
||||
void *user;
|
||||
savedevicestatecallback_t stateCallback;
|
||||
} savedevice_t;
|
||||
|
||||
/**
|
||||
* Initializes the save device.
|
||||
*
|
||||
* @param device The save device to initialize.
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveDeviceInit(savedevice_t *device);
|
||||
|
||||
/**
|
||||
* Updates the save device.
|
||||
*
|
||||
* @param device The save device to update.
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveDeviceUpdate(savedevice_t *device);
|
||||
|
||||
/**
|
||||
* Internal method to fire the save callback, does it at the appropriate time.
|
||||
*
|
||||
* @param device The save device to fire the callback for.
|
||||
*/
|
||||
void saveDeviceFireCallback(savedevice_t *device);
|
||||
|
||||
/**
|
||||
* Requests the device to check its availability, this will call the callback
|
||||
* whence completed.
|
||||
*
|
||||
* @param device The save device to check availability.
|
||||
* @param callback The callback to call when the availability check is complete.
|
||||
* @param user User data to pass to the callback.
|
||||
*/
|
||||
void saveDeviceCheckAvailability(
|
||||
savedevice_t *device,
|
||||
savedevicestatecallback_t callback,
|
||||
void *user
|
||||
);
|
||||
|
||||
/**
|
||||
* Disposes of the save device.
|
||||
*
|
||||
* @param device The save device to dispose.
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveDeviceDispose(savedevice_t *device);
|
||||
@@ -8,23 +8,4 @@
|
||||
#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;
|
||||
typedef uint8_t saveslot_t;
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "savemanager.h"
|
||||
#include "util/memory.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
savemanager_t SAVE_MANAGER;
|
||||
|
||||
errorret_t saveManagerInit() {
|
||||
memoryZero(&SAVE_MANAGER, sizeof(savemanager_t));
|
||||
|
||||
SAVE_MANAGER.deviceCurrent = 0xFF;// No current device.
|
||||
|
||||
// Start by initializing each of the save devices.
|
||||
savedevice_t *device = &SAVE_MANAGER.devices[0];
|
||||
do {
|
||||
saveDeviceInit(device);
|
||||
} while(device++ < &SAVE_MANAGER.devices[SAVE_DEVICE_COUNT - 1]);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveManagerUpdate() {
|
||||
assertIsMainThread("Invalid thread");
|
||||
|
||||
// Start by updating each device
|
||||
savedevice_t *device = &SAVE_MANAGER.devices[0];
|
||||
do {
|
||||
saveDeviceUpdate(device);
|
||||
} while(device++ < &SAVE_MANAGER.devices[SAVE_DEVICE_COUNT - 1]);
|
||||
|
||||
// We may be waiting for a device to become available
|
||||
if(SAVE_MANAGER.findingAvailableDevice) {
|
||||
assertNotNull(
|
||||
SAVE_MANAGER.findAvailableCallback,
|
||||
"Callback cannot be null while looking for devices"
|
||||
);
|
||||
|
||||
// Yes, we have a device available, fire the callback.
|
||||
if(SAVE_MANAGER.deviceCurrent == 0xFF) {
|
||||
// Are we still looking or did we fail to find anything?
|
||||
if(SAVE_MANAGER.noAvailableDeviceFound) {
|
||||
SAVE_MANAGER.findingAvailableDevice = false;
|
||||
SAVE_MANAGER.findAvailableCallback(
|
||||
NULL,
|
||||
SAVE_MANAGER.findAvailableUser
|
||||
);
|
||||
}
|
||||
// Still searching
|
||||
} else {
|
||||
// Device was found!
|
||||
SAVE_MANAGER.findingAvailableDevice = false;
|
||||
SAVE_MANAGER.findAvailableCallback(
|
||||
&SAVE_MANAGER.devices[SAVE_MANAGER.deviceCurrent],
|
||||
SAVE_MANAGER.findAvailableUser
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void saveManagerFindAvailableDevice(
|
||||
savedevicestatecallback_t callback,
|
||||
void *user
|
||||
) {
|
||||
assertIsMainThread("Invalid thread");
|
||||
assertNotNull(callback, "Callback cannot be null");
|
||||
assertFalse(
|
||||
SAVE_MANAGER.findingAvailableDevice,
|
||||
"Already finding an available device"
|
||||
);
|
||||
|
||||
SAVE_MANAGER.findingAvailableDevice = true;
|
||||
SAVE_MANAGER.findAvailableCallback = callback;
|
||||
SAVE_MANAGER.findAvailableUser = user;
|
||||
SAVE_MANAGER.noAvailableDeviceFound = false;
|
||||
|
||||
// Is there an available device marked already?
|
||||
if(SAVE_MANAGER.deviceCurrent != 0xFF) {
|
||||
// Yes, fire the callback next tick.
|
||||
return;
|
||||
}
|
||||
|
||||
// Do we have an available device not yet marked as current?
|
||||
savedevice_t *device = &SAVE_MANAGER.devices[0];
|
||||
do {
|
||||
if(device->state != SAVE_DEVICE_STATE_AVAILABLE) continue;
|
||||
// Yes this device is available, set as current device.
|
||||
SAVE_MANAGER.deviceCurrent = (uint8_t)(device - &SAVE_MANAGER.devices[0]);
|
||||
return;// Next tick will fire the callback.
|
||||
} while(device++ < &SAVE_MANAGER.devices[SAVE_DEVICE_COUNT - 1]);
|
||||
|
||||
// No currently available device, check them IN ORDER, starting at 0
|
||||
savedevice_t *deviceToCheck = &SAVE_MANAGER.devices[0];
|
||||
assertNotNull(deviceToCheck, "Device to check cannot be null");
|
||||
saveDeviceCheckAvailability(
|
||||
deviceToCheck,
|
||||
saveManagerOnDeviceAvailabilityChecked,
|
||||
NULL
|
||||
);
|
||||
}
|
||||
|
||||
void saveManagerOnDeviceAvailabilityChecked(savedevice_t *device, void *user) {
|
||||
assertNotNull(device, "Device cannot be null");
|
||||
assertTrue(
|
||||
SAVE_MANAGER.findingAvailableDevice,
|
||||
"Not currently finding an available device"
|
||||
);
|
||||
|
||||
// Did we already find a device?
|
||||
if(SAVE_MANAGER.deviceCurrent != 0xFF) return;
|
||||
|
||||
// Is this device available?
|
||||
if(device->state != SAVE_DEVICE_STATE_AVAILABLE) {
|
||||
// Since it's unavailable we should tell the next device to check its
|
||||
// availability. If there is no next device we give up.
|
||||
savedevice_t *nextDevice = device + 1;
|
||||
if(nextDevice >= &SAVE_MANAGER.devices[SAVE_DEVICE_COUNT]) {
|
||||
// No more devices to check, we give up.
|
||||
SAVE_MANAGER.noAvailableDeviceFound = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check the next device's availability.
|
||||
saveDeviceCheckAvailability(
|
||||
nextDevice,
|
||||
saveManagerOnDeviceAvailabilityChecked,
|
||||
NULL
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Yes this device is available, set as current device and next tick will
|
||||
// invoke callback
|
||||
SAVE_MANAGER.deviceCurrent = (uint8_t)(device - &SAVE_MANAGER.devices[0]);
|
||||
}
|
||||
|
||||
errorret_t saveManagerDispose() {
|
||||
// Dispose each device.
|
||||
savedevice_t *device = &SAVE_MANAGER.devices[0];
|
||||
do {
|
||||
saveDeviceDispose(device);
|
||||
} while(device++ < &SAVE_MANAGER.devices[SAVE_DEVICE_COUNT - 1]);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "savedevice.h"
|
||||
|
||||
typedef struct {
|
||||
savedevice_t devices[SAVE_DEVICE_COUNT];
|
||||
uint8_t deviceCurrent;
|
||||
|
||||
bool_t findingAvailableDevice;
|
||||
bool_t noAvailableDeviceFound;
|
||||
savedevicestatecallback_t findAvailableCallback;
|
||||
void *findAvailableUser;
|
||||
} savemanager_t;
|
||||
|
||||
extern savemanager_t SAVE_MANAGER;
|
||||
|
||||
/**
|
||||
* Initializes the save manager, this does not do anything related to mounting
|
||||
* files, memory cards, etc, this is entirely prepping for that capability.
|
||||
*
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveManagerInit();
|
||||
|
||||
/**
|
||||
* Updates the save manager.
|
||||
*
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveManagerUpdate();
|
||||
|
||||
/**
|
||||
* Requests the save manager to try and find an available device. This will fire
|
||||
* the callback whenever an avaialble device is found, or will fire with a NULL
|
||||
* save device if no available save devices are found.
|
||||
*
|
||||
* @param callback The callback to fire when an available device is found.
|
||||
* @param user The user data to pass to the callback.
|
||||
*/
|
||||
void saveManagerFindAvailableDevice(
|
||||
savedevicestatecallback_t callback,
|
||||
void *user
|
||||
);
|
||||
|
||||
/**
|
||||
* Internal method to fire the save callback, does it at the appropriate time.
|
||||
*
|
||||
* @param device The save device to fire the callback for.
|
||||
* @param user Unused, present to match savedevicestatecallback_t.
|
||||
*/
|
||||
void saveManagerOnDeviceAvailabilityChecked(savedevice_t *device, void *user);
|
||||
|
||||
/**
|
||||
* Disposes of the save manager.
|
||||
*
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveManagerDispose();
|
||||
@@ -1,340 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "save/savestream.h"
|
||||
#include "util/crypt.h"
|
||||
#include "util/endian.h"
|
||||
#include "util/string.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
errorret_t saveStreamReadBytesRawImpl(
|
||||
savestream_t *stream, void *buf, const size_t len
|
||||
) {
|
||||
#ifdef saveStreamReadBytesPlatform
|
||||
errorChain(saveStreamReadBytesPlatform(stream, buf, len));
|
||||
#endif
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteBytesRawImpl(
|
||||
savestream_t *stream, const void *buf, const size_t len
|
||||
) {
|
||||
#ifdef saveStreamWriteBytesPlatform
|
||||
errorChain(saveStreamWriteBytesPlatform(stream, buf, len));
|
||||
#endif
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadBytesImpl(
|
||||
savestream_t *stream, void *buf, const size_t len
|
||||
) {
|
||||
errorChain(saveStreamReadBytesRawImpl(stream, buf, len));
|
||||
cryptCRC32Update(&stream->checksum, buf, len);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteBytesImpl(
|
||||
savestream_t *stream, const void *buf, const size_t len
|
||||
) {
|
||||
cryptCRC32Update(&stream->checksum, buf, len);
|
||||
errorChain(saveStreamWriteBytesRawImpl(stream, buf, len));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamFinalizeWriteImpl(savestream_t *stream) {
|
||||
uint32_t finalCRC = cryptCRC32End(stream->checksum);
|
||||
uint32_t leChecksum = endianLittleToHost32(finalCRC);
|
||||
|
||||
#ifdef saveStreamSeekPlatform
|
||||
errorChain(saveStreamSeekPlatform(stream, SAVE_FILE_HEADER_SIZE));
|
||||
#endif
|
||||
|
||||
errorChain(saveStreamWriteBytesRawImpl(
|
||||
stream, &leChecksum, sizeof(uint32_t)
|
||||
));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamVerifyChecksumImpl(
|
||||
savestream_t *stream, const uint8_t slot
|
||||
) {
|
||||
uint32_t computed = cryptCRC32End(stream->checksum);
|
||||
if(computed != stream->expectedChecksum) {
|
||||
errorThrow("Save slot %u has invalid checksum", (uint32_t)slot);
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
|
||||
errorret_t saveStreamReadHeaderImpl(
|
||||
savestream_t *stream, char_t header[SAVE_FILE_HEADER_SIZE]
|
||||
) {
|
||||
errorChain(saveStreamReadBytesRawImpl(stream, header, SAVE_FILE_HEADER_SIZE));
|
||||
|
||||
if(
|
||||
header[0] != SAVE_FILE_HEADER[0] ||
|
||||
header[1] != SAVE_FILE_HEADER[1] ||
|
||||
header[2] != SAVE_FILE_HEADER[2]
|
||||
) {
|
||||
errorThrow("Save file has invalid header");
|
||||
}
|
||||
|
||||
uint32_t leChecksum;
|
||||
errorChain(saveStreamReadBytesRawImpl(stream, &leChecksum, sizeof(uint32_t)));
|
||||
stream->expectedChecksum = endianLittleToHost32(leChecksum);
|
||||
stream->checksum = cryptCRC32Begin();
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteHeaderImpl(
|
||||
savestream_t *stream, const char_t header[SAVE_FILE_HEADER_SIZE]
|
||||
) {
|
||||
errorChain(saveStreamWriteBytesRawImpl(
|
||||
stream, header, SAVE_FILE_HEADER_SIZE
|
||||
));
|
||||
|
||||
uint32_t placeholder = 0;
|
||||
errorChain(saveStreamWriteBytesRawImpl(
|
||||
stream, &placeholder, sizeof(uint32_t)
|
||||
));
|
||||
stream->checksum = cryptCRC32Begin();
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadVersionImpl(savestream_t *stream, uint32_t *out) {
|
||||
uint32_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint32_t)));
|
||||
*out = endianLittleToHost32(raw);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteVersionImpl(
|
||||
savestream_t *stream, const uint32_t *input
|
||||
) {
|
||||
uint32_t raw = endianLittleToHost32(*input);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint32_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadBoolImpl(savestream_t *stream, bool_t *out) {
|
||||
uint8_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint8_t)));
|
||||
*out = (bool_t)(raw != 0);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteBoolImpl(savestream_t *stream, const bool_t *input) {
|
||||
uint8_t raw = *input ? 1 : 0;
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint8_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadInt8Impl(savestream_t *stream, int8_t *out) {
|
||||
errorChain(saveStreamReadBytesImpl(stream, out, sizeof(int8_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteInt8Impl(savestream_t *stream, const int8_t *input) {
|
||||
errorChain(saveStreamWriteBytesImpl(stream, input, sizeof(int8_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadUInt8Impl(savestream_t *stream, uint8_t *out) {
|
||||
errorChain(saveStreamReadBytesImpl(stream, out, sizeof(uint8_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteUInt8Impl(
|
||||
savestream_t *stream, const uint8_t *input
|
||||
) {
|
||||
errorChain(saveStreamWriteBytesImpl(stream, input, sizeof(uint8_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadInt16Impl(savestream_t *stream, int16_t *out) {
|
||||
uint16_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint16_t)));
|
||||
uint16_t host = endianLittleToHost16(raw);
|
||||
memoryCopy(out, &host, sizeof(int16_t));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteInt16Impl(
|
||||
savestream_t *stream, const int16_t *input
|
||||
) {
|
||||
uint16_t raw;
|
||||
memoryCopy(&raw, input, sizeof(int16_t));
|
||||
raw = endianLittleToHost16(raw);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint16_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadUInt16Impl(savestream_t *stream, uint16_t *out) {
|
||||
uint16_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint16_t)));
|
||||
*out = endianLittleToHost16(raw);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteUInt16Impl(
|
||||
savestream_t *stream, const uint16_t *input
|
||||
) {
|
||||
uint16_t raw = endianLittleToHost16(*input);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint16_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadInt32Impl(savestream_t *stream, int32_t *out) {
|
||||
uint32_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint32_t)));
|
||||
uint32_t host = endianLittleToHost32(raw);
|
||||
memoryCopy(out, &host, sizeof(int32_t));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteInt32Impl(
|
||||
savestream_t *stream, const int32_t *input
|
||||
) {
|
||||
uint32_t raw;
|
||||
memoryCopy(&raw, input, sizeof(int32_t));
|
||||
raw = endianLittleToHost32(raw);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint32_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadUInt32Impl(savestream_t *stream, uint32_t *out) {
|
||||
uint32_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint32_t)));
|
||||
*out = endianLittleToHost32(raw);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteUInt32Impl(
|
||||
savestream_t *stream, const uint32_t *input
|
||||
) {
|
||||
uint32_t raw = endianLittleToHost32(*input);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint32_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadInt64Impl(savestream_t *stream, int64_t *out) {
|
||||
uint64_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
uint64_t host = endianLittleToHost64(raw);
|
||||
memoryCopy(out, &host, sizeof(int64_t));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteInt64Impl(
|
||||
savestream_t *stream, const int64_t *input
|
||||
) {
|
||||
uint64_t raw;
|
||||
memoryCopy(&raw, input, sizeof(int64_t));
|
||||
raw = endianLittleToHost64(raw);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadUInt64Impl(savestream_t *stream, uint64_t *out) {
|
||||
uint64_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
*out = endianLittleToHost64(raw);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteUInt64Impl(
|
||||
savestream_t *stream, const uint64_t *input
|
||||
) {
|
||||
uint64_t raw = endianLittleToHost64(*input);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadFloatImpl(savestream_t *stream, float_t *out) {
|
||||
float_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(float_t)));
|
||||
*out = endianLittleToHostFloat(raw);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteFloatImpl(
|
||||
savestream_t *stream, const float_t *input
|
||||
) {
|
||||
float_t raw = endianLittleToHostFloat(*input);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(float_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadStringImpl(
|
||||
savestream_t *stream, char_t *out, const size_t maxLen
|
||||
) {
|
||||
for(size_t i = 0; i < maxLen; i++) {
|
||||
errorChain(saveStreamReadBytesImpl(stream, &out[i], sizeof(char_t)));
|
||||
if(out[i] == '\0') errorOk();
|
||||
}
|
||||
out[maxLen - 1] = '\0';
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteStringImpl(
|
||||
savestream_t *stream, const char_t *input, const size_t maxLen
|
||||
) {
|
||||
size_t len = strlen(input);
|
||||
if(len >= maxLen) len = maxLen - 1;
|
||||
errorChain(saveStreamWriteBytesImpl(stream, input, len + 1));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadDateImpl(savestream_t *stream, dusktimeepoch_t *out) {
|
||||
uint64_t raw;
|
||||
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
raw = endianLittleToHost64(raw);
|
||||
memoryCopy(&out->time, &raw, sizeof(double_t));
|
||||
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
raw = endianLittleToHost64(raw);
|
||||
memoryCopy(&out->timeZone, &raw, sizeof(double_t));
|
||||
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
raw = endianLittleToHost64(raw);
|
||||
memoryCopy(&out->offsetTime, &raw, sizeof(double_t));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteDateImpl(
|
||||
savestream_t *stream, const dusktimeepoch_t *input
|
||||
) {
|
||||
uint64_t raw;
|
||||
|
||||
memoryCopy(&raw, &input->time, sizeof(double_t));
|
||||
raw = endianLittleToHost64(raw);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
|
||||
memoryCopy(&raw, &input->timeZone, sizeof(double_t));
|
||||
raw = endianLittleToHost64(raw);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
|
||||
memoryCopy(&raw, &input->offsetTime, sizeof(double_t));
|
||||
raw = endianLittleToHost64(raw);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveFileLoad(savestream_t *stream, savefile_t *file) {
|
||||
saveFileReadHeader(stream, file->header);
|
||||
saveFileReadVersion(stream, &file->version);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveFileWrite(savestream_t *stream, savefile_t *file) {
|
||||
saveFileWriteHeader(stream, file->header);
|
||||
saveFileWriteVersion(stream, &file->version);
|
||||
errorOk();
|
||||
}
|
||||
@@ -1,468 +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 "savefile.h"
|
||||
#include "save/saveplatform.h"
|
||||
#include "time/timeepoch.h"
|
||||
|
||||
typedef struct {
|
||||
bool_t found;
|
||||
uint32_t checksum;
|
||||
uint32_t expectedChecksum;
|
||||
saveplatformstream_t platform;
|
||||
} savestream_t;
|
||||
|
||||
/**
|
||||
* Reads bytes from the platform stream without updating the CRC.
|
||||
*
|
||||
* @param stream Active stream.
|
||||
* @param buf Destination buffer.
|
||||
* @param len Number of bytes to read.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadBytesRawImpl(
|
||||
savestream_t *stream, void *buf, const size_t len
|
||||
);
|
||||
|
||||
/**
|
||||
* Writes bytes to the platform stream without updating the CRC.
|
||||
*
|
||||
* @param stream Active stream.
|
||||
* @param buf Source buffer.
|
||||
* @param len Number of bytes to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteBytesRawImpl(
|
||||
savestream_t *stream, const void *buf, const size_t len
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads bytes from the platform stream and accumulates them into the CRC.
|
||||
*
|
||||
* @param stream Active stream.
|
||||
* @param buf Destination buffer.
|
||||
* @param len Number of bytes to read.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadBytesImpl(
|
||||
savestream_t *stream, void *buf, const size_t len
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates the CRC then writes bytes to the platform stream.
|
||||
*
|
||||
* @param stream Active stream.
|
||||
* @param buf Source buffer.
|
||||
* @param len Number of bytes to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteBytesImpl(
|
||||
savestream_t *stream, const void *buf, const size_t len
|
||||
);
|
||||
|
||||
/**
|
||||
* Finalizes a write stream: computes the final CRC32, seeks to the
|
||||
* checksum field in the header, and writes it in little-endian order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @return An error if the seek or write fails.
|
||||
*/
|
||||
errorret_t saveStreamFinalizeWriteImpl(savestream_t *stream);
|
||||
|
||||
/**
|
||||
* Verifies that the CRC32 accumulated during loading matches the value
|
||||
* stored in the file header.
|
||||
*
|
||||
* @param stream Active read stream (loading must be complete).
|
||||
* @param slot Slot index used in the error message on mismatch.
|
||||
* @return An error if the checksum does not match.
|
||||
*/
|
||||
errorret_t saveStreamVerifyChecksumImpl(
|
||||
savestream_t *stream, const uint8_t slot
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads and validates the magic header, then reads the stored CRC32 and
|
||||
* resets the running accumulator.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param header Buffer of SAVE_FILE_HEADER_SIZE bytes to receive the header.
|
||||
* @return An error if the header is missing or invalid.
|
||||
*/
|
||||
errorret_t saveStreamReadHeaderImpl(
|
||||
savestream_t *stream, char_t header[SAVE_FILE_HEADER_SIZE]
|
||||
);
|
||||
|
||||
/**
|
||||
* Writes the magic header and a zero CRC32 placeholder, then resets the
|
||||
* running accumulator.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param header Buffer of SAVE_FILE_HEADER_SIZE bytes to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteHeaderImpl(
|
||||
savestream_t *stream,
|
||||
const char_t header[SAVE_FILE_HEADER_SIZE]
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a little-endian uint32 version field from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the host-order value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadVersionImpl(savestream_t *stream, uint32_t *out);
|
||||
|
||||
/**
|
||||
* Writes a uint32 version field to the stream in little-endian order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteVersionImpl(
|
||||
savestream_t *stream, const uint32_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a single byte as a boolean (0 = false, non-zero = true).
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the boolean value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadBoolImpl(savestream_t *stream, bool_t *out);
|
||||
|
||||
/**
|
||||
* Writes a boolean as a single byte (true = 1, false = 0).
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteBoolImpl(savestream_t *stream, const bool_t *input);
|
||||
|
||||
/**
|
||||
* Reads a signed 8-bit integer from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadInt8Impl(savestream_t *stream, int8_t *out);
|
||||
|
||||
/**
|
||||
* Writes a signed 8-bit integer to the stream.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteInt8Impl(savestream_t *stream, const int8_t *input);
|
||||
|
||||
/**
|
||||
* Reads an unsigned 8-bit integer from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadUInt8Impl(savestream_t *stream, uint8_t *out);
|
||||
|
||||
/**
|
||||
* Writes an unsigned 8-bit integer to the stream.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteUInt8Impl(savestream_t *stream, const uint8_t *input);
|
||||
|
||||
/**
|
||||
* Reads a little-endian signed 16-bit integer from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the host-order value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadInt16Impl(savestream_t *stream, int16_t *out);
|
||||
|
||||
/**
|
||||
* Writes a signed 16-bit integer to the stream in little-endian order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteInt16Impl(
|
||||
savestream_t *stream, const int16_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a little-endian unsigned 16-bit integer from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the host-order value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadUInt16Impl(savestream_t *stream, uint16_t *out);
|
||||
|
||||
/**
|
||||
* Writes an unsigned 16-bit integer to the stream in little-endian order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteUInt16Impl(
|
||||
savestream_t *stream, const uint16_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a little-endian signed 32-bit integer from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the host-order value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadInt32Impl(savestream_t *stream, int32_t *out);
|
||||
|
||||
/**
|
||||
* Writes a signed 32-bit integer to the stream in little-endian order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteInt32Impl(
|
||||
savestream_t *stream, const int32_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a little-endian unsigned 32-bit integer from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the host-order value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadUInt32Impl(savestream_t *stream, uint32_t *out);
|
||||
|
||||
/**
|
||||
* Writes an unsigned 32-bit integer to the stream in little-endian order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteUInt32Impl(
|
||||
savestream_t *stream, const uint32_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a little-endian signed 64-bit integer from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the host-order value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadInt64Impl(savestream_t *stream, int64_t *out);
|
||||
|
||||
/**
|
||||
* Writes a signed 64-bit integer to the stream in little-endian order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteInt64Impl(
|
||||
savestream_t *stream, const int64_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a little-endian unsigned 64-bit integer from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the host-order value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadUInt64Impl(savestream_t *stream, uint64_t *out);
|
||||
|
||||
/**
|
||||
* Writes an unsigned 64-bit integer to the stream in little-endian order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteUInt64Impl(
|
||||
savestream_t *stream, const uint64_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a little-endian float from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the host-order value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadFloatImpl(savestream_t *stream, float_t *out);
|
||||
|
||||
/**
|
||||
* Writes a float to the stream in little-endian order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteFloatImpl(
|
||||
savestream_t *stream, const float_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a null-terminated string from the stream up to maxLen bytes
|
||||
* (including the terminator). Always null-terminates the output buffer.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Destination buffer of at least maxLen bytes.
|
||||
* @param maxLen Maximum bytes to read, including the null terminator.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadStringImpl(
|
||||
savestream_t *stream, char_t *out, const size_t maxLen
|
||||
);
|
||||
|
||||
/**
|
||||
* Writes a null-terminated string to the stream, truncating to maxLen-1
|
||||
* characters and always appending a null terminator.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Source string.
|
||||
* @param maxLen Maximum bytes to write, including the null terminator.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteStringImpl(
|
||||
savestream_t *stream, const char_t *input, const size_t maxLen
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a dusktimeepoch_t as three little-endian 64-bit IEEE 754 doubles
|
||||
* (time, timeZone, offsetTime).
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the epoch value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadDateImpl(
|
||||
savestream_t *stream, dusktimeepoch_t *out
|
||||
);
|
||||
|
||||
/**
|
||||
* Writes a dusktimeepoch_t as three little-endian 64-bit IEEE 754 doubles
|
||||
* (time, timeZone, offsetTime).
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Epoch value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteDateImpl(
|
||||
savestream_t *stream, const dusktimeepoch_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads the contents of a save slot from the stream into the save file
|
||||
* struct. Use saveFileRead* macros to deserialize fields one at a time.
|
||||
*
|
||||
* @param stream Active read stream for this slot.
|
||||
* @param file Save file struct to populate.
|
||||
* @return An error code if loading fails.
|
||||
*/
|
||||
errorret_t saveFileLoad(savestream_t *stream, savefile_t *file);
|
||||
|
||||
/**
|
||||
* Writes the contents of the save file struct into the stream.
|
||||
* Use saveFileWrite* macros to serialize fields one at a time.
|
||||
*
|
||||
* @param stream Active write stream for this slot.
|
||||
* @param file Save file struct to serialize.
|
||||
* @return An error code if writing fails.
|
||||
*/
|
||||
errorret_t saveFileWrite(savestream_t *stream, savefile_t *file);
|
||||
|
||||
#define saveFileReadHeader(stream, header) \
|
||||
errorChain(saveStreamReadHeaderImpl(stream, header))
|
||||
#define saveFileWriteHeader(stream, header) \
|
||||
errorChain(saveStreamWriteHeaderImpl(stream, header))
|
||||
|
||||
#define saveFileReadVersion(stream, out) \
|
||||
errorChain(saveStreamReadVersionImpl(stream, out))
|
||||
#define saveFileWriteVersion(stream, input) \
|
||||
errorChain(saveStreamWriteVersionImpl(stream, input))
|
||||
|
||||
#define saveFileReadBool(stream, out) \
|
||||
errorChain(saveStreamReadBoolImpl(stream, out))
|
||||
#define saveFileWriteBool(stream, input) \
|
||||
errorChain(saveStreamWriteBoolImpl(stream, input))
|
||||
|
||||
#define saveFileReadInt8(stream, out) \
|
||||
errorChain(saveStreamReadInt8Impl(stream, out))
|
||||
#define saveFileWriteInt8(stream, input) \
|
||||
errorChain(saveStreamWriteInt8Impl(stream, input))
|
||||
|
||||
#define saveFileReadUInt8(stream, out) \
|
||||
errorChain(saveStreamReadUInt8Impl(stream, out))
|
||||
#define saveFileWriteUInt8(stream, input) \
|
||||
errorChain(saveStreamWriteUInt8Impl(stream, input))
|
||||
|
||||
#define saveFileReadInt16(stream, out) \
|
||||
errorChain(saveStreamReadInt16Impl(stream, out))
|
||||
#define saveFileWriteInt16(stream, input) \
|
||||
errorChain(saveStreamWriteInt16Impl(stream, input))
|
||||
|
||||
#define saveFileReadUInt16(stream, out) \
|
||||
errorChain(saveStreamReadUInt16Impl(stream, out))
|
||||
#define saveFileWriteUInt16(stream, input) \
|
||||
errorChain(saveStreamWriteUInt16Impl(stream, input))
|
||||
|
||||
#define saveFileReadInt32(stream, out) \
|
||||
errorChain(saveStreamReadInt32Impl(stream, out))
|
||||
#define saveFileWriteInt32(stream, input) \
|
||||
errorChain(saveStreamWriteInt32Impl(stream, input))
|
||||
|
||||
#define saveFileReadUInt32(stream, out) \
|
||||
errorChain(saveStreamReadUInt32Impl(stream, out))
|
||||
#define saveFileWriteUInt32(stream, input) \
|
||||
errorChain(saveStreamWriteUInt32Impl(stream, input))
|
||||
|
||||
#define saveFileReadInt64(stream, out) \
|
||||
errorChain(saveStreamReadInt64Impl(stream, out))
|
||||
#define saveFileWriteInt64(stream, input) \
|
||||
errorChain(saveStreamWriteInt64Impl(stream, input))
|
||||
|
||||
#define saveFileReadUInt64(stream, out) \
|
||||
errorChain(saveStreamReadUInt64Impl(stream, out))
|
||||
#define saveFileWriteUInt64(stream, input) \
|
||||
errorChain(saveStreamWriteUInt64Impl(stream, input))
|
||||
|
||||
#define saveFileReadFloat(stream, out) \
|
||||
errorChain(saveStreamReadFloatImpl(stream, out))
|
||||
#define saveFileWriteFloat(stream, input) \
|
||||
errorChain(saveStreamWriteFloatImpl(stream, input))
|
||||
|
||||
#define saveFileReadString(stream, out, maxLen) \
|
||||
errorChain(saveStreamReadStringImpl(stream, out, maxLen))
|
||||
#define saveFileWriteString(stream, input, maxLen) \
|
||||
errorChain(saveStreamWriteStringImpl(stream, input, maxLen))
|
||||
|
||||
#define saveFileReadDate(stream, out) \
|
||||
errorChain(saveStreamReadDateImpl(stream, out))
|
||||
#define saveFileWriteDate(stream, input) \
|
||||
errorChain(saveStreamWriteDateImpl(stream, input))
|
||||
|
||||
@@ -10,5 +10,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
)
|
||||
|
||||
# Subdirs
|
||||
add_subdirectory(initial)
|
||||
add_subdirectory(mainmenu)
|
||||
add_subdirectory(overworld)
|
||||
add_subdirectory(battle)
|
||||
@@ -6,7 +6,14 @@
|
||||
*/
|
||||
|
||||
#include "scenebattle.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
#include "display/display.h"
|
||||
#include "display/displaystate.h"
|
||||
#include "display/shader/shader.h"
|
||||
#include "display/shader/shaderunlit.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "display/color.h"
|
||||
#include "scene/scene.h"
|
||||
|
||||
errorret_t sceneBattleInit(scenedata_t *sceneData) {
|
||||
errorOk();
|
||||
@@ -18,9 +25,127 @@ errorret_t sceneBattleUpdate(scenedata_t *sceneData) {
|
||||
}
|
||||
|
||||
errorret_t sceneBattleRender(scenedata_t *sceneData) {
|
||||
scenebattle_t *battle = &sceneData->battle;
|
||||
|
||||
sceneBattleCameraUpdateProjection(battle);
|
||||
sceneBattleCameraUpdateEye(battle);
|
||||
|
||||
errorChain(shaderBind(&SHADER_UNLIT));
|
||||
errorChain(shaderSetMatrix(
|
||||
&SHADER_UNLIT, SHADER_UNLIT_MODEL, SCENE.screenIdentity
|
||||
));
|
||||
errorChain(shaderSetMatrix(
|
||||
&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, battle->projection
|
||||
));
|
||||
errorChain(shaderSetMatrix(
|
||||
&SHADER_UNLIT, SHADER_UNLIT_VIEW, battle->eye
|
||||
));
|
||||
|
||||
errorChain(displaySetState((displaystate_t){
|
||||
.flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL
|
||||
}));
|
||||
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
errorChain(sceneBattleDrawFighter(i));
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t sceneBattleDispose(scenedata_t *sceneData) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void sceneBattleCameraUpdateProjection(scenebattle_t *battle) {
|
||||
glm_perspective(
|
||||
glm_rad(SCENE_BATTLE_CAMERA_FOV),
|
||||
SCREEN.aspect,
|
||||
SCENE_BATTLE_CAMERA_NEAR,
|
||||
SCENE_BATTLE_CAMERA_FAR,
|
||||
battle->projection
|
||||
);
|
||||
}
|
||||
|
||||
void sceneBattleCameraUpdateEye(scenebattle_t *battle) {
|
||||
glm_lookat(
|
||||
(vec3){ 0.0f, 4.0f, 7.0f },
|
||||
(vec3){ 0.0f, 0.5f, 0.0f },
|
||||
(vec3){ 0.0f, 1.0f, 0.0f },
|
||||
battle->eye
|
||||
);
|
||||
}
|
||||
|
||||
void sceneBattleWorldToScreen(
|
||||
scenebattle_t *battle,
|
||||
vec3 renderPos,
|
||||
vec2 out
|
||||
) {
|
||||
mat4 viewProj;
|
||||
glm_mat4_mul(battle->projection, battle->eye, viewProj);
|
||||
|
||||
vec4 viewport = {
|
||||
0.0f, 0.0f, (float_t)SCREEN.width, (float_t)SCREEN.height
|
||||
};
|
||||
vec3 window;
|
||||
glm_project(renderPos, viewProj, viewport, window);
|
||||
|
||||
out[0] = window[0];
|
||||
out[1] = (float_t)SCREEN.height - window[1];
|
||||
}
|
||||
|
||||
void sceneBattleGetFighterPosition(const uint8_t fighterIndex, vec3 out) {
|
||||
const battlefighter_t *fighter = &BATTLE.fighters[fighterIndex];
|
||||
|
||||
// Ordinal (and count) of this fighter among its living-slot teammates,
|
||||
// in slot order, so each team lays out as an evenly spaced row.
|
||||
uint8_t ordinal = 0;
|
||||
uint8_t teamCount = 0;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
if(BATTLE.fighters[i].status == BATTLE_FIGHTER_STATUS_NULL) continue;
|
||||
if(BATTLE.fighters[i].team != fighter->team) continue;
|
||||
if(i < fighterIndex) ordinal++;
|
||||
teamCount++;
|
||||
}
|
||||
|
||||
const float_t x =
|
||||
((float_t)ordinal - ((float_t)(teamCount - 1) * 0.5f)) *
|
||||
SCENE_BATTLE_FIGHTER_SPACING;
|
||||
const float_t z = fighter->team == BATTLE_FIGHTER_TEAM_ALLY
|
||||
? SCENE_BATTLE_ALLY_Z : SCENE_BATTLE_ENEMY_Z;
|
||||
|
||||
out[0] = x;
|
||||
out[1] = 0.0f;
|
||||
out[2] = z;
|
||||
}
|
||||
|
||||
errorret_t sceneBattleDrawFighter(const uint8_t fighterIndex) {
|
||||
const battlefighter_t *fighter = &BATTLE.fighters[fighterIndex];
|
||||
if(fighter->status == BATTLE_FIGHTER_STATUS_NULL) errorOk();
|
||||
|
||||
vec3 position;
|
||||
sceneBattleGetFighterPosition(fighterIndex, position);
|
||||
|
||||
spritebatchsprite_t sprite;
|
||||
glm_vec3_copy(position, sprite.min);
|
||||
glm_vec3_copy(position, sprite.max);
|
||||
glm_vec3_add(sprite.max, (vec3){ 1.0f, 1.0f, 0.0f }, sprite.max);
|
||||
glm_vec2_copy((vec2){ 0.0f, 0.0f }, sprite.uvMin);
|
||||
glm_vec2_copy((vec2){ 1.0f, 1.0f }, sprite.uvMax);
|
||||
|
||||
color_t color;
|
||||
if(fighter->status == BATTLE_FIGHTER_STATUS_DEAD) {
|
||||
color = color4b(96, 96, 96, 255);
|
||||
} else if(fighter->team == BATTLE_FIGHTER_TEAM_ALLY) {
|
||||
color = COLOR_BLUE;
|
||||
} else {
|
||||
color = COLOR_RED;
|
||||
}
|
||||
|
||||
shadermaterial_t material = {
|
||||
.unlit = { .color = color, .texture = NULL }
|
||||
};
|
||||
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
|
||||
errorChain(spriteBatchFlush());
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -7,9 +7,24 @@
|
||||
|
||||
#pragma once
|
||||
#include "scene/scenebase.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
|
||||
// Fixed mock camera, "to begin": looking down at the arena from above and
|
||||
// in front. Not yet driven by anything (no shake, no per-fighter framing).
|
||||
#define SCENE_BATTLE_CAMERA_FOV 45.0f
|
||||
#define SCENE_BATTLE_CAMERA_NEAR 0.1f
|
||||
#define SCENE_BATTLE_CAMERA_FAR 100.0f
|
||||
|
||||
// Fighter row layout, in render-space units. Allies stand closer to the
|
||||
// camera (+Z), enemies further away (-Z); each team is spread evenly
|
||||
// along X, centered on the origin.
|
||||
#define SCENE_BATTLE_FIGHTER_SPACING 2.0f
|
||||
#define SCENE_BATTLE_ALLY_Z 2.5f
|
||||
#define SCENE_BATTLE_ENEMY_Z (-2.5f)
|
||||
|
||||
typedef struct {
|
||||
|
||||
mat4 eye;
|
||||
mat4 projection;
|
||||
} scenebattle_t;
|
||||
|
||||
/**
|
||||
@@ -44,3 +59,52 @@ errorret_t sceneBattleRender(scenedata_t *sceneData);
|
||||
* @return An error if the dispose failed, or errorOk() if it succeeded.
|
||||
*/
|
||||
errorret_t sceneBattleDispose(scenedata_t *sceneData);
|
||||
|
||||
/**
|
||||
* Recomputes the battle camera's projection matrix from the current
|
||||
* screen aspect ratio, storing it in battle->projection.
|
||||
*
|
||||
* @param battle The battle scene data to update.
|
||||
*/
|
||||
void sceneBattleCameraUpdateProjection(scenebattle_t *battle);
|
||||
|
||||
/**
|
||||
* Recomputes the battle camera's eye/view matrix, storing it in
|
||||
* battle->eye. Fixed for now -- doesn't yet track anything.
|
||||
*
|
||||
* @param battle The battle scene data to update.
|
||||
*/
|
||||
void sceneBattleCameraUpdateEye(scenebattle_t *battle);
|
||||
|
||||
/**
|
||||
* Converts a render-space position to screen-space pixel coordinates,
|
||||
* using the battle camera's current eye and projection matrices.
|
||||
*
|
||||
* @param battle The battle scene data holding the camera matrices.
|
||||
* @param renderPos The render-space position to convert.
|
||||
* @param out Output vec2 filled with the screen-space pixel position.
|
||||
*/
|
||||
void sceneBattleWorldToScreen(
|
||||
scenebattle_t *battle,
|
||||
vec3 renderPos,
|
||||
vec2 out
|
||||
);
|
||||
|
||||
/**
|
||||
* Computes the render-space position of a fighter's feet, laying out
|
||||
* each team as an evenly spaced row facing the camera.
|
||||
*
|
||||
* @param fighterIndex Index into BATTLE.fighters.
|
||||
* @param out Output vec3 filled with the fighter's render-space position.
|
||||
*/
|
||||
void sceneBattleGetFighterPosition(const uint8_t fighterIndex, vec3 out);
|
||||
|
||||
/**
|
||||
* Draws a single fighter as a colored 1x1 quad, skipping empty slots.
|
||||
* Color differentiates team (ally/enemy) and status (dead fighters are
|
||||
* drawn dimmed).
|
||||
*
|
||||
* @param fighterIndex Index into BATTLE.fighters.
|
||||
* @return An error if drawing failed, or errorOk() if it succeeded.
|
||||
*/
|
||||
errorret_t sceneBattleDrawFighter(const uint8_t fighterIndex);
|
||||
|
||||
@@ -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,52 @@
|
||||
/**
|
||||
* 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 "console/console.h"
|
||||
#include "save/savemanager.h"
|
||||
|
||||
int32_t testData = 69;
|
||||
|
||||
void testCallback(savedevice_t *device, void *user) {
|
||||
if(device == NULL) {
|
||||
consolePrint("No save device found.");
|
||||
} else {
|
||||
consolePrint("Found save device: %s", device->reasonKey);
|
||||
}
|
||||
}
|
||||
|
||||
errorret_t sceneInitialInit(scenedata_t *sceneData) {
|
||||
assertNotNull(sceneData, "Scene data cannot be null");
|
||||
memoryZero(&sceneData->initial, sizeof(sceneinitial_t));
|
||||
|
||||
consolePrint("Going to find a save device.");
|
||||
saveManagerFindAvailableDevice(
|
||||
testCallback,
|
||||
&testData
|
||||
);
|
||||
|
||||
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 main menu.
|
||||
*
|
||||
* @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);
|
||||
@@ -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
|
||||
scenemainmenu.c
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "scenemainmenu.h"
|
||||
|
||||
errorret_t sceneMainMenuInit(scenedata_t *sceneData) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t sceneMainMenuUpdate(scenedata_t *sceneData) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t sceneMainMenuRender(scenedata_t *sceneData) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t sceneMainMenuDispose(scenedata_t *sceneData) {
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "scene/scenebase.h"
|
||||
|
||||
// Empty for now -- the menu itself lives in ui/frame/mainmenu/uimainmenu.c,
|
||||
// driven by the global UI element pipeline (see uimainmenu.c's own
|
||||
// SCENE.current check for when it shows itself). A byte placeholder keeps
|
||||
// the struct non-empty for portability.
|
||||
typedef struct {
|
||||
uint8_t reserved;
|
||||
} scenemainmenu_t;
|
||||
|
||||
/**
|
||||
* Initializes the main menu scene.
|
||||
*
|
||||
* @param sceneData The scene data used for this scene.
|
||||
* @return An error if the init failed, or errorOk() if it succeeded.
|
||||
*/
|
||||
errorret_t sceneMainMenuInit(scenedata_t *sceneData);
|
||||
|
||||
/**
|
||||
* Updates the main menu scene. Currently a no-op -- the menu drives
|
||||
* itself via the global UI element pipeline.
|
||||
*
|
||||
* @param sceneData The scene data used for this scene.
|
||||
* @return An error if the update failed, or errorOk() if it succeeded.
|
||||
*/
|
||||
errorret_t sceneMainMenuUpdate(scenedata_t *sceneData);
|
||||
|
||||
/**
|
||||
* Renders the main menu scene. Currently a no-op -- the menu draws
|
||||
* itself 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 sceneMainMenuRender(scenedata_t *sceneData);
|
||||
|
||||
/**
|
||||
* Disposes the main menu scene.
|
||||
*
|
||||
* @param sceneData The scene data used for this scene.
|
||||
* @return An error if the dispose failed, or errorOk() if it succeeded.
|
||||
*/
|
||||
errorret_t sceneMainMenuDispose(scenedata_t *sceneData);
|
||||
@@ -10,6 +10,20 @@
|
||||
scenecallbacks_t SCENE_TYPES[SCENE_TYPE_COUNT] = {
|
||||
[SCENE_TYPE_NULL] = { 0 },
|
||||
|
||||
[SCENE_TYPE_INITIAL] = {
|
||||
.init = sceneInitialInit,
|
||||
.update = sceneInitialUpdate,
|
||||
.render = sceneInitialRender,
|
||||
.dispose = sceneInitialDispose
|
||||
},
|
||||
|
||||
[SCENE_TYPE_MAIN_MENU] = {
|
||||
.init = sceneMainMenuInit,
|
||||
.update = sceneMainMenuUpdate,
|
||||
.render = sceneMainMenuRender,
|
||||
.dispose = sceneMainMenuDispose
|
||||
},
|
||||
|
||||
[SCENE_TYPE_OVERWORLD] = {
|
||||
.init = sceneOverworldInit,
|
||||
.update = sceneOverworldUpdate,
|
||||
|
||||
@@ -7,10 +7,14 @@
|
||||
|
||||
#pragma once
|
||||
#include "scene/scenebase.h"
|
||||
#include "scene/initial/sceneinitial.h"
|
||||
#include "scene/mainmenu/scenemainmenu.h"
|
||||
#include "scene/overworld/sceneoverworld.h"
|
||||
#include "scene/battle/scenebattle.h"
|
||||
|
||||
typedef union scenedata_u {
|
||||
sceneinitial_t initial;
|
||||
scenemainmenu_t mainMenu;
|
||||
sceneoverworld_t overworld;
|
||||
scenebattle_t battle;
|
||||
} scenedata_t;
|
||||
@@ -27,6 +31,8 @@ typedef struct {
|
||||
typedef enum {
|
||||
SCENE_TYPE_NULL,
|
||||
|
||||
SCENE_TYPE_INITIAL,
|
||||
SCENE_TYPE_MAIN_MENU,
|
||||
SCENE_TYPE_OVERWORLD,
|
||||
SCENE_TYPE_BATTLE,
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
#error "systemInitPlatform is not defined"
|
||||
#endif
|
||||
|
||||
#ifndef systemGetLocalePlatform
|
||||
#error "systemGetLocalePlatform is not defined"
|
||||
#endif
|
||||
|
||||
errorret_t systemInit() {
|
||||
return systemInitPlatform();
|
||||
}
|
||||
@@ -24,6 +28,10 @@ systemdialogtype_t systemGetActiveDialogType() {
|
||||
return systemGetActiveDialogTypePlatform();
|
||||
}
|
||||
|
||||
const localeinfo_t * systemGetLocale(void) {
|
||||
return systemGetLocalePlatform();
|
||||
}
|
||||
|
||||
systemplatform_t systemGetPlatform(void) {
|
||||
#if defined(DUSK_KNULLI)
|
||||
return SYSTEM_PLATFORM_KNULLI;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "system/systemplatformlist.h"
|
||||
#include "locale/localeinfo.h"
|
||||
|
||||
#define SYSTEM_PLATFORM(name, value) SYSTEM_PLATFORM_##name = value,
|
||||
typedef enum { SYSTEM_PLATFORM_LIST } systemplatform_t;
|
||||
@@ -48,3 +49,10 @@ systemdialogtype_t systemGetActiveDialogType();
|
||||
* @return The current platform.
|
||||
*/
|
||||
systemplatform_t systemGetPlatform(void);
|
||||
|
||||
/**
|
||||
* Returns the current locale of the system.
|
||||
*
|
||||
* @return The current locale.
|
||||
*/
|
||||
const localeinfo_t * systemGetLocale(void);
|
||||
@@ -16,5 +16,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
ui.c
|
||||
uielement.c
|
||||
uielementlist.c
|
||||
# uitextbox.c
|
||||
)
|
||||
@@ -10,6 +10,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
)
|
||||
|
||||
add_subdirectory(game)
|
||||
add_subdirectory(settings)
|
||||
add_subdirectory(mainmenu)
|
||||
add_subdirectory(battle)
|
||||
add_subdirectory(backpack)
|
||||
@@ -14,6 +14,8 @@
|
||||
#include "display/screen/screen.h"
|
||||
#include "display/text/text.h"
|
||||
#include "assert/assert.h"
|
||||
#include "locale/localemanager.h"
|
||||
#include "asset/loader/locale/assetlocaleloader.h"
|
||||
|
||||
uibackpack_t UI_BACKPACK;
|
||||
|
||||
@@ -41,6 +43,14 @@ void uiBackpackTabSelected(
|
||||
errorret_t uiBackpackInit(void) {
|
||||
memoryZero(&UI_BACKPACK, sizeof(uibackpack_t));
|
||||
|
||||
errorChain(assetLocaleGetString(
|
||||
&LOCALE.entry->data.locale,
|
||||
"ui.backpack.category_format",
|
||||
0,
|
||||
UI_BACKPACK.categoryFormat,
|
||||
UI_BACKPACK_CATEGORY_FORMAT_MAX
|
||||
));
|
||||
|
||||
MENU_BEGIN(
|
||||
&UI_BACKPACK.tabsMenu, UI_BACKPACK.tabs,
|
||||
uiBackpackTabSelected, NULL, uiBackpackTabChanged
|
||||
@@ -48,7 +58,7 @@ errorret_t uiBackpackInit(void) {
|
||||
for(uint8_t i = 0; i < UI_BACKPACK_TAB_COUNT; i++) {
|
||||
stringFormat(
|
||||
UI_BACKPACK.tabLabels[i], UI_BACKPACK_TAB_LABEL_MAX - 1,
|
||||
"Category %u", i + 1
|
||||
UI_BACKPACK.categoryFormat, i + 1
|
||||
);
|
||||
MENU_TAB(UI_BACKPACK.tabLabels[i]);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#define UI_BACKPACK_TAB_COUNT (ITEM_TYPE_COUNT - 1)
|
||||
#define UI_BACKPACK_TAB_LABEL_MAX 32
|
||||
#define UI_BACKPACK_CATEGORY_FORMAT_MAX 32
|
||||
#define UI_BACKPACK_ITEM_LIST_COLUMNS 4
|
||||
#define UI_BACKPACK_ITEM_LIST_ROWS 5
|
||||
|
||||
@@ -20,6 +21,7 @@ typedef struct {
|
||||
uimenu_t tabsMenu;
|
||||
uimenuitem_t tabs[UI_BACKPACK_TAB_COUNT];
|
||||
char_t tabLabels[UI_BACKPACK_TAB_COUNT][UI_BACKPACK_TAB_LABEL_MAX];
|
||||
char_t categoryFormat[UI_BACKPACK_CATEGORY_FORMAT_MAX];
|
||||
|
||||
uiitemlist_t itemList;
|
||||
} uibackpack_t;
|
||||
|
||||
@@ -6,4 +6,5 @@
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
uibattlemenu.c
|
||||
uibattlehud.c
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user