Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d95415232 | |||
| 2cbd80a004 | |||
| 9abf8101da | |||
| 24badd06a5 | |||
| 7a03ef8eaf | |||
| f3ea507313 | |||
| 4b0388a0e1 | |||
| a84137b5ff |
@@ -1,455 +0,0 @@
|
||||
# Dusk — Claude Code rules
|
||||
|
||||
## File headers
|
||||
Every C, H, and JS file starts with:
|
||||
|
||||
```c
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
```
|
||||
|
||||
JS files use `//` comment style instead.
|
||||
|
||||
---
|
||||
|
||||
## C conventions
|
||||
|
||||
### Types
|
||||
Always use the project-defined aliases instead of bare C primitives:
|
||||
|
||||
| Use | Not |
|
||||
|-----------|--------------|
|
||||
| `bool_t` | `bool` |
|
||||
| `int_t` | `int` |
|
||||
| `float_t` | `float` |
|
||||
| `char_t` | `char` |
|
||||
|
||||
Use `uint8_t`, `uint16_t`, `int32_t`, etc. for fixed-width integers.
|
||||
All struct and enum types end in `_t` (`animation_t`, `errorret_t`, …).
|
||||
|
||||
### Naming
|
||||
- **Functions** — snake_case, prefixed with their module:
|
||||
`assetLock()`, `entityPositionInit()`, `moduleAssetBatchCtor()`
|
||||
- **Struct fields** — camelCase: `keyframeCount`, `localPosition`
|
||||
- **Macros / constants** — UPPER_SNAKE_CASE:
|
||||
`ENTITY_ID_INVALID`, `ERROR_OK`, `COMPONENT_TYPE_COUNT`
|
||||
- **Files** — snake_case matching the primary type: `entityposition.c`,
|
||||
`moduleassetbatch.c`
|
||||
|
||||
### Header files (`.h`)
|
||||
- Use `#pragma once` — no include guards.
|
||||
- Declare every public function, `#define`, and `extern` global.
|
||||
- Write a JSDoc block (`/** … */`) above every declaration explaining
|
||||
purpose, `@param`s, and `@returns`.
|
||||
- Only include headers that the `.h` file itself strictly requires for
|
||||
the types it exposes. Move everything else to the `.c` file.
|
||||
Do not use forward declarations as a workaround — use the real
|
||||
include in the `.c` file instead.
|
||||
|
||||
### Implementation files (`.c`)
|
||||
- Contain function bodies only; no declarations.
|
||||
- Pull in whatever additional includes the implementation needs.
|
||||
- Do not use `static` or `inline` on **functions**. Every function,
|
||||
including internal helpers, must be declared in the matching `.h` and
|
||||
defined in the `.c` file. Internal helpers belong near the bottom of
|
||||
the `.c` file, not at the top with a `static` qualifier.
|
||||
`static` and `inline` on functions are only appropriate when the
|
||||
function body is written directly inside a `.h` file.
|
||||
`static` on **variables** (file-scope state) is fine and expected.
|
||||
|
||||
### Formatting
|
||||
- Hard-wrap all lines at **80 characters**.
|
||||
|
||||
### Error handling
|
||||
Return `errorret_t` from fallible functions. Use these macros:
|
||||
|
||||
```c
|
||||
errorOk(); // return success
|
||||
errorThrow("msg %d", val); // return failure with message
|
||||
errorChain(someCall()); // propagate failure, continue on success
|
||||
errorIsOk(ret) / errorIsNotOk(ret) // test a result
|
||||
errorCatch(ret); // handle + free an error
|
||||
```
|
||||
|
||||
Never return raw error codes or use `errno` for in-engine errors.
|
||||
|
||||
### Memory
|
||||
Use the project allocator — never raw `malloc`/`free`:
|
||||
|
||||
```c
|
||||
memoryAllocate(size) // allocate
|
||||
memoryFree(ptr) // free
|
||||
memoryZero(dest, size) // zero a block
|
||||
memoryCopy(dest, src, size) // copy
|
||||
```
|
||||
|
||||
### Asserts
|
||||
Prefer specific assert macros over bare `assert()`:
|
||||
|
||||
```c
|
||||
assertNotNull(ptr, "msg");
|
||||
assertTrue(cond, "msg");
|
||||
assertFalse(cond, "msg");
|
||||
assertUnreachable("msg");
|
||||
assertIsMainThread("msg");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build system
|
||||
Each subdirectory has its own `CMakeLists.txt` that adds sources with:
|
||||
|
||||
```cmake
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
myfile.c
|
||||
)
|
||||
```
|
||||
|
||||
Never add source files to the root `CMakeLists.txt` directly.
|
||||
|
||||
---
|
||||
|
||||
## Platform support
|
||||
|
||||
### Targets
|
||||
Set `DUSK_TARGET_SYSTEM` at CMake configure time to select a platform:
|
||||
|
||||
| `DUSK_TARGET_SYSTEM` | Macro defined | Platform |
|
||||
|----------------------|-------------------|------------------|
|
||||
| `linux` | `DUSK_LINUX` | Linux desktop |
|
||||
| `knulli` | `DUSK_KNULLI` | Knulli (handheld)|
|
||||
| `psp` | `DUSK_PSP` | Sony PSP |
|
||||
| `vita` | `DUSK_VITA` | PlayStation Vita |
|
||||
| `gamecube` | `DUSK_GAMECUBE` | Nintendo GameCube|
|
||||
| `wii` | `DUSK_WII` | Nintendo Wii |
|
||||
|
||||
### Layer structure
|
||||
```
|
||||
src/dusk/ core, platform-agnostic game logic
|
||||
src/duskgl/ OpenGL abstraction (Linux, Knulli, PSP, Vita)
|
||||
src/dusksdl2/ SDL2 window + input (Linux, Knulli, PSP, Vita)
|
||||
src/dusklinux/ Linux + Knulli platform impl
|
||||
src/duskpsp/ PSP platform impl
|
||||
src/duskvita/ Vita platform impl
|
||||
src/duskdolphin/ GameCube / Wii platform impl (no SDL2/OpenGL)
|
||||
```
|
||||
|
||||
Dolphin is the only target that bypasses SDL2 and OpenGL entirely —
|
||||
it uses native GameCube/Wii rendering and input APIs.
|
||||
|
||||
### Platform guards
|
||||
Use the compile-time macros for platform-specific code:
|
||||
|
||||
```c
|
||||
#ifdef DUSK_PSP
|
||||
// PSP-only path
|
||||
#elif defined(DUSK_GAMECUBE) || defined(DUSK_WII)
|
||||
// GameCube / Wii path
|
||||
#else
|
||||
// Generic / Linux fallback
|
||||
#endif
|
||||
```
|
||||
|
||||
Additional capability macros set per-target:
|
||||
`DUSK_SDL2`, `DUSK_OPENGL`, `DUSK_OPENGL_ES`, `DUSK_OPENGL_LEGACY`,
|
||||
`DUSK_INPUT_GAMEPAD`, `DUSK_INPUT_KEYBOARD`, `DUSK_INPUT_POINTER`,
|
||||
`DUSK_PLATFORM_ENDIAN_BIG` / `DUSK_PLATFORM_ENDIAN_LITTLE`.
|
||||
|
||||
### Abstraction pattern
|
||||
Platform-specific implementations are wired in via `#define` macros in
|
||||
each platform's `displayplatform.h` / `inputplatform.h` etc., which
|
||||
the core calls through. Functions that a platform does not support are
|
||||
simply left undefined — the core guards calls with `#ifdef`.
|
||||
|
||||
### Adding platform-specific code
|
||||
- Put it under `src/dusk<platform>/` in the matching subsystem folder.
|
||||
- Gate any core call-site with the appropriate `#ifdef DUSK_<PLATFORM>`
|
||||
or capability macro.
|
||||
- Keep the `src/dusk/` core free of platform ifdefs — delegate through
|
||||
the platform header macros instead.
|
||||
|
||||
---
|
||||
|
||||
## Adding a new asset loader type
|
||||
1. Add an enum value to `assetloadertype_t` (before `_COUNT`) in
|
||||
`src/dusk/asset/loader/assetloader.h`.
|
||||
2. Add fields to the input/loading/output unions in `assetloader.h`.
|
||||
3. Implement `assetXxxLoaderSync`, `assetXxxLoaderAsync`, and
|
||||
`assetXxxDispose` in a new `src/dusk/asset/loader/xxx/` directory.
|
||||
4. Register the three callbacks in `ASSET_LOADER_CALLBACKS[]` in
|
||||
`src/dusk/asset/loader/assetloader.c`.
|
||||
5. If user-facing, create a JS module (see below) and a `.d.ts` file.
|
||||
|
||||
---
|
||||
|
||||
## Adding a new entity component
|
||||
1. Create `src/dusk/entity/component/<category>/entityMyComp.h/.c` with
|
||||
struct `entityMyComp_t`, `entityMyCompInit()`, and optionally
|
||||
`entityMyCompDispose()`.
|
||||
2. Add the include to `src/dusk/entity/componentlist.h` header block.
|
||||
3. Add a row to `src/dusk/entity/componentlist.h`:
|
||||
```c
|
||||
X(MYCOMP, entityMyComp_t, myComp, entityMyCompInit, NULL, NULL)
|
||||
```
|
||||
This auto-generates the enum, union field, and definition entry.
|
||||
4. If JS-facing, create the script module and `.d.ts` (see below).
|
||||
|
||||
---
|
||||
|
||||
## Adding a new script (JS) module
|
||||
1. Create `src/dusk/script/module/<category>/moduleMyMod.h/.c`.
|
||||
- Declare `extern scriptproto_t MODULE_MYMOD_PROTO;` in the header.
|
||||
- Use `moduleBaseFunction(name)` to define JS-callable functions.
|
||||
- Register props/funcs in `moduleMyModInit()` with
|
||||
`scriptProtoDefineProp` / `scriptProtoDefineFunc` /
|
||||
`scriptProtoDefineStaticFunc`.
|
||||
2. `#include` the header in
|
||||
`src/dusk/script/module/modulelist.c` and call
|
||||
`moduleMyModInit()` in `moduleListInit()` (and `Dispose` in
|
||||
`moduleListDispose()`).
|
||||
3. For component modules also register in
|
||||
`src/dusk/script/module/entity/component/modulecomponentlist.c`
|
||||
so `entity.add()` returns the typed wrapper.
|
||||
4. Create `types/<category>/mymod.d.ts` and add a
|
||||
`/// <reference path="..." />` line to `types/index.d.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Script module type declarations
|
||||
Whenever a `src/dusk/script/module/**/*.c` file is created or modified,
|
||||
check whether the corresponding `types/**/*.d.ts` needs updating and
|
||||
apply any changes before finishing the task.
|
||||
|
||||
---
|
||||
|
||||
## JavaScript (asset scripts)
|
||||
- Use `var` for module-level state; `const` for values that never
|
||||
change.
|
||||
- Always use semicolons.
|
||||
- Scene objects are plain objects (`var scene = {}`) with assigned
|
||||
methods.
|
||||
- Export via `module.exports = scene`.
|
||||
- Async scene init should use `async function` and `await`.
|
||||
|
||||
---
|
||||
|
||||
## Coding style
|
||||
|
||||
### ASCII only
|
||||
Source files (`.c`, `.h`, `.js`) must contain only ASCII characters (U+0000–U+007F).
|
||||
Non-ASCII characters are banned even in comments and string literals.
|
||||
Use ASCII-only substitutes instead:
|
||||
- `--` or `-` instead of `—` (em dash)
|
||||
- `->` instead of `→` (arrow)
|
||||
- `x` or `*` instead of `×` (multiplication)
|
||||
|
||||
Only non-script asset files (e.g. `.po` locale files) may contain non-ASCII text.
|
||||
|
||||
### Indentation
|
||||
2 spaces. No tabs.
|
||||
|
||||
### Keyword and operator spacing
|
||||
No space between a keyword or function name and its opening parenthesis:
|
||||
|
||||
```c
|
||||
if(!ptr) return;
|
||||
for(uint8_t i = 0; i < count; i++) {
|
||||
while(entry->state != DONE) {
|
||||
switch(type) {
|
||||
sizeof(assetbatch_t)
|
||||
memoryZero(ptr, size)
|
||||
```
|
||||
|
||||
Spaces around all binary operators and after every comma:
|
||||
|
||||
```c
|
||||
pos->flags |= ENTITY_POSITION_FLAG_WORLD_DIRTY;
|
||||
(size_t)end - (size_t)start
|
||||
foo(a, b, c)
|
||||
```
|
||||
|
||||
### Braces
|
||||
Opening brace on the **same line** as the statement (K&R style) for all
|
||||
constructs — functions, `if`, `else`, `for`, `while`, `switch`:
|
||||
|
||||
```c
|
||||
void assetEntryLock(assetentry_t *entry) {
|
||||
...
|
||||
}
|
||||
|
||||
if(dirty) {
|
||||
...
|
||||
} else {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Guard returns
|
||||
Short guards go on one line with no braces:
|
||||
|
||||
```c
|
||||
if(!ptr) return;
|
||||
if(!b || !b->batch) return jerry_undefined();
|
||||
if(!(flags & DIRTY)) return;
|
||||
```
|
||||
|
||||
### Blank lines
|
||||
- One blank line between functions; no blank line at the start or end of
|
||||
a function body.
|
||||
- One blank line between logical blocks inside a function body.
|
||||
- No trailing blank lines at the end of a file.
|
||||
|
||||
### Pointer placement
|
||||
`*` is attached to the variable name, not the type:
|
||||
|
||||
```c
|
||||
assetentry_t *entry
|
||||
const char_t *name
|
||||
void *ptr
|
||||
uint8_t *d = (uint8_t *)dest;
|
||||
```
|
||||
|
||||
### Casts
|
||||
Space between cast and operand:
|
||||
|
||||
```c
|
||||
(assetbatch_t *)user
|
||||
(uint8_t *)dest
|
||||
(textureformat_t)v
|
||||
```
|
||||
|
||||
### Return
|
||||
No parentheses around the return value:
|
||||
|
||||
```c
|
||||
return ptr;
|
||||
return MEMORY_POINTERS_IN_USE;
|
||||
```
|
||||
|
||||
### switch / case
|
||||
`case` indented 2 spaces from `switch`; body indented 2 more from `case`:
|
||||
|
||||
```c
|
||||
switch(type) {
|
||||
case ASSET_LOADER_TYPE_TEXTURE:
|
||||
descs[i].input.texture = (textureformat_t)v;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-line function signatures
|
||||
When parameters don't fit on one line, put each on its own line indented
|
||||
2 spaces; the closing `) {` (definition) or `);` (declaration) goes on
|
||||
its own line at column 0:
|
||||
|
||||
```c
|
||||
void assetEntryInit(
|
||||
assetentry_t *entry,
|
||||
const char_t *name,
|
||||
const assetloadertype_t type,
|
||||
assetloaderinput_t *input
|
||||
) {
|
||||
|
||||
errorret_t memoryCompare(
|
||||
const void *a,
|
||||
const void *b,
|
||||
const size_t size
|
||||
);
|
||||
```
|
||||
|
||||
### Structs and enums
|
||||
Anonymous inner struct or enum with a `typedef`, `_t` suffix, closing
|
||||
brace and name on the same line:
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
errorcode_t code;
|
||||
char_t *message;
|
||||
} errorstate_t;
|
||||
|
||||
typedef enum {
|
||||
ASSET_LOADER_TYPE_NULL,
|
||||
ASSET_LOADER_TYPE_COUNT
|
||||
} assetloadertype_t;
|
||||
```
|
||||
|
||||
### Designated initialisers
|
||||
Spaces inside braces; `.field = value`:
|
||||
|
||||
```c
|
||||
jsassetentry_t e = { .entry = entry };
|
||||
assetbatchloadedpend_t init = { .batch = batch };
|
||||
```
|
||||
|
||||
### Ternary operator
|
||||
Spaces around `?` and `:`:
|
||||
|
||||
```c
|
||||
const float val = psx > 0.0f ? pt[0][0] / psx : 0.0f;
|
||||
```
|
||||
|
||||
### const placement
|
||||
`const` before the type, `*` attached to the variable:
|
||||
|
||||
```c
|
||||
const char_t *name
|
||||
const void *src
|
||||
const size_t size
|
||||
```
|
||||
|
||||
### Comments in `.c` files
|
||||
- Do not use section dividers (`/* ---- ... ---- */`). Just let the
|
||||
functions follow one another with a single blank line between them.
|
||||
- Multi-line explanatory comments inside function bodies use `//` lines:
|
||||
```c
|
||||
// Script modules are freed; orphaned JS wrapper objects now get GC'd
|
||||
// so their finalizers fire before assetDispose() checks ref counts.
|
||||
jerry_heap_gc(JERRY_GC_PRESSURE_HIGH);
|
||||
```
|
||||
- Do not use `/* */` for inline or inline-block comments inside `.c`
|
||||
function bodies.
|
||||
|
||||
### Comments in `.h` files
|
||||
Every public declaration gets a Javadoc block (`/** … */`) with
|
||||
`@param` and `@returns` where relevant. Keep it on the lines immediately
|
||||
above the declaration with no blank line in between.
|
||||
|
||||
---
|
||||
|
||||
## Color system
|
||||
|
||||
Colors are defined in `src/dusk/display/color.csv` and code-generated
|
||||
into a `color.h` header by `tools/color/csv/__main__.py`.
|
||||
|
||||
Each row in the CSV has `name,r,g,b,a` with channel values in `[0.0, 1.0]`.
|
||||
The script emits four `#define` variants per color plus a bare alias:
|
||||
|
||||
```
|
||||
COLOR_<NAME>_4B color4b(r8, g8, b8, a8) // default alias target
|
||||
COLOR_<NAME>_3B color3b(r8, g8, b8)
|
||||
COLOR_<NAME>_3F color3f(rf, gf, bf)
|
||||
COLOR_<NAME>_4F color4f(rf, gf, bf, af)
|
||||
COLOR_<NAME> COLOR_<NAME>_4B
|
||||
```
|
||||
|
||||
`color_t` is `color4b_t` (four `uint8_t` channels).
|
||||
|
||||
To add a new color, append a row to `color.csv` and rebuild — do not
|
||||
hand-edit the generated header.
|
||||
|
||||
---
|
||||
|
||||
## Tests
|
||||
- Tests live in `test/` mirroring `src/dusk/` structure.
|
||||
- Use cmocka; include `dusktest.h`.
|
||||
- Test functions: `static void test_something(void **state)`.
|
||||
- After each test, assert `memoryGetAllocatedCount() == 0` to catch
|
||||
leaks.
|
||||
- Build with `-DDUSK_BUILD_TESTS=ON`.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"pause": "DEFAULT",
|
||||
"items": [
|
||||
{ "type": "text", "text": "Test Two." },
|
||||
{ "type": "entityAdd", "entityType": "npc", "position": [4, 4, 0] },
|
||||
{
|
||||
"type": "textMini",
|
||||
"text": "Hello!",
|
||||
"position": [4, 4, 0],
|
||||
"duration": 3.0
|
||||
},
|
||||
{
|
||||
"type": "emoji",
|
||||
"entityIndex": "lastCreated",
|
||||
"emojiType": "exclamation",
|
||||
"duration": 2.0
|
||||
},
|
||||
{
|
||||
"type": "entityWalkTo",
|
||||
"entityIndex": "lastCreated",
|
||||
"positions": [[8, 2, 0]]
|
||||
},
|
||||
{ "type": "text", "text": "Done." }
|
||||
]
|
||||
}
|
||||
@@ -56,6 +56,10 @@ msgstr "Items"
|
||||
msgid "ui.game_menu.settings"
|
||||
msgstr "Settings"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save"
|
||||
msgstr "Save"
|
||||
|
||||
msgid "item.potion.name"
|
||||
msgstr "Potion"
|
||||
|
||||
|
||||
@@ -57,6 +57,10 @@ 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/rpg/item/item.json
|
||||
msgid "item.potion.name"
|
||||
msgstr "Poción"
|
||||
|
||||
@@ -57,6 +57,10 @@ msgstr "アイテム"
|
||||
msgid "ui.game_menu.settings"
|
||||
msgstr "設定"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save"
|
||||
msgstr "セーブ"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.potion.name"
|
||||
msgstr "ポーション"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0]], "meshes": [{"model": "models/chunks/chunk_-1_0_0_0.json", "offset": [0.0, 0.0, 0.0]}, {"model": "models/buildings/house_4_4.json", "offset": [2.0, 1.0, 0.0]}, {"model": "models/buildings/house_6_3.json", "offset": [1.0, 9.0, 0.0]}, {"model": "models/buildings/house_2_2.json", "offset": [12.0, 6.0, 0.0]}]}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0]], "meshes": [{"model": "models/chunks/chunk_0_1_0_0.json", "offset": [0.0, 0.0, 0.0]}, {"model": "models/buildings/house_8_4.json", "offset": [1.0, 1.0, 0.0]}, {"model": "models/buildings/house_3_3.json", "offset": [11.0, 2.0, 0.0]}, {"model": "models/buildings/house_2_2.json", "offset": [3.0, 11.0, 0.0]}]}
|
||||
@@ -1 +0,0 @@
|
||||
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0]], "meshes": [{"model": "models/chunks/chunk_1_0_0_0.json", "offset": [0.0, 0.0, 0.0]}, {"model": "models/buildings/house_3_2.json", "offset": [1.0, 1.0, 0.0]}, {"model": "models/buildings/house_2_3.json", "offset": [7.0, 2.0, 0.0]}, {"model": "models/buildings/house_4_2.json", "offset": [1.0, 8.0, 0.0]}, {"model": "models/buildings/house_1_1.json", "offset": [11.0, 10.0, 0.0]}, {"model": "models/buildings/house_5_2.json", "offset": [13.0, 6.0, 0.0]}]}
|
||||
@@ -1 +0,0 @@
|
||||
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0]], "meshes": [{"model": "models/chunks/chunk_1_1_0_0.json", "offset": [0.0, 0.0, 0.0]}, {"model": "models/buildings/house_5_5.json", "offset": [3.0, 2.0, 0.0]}, {"model": "models/buildings/house_2_3.json", "offset": [10.0, 3.0, 0.0]}, {"model": "models/buildings/house_3_1.json", "offset": [1.0, 12.0, 0.0]}]}
|
||||
@@ -1 +0,0 @@
|
||||
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [4, 1], [4, 1], [4, 1], [4, 1], [4, 1], [4, 1], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [4, 0], [4, 0], [4, 0], [4, 0], [4, 0], [4, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0]], "meshes": [{"model": "models/chunks/chunk_2_0_0_0.json", "offset": [0.0, 0.0, 0.0]}]}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"name": "Test Map",
|
||||
"entities": [
|
||||
{ "type": "player", "position": [10, 2, 0], "direction": "north" },
|
||||
{ "type": "item", "position": [12, 2, 0], "item": "POTION", "quantity": 1 },
|
||||
{
|
||||
"type": "npc",
|
||||
"position": [8, 8, 1],
|
||||
"path": [[4, 4, 0], [10, 10, 1], [4, 4, 0], [10, 10, 1]],
|
||||
"cutscene": "test_npc"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,17 +8,60 @@
|
||||
#include "assetchunkloader.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "util/endian.h"
|
||||
#include "asset/loader/assetloading.h"
|
||||
#include "asset/loader/assetentry.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "asset/loader/json/assetjsonloader.h"
|
||||
#include "asset/asset.h"
|
||||
#include "yyjson.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.");
|
||||
|
||||
if(loading->loading.chunk.state != ASSET_CHUNK_LOADING_STATE_READ_FILE) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
assertNull(loading->loading.chunk.data, "Data already defined?");
|
||||
|
||||
assetfile_t *file = &loading->loading.chunk.file;
|
||||
assetLoaderErrorChain(loading,
|
||||
assetFileInit(file, loading->entry->name, NULL, NULL)
|
||||
);
|
||||
|
||||
uint8_t *data = memoryAllocate(file->size);
|
||||
assetLoaderErrorChain(loading, assetFileOpen(file));
|
||||
assetLoaderErrorChain(loading, assetFileRead(file, data, file->size));
|
||||
assetLoaderErrorChain(loading, assetFileClose(file));
|
||||
assetLoaderErrorChain(loading, assetFileDispose(file));
|
||||
assertTrue(
|
||||
file->lastRead == file->size,
|
||||
"Failed to read entire chunk file."
|
||||
);
|
||||
|
||||
loading->loading.chunk.data = data;
|
||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_PARSE;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -31,141 +74,12 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
||||
|
||||
switch(loading->loading.chunk.state) {
|
||||
case ASSET_CHUNK_LOADING_STATE_INITIAL:
|
||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_LOAD_JSON;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_READ_FILE;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
|
||||
errorOk();
|
||||
|
||||
case ASSET_CHUNK_LOADING_STATE_LOAD_JSON: {
|
||||
// Lock the chunk's JSON as a sub-asset. The entry key is prefixed
|
||||
// with "json:" to avoid a type-collision with the chunk entry
|
||||
// itself (both share the same filename). The JSON loader reads
|
||||
// the real file path supplied in the input.
|
||||
char_t jsonKey[ASSET_FILE_NAME_MAX];
|
||||
stringFormat(
|
||||
jsonKey, sizeof(jsonKey), "json:%s", loading->entry->name
|
||||
);
|
||||
assetloaderinput_t jsonInput;
|
||||
memoryZero(&jsonInput, sizeof(jsonInput));
|
||||
stringCopy(
|
||||
jsonInput.json.path, loading->entry->name, ASSET_FILE_NAME_MAX
|
||||
);
|
||||
assetentry_t *jsonEntry = assetLock(
|
||||
jsonKey, ASSET_LOADER_TYPE_JSON, &jsonInput
|
||||
);
|
||||
errorret_t ret = assetRequireLoaded(jsonEntry);
|
||||
if(errorIsNotOk(ret)) {
|
||||
assetUnlockEntry(jsonEntry);
|
||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
errorChain(ret);
|
||||
}
|
||||
|
||||
yyjson_val *root = yyjson_doc_get_root(jsonEntry->data.json);
|
||||
|
||||
yyjson_val *tilesVal = yyjson_obj_get(root, "tiles");
|
||||
if(
|
||||
!tilesVal || !yyjson_is_arr(tilesVal) ||
|
||||
yyjson_arr_size(tilesVal) != CHUNK_TILE_COUNT
|
||||
) {
|
||||
assetUnlockEntry(jsonEntry);
|
||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
errorThrow(
|
||||
"Chunk JSON 'tiles' must have exactly %d entries",
|
||||
CHUNK_TILE_COUNT
|
||||
);
|
||||
}
|
||||
|
||||
out->tiles = memoryAllocate(CHUNK_TILE_COUNT * sizeof(tile_t));
|
||||
size_t tileIdx, tileMax;
|
||||
yyjson_val *tileVal;
|
||||
yyjson_arr_foreach(tilesVal, tileIdx, tileMax, tileVal) {
|
||||
yyjson_val *shapeVal = yyjson_arr_get(tileVal, 0);
|
||||
yyjson_val *zVal = yyjson_arr_get(tileVal, 1);
|
||||
if(
|
||||
!yyjson_is_arr(tileVal) || yyjson_arr_size(tileVal) != 2 ||
|
||||
!yyjson_is_int(shapeVal) || !yyjson_is_int(zVal)
|
||||
) {
|
||||
memoryFree(out->tiles);
|
||||
out->tiles = NULL;
|
||||
assetUnlockEntry(jsonEntry);
|
||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
errorThrow("Chunk JSON tile entries must be [shape, z] arrays");
|
||||
}
|
||||
out->tiles[tileIdx] = (tile_t){
|
||||
.shape = (tileshape_t)yyjson_get_int(shapeVal),
|
||||
.z = (uint8_t)yyjson_get_int(zVal)
|
||||
};
|
||||
}
|
||||
|
||||
yyjson_val *meshesVal = yyjson_obj_get(root, "meshes");
|
||||
out->meshCount = 0;
|
||||
if(meshesVal && yyjson_is_arr(meshesVal)) {
|
||||
size_t meshCount = yyjson_arr_size(meshesVal);
|
||||
if(meshCount > CHUNK_MESH_COUNT_MAX) {
|
||||
memoryFree(out->tiles);
|
||||
out->tiles = NULL;
|
||||
assetUnlockEntry(jsonEntry);
|
||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
errorThrow(
|
||||
"Chunk JSON 'meshes' exceeds CHUNK_MESH_COUNT_MAX (%d)",
|
||||
CHUNK_MESH_COUNT_MAX
|
||||
);
|
||||
}
|
||||
|
||||
size_t meshIdx, meshMax;
|
||||
yyjson_val *meshVal;
|
||||
yyjson_arr_foreach(meshesVal, meshIdx, meshMax, meshVal) {
|
||||
yyjson_val *modelVal = yyjson_obj_get(meshVal, "model");
|
||||
if(!modelVal || !yyjson_is_str(modelVal)) {
|
||||
memoryFree(out->tiles);
|
||||
out->tiles = NULL;
|
||||
assetUnlockEntry(jsonEntry);
|
||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
errorThrow("Chunk JSON mesh entry missing 'model' string");
|
||||
}
|
||||
const char_t *modelStr = yyjson_get_str(modelVal);
|
||||
size_t modelLen = yyjson_get_len(modelVal);
|
||||
if(modelLen >= CHUNK_MESH_NAME_MAX) {
|
||||
memoryFree(out->tiles);
|
||||
out->tiles = NULL;
|
||||
assetUnlockEntry(jsonEntry);
|
||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
errorThrow(
|
||||
"Chunk JSON model path '%s' exceeds max length", modelStr
|
||||
);
|
||||
}
|
||||
memoryCopy(out->modelNames[meshIdx], modelStr, modelLen + 1);
|
||||
|
||||
vec3 offset = { 0.0f, 0.0f, 0.0f };
|
||||
yyjson_val *offsetVal = yyjson_obj_get(meshVal, "offset");
|
||||
if(
|
||||
offsetVal && yyjson_is_arr(offsetVal) &&
|
||||
yyjson_arr_size(offsetVal) == 3
|
||||
) {
|
||||
size_t offIdx, offMax;
|
||||
yyjson_val *offElem;
|
||||
yyjson_arr_foreach(offsetVal, offIdx, offMax, offElem) {
|
||||
if(yyjson_is_num(offElem)) {
|
||||
offset[offIdx] = (float_t)yyjson_get_num(offElem);
|
||||
}
|
||||
}
|
||||
}
|
||||
glm_vec3_copy(offset, out->meshOffsets[meshIdx]);
|
||||
}
|
||||
out->meshCount = (uint8_t)meshCount;
|
||||
}
|
||||
|
||||
assetUnlockEntry(jsonEntry);
|
||||
|
||||
if(out->meshCount == 0) {
|
||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_LOAD_MODELS;
|
||||
loading->loading.chunk.modelIndex = 0;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
}
|
||||
case ASSET_CHUNK_LOADING_STATE_PARSE:
|
||||
break;
|
||||
|
||||
case ASSET_CHUNK_LOADING_STATE_LOAD_MODELS:
|
||||
while(loading->loading.chunk.modelIndex < out->meshCount) {
|
||||
@@ -197,6 +111,129 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
||||
default:
|
||||
errorOk();
|
||||
}
|
||||
|
||||
uint8_t *data = loading->loading.chunk.data;
|
||||
assertNotNull(data, "Chunk data should have been loaded by now.");
|
||||
|
||||
if(data[0] != 'D' || data[1] != 'C' || data[2] != 'F') {
|
||||
memoryFree(data);
|
||||
assetLoaderErrorThrow(loading, "Invalid chunk file header");
|
||||
}
|
||||
|
||||
uint32_t version = endianLittleToHost32(*(uint32_t *)(data + 4));
|
||||
if(version != ASSET_CHUNK_FILE_VERSION) {
|
||||
memoryFree(data);
|
||||
assetLoaderErrorThrow(
|
||||
loading, "Unsupported chunk file version %u", version
|
||||
);
|
||||
}
|
||||
|
||||
size_t offset = 8;
|
||||
|
||||
size_t tileSize = CHUNK_TILE_COUNT * sizeof(tile_t);
|
||||
out->tiles = memoryAllocate(tileSize);
|
||||
memoryCopy(out->tiles, data + offset, tileSize);
|
||||
offset += tileSize;
|
||||
|
||||
for(size_t t = 0; t < CHUNK_TILE_COUNT; t++) {
|
||||
uint32_t *shape = (uint32_t *)&out->tiles[t].shape;
|
||||
*shape = endianLittleToHost32(*shape);
|
||||
}
|
||||
|
||||
out->meshCount = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
assertTrue(
|
||||
out->meshCount <= CHUNK_MESH_COUNT_MAX,
|
||||
"Chunk mesh count exceeds maximum."
|
||||
);
|
||||
|
||||
for(uint8_t m = 0; m < out->meshCount; m++) {
|
||||
uint8_t nameLen = 0;
|
||||
while(
|
||||
data[offset + nameLen] != '\0' &&
|
||||
nameLen < CHUNK_MESH_NAME_MAX - 1
|
||||
) {
|
||||
nameLen++;
|
||||
}
|
||||
memoryCopy(out->modelNames[m], data + offset, nameLen);
|
||||
out->modelNames[m][nameLen] = '\0';
|
||||
offset += nameLen + 1;
|
||||
|
||||
memoryCopy(out->meshOffsets[m], data + offset, sizeof(vec3));
|
||||
offset += sizeof(vec3);
|
||||
out->meshOffsets[m][0] = endianLittleToHostFloat(out->meshOffsets[m][0]);
|
||||
out->meshOffsets[m][1] = endianLittleToHostFloat(out->meshOffsets[m][1]);
|
||||
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;
|
||||
|
||||
if(out->meshCount == 0) {
|
||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_LOAD_MODELS;
|
||||
loading->loading.chunk.modelIndex = 0;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetChunkDispose(assetentry_t *entry) {
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "asset/assetfile.h"
|
||||
#include "rpg/overworld/chunk.h"
|
||||
|
||||
#define ASSET_CHUNK_FILE_VERSION 5
|
||||
|
||||
typedef struct assetloading_s assetloading_t;
|
||||
typedef struct assetentry_s assetentry_t;
|
||||
|
||||
@@ -18,27 +20,58 @@ typedef struct {
|
||||
|
||||
typedef enum {
|
||||
ASSET_CHUNK_LOADING_STATE_INITIAL,
|
||||
ASSET_CHUNK_LOADING_STATE_LOAD_JSON,
|
||||
ASSET_CHUNK_LOADING_STATE_LOAD_MODELS
|
||||
ASSET_CHUNK_LOADING_STATE_READ_FILE,
|
||||
ASSET_CHUNK_LOADING_STATE_PARSE,
|
||||
ASSET_CHUNK_LOADING_STATE_LOAD_MODELS,
|
||||
ASSET_CHUNK_LOADING_STATE_DONE
|
||||
} assetchunkloadingstate_t;
|
||||
|
||||
typedef struct {
|
||||
assetfile_t file;
|
||||
assetchunkloadingstate_t state;
|
||||
uint8_t *data;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Asynchronous loader for chunk assets. No-op - the chunk's JSON file is
|
||||
* loaded via a JSON sub-asset in the sync phase (see assetChunkLoaderSync),
|
||||
* which handles its own async file I/O.
|
||||
* Asynchronous loader for chunk assets. Reads the raw DCF file bytes into
|
||||
* the loading buffer so the sync phase can parse without blocking the
|
||||
* main thread on I/O.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @return Error code indicating success or failure of the load operation.
|
||||
@@ -46,22 +79,9 @@ typedef struct {
|
||||
errorret_t assetChunkLoaderAsync(assetloading_t *loading);
|
||||
|
||||
/**
|
||||
* Synchronous loader for chunk assets. Locks and parses the chunk's JSON
|
||||
* file (tiles plus referenced model paths/offsets), then locks each
|
||||
* referenced model asset before marking the entry loaded.
|
||||
*
|
||||
* Expected JSON shape:
|
||||
* {
|
||||
* "tiles": [ [shape, z], ... ] // exactly CHUNK_TILE_COUNT entries,
|
||||
* // one per (x, y) column, x-major
|
||||
* "meshes": [
|
||||
* { "model": "models/chunks/chunk_0_0_0_0.json",
|
||||
* "offset": [0.0, 0.0, 0.0] }
|
||||
* ]
|
||||
* }
|
||||
* "meshes" is optional; each entry's "offset" defaults to [0, 0, 0] if
|
||||
* omitted. By convention mesh index 0 is the chunk's terrain and the
|
||||
* rest are props (see sceneOverworldDrawChunksBase/Props).
|
||||
* Synchronous loader for chunk assets. Validates the DCF binary previously
|
||||
* read by the async phase and populates the output assetchunkoutput_t with
|
||||
* tile data and model paths.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @return Error code indicating success or failure of the load operation.
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include "time/time.h"
|
||||
#include "input/input.h"
|
||||
#include "locale/localemanager.h"
|
||||
#include "rpg/item/item.h"
|
||||
#include "rpg/rpg.h"
|
||||
#include "display/display.h"
|
||||
#include "scene/scene.h"
|
||||
@@ -38,9 +37,8 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
||||
errorChain(systemInit());
|
||||
errorChain(inputInit());
|
||||
errorChain(assetInit());
|
||||
// errorChain(saveInit());
|
||||
errorChain(saveInit());
|
||||
errorChain(localeManagerInit());
|
||||
errorChain(itemInit());
|
||||
errorChain(displayInit());
|
||||
errorChain(uiInit());
|
||||
errorChain(rpgInit());
|
||||
@@ -64,6 +62,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
||||
errorret_t engineUpdate(void) {
|
||||
// Order here is important.
|
||||
errorChain(networkUpdate());
|
||||
errorChain(saveUpdate());
|
||||
timeUpdate();
|
||||
inputUpdate();
|
||||
consoleUpdate();
|
||||
@@ -90,7 +89,7 @@ errorret_t engineDispose(void) {
|
||||
errorChain(uiDispose());
|
||||
consoleDispose();
|
||||
errorChain(displayDispose());
|
||||
// errorChain(saveDispose());
|
||||
errorChain(saveDispose());
|
||||
errorChain(assetDispose());
|
||||
|
||||
errorOk();
|
||||
|
||||
@@ -17,7 +17,6 @@ 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;
|
||||
|
||||
@@ -12,15 +12,11 @@
|
||||
|
||||
#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;
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
cutscene.c
|
||||
cutscenesystem.c
|
||||
)
|
||||
|
||||
|
||||
@@ -1,444 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "cutscene.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "asset/asset.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "asset/loader/assetentry.h"
|
||||
#include "asset/loader/json/assetjsonloader.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "rpg/entity/entitytype.h"
|
||||
#include "rpg/entity/entitydir.h"
|
||||
#include "ui/rpg/uiemoji.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
static cutscenejsoncacheentry_t CUTSCENE_JSON_CACHE[CUTSCENE_JSON_CACHE_MAX];
|
||||
static uint32_t CUTSCENE_JSON_CACHE_COUNT;
|
||||
|
||||
errorret_t cutsceneJsonParsePause(yyjson_val *val, cutscenepause_t *outPause) {
|
||||
assertNotNull(outPause, "Output pause pointer cannot be NULL");
|
||||
if(!val || !yyjson_is_str(val)) {
|
||||
errorThrow("Cutscene pause value must be a string");
|
||||
}
|
||||
|
||||
const char_t *str = yyjson_get_str(val);
|
||||
if(stringEquals(str, "NONE")) {
|
||||
*outPause = CUTSCENE_PAUSE_NONE;
|
||||
} else if(stringEquals(str, "DEFAULT")) {
|
||||
*outPause = CUTSCENE_PAUSE_DEFAULT;
|
||||
} else if(stringEquals(str, "ALL")) {
|
||||
*outPause = CUTSCENE_PAUSE_ALL;
|
||||
} else {
|
||||
errorThrow("Unknown cutscene pause value '%s'", str);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t cutsceneJsonParseEntityIndex(yyjson_val *val, uint8_t *outIndex) {
|
||||
assertNotNull(outIndex, "Output entity index pointer cannot be NULL");
|
||||
|
||||
if(val && yyjson_is_str(val)) {
|
||||
const char_t *str = yyjson_get_str(val);
|
||||
if(stringEquals(str, "interact")) {
|
||||
*outIndex = CUTSCENE_ENTITY_INTERACT;
|
||||
} else if(stringEquals(str, "interacted")) {
|
||||
*outIndex = CUTSCENE_ENTITY_INTERACTED;
|
||||
} else if(stringEquals(str, "lastCreated")) {
|
||||
*outIndex = CUTSCENE_ENTITY_LAST_CREATED;
|
||||
} else {
|
||||
errorThrow("Unknown entity index sentinel '%s'", str);
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
if(!val || !yyjson_is_int(val)) {
|
||||
errorThrow("Entity index must be a number or sentinel string");
|
||||
}
|
||||
*outIndex = (uint8_t)yyjson_get_int(val);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t cutsceneJsonParseWorldPos(yyjson_val *val, worldpos_t *outPos) {
|
||||
assertNotNull(outPos, "Output position pointer cannot be NULL");
|
||||
if(!val || !yyjson_is_arr(val) || yyjson_arr_size(val) != 3) {
|
||||
errorThrow("Position must be a [x, y, z] array");
|
||||
}
|
||||
|
||||
worldunit_t comps[3];
|
||||
size_t idx, max;
|
||||
yyjson_val *elem;
|
||||
yyjson_arr_foreach(val, idx, max, elem) {
|
||||
if(!yyjson_is_num(elem)) {
|
||||
errorThrow("Position elements must be numbers");
|
||||
}
|
||||
comps[idx] = (worldunit_t)yyjson_get_num(elem);
|
||||
}
|
||||
|
||||
*outPos = (worldpos_t){ comps[0], comps[1], comps[2] };
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t cutsceneItemCreateFromJson(
|
||||
yyjson_val *obj,
|
||||
cutsceneitem_t *outItem,
|
||||
worldpos_t *waypoints,
|
||||
const uint8_t waypointsMax
|
||||
) {
|
||||
assertNotNull(obj, "Cutscene item JSON object cannot be NULL");
|
||||
assertNotNull(outItem, "Output item pointer cannot be NULL");
|
||||
|
||||
yyjson_val *typeVal = yyjson_obj_get(obj, "type");
|
||||
if(!typeVal || !yyjson_is_str(typeVal)) {
|
||||
errorThrow("Cutscene item JSON missing 'type' string");
|
||||
}
|
||||
const char_t *typeStr = yyjson_get_str(typeVal);
|
||||
|
||||
memoryZero(outItem, sizeof(cutsceneitem_t));
|
||||
|
||||
if(stringEquals(typeStr, "text")) {
|
||||
yyjson_val *textVal = yyjson_obj_get(obj, "text");
|
||||
if(!textVal || !yyjson_is_str(textVal)) {
|
||||
errorThrow("Cutscene 'text' item missing 'text' string");
|
||||
}
|
||||
|
||||
outItem->type = CUTSCENE_ITEM_TYPE_TEXT;
|
||||
stringCopy(
|
||||
outItem->text.text, yyjson_get_str(textVal), CUTSCENE_TEXT_MAX_CHARS
|
||||
);
|
||||
} else if(stringEquals(typeStr, "textMini")) {
|
||||
yyjson_val *textVal = yyjson_obj_get(obj, "text");
|
||||
if(!textVal || !yyjson_is_str(textVal)) {
|
||||
errorThrow("Cutscene 'textMini' item missing 'text' string");
|
||||
}
|
||||
|
||||
worldpos_t pos;
|
||||
errorChain(
|
||||
cutsceneJsonParseWorldPos(yyjson_obj_get(obj, "position"), &pos)
|
||||
);
|
||||
|
||||
yyjson_val *durVal = yyjson_obj_get(obj, "duration");
|
||||
if(!durVal || !yyjson_is_num(durVal)) {
|
||||
errorThrow("Cutscene 'textMini' item missing 'duration' number");
|
||||
}
|
||||
|
||||
outItem->type = CUTSCENE_ITEM_TYPE_TEXT_MINI;
|
||||
stringCopy(
|
||||
outItem->textMini.text, yyjson_get_str(textVal),
|
||||
CUTSCENE_TEXT_MINI_MAX_CHARS
|
||||
);
|
||||
outItem->textMini.position[0] = (float_t)pos.x;
|
||||
outItem->textMini.position[1] = (float_t)pos.y;
|
||||
outItem->textMini.position[2] = (float_t)pos.z;
|
||||
outItem->textMini.duration = (float_t)yyjson_get_num(durVal);
|
||||
} else if(stringEquals(typeStr, "textMiniHide")) {
|
||||
yyjson_val *indexVal = yyjson_obj_get(obj, "index");
|
||||
if(!indexVal || !yyjson_is_int(indexVal)) {
|
||||
errorThrow("Cutscene 'textMiniHide' item missing 'index' number");
|
||||
}
|
||||
|
||||
outItem->type = CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE;
|
||||
outItem->textMiniHide.index = (uint8_t)yyjson_get_int(indexVal);
|
||||
} else if(stringEquals(typeStr, "wait")) {
|
||||
yyjson_val *durVal = yyjson_obj_get(obj, "duration");
|
||||
if(!durVal || !yyjson_is_num(durVal)) {
|
||||
errorThrow("Cutscene 'wait' item missing 'duration' number");
|
||||
}
|
||||
|
||||
outItem->type = CUTSCENE_ITEM_TYPE_WAIT;
|
||||
outItem->wait = (float_t)yyjson_get_num(durVal);
|
||||
} else if(stringEquals(typeStr, "entityAdd")) {
|
||||
yyjson_val *entityTypeVal = yyjson_obj_get(obj, "entityType");
|
||||
if(!entityTypeVal || !yyjson_is_str(entityTypeVal)) {
|
||||
errorThrow("Cutscene 'entityAdd' item missing 'entityType' string");
|
||||
}
|
||||
const char_t *entityTypeStr = yyjson_get_str(entityTypeVal);
|
||||
|
||||
entitytype_t entityType;
|
||||
if(stringEquals(entityTypeStr, "player")) {
|
||||
entityType = ENTITY_TYPE_PLAYER;
|
||||
} else if(stringEquals(entityTypeStr, "npc")) {
|
||||
entityType = ENTITY_TYPE_NPC;
|
||||
} else if(stringEquals(entityTypeStr, "item")) {
|
||||
entityType = ENTITY_TYPE_ITEM;
|
||||
} else {
|
||||
errorThrow(
|
||||
"Cutscene 'entityAdd' has unknown entityType '%s'", entityTypeStr
|
||||
);
|
||||
}
|
||||
|
||||
worldpos_t pos;
|
||||
errorChain(
|
||||
cutsceneJsonParseWorldPos(yyjson_obj_get(obj, "position"), &pos)
|
||||
);
|
||||
|
||||
outItem->type = CUTSCENE_ITEM_TYPE_ENTITY_ADD;
|
||||
outItem->entityAdd.entityType = entityType;
|
||||
outItem->entityAdd.position = pos;
|
||||
} else if(stringEquals(typeStr, "entityRemove")) {
|
||||
uint8_t entityIndex;
|
||||
errorChain(cutsceneJsonParseEntityIndex(
|
||||
yyjson_obj_get(obj, "entityIndex"), &entityIndex
|
||||
));
|
||||
|
||||
outItem->type = CUTSCENE_ITEM_TYPE_ENTITY_REMOVE;
|
||||
outItem->entityRemove.entityIndex = entityIndex;
|
||||
} else if(stringEquals(typeStr, "entityTurn")) {
|
||||
uint8_t entityIndex;
|
||||
errorChain(cutsceneJsonParseEntityIndex(
|
||||
yyjson_obj_get(obj, "entityIndex"), &entityIndex
|
||||
));
|
||||
|
||||
yyjson_val *dirVal = yyjson_obj_get(obj, "direction");
|
||||
if(!dirVal || !yyjson_is_str(dirVal)) {
|
||||
errorThrow("Cutscene 'entityTurn' item missing 'direction' string");
|
||||
}
|
||||
const char_t *dirStr = yyjson_get_str(dirVal);
|
||||
|
||||
entitydir_t direction;
|
||||
if(stringEquals(dirStr, "north")) {
|
||||
direction = ENTITY_DIR_NORTH;
|
||||
} else if(stringEquals(dirStr, "east")) {
|
||||
direction = ENTITY_DIR_EAST;
|
||||
} else if(stringEquals(dirStr, "south")) {
|
||||
direction = ENTITY_DIR_SOUTH;
|
||||
} else if(stringEquals(dirStr, "west")) {
|
||||
direction = ENTITY_DIR_WEST;
|
||||
} else {
|
||||
errorThrow("Cutscene 'entityTurn' has unknown direction '%s'", dirStr);
|
||||
}
|
||||
|
||||
outItem->type = CUTSCENE_ITEM_TYPE_ENTITY_TURN;
|
||||
outItem->entityTurn.entityIndex = entityIndex;
|
||||
outItem->entityTurn.direction = direction;
|
||||
} else if(stringEquals(typeStr, "entityWalkTo")) {
|
||||
uint8_t entityIndex;
|
||||
errorChain(cutsceneJsonParseEntityIndex(
|
||||
yyjson_obj_get(obj, "entityIndex"), &entityIndex
|
||||
));
|
||||
|
||||
yyjson_val *positionsVal = yyjson_obj_get(obj, "positions");
|
||||
if(!positionsVal || !yyjson_is_arr(positionsVal)) {
|
||||
errorThrow("Cutscene 'entityWalkTo' item missing 'positions' array");
|
||||
}
|
||||
size_t count = yyjson_arr_size(positionsVal);
|
||||
if(count == 0 || count > waypointsMax) {
|
||||
errorThrow(
|
||||
"Cutscene 'entityWalkTo' 'positions' must have 1-%d entries",
|
||||
waypointsMax
|
||||
);
|
||||
}
|
||||
assertNotNull(waypoints, "Waypoint storage cannot be NULL");
|
||||
|
||||
size_t idx, max;
|
||||
yyjson_val *posVal;
|
||||
yyjson_arr_foreach(positionsVal, idx, max, posVal) {
|
||||
errorChain(cutsceneJsonParseWorldPos(posVal, &waypoints[idx]));
|
||||
}
|
||||
|
||||
bool_t walkAround = true;
|
||||
yyjson_val *walkAroundVal = yyjson_obj_get(obj, "walkAround");
|
||||
if(walkAroundVal && yyjson_is_bool(walkAroundVal)) {
|
||||
walkAround = yyjson_get_bool(walkAroundVal);
|
||||
}
|
||||
|
||||
outItem->type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO;
|
||||
outItem->entityWalkTo.entityIndex = entityIndex;
|
||||
outItem->entityWalkTo.positions = waypoints;
|
||||
outItem->entityWalkTo.count = (uint8_t)count;
|
||||
outItem->entityWalkTo.walkAround = walkAround;
|
||||
} else if(stringEquals(typeStr, "entityWalkToEntity")) {
|
||||
uint8_t entityIndex, targetEntityIndex;
|
||||
errorChain(cutsceneJsonParseEntityIndex(
|
||||
yyjson_obj_get(obj, "entityIndex"), &entityIndex
|
||||
));
|
||||
errorChain(cutsceneJsonParseEntityIndex(
|
||||
yyjson_obj_get(obj, "targetEntityIndex"), &targetEntityIndex
|
||||
));
|
||||
|
||||
yyjson_val *offsetXVal = yyjson_obj_get(obj, "offsetX");
|
||||
yyjson_val *offsetYVal = yyjson_obj_get(obj, "offsetY");
|
||||
if(
|
||||
!offsetXVal || !yyjson_is_num(offsetXVal) ||
|
||||
!offsetYVal || !yyjson_is_num(offsetYVal)
|
||||
) {
|
||||
errorThrow(
|
||||
"Cutscene 'entityWalkToEntity' item missing "
|
||||
"'offsetX'/'offsetY' numbers"
|
||||
);
|
||||
}
|
||||
|
||||
outItem->type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY;
|
||||
outItem->entityWalkToEntity.entityIndex = entityIndex;
|
||||
outItem->entityWalkToEntity.targetEntityIndex = targetEntityIndex;
|
||||
outItem->entityWalkToEntity.offsetX =
|
||||
(worldunit_t)yyjson_get_num(offsetXVal);
|
||||
outItem->entityWalkToEntity.offsetY =
|
||||
(worldunit_t)yyjson_get_num(offsetYVal);
|
||||
} else if(stringEquals(typeStr, "entityTeleport")) {
|
||||
uint8_t entityIndex;
|
||||
errorChain(cutsceneJsonParseEntityIndex(
|
||||
yyjson_obj_get(obj, "entityIndex"), &entityIndex
|
||||
));
|
||||
|
||||
worldpos_t pos;
|
||||
errorChain(
|
||||
cutsceneJsonParseWorldPos(yyjson_obj_get(obj, "position"), &pos)
|
||||
);
|
||||
|
||||
outItem->type = CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT;
|
||||
outItem->entityTeleport.entityIndex = entityIndex;
|
||||
outItem->entityTeleport.target = pos;
|
||||
} else if(stringEquals(typeStr, "emoji")) {
|
||||
uint8_t entityIndex;
|
||||
errorChain(cutsceneJsonParseEntityIndex(
|
||||
yyjson_obj_get(obj, "entityIndex"), &entityIndex
|
||||
));
|
||||
|
||||
yyjson_val *emojiTypeVal = yyjson_obj_get(obj, "emojiType");
|
||||
if(!emojiTypeVal || !yyjson_is_str(emojiTypeVal)) {
|
||||
errorThrow("Cutscene 'emoji' item missing 'emojiType' string");
|
||||
}
|
||||
const char_t *emojiTypeStr = yyjson_get_str(emojiTypeVal);
|
||||
|
||||
uiemojitype_t emojiType;
|
||||
if(stringEquals(emojiTypeStr, "question")) {
|
||||
emojiType = UI_EMOJI_QUESTION_MARK;
|
||||
} else if(stringEquals(emojiTypeStr, "exclamation")) {
|
||||
emojiType = UI_EMOJI_EXCLAMATION_MARK;
|
||||
} else {
|
||||
errorThrow(
|
||||
"Cutscene 'emoji' has unknown emojiType '%s'", emojiTypeStr
|
||||
);
|
||||
}
|
||||
|
||||
yyjson_val *durVal = yyjson_obj_get(obj, "duration");
|
||||
if(!durVal || !yyjson_is_num(durVal)) {
|
||||
errorThrow("Cutscene 'emoji' item missing 'duration' number");
|
||||
}
|
||||
|
||||
outItem->type = CUTSCENE_ITEM_TYPE_EMOJI;
|
||||
outItem->emoji.entityIndex = entityIndex;
|
||||
outItem->emoji.emojiType = emojiType;
|
||||
outItem->emoji.duration = (float_t)yyjson_get_num(durVal);
|
||||
} else if(stringEquals(typeStr, "shake")) {
|
||||
yyjson_val *amountVal = yyjson_obj_get(obj, "amount");
|
||||
yyjson_val *durVal = yyjson_obj_get(obj, "duration");
|
||||
if(
|
||||
!amountVal || !yyjson_is_int(amountVal) ||
|
||||
!durVal || !yyjson_is_num(durVal)
|
||||
) {
|
||||
errorThrow("Cutscene 'shake' item missing 'amount'/'duration' numbers");
|
||||
}
|
||||
|
||||
outItem->type = CUTSCENE_ITEM_TYPE_SHAKE;
|
||||
outItem->shake.amount = (uint8_t)yyjson_get_int(amountVal);
|
||||
outItem->shake.duration = (float_t)yyjson_get_num(durVal);
|
||||
} else if(stringEquals(typeStr, "setPause")) {
|
||||
cutscenepause_t pause;
|
||||
errorChain(
|
||||
cutsceneJsonParsePause(yyjson_obj_get(obj, "pause"), &pause)
|
||||
);
|
||||
|
||||
outItem->type = CUTSCENE_ITEM_TYPE_SET_PAUSE;
|
||||
outItem->setPause = pause;
|
||||
} else {
|
||||
errorThrow("Cutscene item JSON has unknown 'type': %s", typeStr);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t cutsceneGetByName(
|
||||
const char_t *name, const cutscene_t **outCutscene
|
||||
) {
|
||||
assertNotNull(name, "Cutscene name cannot be NULL");
|
||||
assertNotNull(outCutscene, "Output cutscene pointer cannot be NULL");
|
||||
assertStrLenMax(name, CUTSCENE_JSON_NAME_MAX, "Cutscene name too long");
|
||||
|
||||
for(uint32_t i = 0; i < CUTSCENE_JSON_CACHE_COUNT; i++) {
|
||||
if(!stringEquals(CUTSCENE_JSON_CACHE[i].name, name)) continue;
|
||||
*outCutscene = &CUTSCENE_JSON_CACHE[i].cutscene;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
if(CUTSCENE_JSON_CACHE_COUNT >= CUTSCENE_JSON_CACHE_MAX) {
|
||||
errorThrow(
|
||||
"Too many cutscenes loaded: exceeds CUTSCENE_JSON_CACHE_MAX (%d)",
|
||||
CUTSCENE_JSON_CACHE_MAX
|
||||
);
|
||||
}
|
||||
|
||||
char_t path[CUTSCENE_JSON_NAME_MAX + 24];
|
||||
stringFormat(path, sizeof(path), "cutscene/%s.json", name);
|
||||
|
||||
assetentry_t *entry = assetLock(path, ASSET_LOADER_TYPE_JSON, NULL);
|
||||
errorret_t ret = assetRequireLoaded(entry);
|
||||
if(errorIsNotOk(ret)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorChain(ret);
|
||||
}
|
||||
|
||||
yyjson_val *root = yyjson_doc_get_root(entry->data.json);
|
||||
|
||||
cutscenepause_t pause = CUTSCENE_PAUSE_DEFAULT;
|
||||
yyjson_val *pauseVal = yyjson_obj_get(root, "pause");
|
||||
if(pauseVal) {
|
||||
errorret_t pauseRet = cutsceneJsonParsePause(pauseVal, &pause);
|
||||
if(errorIsNotOk(pauseRet)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorChain(pauseRet);
|
||||
}
|
||||
}
|
||||
|
||||
yyjson_val *itemsVal = yyjson_obj_get(root, "items");
|
||||
if(!itemsVal || !yyjson_is_arr(itemsVal)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Cutscene '%s' JSON missing 'items' array", name);
|
||||
}
|
||||
|
||||
size_t itemCount = yyjson_arr_size(itemsVal);
|
||||
if(itemCount == 0 || itemCount > CUTSCENE_JSON_ITEM_COUNT_MAX) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow(
|
||||
"Cutscene '%s' 'items' must have 1-%d entries",
|
||||
name, CUTSCENE_JSON_ITEM_COUNT_MAX
|
||||
);
|
||||
}
|
||||
|
||||
cutscenejsoncacheentry_t *cache =
|
||||
&CUTSCENE_JSON_CACHE[CUTSCENE_JSON_CACHE_COUNT];
|
||||
memoryZero(cache, sizeof(cutscenejsoncacheentry_t));
|
||||
stringCopy(cache->name, name, CUTSCENE_JSON_NAME_MAX);
|
||||
|
||||
size_t idx, max;
|
||||
yyjson_val *itemVal;
|
||||
yyjson_arr_foreach(itemsVal, idx, max, itemVal) {
|
||||
errorret_t itemRet = cutsceneItemCreateFromJson(
|
||||
itemVal, &cache->items[idx], cache->waypoints[idx],
|
||||
CUTSCENE_JSON_WAYPOINT_COUNT_MAX
|
||||
);
|
||||
if(errorIsNotOk(itemRet)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorChain(itemRet);
|
||||
}
|
||||
}
|
||||
|
||||
assetUnlockEntry(entry);
|
||||
|
||||
cache->cutscene.items = cache->items;
|
||||
cache->cutscene.itemCount = (uint8_t)itemCount;
|
||||
cache->cutscene.pause = pause;
|
||||
cache->cutscene.dataSize = 0;
|
||||
|
||||
CUTSCENE_JSON_CACHE_COUNT++;
|
||||
*outCutscene = &cache->cutscene;
|
||||
errorOk();
|
||||
}
|
||||
@@ -9,8 +9,6 @@
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/cutscene/cutscenepause.h"
|
||||
|
||||
typedef struct yyjson_val yyjson_val;
|
||||
|
||||
typedef struct cutscene_s {
|
||||
const cutsceneitem_t *items;
|
||||
uint8_t itemCount;
|
||||
@@ -167,10 +165,10 @@ typedef struct cutscene_s {
|
||||
#define CUTSCENE_SET_PAUSE(FLAGS) \
|
||||
{ .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = (FLAGS) }
|
||||
|
||||
#define CUTSCENE_ITEM_GIVE(ITEM_NAME, QUANTITY) \
|
||||
#define CUTSCENE_ITEM_GIVE(ITEM_ID, QUANTITY) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_ITEM_GIVE, \
|
||||
.itemGive = { .itemName = ITEM_NAME, .quantity = QUANTITY } \
|
||||
.itemGive = { .item = ITEM_ID, .quantity = QUANTITY } \
|
||||
}
|
||||
|
||||
// Runs all listed items simultaneously and waits until all are done.
|
||||
@@ -232,118 +230,3 @@ typedef struct cutscene_s {
|
||||
), \
|
||||
CUTSCENE_MAP_AREA_WAIT(CUTSCENE_AREA_LAST_CREATED), \
|
||||
CUTSCENE_MAP_AREA_REMOVE(CUTSCENE_AREA_LAST_CREATED)
|
||||
|
||||
#define CUTSCENE_JSON_NAME_MAX 32
|
||||
#define CUTSCENE_JSON_ITEM_COUNT_MAX 32
|
||||
#define CUTSCENE_JSON_WAYPOINT_COUNT_MAX 8
|
||||
#define CUTSCENE_JSON_CACHE_MAX 16
|
||||
|
||||
// One slot of the cutsceneGetByName cache: a parsed cutscene plus the
|
||||
// backing storage its items point into (items themselves, and the
|
||||
// waypoint lists any "entityWalkTo" items reference).
|
||||
typedef struct {
|
||||
char_t name[CUTSCENE_JSON_NAME_MAX];
|
||||
cutscene_t cutscene;
|
||||
cutsceneitem_t items[CUTSCENE_JSON_ITEM_COUNT_MAX];
|
||||
worldpos_t waypoints
|
||||
[CUTSCENE_JSON_ITEM_COUNT_MAX][CUTSCENE_JSON_WAYPOINT_COUNT_MAX];
|
||||
} cutscenejsoncacheentry_t;
|
||||
|
||||
/**
|
||||
* Parses a cutscene pause value ("NONE", "DEFAULT" or "ALL") from a
|
||||
* yyjson string value.
|
||||
*
|
||||
* @param val The yyjson value to parse.
|
||||
* @param outPause Output pointer, set to the parsed pause flags.
|
||||
* @return Any error that occurs (missing/invalid/unknown value).
|
||||
*/
|
||||
errorret_t cutsceneJsonParsePause(yyjson_val *val, cutscenepause_t *outPause);
|
||||
|
||||
/**
|
||||
* Parses an entityIndex-shaped yyjson value - a raw number, or one of
|
||||
* "interact", "interacted", "lastCreated" - into a raw uint8_t index or
|
||||
* the matching CUTSCENE_ENTITY_* sentinel (see cutscenesystem.h).
|
||||
*
|
||||
* @param val The yyjson value to parse.
|
||||
* @param outIndex Output pointer, set to the parsed index.
|
||||
* @return Any error that occurs (missing/invalid/unknown value).
|
||||
*/
|
||||
errorret_t cutsceneJsonParseEntityIndex(yyjson_val *val, uint8_t *outIndex);
|
||||
|
||||
/**
|
||||
* Parses a [x, y, z] yyjson array into a worldpos_t.
|
||||
*
|
||||
* @param val The yyjson value to parse.
|
||||
* @param outPos Output pointer, set to the parsed position.
|
||||
* @return Any error that occurs (missing/invalid value).
|
||||
*/
|
||||
errorret_t cutsceneJsonParseWorldPos(yyjson_val *val, worldpos_t *outPos);
|
||||
|
||||
/**
|
||||
* Parses a single cutscene item from a yyjson object into outItem.
|
||||
* "type" selects the item shape (required):
|
||||
* { "type": "text", "text": "Hello!" }
|
||||
* { "type": "textMini", "text": "Hi", "position": [x, y, z],
|
||||
* "duration": 3.0 }
|
||||
* { "type": "textMiniHide", "index": 0 }
|
||||
* { "type": "wait", "duration": 1.5 }
|
||||
* { "type": "entityAdd", "entityType": "npc", "position": [x, y, z] }
|
||||
* { "type": "entityRemove", "entityIndex": 0 }
|
||||
* { "type": "entityTurn", "entityIndex": 0, "direction": "south" }
|
||||
* { "type": "entityWalkTo", "entityIndex": 0,
|
||||
* "positions": [[x, y, z], ...], "walkAround": true }
|
||||
* { "type": "entityWalkToEntity", "entityIndex": 0,
|
||||
* "targetEntityIndex": 1, "offsetX": 1, "offsetY": 0 }
|
||||
* { "type": "entityTeleport", "entityIndex": 0, "position": [x, y, z] }
|
||||
* { "type": "emoji", "entityIndex": 0, "emojiType": "exclamation",
|
||||
* "duration": 2.0 }
|
||||
* { "type": "shake", "amount": 2, "duration": 0.5 }
|
||||
* { "type": "setPause", "pause": "ALL" }
|
||||
* "entityIndex"/"targetEntityIndex" accept a raw number, or one of
|
||||
* "interact", "interacted", "lastCreated" for the matching
|
||||
* CUTSCENE_ENTITY_* sentinel. "entityType" uses the same strings as
|
||||
* entityCreateFromJson ("player", "npc", "item"); "direction" is one of
|
||||
* "north"/"east"/"south"/"west"; "emojiType" is "question" or
|
||||
* "exclamation"; "pause" is "NONE", "DEFAULT" or "ALL".
|
||||
*
|
||||
* Not supported (would need persistent string/array storage or function
|
||||
* pointers this parser doesn't provide): itemGive, concurrent, fade,
|
||||
* map area items, callbacks, nested cutscene references.
|
||||
*
|
||||
* @param obj The yyjson object describing a single cutscene item.
|
||||
* @param outItem Output pointer, filled with the parsed item.
|
||||
* @param waypoints Backing storage for an "entityWalkTo" item's waypoint
|
||||
* list; must remain valid for as long as the parsed item is used.
|
||||
* Unused (may be NULL) for any other item type.
|
||||
* @param waypointsMax Capacity of waypoints.
|
||||
* @return Any error that occurs (missing/invalid/unknown fields).
|
||||
*/
|
||||
errorret_t cutsceneItemCreateFromJson(
|
||||
yyjson_val *obj,
|
||||
cutsceneitem_t *outItem,
|
||||
worldpos_t *waypoints,
|
||||
const uint8_t waypointsMax
|
||||
);
|
||||
|
||||
/**
|
||||
* Loads (or returns the cached result of an earlier load of) the named
|
||||
* cutscene from assets/cutscene/<name>.json, parsing each entry of its
|
||||
* "items" array with cutsceneItemCreateFromJson. Parsed once per name
|
||||
* and cached for the lifetime of the process; holds at most
|
||||
* CUTSCENE_JSON_CACHE_MAX distinct names, each with at most
|
||||
* CUTSCENE_JSON_ITEM_COUNT_MAX items.
|
||||
*
|
||||
* File shape:
|
||||
* { "pause": "DEFAULT", "items": [ { "type": "text", ... }, ... ] }
|
||||
* "pause" is optional (one of "NONE", "DEFAULT", "ALL"; defaults to
|
||||
* "DEFAULT" when absent).
|
||||
*
|
||||
* @param name The cutscene's file name (without extension), under
|
||||
* assets/cutscene/.
|
||||
* @param outCutscene Output pointer, set to the loaded cutscene.
|
||||
* @return Any error that occurs (missing/malformed file, cache full,
|
||||
* unsupported/invalid item).
|
||||
*/
|
||||
errorret_t cutsceneGetByName(
|
||||
const char_t *name, const cutscene_t **outCutscene
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "rpg/entity/entitypathstep.h"
|
||||
#include "rpg/overworld/chunk.h"
|
||||
#include "rpg/overworld/map.h"
|
||||
|
||||
void cutsceneEntityWalkToEntityStart(
|
||||
const cutsceneitem_t *item,
|
||||
@@ -35,7 +35,7 @@ bool_t cutsceneEntityWalkToEntityUpdate(
|
||||
};
|
||||
|
||||
worldunit_t z;
|
||||
if(chunkGetWalkableZNear(dest.x, dest.y, target->position.z, &z)) dest.z = z;
|
||||
if(mapGetWalkableZNear(dest.x, dest.y, target->position.z, &z)) dest.z = z;
|
||||
|
||||
return entityPathStep(entity, dest, true);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/item/item.h"
|
||||
#include "rpg/item/itemgive.h"
|
||||
#include "ui/rpg/textbox/uitextboxmain.h"
|
||||
|
||||
@@ -14,8 +13,7 @@ void cutsceneItemGiveStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
itemid_t itemId = itemGetIdByName(item->itemGive.itemName);
|
||||
itemGive(itemId, item->itemGive.quantity);
|
||||
itemGive(item->itemGive.item, item->itemGive.quantity);
|
||||
}
|
||||
|
||||
bool_t cutsceneItemGiveUpdate(
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "rpg/item/item.h"
|
||||
|
||||
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||
|
||||
typedef struct {
|
||||
const char_t *itemName;
|
||||
itemid_t item;
|
||||
uint8_t quantity;
|
||||
} cutsceneitemgive_t;
|
||||
|
||||
|
||||
@@ -12,3 +12,20 @@
|
||||
CUTSCENE(TEST_ONE, 0, DEFAULT,
|
||||
CUTSCENE_TEXT("Test One."),
|
||||
);
|
||||
|
||||
CUTSCENE(TEST_TWO, 0, DEFAULT,
|
||||
CUTSCENE_TEXT("Test Two."),
|
||||
CUTSCENE_ENTITY_ADD(ENTITY_TYPE_NPC, 4, 4, 0),
|
||||
CUTSCENE_TEXT_MINI("Hello!", 4, 4, 0, 3.0f),
|
||||
CUTSCENE_EMOJI(
|
||||
CUTSCENE_ENTITY_LAST_CREATED, UI_EMOJI_EXCLAMATION_MARK, 2.0f
|
||||
),
|
||||
CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_LAST_CREATED, 8, 2, 0),
|
||||
// CUTSCENE_CONCURRENT(
|
||||
// CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_INTERACT, 4, 4, 0),
|
||||
// CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_INTERACTED, 8, 2, 0),
|
||||
// ),
|
||||
// CUTSCENE_ITEM_GIVE(ITEM_ID_POTATO, 3),
|
||||
// CUTSCENE_ENTITY_REMOVE(CUTSCENE_ENTITY_INTERACT),
|
||||
CUTSCENE_TEXT("Done."),
|
||||
);
|
||||
@@ -16,3 +16,4 @@ add_subdirectory(anim)
|
||||
add_subdirectory(interact)
|
||||
add_subdirectory(npc)
|
||||
add_subdirectory(item)
|
||||
add_subdirectory(global)
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "rpg/overworld/chunk.h"
|
||||
#include "rpg/overworld/map.h"
|
||||
#include "rpg/overworld/tile.h"
|
||||
#include "time/time.h"
|
||||
#include "entityanimwalk.h"
|
||||
@@ -19,7 +19,7 @@ const entityanimcallback_t ENTITY_ANIM_CALLBACKS[ENTITY_ANIM_COUNT] = {
|
||||
};
|
||||
|
||||
float_t entityAnimTileZOffset(const worldpos_t pos) {
|
||||
return tileShapeIsRamp(chunkGetTile(pos).shape) ? 0.5f : 0.0f;
|
||||
return tileShapeIsRamp(mapGetTile(pos).shape) ? 0.5f : 0.0f;
|
||||
}
|
||||
|
||||
void entityAnimUpdate(entity_t *entity) {
|
||||
|
||||
+21
-164
@@ -8,14 +8,13 @@
|
||||
#include "entity.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.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"
|
||||
#include "rpg/overworld/tile.h"
|
||||
#include "rpg/cutscene/cutscene.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
entity_t ENTITIES[ENTITY_COUNT];
|
||||
|
||||
@@ -90,8 +89,8 @@ void entityWalk(entity_t *entity, const entitydir_t direction) {
|
||||
}
|
||||
|
||||
// Get tile under foot
|
||||
tile_t tileCurrent = chunkGetTile(entity->position);
|
||||
tile_t tileNew = chunkGetTile(newPos);
|
||||
tile_t tileCurrent = mapGetTile(entity->position);
|
||||
tile_t tileNew = mapGetTile(newPos);
|
||||
bool_t fall = false;
|
||||
bool_t raise = false;
|
||||
|
||||
@@ -140,7 +139,7 @@ void entityWalk(entity_t *entity, const entitydir_t direction) {
|
||||
tileNew = TILE_NULL;
|
||||
worldpos_t abovePos = newPos;
|
||||
abovePos.z += 1;
|
||||
tile_t tileAbove = chunkGetTile(abovePos);
|
||||
tile_t tileAbove = mapGetTile(abovePos);
|
||||
|
||||
if(
|
||||
tileAbove.shape != TILE_SHAPE_NULL &&
|
||||
@@ -154,7 +153,7 @@ void entityWalk(entity_t *entity, const entitydir_t direction) {
|
||||
// Falling down?
|
||||
worldpos_t belowPos = newPos;
|
||||
belowPos.z -= 1;
|
||||
tile_t tileBelow = chunkGetTile(belowPos);
|
||||
tile_t tileBelow = mapGetTile(belowPos);
|
||||
if(
|
||||
tileBelow.shape != TILE_SHAPE_NULL &&
|
||||
tileShapeIsRamp(tileBelow.shape) &&
|
||||
@@ -284,7 +283,7 @@ void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
|
||||
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||
|
||||
if(entity->chunkIndex != 0xFF) {
|
||||
chunk_t *old = chunkGet(entity->chunkIndex);
|
||||
chunk_t *old = mapGetChunk(entity->chunkIndex);
|
||||
if(old != NULL) {
|
||||
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
|
||||
if(old->entities[i] != entity->id) continue;
|
||||
@@ -294,16 +293,27 @@ 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 = chunkGet(chunkIndex);
|
||||
chunk_t *next = mapGetChunk(chunkIndex);
|
||||
if(next != NULL) {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,159 +323,6 @@ void entityUpdateChunk(entity_t *entity) {
|
||||
|
||||
chunkpos_t cp;
|
||||
worldPosToChunkPos(&entity->position, &cp);
|
||||
chunkindex_t ci = chunkGetIndexAt(cp);
|
||||
chunkindex_t ci = mapGetChunkIndexAt(cp);
|
||||
if(ci != -1) entitySetChunk(entity, (uint8_t)ci);
|
||||
}
|
||||
|
||||
errorret_t entityCreateFromJson(yyjson_val *obj, entity_t **outEntity) {
|
||||
assertNotNull(obj, "Entity JSON object cannot be NULL");
|
||||
assertNotNull(outEntity, "Output entity pointer cannot be NULL");
|
||||
|
||||
yyjson_val *typeVal = yyjson_obj_get(obj, "type");
|
||||
if(!typeVal || !yyjson_is_str(typeVal)) {
|
||||
errorThrow("Entity JSON missing 'type' string");
|
||||
}
|
||||
const char_t *typeStr = yyjson_get_str(typeVal);
|
||||
|
||||
entitytype_t type;
|
||||
if(stringEquals(typeStr, "player")) {
|
||||
type = ENTITY_TYPE_PLAYER;
|
||||
} else if(stringEquals(typeStr, "npc")) {
|
||||
type = ENTITY_TYPE_NPC;
|
||||
} else if(stringEquals(typeStr, "item")) {
|
||||
type = ENTITY_TYPE_ITEM;
|
||||
} else {
|
||||
errorThrow("Entity JSON has unknown 'type': %s", typeStr);
|
||||
}
|
||||
|
||||
yyjson_val *posVal = yyjson_obj_get(obj, "position");
|
||||
if(!posVal || !yyjson_is_arr(posVal) || yyjson_arr_size(posVal) != 3) {
|
||||
errorThrow("Entity JSON missing 'position' [x, y, z] array");
|
||||
}
|
||||
worldunit_t pos[3];
|
||||
size_t posIdx, posLen;
|
||||
yyjson_val *posElem;
|
||||
yyjson_arr_foreach(posVal, posIdx, posLen, posElem) {
|
||||
if(!yyjson_is_num(posElem)) {
|
||||
errorThrow("Entity JSON 'position' elements must be numbers");
|
||||
}
|
||||
pos[posIdx] = (worldunit_t)yyjson_get_num(posElem);
|
||||
}
|
||||
|
||||
entitydir_t direction = ENTITY_DIR_SOUTH;
|
||||
yyjson_val *dirVal = yyjson_obj_get(obj, "direction");
|
||||
if(dirVal && yyjson_is_str(dirVal)) {
|
||||
const char_t *dirStr = yyjson_get_str(dirVal);
|
||||
if(stringEquals(dirStr, "north")) {
|
||||
direction = ENTITY_DIR_NORTH;
|
||||
} else if(stringEquals(dirStr, "east")) {
|
||||
direction = ENTITY_DIR_EAST;
|
||||
} else if(stringEquals(dirStr, "south")) {
|
||||
direction = ENTITY_DIR_SOUTH;
|
||||
} else if(stringEquals(dirStr, "west")) {
|
||||
direction = ENTITY_DIR_WEST;
|
||||
} else {
|
||||
errorThrow("Entity JSON has unknown 'direction': %s", dirStr);
|
||||
}
|
||||
}
|
||||
|
||||
// Item entities require a valid item reference, resolved up front so a
|
||||
// bad reference fails before an entity slot is ever claimed.
|
||||
itemid_t itemId = ITEM_ID_NULL;
|
||||
uint8_t itemQuantity = 1;
|
||||
if(type == ENTITY_TYPE_ITEM) {
|
||||
yyjson_val *itemVal = yyjson_obj_get(obj, "item");
|
||||
if(!itemVal || !yyjson_is_str(itemVal)) {
|
||||
errorThrow("Entity JSON with type 'item' missing 'item' string");
|
||||
}
|
||||
const char_t *itemStr = yyjson_get_str(itemVal);
|
||||
itemId = itemGetIdByName(itemStr);
|
||||
if(itemId == ITEM_ID_NULL) {
|
||||
errorThrow("Entity JSON references unknown item '%s'", itemStr);
|
||||
}
|
||||
|
||||
yyjson_val *quantityVal = yyjson_obj_get(obj, "quantity");
|
||||
if(quantityVal && yyjson_is_int(quantityVal)) {
|
||||
itemQuantity = (uint8_t)yyjson_get_int(quantityVal);
|
||||
}
|
||||
}
|
||||
|
||||
// Only one player may exist at a time - it always holds the reserved
|
||||
// ENTITY_GLOBAL_ID_PLAYER global ID, so that's how callers (e.g. the
|
||||
// camera) find it regardless of how it was spawned.
|
||||
if(
|
||||
type == ENTITY_TYPE_PLAYER &&
|
||||
entityGetByGlobalId(ENTITY_GLOBAL_ID_PLAYER) != NULL
|
||||
) {
|
||||
errorThrow("A player entity has already been spawned");
|
||||
}
|
||||
|
||||
// NPC path waypoints, resolved up front for the same reason as above.
|
||||
worldpos_t path[NPC_PATH_COUNT_MAX];
|
||||
uint8_t pathCount = 0;
|
||||
if(type == ENTITY_TYPE_NPC) {
|
||||
yyjson_val *pathVal = yyjson_obj_get(obj, "path");
|
||||
if(pathVal) {
|
||||
if(!yyjson_is_arr(pathVal)) {
|
||||
errorThrow("Entity JSON 'path' must be an array");
|
||||
}
|
||||
size_t count = yyjson_arr_size(pathVal);
|
||||
if(count == 0 || count > NPC_PATH_COUNT_MAX) {
|
||||
errorThrow(
|
||||
"Entity JSON 'path' must have 1-%d waypoints", NPC_PATH_COUNT_MAX
|
||||
);
|
||||
}
|
||||
|
||||
size_t pathIdx, pathLen;
|
||||
yyjson_val *pathElem;
|
||||
yyjson_arr_foreach(pathVal, pathIdx, pathLen, pathElem) {
|
||||
if(!yyjson_is_arr(pathElem) || yyjson_arr_size(pathElem) != 3) {
|
||||
errorThrow("Entity JSON 'path' entries must be [x, y, z] arrays");
|
||||
}
|
||||
worldunit_t comps[3];
|
||||
size_t compIdx, compLen;
|
||||
yyjson_val *compElem;
|
||||
yyjson_arr_foreach(pathElem, compIdx, compLen, compElem) {
|
||||
if(!yyjson_is_num(compElem)) {
|
||||
errorThrow("Entity JSON 'path' elements must be numbers");
|
||||
}
|
||||
comps[compIdx] = (worldunit_t)yyjson_get_num(compElem);
|
||||
}
|
||||
path[pathIdx] = (worldpos_t){ comps[0], comps[1], comps[2] };
|
||||
}
|
||||
pathCount = (uint8_t)count;
|
||||
}
|
||||
}
|
||||
|
||||
// Interact cutscene, resolved up front for the same reason as above.
|
||||
const cutscene_t *cutscene = NULL;
|
||||
yyjson_val *cutsceneVal = yyjson_obj_get(obj, "cutscene");
|
||||
if(cutsceneVal) {
|
||||
if(!yyjson_is_str(cutsceneVal)) {
|
||||
errorThrow("Entity JSON 'cutscene' must be a string");
|
||||
}
|
||||
errorChain(cutsceneGetByName(yyjson_get_str(cutsceneVal), &cutscene));
|
||||
}
|
||||
|
||||
uint8_t index = entityGetAvailable();
|
||||
if(index == 0xFF) errorThrow("No available entity slots");
|
||||
|
||||
entity_t *entity = &ENTITIES[index];
|
||||
entityInit(entity, type);
|
||||
entity->direction = direction;
|
||||
entityPositionSet(entity, (worldpos_t){ pos[0], pos[1], pos[2] });
|
||||
if(type == ENTITY_TYPE_ITEM) entityItemSet(entity, itemId, itemQuantity);
|
||||
if(pathCount > 0) {
|
||||
npcSetMoveType(entity, NPC_MOVE_TYPE_PATH);
|
||||
for(uint8_t i = 0; i < pathCount; i++) {
|
||||
npcPathAddNode(&entity->data.npc, path[i]);
|
||||
}
|
||||
}
|
||||
if(cutscene != NULL) {
|
||||
entity->interact.type = ENTITY_INTERACT_CUTSCENE;
|
||||
entity->interact.data.cutscene = cutscene;
|
||||
}
|
||||
|
||||
*outEntity = entity;
|
||||
errorOk();
|
||||
}
|
||||
@@ -13,7 +13,6 @@
|
||||
#include "npc/npc.h"
|
||||
|
||||
typedef struct map_s map_t;
|
||||
typedef struct yyjson_val yyjson_val;
|
||||
|
||||
typedef uint16_t entityglobalid_t;
|
||||
|
||||
@@ -143,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.
|
||||
@@ -166,41 +168,3 @@ void entityUpdateChunk(entity_t *entity);
|
||||
* @param pos The world position to place the entity at.
|
||||
*/
|
||||
void entityPositionSet(entity_t *entity, const worldpos_t pos);
|
||||
|
||||
/**
|
||||
* Parses an entity descriptor from a yyjson object and spawns it as a new
|
||||
* entity in an available slot. Expected shape:
|
||||
* { "type": "npc", "position": [x, y, z], "direction": "south" }
|
||||
* "type" must be one of "player", "npc", "item". "direction" is optional
|
||||
* (one of "north", "east", "south", "west") and defaults to
|
||||
* ENTITY_DIR_SOUTH when absent.
|
||||
*
|
||||
* When "type" is "item", two extra fields apply:
|
||||
* { "type": "item", "position": [x, y, z], "item": "POTION",
|
||||
* "quantity": 1 }
|
||||
* "item" (required) is the item's string ID, resolved via
|
||||
* itemGetIdByName. "quantity" (optional) defaults to 1.
|
||||
*
|
||||
* When "type" is "player", the spawned entity is assigned the reserved
|
||||
* ENTITY_GLOBAL_ID_PLAYER global ID (so entityGetByGlobalId can find it
|
||||
* regardless of how it was spawned), and it is an error to spawn a
|
||||
* second one while one is already loaded.
|
||||
*
|
||||
* When "type" is "npc", an optional "path" field sets it up with
|
||||
* NPC_MOVE_TYPE_PATH movement:
|
||||
* { "type": "npc", "position": [x, y, z],
|
||||
* "path": [[4, 4, 0], [10, 10, 1]] }
|
||||
* "path" must have 1-NPC_PATH_COUNT_MAX waypoints.
|
||||
*
|
||||
* Any entity type may set an optional "cutscene" field to wire up an
|
||||
* interact component that starts the named cutscene (resolved via
|
||||
* cutsceneGetByName) when interacted with:
|
||||
* { "type": "npc", "position": [x, y, z], "cutscene": "test_npc" }
|
||||
*
|
||||
* @param obj The yyjson object describing the entity.
|
||||
* @param outEntity Output pointer, set to the newly spawned entity on
|
||||
* success.
|
||||
* @return Any error that occurs (missing/invalid fields, unknown item,
|
||||
* duplicate player, unknown/malformed cutscene, no free slots).
|
||||
*/
|
||||
errorret_t entityCreateFromJson(yyjson_val *obj, entity_t **outEntity);
|
||||
@@ -0,0 +1,10 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
globalitemstore.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 "entityglobaldefs.h"
|
||||
#include "entitygloballist.h"
|
||||
|
||||
#define ENTITY_GLOBAL_LIST_COUNT ( \
|
||||
sizeof(ENTITY_GLOBAL_LIST) / \
|
||||
sizeof(ENTITY_GLOBAL_LIST[0]) \
|
||||
)
|
||||
|
||||
//EOF
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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 "rpg/overworld/worldpos.h"
|
||||
#include "rpg/entity/entity.h"
|
||||
|
||||
typedef struct entity_s entity_t;
|
||||
|
||||
typedef struct {
|
||||
entity_t *entity;
|
||||
worldpos_t position;
|
||||
} entityglobalcreate_t;
|
||||
|
||||
/**
|
||||
* Callback invoked to initialize a global entity.
|
||||
*
|
||||
* @param create Pointer to the entity/position being initialized.
|
||||
* @returns An error code.
|
||||
*/
|
||||
typedef void (*entityglobalinitcallback_t)(
|
||||
entityglobalcreate_t *create
|
||||
);
|
||||
|
||||
typedef struct {
|
||||
entitytype_t type;
|
||||
entityglobalinitcallback_t callback;
|
||||
} entityglobaldef_t;
|
||||
|
||||
#define ENTITY_GLOBAL(id, entType, callbackFn) \
|
||||
[id] = { .type = entType, .callback = callbackFn }
|
||||
|
||||
#define ENTITY_GLOBAL_CALLBACK(id) \
|
||||
static void ENTTIYT_GLOBAL_CALLBACK_##id(entityglobalcreate_t *create)
|
||||
|
||||
#define ENTITY_GLOBAL_REF(id) \
|
||||
ENTTIYT_GLOBAL_CALLBACK_##id
|
||||
|
||||
//EOF
|
||||
@@ -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 "entityglobaldefs.h"
|
||||
#include "rpg/cutscene/scene/testcutscene.h"
|
||||
|
||||
ENTITY_GLOBAL_CALLBACK(3) {
|
||||
create->entity->data.npc.moveType = NPC_MOVE_TYPE_PATH;
|
||||
npcPathAddNode(&create->entity->data.npc, (worldpos_t){ 4, 4, 0 });
|
||||
npcPathAddNode(&create->entity->data.npc, (worldpos_t){ 10, 10, 1 });
|
||||
npcPathAddNode(&create->entity->data.npc, (worldpos_t){ 4, 4, 0 });
|
||||
npcPathAddNode(&create->entity->data.npc, (worldpos_t){ 10, 10, 1 });
|
||||
|
||||
create->entity->interact.type = ENTITY_INTERACT_CUTSCENE;
|
||||
create->entity->interact.data.cutscene = CUTSCENE_REFERENCE(TEST_TWO);
|
||||
}
|
||||
|
||||
static const entityglobaldef_t ENTITY_GLOBAL_LIST[] = {
|
||||
ENTITY_GLOBAL(ENTITY_GLOBAL_ID_NULL, ENTITY_TYPE_NULL, NULL),
|
||||
ENTITY_GLOBAL(ENTITY_GLOBAL_ID_PLAYER, ENTITY_TYPE_PLAYER, NULL),
|
||||
|
||||
ENTITY_GLOBAL(3, ENTITY_TYPE_NPC, ENTITY_GLOBAL_REF(3)),
|
||||
};
|
||||
|
||||
//EOF
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "globalitemstore.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
bool_t globalItemStoreIsCollected(
|
||||
const savefile_t *file, const entityglobalid_t id
|
||||
) {
|
||||
assertNotNull(file, "Save file cannot be NULL");
|
||||
assertTrue(id < SAVE_GLOBAL_ITEM_COUNT_MAX, "Global item ID out of range");
|
||||
return file->globalItemCollected[id];
|
||||
}
|
||||
|
||||
void globalItemStoreSetCollected(
|
||||
savefile_t *file, const entityglobalid_t id, const bool_t collected
|
||||
) {
|
||||
assertNotNull(file, "Save file cannot be NULL");
|
||||
assertTrue(id < SAVE_GLOBAL_ITEM_COUNT_MAX, "Global item ID out of range");
|
||||
file->globalItemCollected[id] = collected;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "save/savefile.h"
|
||||
#include "rpg/entity/entity.h"
|
||||
|
||||
/**
|
||||
* Checks whether the global entity with the given ID has already been
|
||||
* marked collected in the given save file's data - e.g. so a global item
|
||||
* entity's init callback (see rpg/entity/global/entitygloballist.h) can
|
||||
* skip spawning itself if the player already picked it up in a prior
|
||||
* session, without needing to keep the entity itself alive to remember
|
||||
* that (which would need render/collision special-casing - this doesn't).
|
||||
*
|
||||
* @param file The save file to check.
|
||||
* @param id The global entity ID to check.
|
||||
* @return True if already marked collected.
|
||||
*/
|
||||
bool_t globalItemStoreIsCollected(
|
||||
const savefile_t *file, const entityglobalid_t id
|
||||
);
|
||||
|
||||
/**
|
||||
* Marks the global entity with the given ID as collected (or not) in the
|
||||
* given save file's data. Does not itself write the save to disk - call
|
||||
* saveWrite() separately once ready to persist it.
|
||||
*
|
||||
* @param file The save file to write into.
|
||||
* @param id The global entity ID to mark.
|
||||
* @param collected The new collected state.
|
||||
*/
|
||||
void globalItemStoreSetCollected(
|
||||
savefile_t *file, const entityglobalid_t id, const bool_t collected
|
||||
);
|
||||
@@ -11,3 +11,12 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
backpack.c
|
||||
itemgive.c
|
||||
)
|
||||
|
||||
# Item Definitions
|
||||
dusk_run_python(
|
||||
dusk_item_json_defs
|
||||
tools.item
|
||||
--json ${CMAKE_CURRENT_SOURCE_DIR}/item.json
|
||||
--output ${DUSK_GENERATED_HEADERS_DIR}/rpg/item/itemdef.h
|
||||
)
|
||||
add_dependencies(${DUSK_LIBRARY_TARGET_NAME} dusk_item_json_defs)
|
||||
@@ -11,69 +11,69 @@
|
||||
backpack_t BACKPACK;
|
||||
|
||||
void backpackInit() {
|
||||
for(uint32_t i = 0; i < ITEM_TYPE_COUNT_MAX; i++) {
|
||||
for(uint8_t i = 0; i < ITEM_TYPE_COUNT; i++) {
|
||||
inventoryInit(
|
||||
&BACKPACK.inventories[i],
|
||||
BACKPACK.storage[i],
|
||||
INVENTORY_CAPACITY_MAX
|
||||
ITEM_TYPE_COUNT_MAX
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
inventory_t *backpackGetInventory(const itemtypeid_t type) {
|
||||
inventory_t *backpackGetInventory(const itemtype_t type) {
|
||||
assertTrue(type > ITEM_TYPE_NULL, "Item type must not be null");
|
||||
assertTrue(type <= ITEM_TYPE_COUNT, "Item type out of range");
|
||||
assertTrue(type < ITEM_TYPE_COUNT, "Item type out of range");
|
||||
return &BACKPACK.inventories[type];
|
||||
}
|
||||
|
||||
void backpackAdd(const itemid_t item, const uint8_t quantity) {
|
||||
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
||||
assertTrue(item <= ITEM_COUNT, "Item ID out of range");
|
||||
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range");
|
||||
inventoryAdd(backpackGetInventory(ITEMS[item].type), item, quantity);
|
||||
}
|
||||
|
||||
void backpackRemove(const itemid_t item) {
|
||||
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
||||
assertTrue(item <= ITEM_COUNT, "Item ID out of range");
|
||||
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range");
|
||||
inventoryRemove(backpackGetInventory(ITEMS[item].type), item);
|
||||
}
|
||||
|
||||
void backpackSet(const itemid_t item, const uint8_t quantity) {
|
||||
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
||||
assertTrue(item <= ITEM_COUNT, "Item ID out of range");
|
||||
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range");
|
||||
inventorySet(backpackGetInventory(ITEMS[item].type), item, quantity);
|
||||
}
|
||||
|
||||
uint8_t backpackGetCount(const itemid_t item) {
|
||||
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
||||
assertTrue(item <= ITEM_COUNT, "Item ID out of range");
|
||||
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range");
|
||||
return inventoryGetCount(backpackGetInventory(ITEMS[item].type), item);
|
||||
}
|
||||
|
||||
bool_t backpackItemExists(const itemid_t item) {
|
||||
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
||||
assertTrue(item <= ITEM_COUNT, "Item ID out of range");
|
||||
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range");
|
||||
return inventoryItemExists(backpackGetInventory(ITEMS[item].type), item);
|
||||
}
|
||||
|
||||
bool_t backpackIsFull(const itemtypeid_t type) {
|
||||
bool_t backpackIsFull(const itemtype_t type) {
|
||||
assertTrue(type > ITEM_TYPE_NULL, "Item type must not be null");
|
||||
assertTrue(type <= ITEM_TYPE_COUNT, "Item type out of range");
|
||||
assertTrue(type < ITEM_TYPE_COUNT, "Item type out of range");
|
||||
return inventoryIsFull(backpackGetInventory(type));
|
||||
}
|
||||
|
||||
bool_t backpackItemFull(const itemid_t item) {
|
||||
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
||||
assertTrue(item <= ITEM_COUNT, "Item ID out of range");
|
||||
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range");
|
||||
return inventoryItemFull(backpackGetInventory(ITEMS[item].type), item);
|
||||
}
|
||||
|
||||
void backpackSort(
|
||||
const itemtypeid_t type,
|
||||
const itemtype_t type,
|
||||
const inventorysort_t sortBy,
|
||||
const bool_t reverse
|
||||
) {
|
||||
assertTrue(type > ITEM_TYPE_NULL, "Item type must not be null");
|
||||
assertTrue(type <= ITEM_TYPE_COUNT, "Item type out of range");
|
||||
assertTrue(type < ITEM_TYPE_COUNT, "Item type out of range");
|
||||
inventorySort(backpackGetInventory(type), sortBy, reverse);
|
||||
}
|
||||
@@ -9,8 +9,8 @@
|
||||
#include "inventory.h"
|
||||
|
||||
typedef struct {
|
||||
inventorystack_t storage[ITEM_TYPE_COUNT_MAX][INVENTORY_CAPACITY_MAX];
|
||||
inventory_t inventories[ITEM_TYPE_COUNT_MAX];
|
||||
inventorystack_t storage[ITEM_TYPE_COUNT][ITEM_TYPE_COUNT_MAX];
|
||||
inventory_t inventories[ITEM_TYPE_COUNT];
|
||||
} backpack_t;
|
||||
|
||||
extern backpack_t BACKPACK;
|
||||
@@ -26,7 +26,7 @@ void backpackInit();
|
||||
* @param type The item type.
|
||||
* @returns Pointer to the inventory for that type.
|
||||
*/
|
||||
inventory_t *backpackGetInventory(const itemtypeid_t type);
|
||||
inventory_t *backpackGetInventory(const itemtype_t type);
|
||||
|
||||
/**
|
||||
* Adds a quantity of an item to the backpack.
|
||||
@@ -73,7 +73,7 @@ bool_t backpackItemExists(const itemid_t item);
|
||||
* @param type The item type to check.
|
||||
* @returns true if the type's inventory is full.
|
||||
*/
|
||||
bool_t backpackIsFull(const itemtypeid_t type);
|
||||
bool_t backpackIsFull(const itemtype_t type);
|
||||
|
||||
/**
|
||||
* Checks if an item's stack is full in the backpack.
|
||||
@@ -91,7 +91,7 @@ bool_t backpackItemFull(const itemid_t item);
|
||||
* @param reverse Whether to sort in reverse order.
|
||||
*/
|
||||
void backpackSort(
|
||||
const itemtypeid_t type,
|
||||
const itemtype_t type,
|
||||
const inventorysort_t sortBy,
|
||||
const bool_t reverse
|
||||
);
|
||||
@@ -188,8 +188,8 @@ int_t inventorySortByIdReverse(const void *a, const void *b) {
|
||||
int_t inventorySortByType(const void *a, const void *b) {
|
||||
const inventorystack_t *stackA = (const inventorystack_t*)a;
|
||||
const inventorystack_t *stackB = (const inventorystack_t*)b;
|
||||
const itemtypeid_t typeA = ITEMS[stackA->item].type;
|
||||
const itemtypeid_t typeB = ITEMS[stackB->item].type;
|
||||
const itemtype_t typeA = ITEMS[stackA->item].type;
|
||||
const itemtype_t typeB = ITEMS[stackB->item].type;
|
||||
if(typeA < typeB) return -1;
|
||||
if(typeA > typeB) return 1;
|
||||
return 0;
|
||||
@@ -198,8 +198,8 @@ int_t inventorySortByType(const void *a, const void *b) {
|
||||
int_t inventorySortByTypeReverse(const void *a, const void *b) {
|
||||
const inventorystack_t *stackA = (const inventorystack_t*)a;
|
||||
const inventorystack_t *stackB = (const inventorystack_t*)b;
|
||||
const itemtypeid_t typeA = ITEMS[stackA->item].type;
|
||||
const itemtypeid_t typeB = ITEMS[stackB->item].type;
|
||||
const itemtype_t typeA = ITEMS[stackA->item].type;
|
||||
const itemtype_t typeB = ITEMS[stackB->item].type;
|
||||
if(typeA < typeB) return 1;
|
||||
if(typeA > typeB) return -1;
|
||||
return 0;
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "rpg/item/item.h"
|
||||
|
||||
#define ITEM_STACK_QUANTITY_MAX 99
|
||||
#define INVENTORY_CAPACITY_MAX 250
|
||||
|
||||
typedef enum {
|
||||
INVENTORY_SORT_BY_ID,
|
||||
|
||||
+1
-132
@@ -7,125 +7,8 @@
|
||||
|
||||
#include "item.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "asset/asset.h"
|
||||
#include "asset/loader/assetentry.h"
|
||||
#include "asset/loader/json/assetjsonloader.h"
|
||||
#include "locale/localemanager.h"
|
||||
#include "asset/loader/locale/assetlocaleloader.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
#define ITEM_JSON_PATH "item.json"
|
||||
|
||||
itemdef_t ITEMS[ITEM_COUNT_MAX];
|
||||
uint32_t ITEM_COUNT;
|
||||
itemtype_t ITEM_TYPES[ITEM_TYPE_COUNT_MAX];
|
||||
uint32_t ITEM_TYPE_COUNT;
|
||||
|
||||
errorret_t itemInit(void) {
|
||||
memoryZero(ITEMS, sizeof(ITEMS));
|
||||
memoryZero(ITEM_TYPES, sizeof(ITEM_TYPES));
|
||||
ITEM_COUNT = 0;
|
||||
ITEM_TYPE_COUNT = 0;
|
||||
|
||||
assetentry_t *jsonEntry = assetLock(
|
||||
ITEM_JSON_PATH, ASSET_LOADER_TYPE_JSON, NULL
|
||||
);
|
||||
errorret_t ret = assetRequireLoaded(jsonEntry);
|
||||
if(errorIsNotOk(ret)) {
|
||||
assetUnlockEntry(jsonEntry);
|
||||
errorChain(ret);
|
||||
}
|
||||
|
||||
yyjson_val *root = yyjson_doc_get_root(jsonEntry->data.json);
|
||||
if(!yyjson_is_arr(root)) {
|
||||
assetUnlockEntry(jsonEntry);
|
||||
errorThrow("item.json root must be an array");
|
||||
}
|
||||
|
||||
size_t idx, max;
|
||||
yyjson_val *entry;
|
||||
yyjson_arr_foreach(root, idx, max, entry) {
|
||||
if(ITEM_COUNT >= ITEM_COUNT_MAX - 1) {
|
||||
assetUnlockEntry(jsonEntry);
|
||||
errorThrow(
|
||||
"Too many items defined: exceeds ITEM_COUNT_MAX (%d)",
|
||||
ITEM_COUNT_MAX
|
||||
);
|
||||
}
|
||||
|
||||
yyjson_val *idVal = yyjson_obj_get(entry, "id");
|
||||
yyjson_val *typeVal = yyjson_obj_get(entry, "type");
|
||||
yyjson_val *nameVal = yyjson_obj_get(entry, "name");
|
||||
yyjson_val *weightVal = yyjson_obj_get(entry, "weight");
|
||||
|
||||
if(!idVal || !yyjson_is_str(idVal)) {
|
||||
assetUnlockEntry(jsonEntry);
|
||||
errorThrow("Item entry %zu missing 'id' string", idx);
|
||||
}
|
||||
if(!typeVal || !yyjson_is_str(typeVal)) {
|
||||
assetUnlockEntry(jsonEntry);
|
||||
errorThrow("Item entry %zu missing 'type' string", idx);
|
||||
}
|
||||
if(!nameVal || !yyjson_is_str(nameVal)) {
|
||||
assetUnlockEntry(jsonEntry);
|
||||
errorThrow("Item entry %zu missing 'name' string", idx);
|
||||
}
|
||||
|
||||
const char_t *idStr = yyjson_get_str(idVal);
|
||||
size_t idLen = yyjson_get_len(idVal);
|
||||
const char_t *typeStr = yyjson_get_str(typeVal);
|
||||
size_t typeLen = yyjson_get_len(typeVal);
|
||||
const char_t *nameStr = yyjson_get_str(nameVal);
|
||||
size_t nameLen = yyjson_get_len(nameVal);
|
||||
|
||||
if(idLen >= ITEM_STRING_MAX) {
|
||||
assetUnlockEntry(jsonEntry);
|
||||
errorThrow("Item id '%s' exceeds max length", idStr);
|
||||
}
|
||||
if(nameLen + 10 >= ITEM_STRING_MAX) {
|
||||
assetUnlockEntry(jsonEntry);
|
||||
errorThrow("Item name '%s' exceeds max length", nameStr);
|
||||
}
|
||||
if(typeLen >= ITEM_STRING_MAX) {
|
||||
assetUnlockEntry(jsonEntry);
|
||||
errorThrow("Item type '%s' exceeds max length", typeStr);
|
||||
}
|
||||
|
||||
itemtypeid_t typeId = itemResolveType(typeStr, typeLen);
|
||||
if(typeId == ITEM_TYPE_NULL) {
|
||||
assetUnlockEntry(jsonEntry);
|
||||
errorThrow(
|
||||
"Too many item types defined: exceeds ITEM_TYPE_COUNT_MAX (%d)",
|
||||
ITEM_TYPE_COUNT_MAX
|
||||
);
|
||||
}
|
||||
|
||||
ITEM_COUNT++;
|
||||
itemid_t id = (itemid_t)ITEM_COUNT;
|
||||
itemdef_t *def = &ITEMS[id];
|
||||
def->id = id;
|
||||
def->type = typeId;
|
||||
def->weight = (weightVal && yyjson_is_num(weightVal)) ?
|
||||
(float_t)yyjson_get_num(weightVal) : 0.0f;
|
||||
memoryCopy(def->idName, idStr, idLen + 1);
|
||||
stringFormat(def->name, ITEM_STRING_MAX - 1, "item.%s.name", nameStr);
|
||||
}
|
||||
|
||||
assetUnlockEntry(jsonEntry);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
itemid_t itemGetIdByName(const char_t *name) {
|
||||
assertNotNull(name, "Item name cannot be NULL");
|
||||
|
||||
for(uint32_t i = 1; i <= ITEM_COUNT; i++) {
|
||||
if(stringEquals(ITEMS[i].idName, name)) return (itemid_t)i;
|
||||
}
|
||||
|
||||
return ITEM_ID_NULL;
|
||||
}
|
||||
|
||||
errorret_t itemGetName(
|
||||
const itemid_t item,
|
||||
@@ -133,7 +16,7 @@ errorret_t itemGetName(
|
||||
const size_t bufferSize
|
||||
) {
|
||||
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
||||
assertTrue(item <= ITEM_COUNT, "Item ID out of range");
|
||||
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range");
|
||||
|
||||
errorChain(assetLocaleGetString(
|
||||
&LOCALE.entry->data.locale,
|
||||
@@ -145,17 +28,3 @@ errorret_t itemGetName(
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
itemtypeid_t itemResolveType(const char_t *name, const size_t nameLen) {
|
||||
for(uint32_t i = 1; i <= ITEM_TYPE_COUNT; i++) {
|
||||
if(stringEquals(ITEM_TYPES[i].name, name)) return (itemtypeid_t)i;
|
||||
}
|
||||
|
||||
if(ITEM_TYPE_COUNT >= ITEM_TYPE_COUNT_MAX - 1) return ITEM_TYPE_NULL;
|
||||
|
||||
ITEM_TYPE_COUNT++;
|
||||
itemtypeid_t typeId = (itemtypeid_t)ITEM_TYPE_COUNT;
|
||||
ITEM_TYPES[typeId].id = typeId;
|
||||
memoryCopy(ITEM_TYPES[typeId].name, name, nameLen + 1);
|
||||
return typeId;
|
||||
}
|
||||
|
||||
@@ -7,62 +7,7 @@
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
|
||||
#define ITEM_COUNT_MAX 256
|
||||
#define ITEM_TYPE_COUNT_MAX 16
|
||||
#define ITEM_STRING_MAX 48
|
||||
|
||||
typedef uint16_t itemid_t;
|
||||
typedef uint8_t itemtypeid_t;
|
||||
|
||||
#define ITEM_ID_NULL ((itemid_t)0)
|
||||
#define ITEM_TYPE_NULL ((itemtypeid_t)0)
|
||||
|
||||
typedef struct {
|
||||
itemid_t id;
|
||||
itemtypeid_t type;
|
||||
float_t weight;
|
||||
char_t idName[ITEM_STRING_MAX];
|
||||
char_t name[ITEM_STRING_MAX];
|
||||
} itemdef_t;
|
||||
|
||||
typedef struct {
|
||||
itemtypeid_t id;
|
||||
char_t name[ITEM_STRING_MAX];
|
||||
} itemtype_t;
|
||||
|
||||
extern itemdef_t ITEMS[ITEM_COUNT_MAX];
|
||||
extern uint32_t ITEM_COUNT;
|
||||
extern itemtype_t ITEM_TYPES[ITEM_TYPE_COUNT_MAX];
|
||||
extern uint32_t ITEM_TYPE_COUNT;
|
||||
|
||||
/**
|
||||
* Loads assets/item.json and parses it into the ITEMS/ITEM_TYPES tables.
|
||||
* Must be called once, before any other item/backpack function.
|
||||
*
|
||||
* @return Any error that occurs (missing/malformed JSON, or too many
|
||||
* items/types defined for ITEM_COUNT_MAX/ITEM_TYPE_COUNT_MAX).
|
||||
*/
|
||||
errorret_t itemInit(void);
|
||||
|
||||
/**
|
||||
* Looks up an item's numeric ID from its JSON "id" string.
|
||||
*
|
||||
* @param name The item's string ID (e.g. "POTION"), case-sensitive.
|
||||
* @return The matching item ID, or ITEM_ID_NULL if not found.
|
||||
*/
|
||||
itemid_t itemGetIdByName(const char_t *name);
|
||||
|
||||
/**
|
||||
* Resolves a type name string to its numeric type ID, registering it as
|
||||
* a new type in ITEM_TYPES if not already known.
|
||||
*
|
||||
* @param name The type's string name (e.g. "MEDICINE").
|
||||
* @param nameLen Length of name, excluding the null terminator.
|
||||
* @return The resolved type ID, or ITEM_TYPE_NULL if ITEM_TYPE_COUNT_MAX
|
||||
* would be exceeded.
|
||||
*/
|
||||
itemtypeid_t itemResolveType(const char_t *name, const size_t nameLen);
|
||||
#include "rpg/item/itemdef.h"
|
||||
|
||||
/**
|
||||
* Gets the localized display name for an item.
|
||||
|
||||
@@ -14,3 +14,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
tileshape.c
|
||||
)
|
||||
|
||||
add_subdirectory(global)
|
||||
|
||||
|
||||
@@ -6,24 +6,6 @@
|
||||
*/
|
||||
|
||||
#include "chunk.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "asset/asset.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "asset/loader/assetentry.h"
|
||||
#include "console/console.h"
|
||||
#include "event/event.h"
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "rpg/overworld/map.h"
|
||||
|
||||
chunk_t CHUNKS[MAP_CHUNK_COUNT];
|
||||
chunk_t *CHUNK_ORDER[MAP_CHUNK_COUNT];
|
||||
|
||||
static chunkpos_t CHUNK_POSITION;
|
||||
static chunk_t *CHUNK_LOAD_QUEUE[MAP_CHUNK_COUNT];
|
||||
static uint32_t CHUNK_LOAD_QUEUE_COUNT;
|
||||
static chunk_t *CHUNK_LOADING;
|
||||
|
||||
uint32_t chunkGetTileIndex(const chunkpos_t position) {
|
||||
return (position.y * CHUNK_WIDTH) + position.x;
|
||||
@@ -32,366 +14,3 @@ uint32_t chunkGetTileIndex(const chunkpos_t position) {
|
||||
bool_t chunkPositionIsEqual(const chunkpos_t a, const chunkpos_t b) {
|
||||
return (a.x == b.x) && (a.y == b.y) && (a.z == b.z);
|
||||
}
|
||||
|
||||
errorret_t chunksLoadGrid(void) {
|
||||
CHUNK_POSITION = (chunkpos_t){ 0, 0, 0 };
|
||||
|
||||
chunkindex_t i = 0;
|
||||
for(chunkunit_t z = 0; z < MAP_CHUNK_DEPTH; z++) {
|
||||
for(chunkunit_t y = 0; y < MAP_CHUNK_HEIGHT; y++) {
|
||||
for(chunkunit_t x = 0; x < MAP_CHUNK_WIDTH; x++) {
|
||||
chunk_t *chunk = &CHUNKS[i++];
|
||||
chunk->position = (chunkpos_t){
|
||||
(chunkunit_t)x, (chunkunit_t)y, (chunkunit_t)z
|
||||
};
|
||||
errorChain(chunkLoad(chunk));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chunkRebuildOrder();
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void chunksUnloadAll(void) {
|
||||
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||
chunkUnload(&CHUNKS[i]);
|
||||
}
|
||||
}
|
||||
|
||||
errorret_t chunkPositionSet(const chunkpos_t newPos) {
|
||||
if(!mapIsLoaded()) errorThrow("No map loaded");
|
||||
if(chunkPositionIsEqual(newPos, CHUNK_POSITION)) errorOk();
|
||||
|
||||
// Separate loaded chunks into "keep" and "free" buckets.
|
||||
chunkindex_t chunksFreed[MAP_CHUNK_COUNT];
|
||||
uint32_t freedCount = 0;
|
||||
|
||||
// Use a boolean grid so the inner load loop can check O(1).
|
||||
bool_t posLoaded[MAP_CHUNK_WIDTH][MAP_CHUNK_HEIGHT][MAP_CHUNK_DEPTH];
|
||||
memoryZero(posLoaded, sizeof(posLoaded));
|
||||
|
||||
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||
chunk_t *chunk = &CHUNKS[i];
|
||||
chunkunit_t rx = chunk->position.x - newPos.x;
|
||||
chunkunit_t ry = chunk->position.y - newPos.y;
|
||||
chunkunit_t rz = chunk->position.z - newPos.z;
|
||||
if(
|
||||
rx >= 0 && rx < MAP_CHUNK_WIDTH &&
|
||||
ry >= 0 && ry < MAP_CHUNK_HEIGHT &&
|
||||
rz >= 0 && rz < MAP_CHUNK_DEPTH
|
||||
) {
|
||||
posLoaded[rx][ry][rz] = true;
|
||||
} else {
|
||||
chunkUnload(chunk);
|
||||
chunksFreed[freedCount++] = i;
|
||||
}
|
||||
}
|
||||
|
||||
for(chunkunit_t z = 0; z < MAP_CHUNK_DEPTH; z++) {
|
||||
for(chunkunit_t y = 0; y < MAP_CHUNK_HEIGHT; y++) {
|
||||
for(chunkunit_t x = 0; x < MAP_CHUNK_WIDTH; x++) {
|
||||
if(posLoaded[x][y][z]) continue;
|
||||
assertTrue(freedCount > 0, "No free chunk slot available.");
|
||||
chunk_t *chunk = &CHUNKS[chunksFreed[--freedCount]];
|
||||
chunk->position = (chunkpos_t){
|
||||
newPos.x + (chunkunit_t)x,
|
||||
newPos.y + (chunkunit_t)y,
|
||||
newPos.z + (chunkunit_t)z
|
||||
};
|
||||
errorChain(chunkLoad(chunk));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CHUNK_POSITION = newPos;
|
||||
chunkRebuildOrder();
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void chunkUnload(chunk_t *chunk) {
|
||||
chunkLoadQueueRemove(chunk);
|
||||
if(CHUNK_LOADING == chunk) CHUNK_LOADING = NULL;
|
||||
|
||||
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
|
||||
if(chunk->entities[i] == 0xFF) continue;
|
||||
entity_t *entity = &ENTITIES[chunk->entities[i]];
|
||||
if(!entityCanUnload(entity)) {
|
||||
entitySetChunk(entity, 0xFF);
|
||||
} else {
|
||||
entity->type = ENTITY_TYPE_NULL;
|
||||
}
|
||||
}
|
||||
|
||||
memorySet(chunk->entities, 0xFF, sizeof(chunk->entities));
|
||||
|
||||
if(chunk->dataEntry != NULL) {
|
||||
eventUnsubscribe(&chunk->dataEntry->onLoaded, chunkLoaded);
|
||||
eventUnsubscribe(&chunk->dataEntry->onError, chunkLoadError);
|
||||
assetUnlockEntry(chunk->dataEntry);
|
||||
chunk->dataEntry = NULL;
|
||||
}
|
||||
|
||||
// modelEntries are borrowed pointers, not independently locked - the
|
||||
// chunk asset entry (released above) is what actually holds the ref on
|
||||
// each model, so nothing to unlock here, just drop our own copies.
|
||||
for(uint8_t m = 0; m < chunk->meshCount; m++) {
|
||||
chunk->modelEntries[m] = NULL;
|
||||
}
|
||||
chunk->meshCount = 0;
|
||||
}
|
||||
|
||||
errorret_t chunkLoad(chunk_t *chunk) {
|
||||
if(!mapIsLoaded()) errorThrow("No map loaded");
|
||||
|
||||
chunkLoadQueueRemove(chunk);
|
||||
if(CHUNK_LOADING == chunk) CHUNK_LOADING = NULL;
|
||||
|
||||
if(chunk->dataEntry != NULL) {
|
||||
eventUnsubscribe(&chunk->dataEntry->onLoaded, chunkLoaded);
|
||||
eventUnsubscribe(&chunk->dataEntry->onError, chunkLoadError);
|
||||
assetUnlockEntry(chunk->dataEntry);
|
||||
chunk->dataEntry = NULL;
|
||||
}
|
||||
|
||||
memorySet(chunk->entities, 0xFF, sizeof(chunk->entities));
|
||||
chunk->meshCount = 0;
|
||||
|
||||
char_t path[MAP_FILE_PATH_MAX + 64];
|
||||
stringFormat(
|
||||
path, sizeof(path),
|
||||
"map/%s/chunks/%d_%d_%d.json",
|
||||
MAP.name,
|
||||
(int32_t)chunk->position.x,
|
||||
(int32_t)chunk->position.y,
|
||||
(int32_t)chunk->position.z
|
||||
);
|
||||
|
||||
if(!assetFileExists(path)) {
|
||||
for(uint32_t i = 0; i < CHUNK_TILE_COUNT; i++) {
|
||||
chunk->tiles[i] = (tile_t){ .shape = TILE_SHAPE_GROUND };
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
assertTrue(
|
||||
CHUNK_LOAD_QUEUE_COUNT < MAP_CHUNK_COUNT,
|
||||
"Chunk load queue overflow"
|
||||
);
|
||||
CHUNK_LOAD_QUEUE[CHUNK_LOAD_QUEUE_COUNT++] = chunk;
|
||||
chunkLoadNext();
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void chunkLoadNext(void) {
|
||||
if(CHUNK_LOADING != NULL) return;
|
||||
if(CHUNK_LOAD_QUEUE_COUNT == 0) return;
|
||||
|
||||
chunk_t *chunk = CHUNK_LOAD_QUEUE[0];
|
||||
for(uint32_t i = 1; i < CHUNK_LOAD_QUEUE_COUNT; i++) {
|
||||
CHUNK_LOAD_QUEUE[i - 1] = CHUNK_LOAD_QUEUE[i];
|
||||
}
|
||||
CHUNK_LOAD_QUEUE_COUNT--;
|
||||
CHUNK_LOADING = chunk;
|
||||
|
||||
char_t path[MAP_FILE_PATH_MAX + 64];
|
||||
stringFormat(
|
||||
path, sizeof(path),
|
||||
"map/%s/chunks/%d_%d_%d.json",
|
||||
MAP.name,
|
||||
(int32_t)chunk->position.x,
|
||||
(int32_t)chunk->position.y,
|
||||
(int32_t)chunk->position.z
|
||||
);
|
||||
|
||||
assetentry_t *entry = assetLock(path, ASSET_LOADER_TYPE_CHUNK, NULL);
|
||||
assertNotNull(entry, "Failed to get chunk asset entry");
|
||||
chunk->dataEntry = entry;
|
||||
|
||||
// The entry may already be resident from an earlier load that hasn't been
|
||||
// reaped yet - in that case onLoaded/onError already fired once and never
|
||||
// will again, so handle the terminal state directly instead of waiting on
|
||||
// a subscription that would never trigger.
|
||||
if(entry->state == ASSET_ENTRY_STATE_LOADED) {
|
||||
chunkLoaded(entry, chunk);
|
||||
return;
|
||||
}
|
||||
if(entry->state == ASSET_ENTRY_STATE_ERROR) {
|
||||
chunkLoadError(entry, chunk);
|
||||
return;
|
||||
}
|
||||
|
||||
eventSubscribe(&entry->onLoaded, chunkLoaded, chunk);
|
||||
eventSubscribe(&entry->onError, chunkLoadError, chunk);
|
||||
}
|
||||
|
||||
void chunkLoadQueueRemove(chunk_t *chunk) {
|
||||
for(uint32_t i = 0; i < CHUNK_LOAD_QUEUE_COUNT; i++) {
|
||||
if(CHUNK_LOAD_QUEUE[i] != chunk) continue;
|
||||
for(uint32_t j = i + 1; j < CHUNK_LOAD_QUEUE_COUNT; j++) {
|
||||
CHUNK_LOAD_QUEUE[j - 1] = CHUNK_LOAD_QUEUE[j];
|
||||
}
|
||||
CHUNK_LOAD_QUEUE_COUNT--;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
chunkindex_t chunkGetIndexAt(const chunkpos_t position) {
|
||||
if(!mapIsLoaded()) return -1;
|
||||
|
||||
chunkpos_t relPos = {
|
||||
position.x - CHUNK_POSITION.x,
|
||||
position.y - CHUNK_POSITION.y,
|
||||
position.z - CHUNK_POSITION.z
|
||||
};
|
||||
|
||||
if(
|
||||
relPos.x < 0 || relPos.y < 0 || relPos.z < 0 ||
|
||||
relPos.x >= MAP_CHUNK_WIDTH ||
|
||||
relPos.y >= MAP_CHUNK_HEIGHT ||
|
||||
relPos.z >= MAP_CHUNK_DEPTH
|
||||
) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return chunkPosToIndex(&relPos);
|
||||
}
|
||||
|
||||
chunk_t *chunkGet(const uint8_t index) {
|
||||
if(index >= MAP_CHUNK_COUNT) return NULL;
|
||||
if(!mapIsLoaded()) return NULL;
|
||||
return CHUNK_ORDER[index];
|
||||
}
|
||||
|
||||
tile_t chunkGetTile(const worldpos_t position) {
|
||||
if(!mapIsLoaded()) return TILE_NULL;
|
||||
|
||||
chunkpos_t chunkPos;
|
||||
worldPosToChunkPos(&position, &chunkPos);
|
||||
chunkindex_t chunkIndex = chunkGetIndexAt(chunkPos);
|
||||
if(chunkIndex == -1) return TILE_NULL;
|
||||
|
||||
chunk_t *chunk = chunkGet(chunkIndex);
|
||||
assertNotNull(chunk, "Chunk pointer cannot be NULL");
|
||||
chunktileindex_t tileIndex = worldPosToChunkTileIndex(&position);
|
||||
tile_t tile = chunk->tiles[tileIndex];
|
||||
if(tile.z != worldPosToChunkLocalZ(&position)) return TILE_NULL;
|
||||
return tile;
|
||||
}
|
||||
|
||||
bool_t chunkGetWalkableZNear(
|
||||
const worldunit_t x,
|
||||
const worldunit_t y,
|
||||
const worldunit_t nearZ,
|
||||
worldunit_t *outZ
|
||||
) {
|
||||
assertNotNull(outZ, "Output Z pointer cannot be NULL");
|
||||
|
||||
const worldunit_t candidates[] = {
|
||||
nearZ, (worldunit_t)(nearZ + 1), (worldunit_t)(nearZ - 1)
|
||||
};
|
||||
for(uint8_t i = 0; i < 3; i++) {
|
||||
const worldpos_t pos = { x, y, candidates[i] };
|
||||
if(!tileShapeIsWalkable(chunkGetTile(pos).shape)) continue;
|
||||
*outZ = candidates[i];
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void chunkRebuildOrder(void) {
|
||||
memoryZero(CHUNK_ORDER, sizeof(CHUNK_ORDER));
|
||||
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||
chunk_t *chunk = &CHUNKS[i];
|
||||
const chunkpos_t rel = {
|
||||
chunk->position.x - CHUNK_POSITION.x,
|
||||
chunk->position.y - CHUNK_POSITION.y,
|
||||
chunk->position.z - CHUNK_POSITION.z
|
||||
};
|
||||
if(
|
||||
rel.x < 0 || rel.x >= MAP_CHUNK_WIDTH ||
|
||||
rel.y < 0 || rel.y >= MAP_CHUNK_HEIGHT ||
|
||||
rel.z < 0 || rel.z >= MAP_CHUNK_DEPTH
|
||||
) continue;
|
||||
CHUNK_ORDER[chunkPosToIndex(&rel)] = chunk;
|
||||
}
|
||||
}
|
||||
|
||||
void chunkLoadError(void *params, void *user) {
|
||||
assertNotNull(params, "chunkLoadError: params cannot be NULL");
|
||||
assertNotNull(user, "chunkLoadError: user cannot be NULL");
|
||||
assetentry_t *entry = (assetentry_t *)params;
|
||||
chunk_t *chunk = (chunk_t *)user;
|
||||
if(chunk->dataEntry != entry) return;
|
||||
consolePrint(
|
||||
"Chunk load error: %d %d %d",
|
||||
(int32_t)chunk->position.x,
|
||||
(int32_t)chunk->position.y,
|
||||
(int32_t)chunk->position.z
|
||||
);
|
||||
eventUnsubscribe(&entry->onLoaded, chunkLoaded);
|
||||
eventUnsubscribe(&entry->onError, chunkLoadError);
|
||||
assetUnlockEntry(chunk->dataEntry);
|
||||
chunk->dataEntry = NULL;
|
||||
memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles));
|
||||
|
||||
if(CHUNK_LOADING == chunk) CHUNK_LOADING = NULL;
|
||||
chunkLoadNext();
|
||||
}
|
||||
|
||||
void chunkLoaded(void *params, void *user) {
|
||||
assertNotNull(params, "chunkLoaded: params cannot be NULL");
|
||||
assertNotNull(user, "chunkLoaded: user cannot be NULL");
|
||||
assetentry_t *entry = (assetentry_t *)params;
|
||||
chunk_t *chunk = (chunk_t *)user;
|
||||
if(chunk->dataEntry != entry) return;
|
||||
|
||||
uint8_t meshCount = entry->data.chunk.meshCount;
|
||||
memoryCopy(
|
||||
chunk->tiles,
|
||||
entry->data.chunk.tiles,
|
||||
sizeof(chunk->tiles)
|
||||
);
|
||||
worldpos_t wp;
|
||||
chunkPosToWorldPos(&chunk->position, &wp);
|
||||
vec3 wpf = {
|
||||
(float_t)wp.x, (float_t)wp.y, (float_t)wp.z * WORLD_LAYER_HEIGHT
|
||||
};
|
||||
for(uint8_t m = 0; m < meshCount; m++) {
|
||||
stringCopy(
|
||||
chunk->modelNames[m],
|
||||
entry->data.chunk.modelNames[m],
|
||||
CHUNK_MESH_NAME_MAX
|
||||
);
|
||||
glm_vec3_copy(
|
||||
entry->data.chunk.meshOffsets[m],
|
||||
chunk->meshOffsets[m]
|
||||
);
|
||||
vec3 scaledOffset = {
|
||||
chunk->meshOffsets[m][0],
|
||||
chunk->meshOffsets[m][1],
|
||||
chunk->meshOffsets[m][2] * WORLD_LAYER_HEIGHT
|
||||
};
|
||||
vec3 pos;
|
||||
glm_vec3_add(wpf, scaledOffset, pos);
|
||||
glm_translate_make(chunk->meshModels[m], pos);
|
||||
// Borrow the pointer rather than stealing it - the chunk asset entry
|
||||
// keeps its own lock on each model (taken once while it loaded) and we
|
||||
// keep the chunk asset entry itself locked (see below), so the models
|
||||
// stay valid for as long as this chunk_t is using them. The entry may
|
||||
// now be reused by a later chunkLoad for a different chunk_t once we
|
||||
// eventually unlock it in chunkUnload, at which point its modelEntries
|
||||
// must still be intact for that next reuse to copy from.
|
||||
chunk->modelEntries[m] = entry->data.chunk.modelEntries[m];
|
||||
}
|
||||
eventUnsubscribe(&entry->onLoaded, chunkLoaded);
|
||||
eventUnsubscribe(&entry->onError, chunkLoadError);
|
||||
// Deliberately keep chunk->dataEntry locked and set - it is what keeps the
|
||||
// chunk asset entry (and therefore its model locks) alive for as long as
|
||||
// this chunk_t is displaying it. Released in chunkUnload instead.
|
||||
chunk->meshCount = meshCount;
|
||||
|
||||
if(CHUNK_LOADING == chunk) CHUNK_LOADING = NULL;
|
||||
chunkLoadNext();
|
||||
}
|
||||
|
||||
+10
-134
@@ -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;
|
||||
|
||||
@@ -19,7 +21,7 @@ typedef struct chunk_s {
|
||||
chunkpos_t position;
|
||||
tile_t tiles[CHUNK_TILE_COUNT];
|
||||
|
||||
assetentry_t *dataEntry;
|
||||
assetentry_t *dcfEntry;
|
||||
|
||||
uint8_t meshCount;
|
||||
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
|
||||
@@ -28,18 +30,15 @@ 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;
|
||||
|
||||
/** Every chunk slot for the currently loaded map. */
|
||||
extern chunk_t CHUNKS[MAP_CHUNK_COUNT];
|
||||
|
||||
/**
|
||||
* Chunk pointers arranged by position relative to the currently loaded
|
||||
* window, indexed via chunkPosToIndex(). NULL where no chunk occupies
|
||||
* that slot. Rebuilt by chunkRebuildOrder() whenever the window moves.
|
||||
*/
|
||||
extern chunk_t *CHUNK_ORDER[MAP_CHUNK_COUNT];
|
||||
|
||||
/**
|
||||
* Gets the tile index for a tile position within a chunk.
|
||||
*
|
||||
@@ -56,126 +55,3 @@ uint32_t chunkGetTileIndex(const chunkpos_t position);
|
||||
* @return true if equal, false otherwise.
|
||||
*/
|
||||
bool_t chunkPositionIsEqual(const chunkpos_t a, const chunkpos_t b);
|
||||
|
||||
/**
|
||||
* Resets and starts loading the initial MAP_CHUNK_WIDTH x HEIGHT x DEPTH
|
||||
* grid of chunks (async, see chunkLoad), anchored at chunk position
|
||||
* (0,0,0). Does not unload chunks already loaded - call chunksUnloadAll()
|
||||
* first when switching to a different map.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t chunksLoadGrid(void);
|
||||
|
||||
/**
|
||||
* Unloads every chunk slot in CHUNKS.
|
||||
*/
|
||||
void chunksUnloadAll(void);
|
||||
|
||||
/**
|
||||
* Moves the loaded chunk window to be centered around newPos, unloading
|
||||
* chunks that fall outside the new window and loading any newly exposed
|
||||
* ones. No-op if newPos matches the currently loaded window.
|
||||
*
|
||||
* @param newPos The new chunk position.
|
||||
* @return An error code.
|
||||
*/
|
||||
errorret_t chunkPositionSet(const chunkpos_t newPos);
|
||||
|
||||
/**
|
||||
* Unloads a chunk.
|
||||
*
|
||||
* @param chunk The chunk to unload.
|
||||
*/
|
||||
void chunkUnload(chunk_t *chunk);
|
||||
|
||||
/**
|
||||
* Loads a chunk. Starts async loading without blocking.
|
||||
*
|
||||
* @param chunk The chunk to load.
|
||||
* @return An error code.
|
||||
*/
|
||||
errorret_t chunkLoad(chunk_t *chunk);
|
||||
|
||||
/**
|
||||
* Starts loading the next queued chunk, if no chunk is currently mid-load.
|
||||
* Called after chunkLoad enqueues a chunk, and again after the
|
||||
* currently-loading chunk finishes (or is unloaded) to advance the queue.
|
||||
*/
|
||||
void chunkLoadNext(void);
|
||||
|
||||
/**
|
||||
* Removes a chunk from the load queue if present. Used when a chunk is
|
||||
* re-queued or unloaded before its turn to load has come up.
|
||||
*
|
||||
* @param chunk The chunk to remove from the load queue.
|
||||
*/
|
||||
void chunkLoadQueueRemove(chunk_t *chunk);
|
||||
|
||||
/**
|
||||
* Callback invoked when a chunk JSON asset fails to load. Fills the
|
||||
* chunk tiles with TILE_SHAPE_GROUND as a fallback.
|
||||
* Always invoked on the main thread.
|
||||
*
|
||||
* @param params The failed assetentry_t.
|
||||
* @param user The chunk_t that owns the entry.
|
||||
*/
|
||||
void chunkLoadError(void *params, void *user);
|
||||
|
||||
/**
|
||||
* Callback invoked when a chunk JSON asset finishes loading.
|
||||
* Always invoked on the main thread.
|
||||
*
|
||||
* @param params The loaded assetentry_t.
|
||||
* @param user The chunk_t that owns the entry.
|
||||
*/
|
||||
void chunkLoaded(void *params, void *user);
|
||||
|
||||
/**
|
||||
* Rebuilds CHUNK_ORDER from the loaded chunks that fall within the
|
||||
* current render window. Called whenever the chunk position changes.
|
||||
*/
|
||||
void chunkRebuildOrder(void);
|
||||
|
||||
/**
|
||||
* Gets the index of a chunk, within the currently loaded window, at the
|
||||
* given position.
|
||||
*
|
||||
* @param position The chunk position.
|
||||
* @return The index of the chunk, or -1 if out of bounds.
|
||||
*/
|
||||
chunkindex_t chunkGetIndexAt(const chunkpos_t position);
|
||||
|
||||
/**
|
||||
* Gets a chunk by its index in CHUNK_ORDER.
|
||||
*
|
||||
* @param index The index of the chunk.
|
||||
* @return A pointer to the chunk.
|
||||
*/
|
||||
chunk_t *chunkGet(const uint8_t index);
|
||||
|
||||
/**
|
||||
* Gets the tile at the given world position.
|
||||
*
|
||||
* @param position The world position.
|
||||
* @return The tile at that position, or TILE_NULL if the chunk is unloaded.
|
||||
*/
|
||||
tile_t chunkGetTile(const worldpos_t position);
|
||||
|
||||
/**
|
||||
* Finds the closest walkable Z layer to nearZ at the given X/Y. Checks
|
||||
* nearZ first, then nearZ + 1, then nearZ - 1, since ramps only ever
|
||||
* change height by one Z layer between adjacent tiles.
|
||||
*
|
||||
* @param x The world X coordinate to check.
|
||||
* @param y The world Y coordinate to check.
|
||||
* @param nearZ The reference Z layer to search outward from.
|
||||
* @param outZ Output pointer, set to the resolved Z layer on success.
|
||||
* @return true if a walkable tile was found, false otherwise.
|
||||
*/
|
||||
bool_t chunkGetWalkableZNear(
|
||||
const worldunit_t x,
|
||||
const worldunit_t y,
|
||||
const worldunit_t nearZ,
|
||||
worldunit_t *outZ
|
||||
);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "mapareaglobaldefs.h"
|
||||
#include "mapareagloballist.h"
|
||||
|
||||
#define MAP_AREA_CALLBACK_LIST_COUNT ( \
|
||||
sizeof(MAP_AREA_CALLBACK_LIST) / \
|
||||
sizeof(MAP_AREA_CALLBACK_LIST[0]) \
|
||||
)
|
||||
|
||||
//EOF
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "rpg/overworld/maparea.h"
|
||||
|
||||
#define MAP_AREA_CALLBACK(id) \
|
||||
static void MAP_AREA_CALLBACK_##id(entity_t *entity, const uint8_t trigger)
|
||||
|
||||
#define MAP_AREA_CALLBACK_REF(id) \
|
||||
MAP_AREA_CALLBACK_##id
|
||||
|
||||
//EOF
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "mapareaglobaldefs.h"
|
||||
#include "console/console.h"
|
||||
|
||||
MAP_AREA_CALLBACK(1) {
|
||||
consolePrint("mapAreaGlobalCallback 1: trigger=%u", trigger);
|
||||
}
|
||||
|
||||
// Index 0 is reserved (not a valid callback ID) - see mapAreaAddGlobal.
|
||||
static const mapareacallback_t MAP_AREA_CALLBACK_LIST[] = {
|
||||
NULL,
|
||||
MAP_AREA_CALLBACK_REF(1),
|
||||
};
|
||||
|
||||
//EOF
|
||||
+459
-84
@@ -10,64 +10,42 @@
|
||||
#include "assert/assert.h"
|
||||
#include "asset/asset.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "asset/loader/assetentry.h"
|
||||
#include "asset/loader/json/assetjsonloader.h"
|
||||
#include "console/console.h"
|
||||
#include "event/event.h"
|
||||
#include "util/string.h"
|
||||
#include "rpg/overworld/chunk.h"
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "yyjson.h"
|
||||
#include "rpg/entity/global/entityglobal.h"
|
||||
#include "rpg/entity/item/entityitem.h"
|
||||
#include "rpg/overworld/maparea.h"
|
||||
|
||||
map_t MAP;
|
||||
|
||||
errorret_t mapInit(const char_t *name) {
|
||||
errorChain(mapSetMap(name));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t mapSetMap(const char_t *name) {
|
||||
assertNotNull(name, "Map name cannot be NULL");
|
||||
assertStrLenMin(name, 1, "Map name cannot be empty");
|
||||
assertStrLenMax(name, MAP_FILE_PATH_MAX, "Map name too long");
|
||||
|
||||
if(mapIsLoaded() && stringEquals(MAP.name, name)) errorOk();
|
||||
|
||||
if(mapIsLoaded()) {
|
||||
chunksUnloadAll();
|
||||
if(MAP.defEntry != NULL) {
|
||||
eventUnsubscribe(&MAP.defEntry->onLoaded, mapDefLoaded);
|
||||
eventUnsubscribe(&MAP.defEntry->onError, mapDefLoadError);
|
||||
assetUnlockEntry(MAP.defEntry);
|
||||
MAP.defEntry = NULL;
|
||||
// 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));
|
||||
stringCopy(MAP.name, name, MAP_FILE_PATH_MAX);
|
||||
MAP.loaded = true;
|
||||
|
||||
char_t defPath[MAP_FILE_PATH_MAX + 16];
|
||||
stringFormat(defPath, sizeof(defPath), "map/%s/map.json", MAP.name);
|
||||
|
||||
assetentry_t *defEntry = assetLock(defPath, ASSET_LOADER_TYPE_JSON, NULL);
|
||||
assertNotNull(defEntry, "Failed to get map def asset entry");
|
||||
MAP.defEntry = defEntry;
|
||||
|
||||
// The entry may already be resident from an earlier load that hasn't been
|
||||
// reaped yet - in that case onLoaded/onError already fired once and never
|
||||
// will again, so handle the terminal state directly instead of waiting on
|
||||
// a subscription that would never trigger.
|
||||
if(defEntry->state == ASSET_ENTRY_STATE_LOADED) {
|
||||
mapDefLoaded(defEntry, NULL);
|
||||
} else if(defEntry->state == ASSET_ENTRY_STATE_ERROR) {
|
||||
mapDefLoadError(defEntry, NULL);
|
||||
} else {
|
||||
eventSubscribe(&defEntry->onLoaded, mapDefLoaded, NULL);
|
||||
eventSubscribe(&defEntry->onError, mapDefLoadError, NULL);
|
||||
chunkindex_t i = 0;
|
||||
for(chunkunit_t z = 0; z < MAP_CHUNK_DEPTH; z++) {
|
||||
for(chunkunit_t y = 0; y < MAP_CHUNK_HEIGHT; y++) {
|
||||
for(chunkunit_t x = 0; x < MAP_CHUNK_WIDTH; x++) {
|
||||
chunk_t *chunk = &MAP.chunks[i++];
|
||||
chunk->position = (chunkpos_t){
|
||||
(chunkunit_t)x, (chunkunit_t)y, (chunkunit_t)z
|
||||
};
|
||||
errorChain(mapChunkLoad(chunk));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errorChain(chunksLoadGrid());
|
||||
mapRebuildChunkOrder();
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -75,61 +53,458 @@ bool_t mapIsLoaded() {
|
||||
return MAP.loaded;
|
||||
}
|
||||
|
||||
errorret_t mapPositionSet(const chunkpos_t newPos) {
|
||||
if(!mapIsLoaded()) errorThrow("No map loaded");
|
||||
if(chunkPositionIsEqual(newPos, MAP.chunkPosition)) errorOk();
|
||||
|
||||
// Separate loaded chunks into "keep" and "free" buckets.
|
||||
chunkindex_t chunksFreed[MAP_CHUNK_COUNT];
|
||||
uint32_t freedCount = 0;
|
||||
|
||||
// Use a boolean grid so the inner load loop can check O(1).
|
||||
bool_t posLoaded[MAP_CHUNK_WIDTH][MAP_CHUNK_HEIGHT][MAP_CHUNK_DEPTH];
|
||||
memoryZero(posLoaded, sizeof(posLoaded));
|
||||
|
||||
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||
chunk_t *chunk = &MAP.chunks[i];
|
||||
chunkunit_t rx = chunk->position.x - newPos.x;
|
||||
chunkunit_t ry = chunk->position.y - newPos.y;
|
||||
chunkunit_t rz = chunk->position.z - newPos.z;
|
||||
if(
|
||||
rx >= 0 && rx < MAP_CHUNK_WIDTH &&
|
||||
ry >= 0 && ry < MAP_CHUNK_HEIGHT &&
|
||||
rz >= 0 && rz < MAP_CHUNK_DEPTH
|
||||
) {
|
||||
posLoaded[rx][ry][rz] = true;
|
||||
} else {
|
||||
mapChunkUnload(chunk);
|
||||
chunksFreed[freedCount++] = i;
|
||||
}
|
||||
}
|
||||
|
||||
for(chunkunit_t z = 0; z < MAP_CHUNK_DEPTH; z++) {
|
||||
for(chunkunit_t y = 0; y < MAP_CHUNK_HEIGHT; y++) {
|
||||
for(chunkunit_t x = 0; x < MAP_CHUNK_WIDTH; x++) {
|
||||
if(posLoaded[x][y][z]) continue;
|
||||
assertTrue(freedCount > 0, "No free chunk slot available.");
|
||||
chunk_t *chunk = &MAP.chunks[chunksFreed[--freedCount]];
|
||||
chunk->position = (chunkpos_t){
|
||||
newPos.x + (chunkunit_t)x,
|
||||
newPos.y + (chunkunit_t)y,
|
||||
newPos.z + (chunkunit_t)z
|
||||
};
|
||||
errorChain(mapChunkLoad(chunk));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MAP.chunkPosition = newPos;
|
||||
mapRebuildChunkOrder();
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t mapUpdate() {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t mapDispose() {
|
||||
chunksUnloadAll();
|
||||
|
||||
if(MAP.defEntry != NULL) {
|
||||
eventUnsubscribe(&MAP.defEntry->onLoaded, mapDefLoaded);
|
||||
eventUnsubscribe(&MAP.defEntry->onError, mapDefLoadError);
|
||||
assetUnlockEntry(MAP.defEntry);
|
||||
MAP.defEntry = NULL;
|
||||
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||
mapChunkUnload(&MAP.chunks[i]);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void mapDefLoadError(void *params, void *user) {
|
||||
assertNotNull(params, "mapDefLoadError: params cannot be NULL");
|
||||
assetentry_t *entry = (assetentry_t *)params;
|
||||
if(MAP.defEntry != entry) return;
|
||||
consolePrint("Failed to load map.json for '%s'", MAP.name);
|
||||
eventUnsubscribe(&entry->onLoaded, mapDefLoaded);
|
||||
eventUnsubscribe(&entry->onError, mapDefLoadError);
|
||||
}
|
||||
void mapChunkUnload(chunk_t *chunk) {
|
||||
mapChunkLoadQueueRemove(chunk);
|
||||
mapChunkLoadingSlotClear(chunk);
|
||||
|
||||
void mapDefLoaded(void *params, void *user) {
|
||||
assertNotNull(params, "mapDefLoaded: params cannot be NULL");
|
||||
assetentry_t *entry = (assetentry_t *)params;
|
||||
if(MAP.defEntry != entry) return;
|
||||
|
||||
yyjson_val *root = yyjson_doc_get_root(entry->data.json);
|
||||
yyjson_val *nameVal = yyjson_obj_get(root, "name");
|
||||
if(!nameVal || !yyjson_is_str(nameVal)) {
|
||||
consolePrint("map.json for '%s' missing 'name' string", MAP.name);
|
||||
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
|
||||
if(chunk->entities[i] == 0xFF) continue;
|
||||
entity_t *entity = &ENTITIES[chunk->entities[i]];
|
||||
if(!entityCanUnload(entity)) {
|
||||
entitySetChunk(entity, 0xFF);
|
||||
} else {
|
||||
const char_t *nameStr = yyjson_get_str(nameVal);
|
||||
size_t nameLen = yyjson_get_len(nameVal);
|
||||
if(nameLen >= MAP_DISPLAY_NAME_MAX) {
|
||||
consolePrint("Map display name '%s' exceeds max length", nameStr);
|
||||
} else {
|
||||
memoryCopy(MAP.displayName, nameStr, nameLen + 1);
|
||||
entity->type = ENTITY_TYPE_NULL;
|
||||
}
|
||||
}
|
||||
|
||||
yyjson_val *entitiesVal = yyjson_obj_get(root, "entities");
|
||||
if(entitiesVal && yyjson_is_arr(entitiesVal)) {
|
||||
size_t entIdx, entMax;
|
||||
yyjson_val *entObj;
|
||||
yyjson_arr_foreach(entitiesVal, entIdx, entMax, entObj) {
|
||||
entity_t *spawned = NULL;
|
||||
errorCatch(errorPrint(entityCreateFromJson(entObj, &spawned)));
|
||||
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);
|
||||
assetUnlockEntry(chunk->dcfEntry);
|
||||
chunk->dcfEntry = NULL;
|
||||
}
|
||||
|
||||
// modelEntries are borrowed pointers, not independently locked - the
|
||||
// chunk asset entry (released above) is what actually holds the ref on
|
||||
// each model, so nothing to unlock here, just drop our own copies.
|
||||
for(uint8_t m = 0; m < chunk->meshCount; m++) {
|
||||
chunk->modelEntries[m] = NULL;
|
||||
}
|
||||
chunk->meshCount = 0;
|
||||
}
|
||||
|
||||
errorret_t mapChunkLoad(chunk_t *chunk) {
|
||||
if(!mapIsLoaded()) errorThrow("No map loaded");
|
||||
|
||||
mapChunkLoadQueueRemove(chunk);
|
||||
mapChunkLoadingSlotClear(chunk);
|
||||
|
||||
if(chunk->dcfEntry != NULL) {
|
||||
eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded);
|
||||
eventUnsubscribe(&chunk->dcfEntry->onError, mapChunkLoadError);
|
||||
assetUnlockEntry(chunk->dcfEntry);
|
||||
chunk->dcfEntry = NULL;
|
||||
}
|
||||
|
||||
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];
|
||||
stringFormat(
|
||||
name, sizeof(name),
|
||||
"chunks/%d_%d_%d.dcf",
|
||||
(int32_t)chunk->position.x,
|
||||
(int32_t)chunk->position.y,
|
||||
(int32_t)chunk->position.z
|
||||
);
|
||||
|
||||
if(!assetFileExists(name)) {
|
||||
for(uint32_t i = 0; i < CHUNK_TILE_COUNT; i++) {
|
||||
// chunk->tiles[i] = (tile_t){ .shape = TILE_SHAPE_GROUND, .z = 0 };
|
||||
chunk->tiles[i] = (tile_t){ .shape = TILE_SHAPE_GROUND };
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
assertTrue(
|
||||
MAP.loadQueueCount < MAP_CHUNK_COUNT,
|
||||
"Chunk load queue overflow"
|
||||
);
|
||||
MAP.loadQueue[MAP.loadQueueCount++] = chunk;
|
||||
mapChunkLoadNext();
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void mapChunkLoadNext() {
|
||||
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];
|
||||
for(uint32_t i = 1; i < MAP.loadQueueCount; i++) {
|
||||
MAP.loadQueue[i - 1] = MAP.loadQueue[i];
|
||||
}
|
||||
MAP.loadQueueCount--;
|
||||
MAP.loadingChunks[slot] = chunk;
|
||||
|
||||
char_t name[64];
|
||||
stringFormat(
|
||||
name, sizeof(name),
|
||||
"chunks/%d_%d_%d.dcf",
|
||||
(int32_t)chunk->position.x,
|
||||
(int32_t)chunk->position.y,
|
||||
(int32_t)chunk->position.z
|
||||
);
|
||||
|
||||
assetentry_t *entry = assetLock(name, ASSET_LOADER_TYPE_CHUNK, NULL);
|
||||
assertNotNull(entry, "Failed to get chunk asset entry");
|
||||
chunk->dcfEntry = entry;
|
||||
|
||||
// The entry may already be resident from an earlier load that hasn't
|
||||
// been reaped yet - in that case onLoaded/onError already fired once
|
||||
// and never will again, so handle the terminal state directly instead
|
||||
// of waiting on a subscription that would never trigger. Both of these
|
||||
// recurse back into mapChunkLoadNext once they clear this slot, so the
|
||||
// outer loop just continues on to try filling the next one.
|
||||
if(entry->state == ASSET_ENTRY_STATE_LOADED) {
|
||||
mapChunkLoaded(entry, chunk);
|
||||
continue;
|
||||
}
|
||||
if(entry->state == ASSET_ENTRY_STATE_ERROR) {
|
||||
mapChunkLoadError(entry, chunk);
|
||||
continue;
|
||||
}
|
||||
|
||||
eventSubscribe(&entry->onLoaded, mapChunkLoaded, chunk);
|
||||
eventSubscribe(&entry->onError, mapChunkLoadError, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
eventUnsubscribe(&entry->onLoaded, mapDefLoaded);
|
||||
eventUnsubscribe(&entry->onError, mapDefLoadError);
|
||||
void mapChunkLoadQueueRemove(chunk_t *chunk) {
|
||||
for(uint32_t i = 0; i < MAP.loadQueueCount; i++) {
|
||||
if(MAP.loadQueue[i] != chunk) continue;
|
||||
for(uint32_t j = i + 1; j < MAP.loadQueueCount; j++) {
|
||||
MAP.loadQueue[j - 1] = MAP.loadQueue[j];
|
||||
}
|
||||
MAP.loadQueueCount--;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
chunkindex_t mapGetChunkIndexAt(const chunkpos_t position) {
|
||||
if(!mapIsLoaded()) return -1;
|
||||
|
||||
chunkpos_t relPos = {
|
||||
position.x - MAP.chunkPosition.x,
|
||||
position.y - MAP.chunkPosition.y,
|
||||
position.z - MAP.chunkPosition.z
|
||||
};
|
||||
|
||||
if(
|
||||
relPos.x < 0 || relPos.y < 0 || relPos.z < 0 ||
|
||||
relPos.x >= MAP_CHUNK_WIDTH ||
|
||||
relPos.y >= MAP_CHUNK_HEIGHT ||
|
||||
relPos.z >= MAP_CHUNK_DEPTH
|
||||
) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return chunkPosToIndex(&relPos);
|
||||
}
|
||||
|
||||
chunk_t *mapGetChunk(const uint8_t index) {
|
||||
if(index >= MAP_CHUNK_COUNT) return NULL;
|
||||
if(!mapIsLoaded()) return NULL;
|
||||
return MAP.chunkOrder[index];
|
||||
}
|
||||
|
||||
tile_t mapGetTile(const worldpos_t position) {
|
||||
if(!mapIsLoaded()) return TILE_NULL;
|
||||
|
||||
chunkpos_t chunkPos;
|
||||
worldPosToChunkPos(&position, &chunkPos);
|
||||
chunkindex_t chunkIndex = mapGetChunkIndexAt(chunkPos);
|
||||
if(chunkIndex == -1) return TILE_NULL;
|
||||
|
||||
chunk_t *chunk = mapGetChunk(chunkIndex);
|
||||
assertNotNull(chunk, "Chunk pointer cannot be NULL");
|
||||
chunktileindex_t tileIndex = worldPosToChunkTileIndex(&position);
|
||||
tile_t tile = chunk->tiles[tileIndex];
|
||||
if(tile.z != worldPosToChunkLocalZ(&position)) return TILE_NULL;
|
||||
return tile;
|
||||
}
|
||||
|
||||
bool_t mapGetWalkableZNear(
|
||||
const worldunit_t x,
|
||||
const worldunit_t y,
|
||||
const worldunit_t nearZ,
|
||||
worldunit_t *outZ
|
||||
) {
|
||||
assertNotNull(outZ, "Output Z pointer cannot be NULL");
|
||||
|
||||
const worldunit_t candidates[] = {
|
||||
nearZ, (worldunit_t)(nearZ + 1), (worldunit_t)(nearZ - 1)
|
||||
};
|
||||
for(uint8_t i = 0; i < 3; i++) {
|
||||
const worldpos_t pos = { x, y, candidates[i] };
|
||||
if(!tileShapeIsWalkable(mapGetTile(pos).shape)) continue;
|
||||
*outZ = candidates[i];
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
entity_t * mapSpawnEntity(
|
||||
const entityglobalid_t globalId,
|
||||
const worldpos_t position
|
||||
) {
|
||||
assertTrue(
|
||||
globalId > ENTITY_GLOBAL_ID_START,
|
||||
"mapSpawnEntity requires a global ID greater than ENTITY_GLOBAL_ID_START"
|
||||
);
|
||||
assertTrue(
|
||||
globalId < ENTITY_GLOBAL_LIST_COUNT,
|
||||
"Global ID is out of range for entity global init callbacks"
|
||||
);
|
||||
|
||||
// Already spawned? Reuse the existing entity instead of making a
|
||||
// duplicate - two entities must never share a global ID.
|
||||
entity_t *existing = entityGetByGlobalId(globalId);
|
||||
if(existing != NULL) return existing;
|
||||
|
||||
// See if there is a callback for this entity first.
|
||||
const entityglobaldef_t *def = &ENTITY_GLOBAL_LIST[globalId];
|
||||
assertNotNull(def, "No global entity definition for this ID");
|
||||
assertNotNull(def->callback, "No callback registered for this global ID");
|
||||
|
||||
// Get available entity.
|
||||
uint8_t index = entityGetAvailable();
|
||||
assertTrue(index != 0xFF, "No available entity slots for mapSpawnEntity");
|
||||
|
||||
// Get the pointer and do the init.
|
||||
entity_t *entity = &ENTITIES[index];
|
||||
entityInit(entity, def->type);
|
||||
entity->globalId = globalId;
|
||||
entityPositionSet(entity, position);// Also assigns the entity's chunk.
|
||||
|
||||
// Invoke the callback to initialize the entity.
|
||||
entityglobalcreate_t create = {
|
||||
.entity = entity,
|
||||
.position = position
|
||||
};
|
||||
def->callback(&create);
|
||||
return entity;
|
||||
}
|
||||
|
||||
void mapRebuildChunkOrder() {
|
||||
memoryZero(MAP.chunkOrder, sizeof(MAP.chunkOrder));
|
||||
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||
chunk_t *chunk = &MAP.chunks[i];
|
||||
const chunkpos_t rel = {
|
||||
chunk->position.x - MAP.chunkPosition.x,
|
||||
chunk->position.y - MAP.chunkPosition.y,
|
||||
chunk->position.z - MAP.chunkPosition.z
|
||||
};
|
||||
if(
|
||||
rel.x < 0 || rel.x >= MAP_CHUNK_WIDTH ||
|
||||
rel.y < 0 || rel.y >= MAP_CHUNK_HEIGHT ||
|
||||
rel.z < 0 || rel.z >= MAP_CHUNK_DEPTH
|
||||
) continue;
|
||||
MAP.chunkOrder[chunkPosToIndex(&rel)] = chunk;
|
||||
}
|
||||
}
|
||||
|
||||
void mapChunkLoadError(void *params, void *user) {
|
||||
assertNotNull(params, "mapChunkLoadError: params cannot be NULL");
|
||||
assertNotNull(user, "mapChunkLoadError: user cannot be NULL");
|
||||
assetentry_t *entry = (assetentry_t *)params;
|
||||
chunk_t *chunk = (chunk_t *)user;
|
||||
if(chunk->dcfEntry != entry) return;
|
||||
consolePrint(
|
||||
"Chunk load error: %d %d %d",
|
||||
(int32_t)chunk->position.x,
|
||||
(int32_t)chunk->position.y,
|
||||
(int32_t)chunk->position.z
|
||||
);
|
||||
eventUnsubscribe(&entry->onLoaded, mapChunkLoaded);
|
||||
eventUnsubscribe(&entry->onError, mapChunkLoadError);
|
||||
assetUnlockEntry(chunk->dcfEntry);
|
||||
chunk->dcfEntry = NULL;
|
||||
memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles));
|
||||
|
||||
mapChunkLoadingSlotClear(chunk);
|
||||
mapChunkLoadNext();
|
||||
}
|
||||
|
||||
void mapChunkLoaded(void *params, void *user) {
|
||||
assertNotNull(params, "mapChunkLoaded: params cannot be NULL");
|
||||
assertNotNull(user, "mapChunkLoaded: user cannot be NULL");
|
||||
assetentry_t *entry = (assetentry_t *)params;
|
||||
chunk_t *chunk = (chunk_t *)user;
|
||||
if(chunk->dcfEntry != entry) return;
|
||||
// consolePrint(
|
||||
// "Chunk loaded: %d %d %d",
|
||||
// (int32_t)chunk->position.x,
|
||||
// (int32_t)chunk->position.y,
|
||||
// (int32_t)chunk->position.z
|
||||
// );
|
||||
uint8_t meshCount = entry->data.chunk.meshCount;
|
||||
memoryCopy(
|
||||
chunk->tiles,
|
||||
entry->data.chunk.tiles,
|
||||
sizeof(chunk->tiles)
|
||||
);
|
||||
worldpos_t wp;
|
||||
chunkPosToWorldPos(&chunk->position, &wp);
|
||||
vec3 wpf = {
|
||||
(float_t)wp.x, (float_t)wp.y, (float_t)wp.z * WORLD_LAYER_HEIGHT
|
||||
};
|
||||
for(uint8_t m = 0; m < meshCount; m++) {
|
||||
stringCopy(
|
||||
chunk->modelNames[m],
|
||||
entry->data.chunk.modelNames[m],
|
||||
CHUNK_MESH_NAME_MAX
|
||||
);
|
||||
glm_vec3_copy(
|
||||
entry->data.chunk.meshOffsets[m],
|
||||
chunk->meshOffsets[m]
|
||||
);
|
||||
vec3 scaledOffset = {
|
||||
chunk->meshOffsets[m][0],
|
||||
chunk->meshOffsets[m][1],
|
||||
chunk->meshOffsets[m][2] * WORLD_LAYER_HEIGHT
|
||||
};
|
||||
vec3 pos;
|
||||
glm_vec3_add(wpf, scaledOffset, pos);
|
||||
glm_translate_make(chunk->meshModels[m], pos);
|
||||
// Borrow the pointer rather than stealing it - the chunk asset entry
|
||||
// keeps its own lock on each model (taken once while it loaded) and we
|
||||
// keep the chunk asset entry itself locked (see below), so the models
|
||||
// stay valid for as long as this chunk_t is using them. The entry may
|
||||
// now be reused by a later mapChunkLoad for a different chunk_t once we
|
||||
// eventually unlock it in mapChunkUnload, at which point its
|
||||
// modelEntries must still be intact for that next reuse to copy from.
|
||||
chunk->modelEntries[m] = entry->data.chunk.modelEntries[m];
|
||||
}
|
||||
eventUnsubscribe(&entry->onLoaded, mapChunkLoaded);
|
||||
eventUnsubscribe(&entry->onError, mapChunkLoadError);
|
||||
// Deliberately keep chunk->dcfEntry locked and set - it is what keeps the
|
||||
// chunk asset entry (and therefore its model locks) alive for as long as
|
||||
// this chunk_t is displaying it. Released in mapChunkUnload instead.
|
||||
chunk->meshCount = meshCount;
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
+123
-37
@@ -7,48 +7,35 @@
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "rpg/overworld/chunk.h"
|
||||
#include "rpg/entity/entity.h"
|
||||
|
||||
#define MAP_FILE_PATH_MAX 32
|
||||
#define MAP_DISPLAY_NAME_MAX 64
|
||||
#define MAP_FILE_PATH_MAX 128
|
||||
|
||||
typedef struct assetentry_s assetentry_t;
|
||||
// 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 {
|
||||
char_t name[MAP_FILE_PATH_MAX];
|
||||
char_t displayName[MAP_DISPLAY_NAME_MAX];
|
||||
bool_t loaded;
|
||||
|
||||
// Asset lock for the map's map.json, held while its async load is
|
||||
// pending and for as long as the map stays loaded.
|
||||
assetentry_t *defEntry;
|
||||
chunk_t chunks[MAP_CHUNK_COUNT];
|
||||
chunk_t *chunkOrder[MAP_CHUNK_COUNT];
|
||||
chunkpos_t chunkPosition;
|
||||
|
||||
chunk_t *loadQueue[MAP_CHUNK_COUNT];
|
||||
uint32_t loadQueueCount;
|
||||
chunk_t *loadingChunks[MAP_CHUNK_LOAD_CONCURRENCY];
|
||||
} map_t;
|
||||
|
||||
extern map_t MAP;
|
||||
|
||||
/**
|
||||
* Initializes the map, loading its chunks from beneath the given name's
|
||||
* asset directory (e.g. "testmap" -> assets/map/testmap/chunks/X_Y_Z.dcf).
|
||||
* Initializes the map.
|
||||
*
|
||||
* @param name The map's directory name, under assets/map/.
|
||||
* @return An error code.
|
||||
*/
|
||||
errorret_t mapInit(const char_t *name);
|
||||
|
||||
/**
|
||||
* Switches to a different map, unloading every currently loaded chunk and
|
||||
* starting an async load of the initial chunk grid (at chunk position
|
||||
* 0,0,0) plus map.json (-> MAP.displayName), both from beneath the new
|
||||
* name's asset directory. Once map.json loads, its optional "entities"
|
||||
* array (see entityCreateFromJson) is spawned; entries that fail to parse
|
||||
* are logged and skipped rather than failing the whole map load. Returns
|
||||
* before either finishes loading - callers must not assume
|
||||
* MAP.displayName, spawned entities, or chunk data is populated yet.
|
||||
* No-op if name matches the currently loaded map.
|
||||
*
|
||||
* @param name The map's directory name, under assets/map/.
|
||||
* @return An error code.
|
||||
*/
|
||||
errorret_t mapSetMap(const char_t *name);
|
||||
errorret_t mapInit();
|
||||
|
||||
/**
|
||||
* Checks if a map is loaded.
|
||||
@@ -72,22 +59,121 @@ errorret_t mapUpdate();
|
||||
errorret_t mapDispose();
|
||||
|
||||
/**
|
||||
* Callback invoked when a map's map.json asset fails to load. Leaves
|
||||
* MAP.displayName empty and logs a console message.
|
||||
* Sets the map position and updates chunks accordingly.
|
||||
*
|
||||
* @param newPos The new chunk position.
|
||||
* @return An error code.
|
||||
*/
|
||||
errorret_t mapPositionSet(const chunkpos_t newPos);
|
||||
|
||||
/**
|
||||
* Unloads a chunk.
|
||||
*
|
||||
* @param chunk The chunk to unload.
|
||||
*/
|
||||
void mapChunkUnload(chunk_t* chunk);
|
||||
|
||||
/**
|
||||
* Loads a chunk. Starts async loading without blocking.
|
||||
*
|
||||
* @param chunk The chunk to load.
|
||||
* @return An error code.
|
||||
*/
|
||||
errorret_t mapChunkLoad(chunk_t* chunk);
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
/**
|
||||
* Removes a chunk from the load queue if present. Used when a chunk is
|
||||
* re-queued or unloaded before its turn to load has come up.
|
||||
*
|
||||
* @param chunk The chunk to remove from the load queue.
|
||||
*/
|
||||
void mapChunkLoadQueueRemove(chunk_t *chunk);
|
||||
|
||||
/**
|
||||
* Callback invoked when a chunk DCF asset fails to load. Fills the
|
||||
* chunk tiles with TILE_SHAPE_GROUND as a fallback.
|
||||
* Always invoked on the main thread.
|
||||
*
|
||||
* @param params The failed assetentry_t.
|
||||
* @param user Unused.
|
||||
* @param user The chunk_t that owns the entry.
|
||||
*/
|
||||
void mapDefLoadError(void *params, void *user);
|
||||
void mapChunkLoadError(void *params, void *user);
|
||||
|
||||
/**
|
||||
* Callback invoked when a map's map.json asset finishes loading. Parses
|
||||
* out the "name" string into MAP.displayName, and spawns each entry of
|
||||
* the optional "entities" array via entityCreateFromJson.
|
||||
* Callback invoked when a chunk DCF asset finishes loading.
|
||||
* Always invoked on the main thread.
|
||||
*
|
||||
* @param params The loaded assetentry_t.
|
||||
* @param user Unused.
|
||||
* @param user The chunk_t that owns the entry.
|
||||
*/
|
||||
void mapDefLoaded(void *params, void *user);
|
||||
void mapChunkLoaded(void *params, void *user);
|
||||
|
||||
/**
|
||||
* Rebuilds chunkOrder from the loaded chunks that fall within the
|
||||
* current render window. Called whenever chunkPosition changes.
|
||||
*/
|
||||
void mapRebuildChunkOrder();
|
||||
|
||||
/**
|
||||
* Gets the index of a chunk, within the world, at the given position.
|
||||
*
|
||||
* @param position The chunk position.
|
||||
* @return The index of the chunk, or -1 if out of bounds.
|
||||
*/
|
||||
chunkindex_t mapGetChunkIndexAt(const chunkpos_t position);
|
||||
|
||||
/**
|
||||
* Gets a chunk by its index.
|
||||
*
|
||||
* @param chunkIndex The index of the chunk.
|
||||
* @return A pointer to the chunk.
|
||||
*/
|
||||
chunk_t * mapGetChunk(const uint8_t chunkIndex);
|
||||
|
||||
/**
|
||||
* Gets the tile at the given world position.
|
||||
*
|
||||
* @param position The world position.
|
||||
* @return The tile at that position, or TILE_NULL if the chunk is unloaded.
|
||||
*/
|
||||
tile_t mapGetTile(const worldpos_t position);
|
||||
|
||||
/**
|
||||
* Finds the closest walkable Z layer to nearZ at the given X/Y. Checks
|
||||
* nearZ first, then nearZ + 1, then nearZ - 1, since ramps only ever
|
||||
* change height by one Z layer between adjacent tiles.
|
||||
*
|
||||
* @param x The world X coordinate to check.
|
||||
* @param y The world Y coordinate to check.
|
||||
* @param nearZ The reference Z layer to search outward from.
|
||||
* @param outZ Output pointer, set to the resolved Z layer on success.
|
||||
* @return true if a walkable tile was found, false otherwise.
|
||||
*/
|
||||
bool_t mapGetWalkableZNear(
|
||||
const worldunit_t x,
|
||||
const worldunit_t y,
|
||||
const worldunit_t nearZ,
|
||||
worldunit_t *outZ
|
||||
);
|
||||
|
||||
/**
|
||||
* Spawns a global (persistent) entity into the world at the given position.
|
||||
* Asserts globalId is greater than ENTITY_GLOBAL_ID_START - use entityInit
|
||||
* directly for ephemeral, non-global entities.
|
||||
*
|
||||
* @param globalId The global entity ID to assign, must be greater than
|
||||
* ENTITY_GLOBAL_ID_START.
|
||||
* @param position The world position to spawn the entity at.
|
||||
* @return Pointer to the spawned entity.
|
||||
*/
|
||||
entity_t * mapSpawnEntity(
|
||||
const entityglobalid_t globalId,
|
||||
const worldpos_t position
|
||||
);
|
||||
@@ -9,7 +9,8 @@
|
||||
#include "assert/assert.h"
|
||||
#include "util/math.h"
|
||||
#include "util/memory.h"
|
||||
#include "rpg/overworld/chunk.h"
|
||||
#include "rpg/overworld/map.h"
|
||||
#include "rpg/overworld/global/mapareaglobal.h"
|
||||
|
||||
maparea_t MAP_AREAS[MAP_AREA_COUNT_MAX];
|
||||
|
||||
@@ -74,7 +75,7 @@ bool_t mapAreaCanUnload(const maparea_t *area) {
|
||||
assertNotNull(area, "Map area pointer cannot be NULL");
|
||||
|
||||
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||
if(mapAreaIsChunkOverlappingOrInside(area, &CHUNKS[i])) return false;
|
||||
if(mapAreaIsChunkOverlappingOrInside(area, &MAP.chunks[i])) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -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
|
||||
);
|
||||
+34
-19
@@ -8,7 +8,6 @@
|
||||
#include "rpg.h"
|
||||
#include "entity/entity.h"
|
||||
#include "rpg/overworld/map.h"
|
||||
#include "rpg/overworld/chunk.h"
|
||||
#include "rpg/overworld/maparea.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "rpg/item/backpack.h"
|
||||
@@ -19,42 +18,58 @@
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
#include "console/console.h"
|
||||
#include "save/save.h"
|
||||
#include "error/error.h"
|
||||
|
||||
#include "ui/rpg/uiemoji.h"
|
||||
#include "rpg/story/storyflag.h"
|
||||
|
||||
void rpgTestAreaCallback(entity_t *entity, const uint8_t trigger) {
|
||||
consolePrint("rpgTestAreaCallback: trigger=%u", trigger);
|
||||
static void rpgTestSaveComplete(errorret_t result, void *user) {
|
||||
if(errorIsNotOk(result)) errorCatch(errorPrint(result));
|
||||
}
|
||||
|
||||
errorret_t rpgInit(void) {
|
||||
memoryZero(ENTITIES, sizeof(ENTITIES));
|
||||
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
|
||||
|
||||
// Must run before any code reads a story flag - stamps CSV-defined
|
||||
// defaults onto the active save slot if it's never actually been
|
||||
// loaded from disk yet.
|
||||
storyFlagInitDefaults(saveGet(SAVE_ACTIVE_SLOT));
|
||||
|
||||
backpackInit();
|
||||
partyInit();
|
||||
cutsceneSystemInit();
|
||||
|
||||
errorChain(mapInit("testmap"));
|
||||
errorChain(mapInit());
|
||||
|
||||
rpgCameraInit();
|
||||
// Init world
|
||||
errorChain(chunkPositionSet((chunkpos_t){ 0, 0, 0 }));
|
||||
errorChain(mapPositionSet((chunkpos_t){ 0, 0, 0 }));
|
||||
|
||||
// TEST: Give the player a starting assortment of items.
|
||||
backpackAdd(itemGetIdByName("POTION"), 5);
|
||||
backpackAdd(itemGetIdByName("POTATO"), 3);
|
||||
backpackAdd(itemGetIdByName("APPLE"), 8);
|
||||
// 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];
|
||||
entityInit(ent, ENTITY_TYPE_PLAYER);
|
||||
entityPositionSet(ent, (worldpos_t){ 10, 2, 0 });// Also assigns the chunk.
|
||||
RPG_CAMERA.mode = RPG_CAMERA_MODE_FOLLOW_ENTITY;
|
||||
RPG_CAMERA.followEntity.followEntityId = ent->id;
|
||||
|
||||
// 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!.");
|
||||
// Starting inventory.
|
||||
backpackAdd(ITEM_ID_POTION, 5);
|
||||
backpackAdd(ITEM_ID_POTATO, 3);
|
||||
backpackAdd(ITEM_ID_APPLE, 8);
|
||||
|
||||
// TEST: Verify the save system round-trips real game data, not just the
|
||||
// header/version. Remove once there's an actual name-entry flow. On PSP
|
||||
// this shows the real native save dialog every boot - expected while
|
||||
// testing that path, not something to ship as-is.
|
||||
savefile_t *saveFile = saveGet(SAVE_ACTIVE_SLOT);
|
||||
stringCopy(saveFile->playerName, "Dusk", SAVE_PLAYER_NAME_MAX);
|
||||
saveWrite(SAVE_ACTIVE_SLOT, rpgTestSaveComplete, NULL);
|
||||
|
||||
// All Good!
|
||||
errorOk();
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include "util/random.h"
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "rpg/overworld/map.h"
|
||||
#include "rpg/overworld/chunk.h"
|
||||
#include "assert/assert.h"
|
||||
#include "time/time.h"
|
||||
|
||||
@@ -115,17 +114,6 @@ errorret_t rpgCameraUpdate(void) {
|
||||
RPG_CAMERA.shakeTime += TIME.delta;
|
||||
}
|
||||
|
||||
// The player entity may spawn asynchronously (e.g. via map.json), so
|
||||
// start following it as soon as it shows up rather than requiring
|
||||
// whoever creates it to also wire up the camera.
|
||||
if(RPG_CAMERA.mode == RPG_CAMERA_MODE_FREE) {
|
||||
entity_t *player = entityGetByGlobalId(ENTITY_GLOBAL_ID_PLAYER);
|
||||
if(player != NULL) {
|
||||
RPG_CAMERA.mode = RPG_CAMERA_MODE_FOLLOW_ENTITY;
|
||||
RPG_CAMERA.followEntity.followEntityId = player->id;
|
||||
}
|
||||
}
|
||||
|
||||
if(!mapIsLoaded()) errorOk();
|
||||
|
||||
vec3 pos;
|
||||
@@ -137,7 +125,7 @@ errorret_t rpgCameraUpdate(void) {
|
||||
.z = (chunkunit_t)floorf(pos[2] / WORLD_LAYER_HEIGHT / CHUNK_DEPTH)
|
||||
};
|
||||
|
||||
errorChain(chunkPositionSet((chunkpos_t){
|
||||
errorChain(mapPositionSet((chunkpos_t){
|
||||
.x = chunkPos.x - (MAP_CHUNK_WIDTH / 2),
|
||||
.y = chunkPos.y - (MAP_CHUNK_HEIGHT / 2),
|
||||
.z = chunkPos.z - (MAP_CHUNK_DEPTH / 2)
|
||||
|
||||
@@ -10,5 +10,17 @@
|
||||
|
||||
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;
|
||||
saveGet(SAVE_ACTIVE_SLOT)->storyFlags[flag] = value;
|
||||
}
|
||||
|
||||
void storyFlagInitDefaults(savefile_t *file) {
|
||||
assertNotNull(file, "Save file cannot be NULL");
|
||||
if(file->exists) return;
|
||||
assertTrue(
|
||||
STORY_FLAG_COUNT <= SAVE_STORY_FLAG_COUNT_MAX,
|
||||
"Too many story flags for the save format - bump SAVE_STORY_FLAG_COUNT_MAX"
|
||||
);
|
||||
for(storyflag_t i = 0; i < STORY_FLAG_COUNT; i++) {
|
||||
file->storyFlags[i] = STORY_FLAG_DEFAULTS[i];
|
||||
}
|
||||
}
|
||||
@@ -7,19 +7,34 @@
|
||||
|
||||
#pragma once
|
||||
#include "rpg/story/storyflagvalue.h"
|
||||
#include "save/save.h"
|
||||
|
||||
/**
|
||||
* Gets the value of a story flag.
|
||||
* Gets the value of a story flag. Reads directly from the active save
|
||||
* file (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)])
|
||||
#define storyFlagGet(flag) (saveGet(SAVE_ACTIVE_SLOT)->storyFlags[(flag)])
|
||||
|
||||
/**
|
||||
* Sets the value of a story flag.
|
||||
* Sets the value of a story flag, directly in the active save file (see
|
||||
* SAVE_ACTIVE_SLOT). Does not itself write the save to disk - call
|
||||
* saveWrite() separately once ready to persist it.
|
||||
*
|
||||
* @param flag The story flag to set.
|
||||
* @param value The value to set the story flag to.
|
||||
*/
|
||||
void storyFlagSet(const storyflag_t flag, const storyflagvalue_t value);
|
||||
|
||||
/**
|
||||
* Stamps each story flag's CSV-defined default (STORY_FLAG_DEFAULTS) onto
|
||||
* the given save file, but only if it hasn't actually been loaded from
|
||||
* disk yet (file->exists is false) - otherwise leaves already-played
|
||||
* progress alone. Call once, e.g. during rpgInit(), before any gameplay
|
||||
* code reads a story flag.
|
||||
*
|
||||
* @param file The save file to stamp defaults onto.
|
||||
*/
|
||||
void storyFlagInitDefaults(savefile_t *file);
|
||||
|
||||
+75
-17
@@ -9,19 +9,38 @@
|
||||
#include "save/savestream.h"
|
||||
#include "util/memory.h"
|
||||
#include "assert/assert.h"
|
||||
#include "error/error.h"
|
||||
|
||||
save_t SAVE;
|
||||
|
||||
errorret_t saveInit(void) {
|
||||
memoryZero(&SAVE, sizeof(save_t));
|
||||
|
||||
// Establishes the default for a slot that hasn't actually been loaded
|
||||
// from disk yet - saveLoad() overwrites this the moment a real file is
|
||||
// found, so this only matters for a brand new save.
|
||||
for(uint8_t i = 0; i < SAVE_FILE_COUNT_MAX; i++) {
|
||||
SAVE.files[i].deadzone = SAVE_DEADZONE_DEFAULT;
|
||||
}
|
||||
|
||||
#ifdef saveInitPlatform
|
||||
errorChain(saveInitPlatform());
|
||||
// A missing/unreachable save medium is expected, recoverable state,
|
||||
// not a reason to fail booting the whole game - log it and carry on
|
||||
// with SAVE.available false instead of chaining the error upward.
|
||||
errorret_t result = saveInitPlatform();
|
||||
SAVE.available = errorIsOk(result);
|
||||
if(!SAVE.available) errorCatch(errorPrint(result));
|
||||
#else
|
||||
SAVE.available = false;
|
||||
#endif
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
bool_t saveIsAvailable(void) {
|
||||
return SAVE.available;
|
||||
}
|
||||
|
||||
errorret_t saveDispose(void) {
|
||||
#ifdef saveDisposePlatform
|
||||
errorChain(saveDisposePlatform());
|
||||
@@ -29,20 +48,46 @@ errorret_t saveDispose(void) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveLoad(const uint8_t slot) {
|
||||
errorret_t saveUpdate(void) {
|
||||
#ifdef savePlatformUpdate
|
||||
errorChain(savePlatformUpdate());
|
||||
#endif
|
||||
errorOk();
|
||||
}
|
||||
|
||||
bool_t saveIsBusy(void) {
|
||||
#ifdef saveIsBusyPlatform
|
||||
return saveIsBusyPlatform();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void saveLoad(const uint8_t slot, savecallback_t onComplete, void *user) {
|
||||
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
|
||||
assertNotNull(onComplete, "onComplete cannot be NULL");
|
||||
|
||||
savefile_t *file = &SAVE.files[slot];
|
||||
file->exists = false;
|
||||
|
||||
// Some platforms (PSP's native save dialog) can't complete within this
|
||||
// call - they take over entirely and invoke onComplete later, from
|
||||
// saveUpdate(), once their own multi-frame flow finishes.
|
||||
#ifdef saveAsyncLoadPlatform
|
||||
saveAsyncLoadPlatform(slot, onComplete, user);
|
||||
return;
|
||||
#endif
|
||||
|
||||
savestream_t stream;
|
||||
memoryZero(&stream, sizeof(savestream_t));
|
||||
|
||||
#ifdef saveStreamOpenReadPlatform
|
||||
errorChain(saveStreamOpenReadPlatform(&stream, slot));
|
||||
errorret_t openRet = saveStreamOpenReadPlatform(&stream, slot);
|
||||
SAVE.available = errorIsOk(openRet);
|
||||
if(errorIsNotOk(openRet)) { onComplete(openRet, user); return; }
|
||||
#endif
|
||||
|
||||
if(!stream.found) errorOk();
|
||||
if(!stream.found) { onComplete(errorOkImpl(), user); return; }
|
||||
|
||||
errorret_t ret = saveFileLoad(&stream, file);
|
||||
|
||||
@@ -50,24 +95,37 @@ errorret_t saveLoad(const uint8_t slot) {
|
||||
saveStreamClosePlatform(&stream);
|
||||
#endif
|
||||
|
||||
if(errorIsNotOk(ret)) return ret;
|
||||
|
||||
errorChain(saveStreamVerifyChecksumImpl(&stream, slot));
|
||||
|
||||
file->exists = true;
|
||||
errorOk();
|
||||
if(errorIsOk(ret)) ret = saveStreamVerifyChecksumImpl(&stream, slot);
|
||||
file->exists = errorIsOk(ret);
|
||||
onComplete(ret, user);
|
||||
}
|
||||
|
||||
errorret_t saveWrite(const uint8_t slot) {
|
||||
void saveWrite(const uint8_t slot, savecallback_t onComplete, void *user) {
|
||||
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
|
||||
assertNotNull(onComplete, "onComplete cannot be NULL");
|
||||
|
||||
savefile_t *file = &SAVE.files[slot];
|
||||
// These are metadata about the file itself, not game data - always stamp
|
||||
// the current magic/version on every write rather than relying on
|
||||
// whatever happened to already be in memory (zeroed at saveInit, or
|
||||
// whatever version an old loaded file had), otherwise the written file
|
||||
// fails its own header check the next time it's loaded.
|
||||
memoryCopy(file->header, SAVE_FILE_HEADER, SAVE_FILE_HEADER_SIZE);
|
||||
file->version = SAVE_FILE_VERSION;
|
||||
|
||||
// See saveLoad() - some platforms take over and complete later.
|
||||
#ifdef saveAsyncWritePlatform
|
||||
saveAsyncWritePlatform(slot, onComplete, user);
|
||||
return;
|
||||
#endif
|
||||
|
||||
savestream_t stream;
|
||||
memoryZero(&stream, sizeof(savestream_t));
|
||||
|
||||
#ifdef saveStreamOpenWritePlatform
|
||||
errorChain(saveStreamOpenWritePlatform(&stream, slot));
|
||||
errorret_t openRet = saveStreamOpenWritePlatform(&stream, slot);
|
||||
SAVE.available = errorIsOk(openRet);
|
||||
if(errorIsNotOk(openRet)) { onComplete(openRet, user); return; }
|
||||
#endif
|
||||
|
||||
errorret_t ret = saveFileWrite(&stream, file);
|
||||
@@ -80,17 +138,17 @@ errorret_t saveWrite(const uint8_t slot) {
|
||||
saveStreamClosePlatform(&stream);
|
||||
#endif
|
||||
|
||||
if(errorIsNotOk(ret)) return ret;
|
||||
|
||||
file->exists = true;
|
||||
errorOk();
|
||||
file->exists = errorIsOk(ret);
|
||||
onComplete(ret, user);
|
||||
}
|
||||
|
||||
errorret_t saveDelete(const uint8_t slot) {
|
||||
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
|
||||
|
||||
#ifdef saveDeletePlatform
|
||||
errorChain(saveDeletePlatform(slot));
|
||||
errorret_t deleteRet = saveDeletePlatform(slot);
|
||||
SAVE.available = errorIsOk(deleteRet);
|
||||
errorChain(deleteRet);
|
||||
#endif
|
||||
|
||||
SAVE.files[slot].exists = false;
|
||||
|
||||
+67
-9
@@ -15,17 +15,46 @@ typedef struct {
|
||||
savefile_t files[SAVE_FILE_COUNT_MAX];
|
||||
/** Platform-specific save system state (paths, card handles, etc.). */
|
||||
saveplatform_t platform;
|
||||
/**
|
||||
* True if the save medium (memory card/stick/disk) was reachable the
|
||||
* last time it was checked - at saveInit(), and refreshed by every
|
||||
* subsequent saveLoad()/saveWrite() attempt. Starting the game with no
|
||||
* card/stick inserted, or one being removed mid-session, are both
|
||||
* expected conditions here, not fatal errors - see saveIsAvailable().
|
||||
*/
|
||||
bool_t available;
|
||||
/**
|
||||
* Scratch error state used by platforms whose save/load completes
|
||||
* asynchronously (see saveIsBusy()) to construct a result to hand to a
|
||||
* savecallback_t from inside saveUpdate(), rather than from a direct
|
||||
* errorThrow() return - mirrors network_t.errorState for the same reason.
|
||||
*/
|
||||
errorstate_t errorState;
|
||||
} save_t;
|
||||
|
||||
extern save_t SAVE;
|
||||
|
||||
/**
|
||||
* Initializes the save system.
|
||||
* Initializes the save system. Never fails the way saveWrite/saveLoad can -
|
||||
* if the platform's save medium isn't reachable (e.g. no memory card/stick
|
||||
* inserted), that's logged and reflected in saveIsAvailable() rather than
|
||||
* treated as fatal, since the game should still be playable without save
|
||||
* support.
|
||||
*
|
||||
* @return An error code if initialization fails.
|
||||
* @return An error code only for unexpected platform failures.
|
||||
*/
|
||||
errorret_t saveInit(void);
|
||||
|
||||
/**
|
||||
* Checks whether the save medium was reachable as of the last save/load
|
||||
* attempt (or saveInit(), if none has been attempted yet). Intended for UI
|
||||
* to decide whether to offer saving/loading at all, or to explain why it
|
||||
* isn't available right now - e.g. "No memory card inserted".
|
||||
*
|
||||
* @return true if the save medium was available last time it was checked.
|
||||
*/
|
||||
bool_t saveIsAvailable(void);
|
||||
|
||||
/**
|
||||
* Disposes of the save system.
|
||||
*
|
||||
@@ -34,20 +63,49 @@ errorret_t saveInit(void);
|
||||
errorret_t saveDispose(void);
|
||||
|
||||
/**
|
||||
* Loads the save file for a given slot from persistent storage.
|
||||
* Updates the save manager, pumping any in-progress async save/load and
|
||||
* dispatching its callback once complete. No-op on platforms where
|
||||
* saveWrite()/saveLoad() always complete synchronously (see saveIsBusy()).
|
||||
* Must be called every engine frame for platforms that need it (PSP's
|
||||
* native save dialog spans multiple frames).
|
||||
*
|
||||
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1).
|
||||
* @return An error code if the load fails.
|
||||
* @return An error code indicating success or failure.
|
||||
*/
|
||||
errorret_t saveLoad(const uint8_t slot);
|
||||
errorret_t saveUpdate(void);
|
||||
|
||||
/**
|
||||
* Writes the save file for a given slot to persistent storage.
|
||||
* True while an async saveWrite()/saveLoad() is in progress (e.g. PSP's
|
||||
* native save dialog is open). Calling saveWrite()/saveLoad() again while
|
||||
* this is true is undefined behavior - wait for the previous call's
|
||||
* callback first.
|
||||
*
|
||||
* @return True if a save/load request is currently in progress.
|
||||
*/
|
||||
bool_t saveIsBusy(void);
|
||||
|
||||
/**
|
||||
* Loads the save file for a given slot from persistent storage. Slow/async
|
||||
* on some platforms (PSP's native save dialog spans multiple frames) - on
|
||||
* others (Linux, Dolphin) onComplete is invoked before this call returns.
|
||||
* See saveIsBusy().
|
||||
*
|
||||
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1).
|
||||
* @return An error code if the write fails.
|
||||
* @param onComplete Callback invoked with the result once loading finishes.
|
||||
* @param user User data passed through to onComplete.
|
||||
*/
|
||||
errorret_t saveWrite(const uint8_t slot);
|
||||
void saveLoad(const uint8_t slot, savecallback_t onComplete, void *user);
|
||||
|
||||
/**
|
||||
* Writes the save file for a given slot to persistent storage. Slow/async
|
||||
* on some platforms (PSP's native save dialog spans multiple frames) - on
|
||||
* others (Linux, Dolphin) onComplete is invoked before this call returns.
|
||||
* See saveIsBusy().
|
||||
*
|
||||
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1).
|
||||
* @param onComplete Callback invoked with the result once writing finishes.
|
||||
* @param user User data passed through to onComplete.
|
||||
*/
|
||||
void saveWrite(const uint8_t slot, savecallback_t onComplete, void *user);
|
||||
|
||||
/**
|
||||
* Deletes the save file for a given slot from persistent storage.
|
||||
|
||||
@@ -20,6 +20,44 @@
|
||||
/** Maximum number of independent save slots supported. */
|
||||
#define SAVE_FILE_COUNT_MAX 3
|
||||
|
||||
/**
|
||||
* The save slot actually used for gameplay right now - there's no slot
|
||||
* select/multi-save UX yet (SAVE_FILE_COUNT_MAX > 1 exists for later), so
|
||||
* every part of the game that needs "the" save file (settings, the game
|
||||
* menu's Save button, etc.) reads/writes this one slot.
|
||||
*/
|
||||
#define SAVE_ACTIVE_SLOT 0
|
||||
|
||||
/** Maximum length of a saved player name, including the null terminator. */
|
||||
#define SAVE_PLAYER_NAME_MAX 32
|
||||
|
||||
/**
|
||||
* Maximum number of global entities whose "collected" state can be
|
||||
* tracked - see rpg/entity/global/globalitemstore.h. Bounded/fixed here
|
||||
* rather than tied to ENTITY_GLOBAL_LIST_COUNT, since savefile.h is a
|
||||
* leaf header with no dependency on the entity system (and no reason to
|
||||
* take one just for a size constant).
|
||||
*/
|
||||
#define SAVE_GLOBAL_ITEM_COUNT_MAX 64
|
||||
|
||||
/**
|
||||
* Default gamepad deadzone for a save slot that's never actually been
|
||||
* loaded from disk yet (see saveInit(), which stamps this onto every
|
||||
* slot up front) - defined here, rather than by the input system, since
|
||||
* the save file is now the single source of truth for this value (see
|
||||
* savefile_t.deadzone) - nothing else stores or defaults it.
|
||||
*/
|
||||
#define SAVE_DEADZONE_DEFAULT 0.1f
|
||||
|
||||
/**
|
||||
* Maximum number of story flags the save format can hold - see
|
||||
* rpg/story/storyflag.h. Bounded/fixed here (with real headroom over the
|
||||
* current flag count) rather than tied to STORY_FLAG_COUNT, since
|
||||
* savefile.h is a leaf header with no dependency on generated story
|
||||
* content, matching SAVE_GLOBAL_ITEM_COUNT_MAX's reasoning.
|
||||
*/
|
||||
#define SAVE_STORY_FLAG_COUNT_MAX 128
|
||||
|
||||
typedef struct {
|
||||
/** Magic header bytes read from the file; must equal SAVE_FILE_HEADER. */
|
||||
char_t header[SAVE_FILE_HEADER_SIZE];
|
||||
@@ -27,4 +65,31 @@ typedef struct {
|
||||
uint32_t version;
|
||||
/** Runtime flag - true if this slot was successfully loaded or written. */
|
||||
bool_t exists;
|
||||
/** The player's saved name. */
|
||||
char_t playerName[SAVE_PLAYER_NAME_MAX];
|
||||
/** Per-global-ID "already collected" flags - see globalitemstore.h. */
|
||||
bool_t globalItemCollected[SAVE_GLOBAL_ITEM_COUNT_MAX];
|
||||
/**
|
||||
* User-configured gamepad deadzone (0.0f-1.0f) - the save file is the
|
||||
* only place this lives; read it directly via saveGet(SAVE_ACTIVE_SLOT)
|
||||
* ->deadzone rather than caching it anywhere else.
|
||||
*/
|
||||
float_t deadzone;
|
||||
/**
|
||||
* Story flag values, indexed by storyflag_t - the save file is the only
|
||||
* place these live; read/write via storyFlagGet()/storyFlagSet() (see
|
||||
* rpg/story/storyflag.h), not directly.
|
||||
*/
|
||||
uint8_t storyFlags[SAVE_STORY_FLAG_COUNT_MAX];
|
||||
} savefile_t;
|
||||
|
||||
/**
|
||||
* Callback invoked when an async saveWrite()/saveLoad() request completes.
|
||||
* Declared here (rather than save.h) so platform save headers - which
|
||||
* save.h's platform indirection pulls in before save.h finishes defining
|
||||
* anything else - can reference it without a circular include.
|
||||
*
|
||||
* @param result Whether the request succeeded.
|
||||
* @param user User data passed through from the original call.
|
||||
*/
|
||||
typedef void (*savecallback_t)(errorret_t result, void *user);
|
||||
|
||||
@@ -330,11 +330,27 @@ errorret_t saveStreamWriteDateImpl(
|
||||
errorret_t saveFileLoad(savestream_t *stream, savefile_t *file) {
|
||||
saveFileReadHeader(stream, file->header);
|
||||
saveFileReadVersion(stream, &file->version);
|
||||
saveFileReadString(stream, file->playerName, SAVE_PLAYER_NAME_MAX);
|
||||
for(size_t i = 0; i < SAVE_GLOBAL_ITEM_COUNT_MAX; i++) {
|
||||
saveFileReadBool(stream, &file->globalItemCollected[i]);
|
||||
}
|
||||
saveFileReadFloat(stream, &file->deadzone);
|
||||
for(size_t i = 0; i < SAVE_STORY_FLAG_COUNT_MAX; i++) {
|
||||
saveFileReadUInt8(stream, &file->storyFlags[i]);
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveFileWrite(savestream_t *stream, savefile_t *file) {
|
||||
saveFileWriteHeader(stream, file->header);
|
||||
saveFileWriteVersion(stream, &file->version);
|
||||
saveFileWriteString(stream, file->playerName, SAVE_PLAYER_NAME_MAX);
|
||||
for(size_t i = 0; i < SAVE_GLOBAL_ITEM_COUNT_MAX; i++) {
|
||||
saveFileWriteBool(stream, &file->globalItemCollected[i]);
|
||||
}
|
||||
saveFileWriteFloat(stream, &file->deadzone);
|
||||
for(size_t i = 0; i < SAVE_STORY_FLAG_COUNT_MAX; i++) {
|
||||
saveFileWriteUInt8(stream, &file->storyFlags[i]);
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/texture/texture.h"
|
||||
|
||||
#include "rpg/overworld/chunk.h"
|
||||
#include "rpg/overworld/map.h"
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "rpg/rpgcamera.h"
|
||||
|
||||
@@ -183,7 +183,7 @@ errorret_t sceneOverworldDrawEntity(
|
||||
|
||||
errorret_t sceneOverworldDrawChunksBase(const sceneoverworld_t *overworld) {
|
||||
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||
chunk_t *chunk = CHUNK_ORDER[i];
|
||||
chunk_t *chunk = MAP.chunkOrder[i];
|
||||
if(chunk == NULL) continue;
|
||||
if(!sceneOverworldChunkShouldRender(overworld, chunk)) continue;
|
||||
if(chunk->meshCount == 0) continue;
|
||||
@@ -220,7 +220,7 @@ errorret_t sceneOverworldDrawChunksBase(const sceneoverworld_t *overworld) {
|
||||
|
||||
errorret_t sceneOverworldDrawChunksProps(const sceneoverworld_t *overworld) {
|
||||
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||
chunk_t *chunk = CHUNK_ORDER[i];
|
||||
chunk_t *chunk = MAP.chunkOrder[i];
|
||||
if(chunk == NULL) continue;
|
||||
if(!sceneOverworldChunkShouldRender(overworld, chunk)) continue;
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ void uiBackpackTabChanged(
|
||||
const uint8_t index,
|
||||
const uimenuitem_t *item
|
||||
) {
|
||||
const itemtypeid_t type = (itemtypeid_t)(index + 1);
|
||||
const itemtype_t type = (itemtype_t)(index + 1);
|
||||
const inventory_t *inventory = backpackGetInventory(type);
|
||||
|
||||
errorCatch(uiItemListSetItemStacks(
|
||||
@@ -41,16 +41,11 @@ void uiBackpackTabSelected(
|
||||
errorret_t uiBackpackInit(void) {
|
||||
memoryZero(&UI_BACKPACK, sizeof(uibackpack_t));
|
||||
|
||||
assertTrue(
|
||||
ITEM_TYPE_COUNT - 1 <= UI_BACKPACK_TAB_COUNT,
|
||||
"Item type count exceeds UI_BACKPACK_TAB_COUNT"
|
||||
);
|
||||
|
||||
MENU_BEGIN(
|
||||
&UI_BACKPACK.tabsMenu, UI_BACKPACK.tabs,
|
||||
uiBackpackTabSelected, NULL, uiBackpackTabChanged
|
||||
);
|
||||
for(uint32_t i = 0; i < ITEM_TYPE_COUNT - 1; i++) {
|
||||
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
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
#include "ui/widget/uiitemlist.h"
|
||||
#include "rpg/item/item.h"
|
||||
|
||||
#define UI_BACKPACK_TAB_COUNT (ITEM_TYPE_COUNT_MAX - 1)
|
||||
#define UI_BACKPACK_TAB_COUNT (ITEM_TYPE_COUNT - 1)
|
||||
#define UI_BACKPACK_TAB_LABEL_MAX 32
|
||||
#define UI_BACKPACK_ITEM_LIST_COLUMNS 4
|
||||
#define UI_BACKPACK_ITEM_LIST_ROWS 5
|
||||
|
||||
@@ -7,21 +7,88 @@
|
||||
|
||||
#include "uigamemenu.h"
|
||||
#include "ui/frame/uiframe.h"
|
||||
#include "ui/frame/uiconfirm.h"
|
||||
#include "ui/frame/settings/uisettings.h"
|
||||
#include "ui/frame/backpack/uibackpack.h"
|
||||
#include "ui/rpg/textbox/uitextboxmain.h"
|
||||
#include "util/memory.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "display/text/text.h"
|
||||
#include "display/color.h"
|
||||
#include "assert/assert.h"
|
||||
#include "locale/localemanager.h"
|
||||
#include "asset/loader/locale/assetlocaleloader.h"
|
||||
#include "rpg/overworld/map.h"
|
||||
#include "save/save.h"
|
||||
#include "error/error.h"
|
||||
#include "util/string.h"
|
||||
|
||||
#define UI_GAME_MENU_INDEX_CHARACTERS 0
|
||||
#define UI_GAME_MENU_INDEX_ITEMS 1
|
||||
#define UI_GAME_MENU_INDEX_SETTINGS 2
|
||||
#define UI_GAME_MENU_INDEX_SAVE 3
|
||||
|
||||
static void uiGameMenuSaveWriteComplete(errorret_t result, void *user) {
|
||||
if(errorIsNotOk(result)) {
|
||||
// Generously sized - stringFormat asserts (crashes) rather than
|
||||
// truncating if the message doesn't fit, so this must comfortably fit
|
||||
// the longest platform save-error message plus this prefix.
|
||||
char_t msg[256];
|
||||
stringFormat(
|
||||
msg, sizeof(msg), "Save failed: %s", result.state->message
|
||||
);
|
||||
errorCatch(result);
|
||||
uiTextboxMainSetText(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
uiTextboxMainSetText("Game saved.");
|
||||
}
|
||||
|
||||
static void uiGameMenuSaveCreateConfirmed(const bool_t confirmed, void *user) {
|
||||
if(!confirmed) {
|
||||
uiTextboxMainSetText("Save cancelled.");
|
||||
return;
|
||||
}
|
||||
|
||||
saveWrite(SAVE_ACTIVE_SLOT, uiGameMenuSaveWriteComplete, NULL);
|
||||
}
|
||||
|
||||
// Determines whether there's actually save data to overwrite (not just
|
||||
// whether the medium is present) by attempting a real load first - this is
|
||||
// what lets a fresh memory card/stick, with no prior save on it yet, be
|
||||
// told apart from one that already has our data on it. Cheap either way
|
||||
// (a single sector/file read), and correct on every platform without any
|
||||
// platform-specific UI code - saveExists() already reflects each
|
||||
// platform's own notion of "found something."
|
||||
static void uiGameMenuSaveCheckComplete(errorret_t result, void *user) {
|
||||
if(errorIsNotOk(result)) {
|
||||
char_t msg[256];
|
||||
stringFormat(msg, sizeof(msg), "Can't save: %s", result.state->message);
|
||||
errorCatch(result);
|
||||
uiTextboxMainSetText(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if(saveExists(SAVE_ACTIVE_SLOT)) {
|
||||
saveWrite(SAVE_ACTIVE_SLOT, uiGameMenuSaveWriteComplete, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
uiConfirmOpen(
|
||||
"No save data found. Create a new save?",
|
||||
uiGameMenuSaveCreateConfirmed,
|
||||
NULL
|
||||
);
|
||||
}
|
||||
|
||||
static void uiGameMenuSave(void) {
|
||||
if(!saveIsAvailable()) {
|
||||
uiTextboxMainSetText("Can't save - no save device found.");
|
||||
return;
|
||||
}
|
||||
if(saveIsBusy()) return;// A save/load dialog (e.g. on PSP) is already up.
|
||||
|
||||
saveLoad(SAVE_ACTIVE_SLOT, uiGameMenuSaveCheckComplete, NULL);
|
||||
}
|
||||
|
||||
uigamemenu_t UI_GAME_MENU;
|
||||
|
||||
@@ -32,6 +99,7 @@ void uiGameMenuSelected(
|
||||
) {
|
||||
if(index == UI_GAME_MENU_INDEX_ITEMS) uiBackpackOpen();
|
||||
if(index == UI_GAME_MENU_INDEX_SETTINGS) uiSettingsOpen();
|
||||
if(index == UI_GAME_MENU_INDEX_SAVE) uiGameMenuSave();
|
||||
}
|
||||
|
||||
errorret_t uiGameMenuInit(void) {
|
||||
@@ -58,6 +126,13 @@ errorret_t uiGameMenuInit(void) {
|
||||
UI_GAME_MENU.settingsLabel,
|
||||
UI_GAME_MENU_LABEL_MAX
|
||||
));
|
||||
errorChain(assetLocaleGetString(
|
||||
&LOCALE.entry->data.locale,
|
||||
"ui.game_menu.save",
|
||||
0,
|
||||
UI_GAME_MENU.saveLabel,
|
||||
UI_GAME_MENU_LABEL_MAX
|
||||
));
|
||||
|
||||
MENU_BEGIN(
|
||||
&UI_GAME_MENU.menu, UI_GAME_MENU.items, uiGameMenuSelected, NULL, NULL
|
||||
@@ -65,6 +140,7 @@ errorret_t uiGameMenuInit(void) {
|
||||
MENU_BUTTON(UI_GAME_MENU.charactersLabel);
|
||||
MENU_BUTTON(UI_GAME_MENU.itemsLabel);
|
||||
MENU_BUTTON(UI_GAME_MENU.settingsLabel);
|
||||
MENU_BUTTON(UI_GAME_MENU.saveLabel);
|
||||
|
||||
MENU_END(UI_GAME_MENU.items, 1);
|
||||
|
||||
@@ -80,25 +156,12 @@ errorret_t uiGameMenuDraw(void) {
|
||||
const float_t y = (float_t)SCREEN.scanY;
|
||||
|
||||
errorChain(uiFrameDraw(x, y, width, height));
|
||||
|
||||
const float_t contentX = x + UI_FRAME_START_X;
|
||||
const float_t contentY = y + UI_FRAME_START_Y;
|
||||
const float_t contentWidth = width - (UI_FRAME_START_X * 2);
|
||||
const float_t contentHeight = height - (UI_FRAME_START_Y * 2);
|
||||
|
||||
// Map display name header - stopgap placement until this gets a proper
|
||||
// HUD element of its own.
|
||||
const float_t nameRowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||
errorChain(textDraw(
|
||||
contentX, contentY, MAP.displayName, COLOR_WHITE, &FONT_DEFAULT
|
||||
));
|
||||
|
||||
errorChain(uiMenuDraw(
|
||||
&UI_GAME_MENU.menu,
|
||||
contentX,
|
||||
contentY + nameRowHeight + UI_FRAME_PADDING_Y,
|
||||
contentWidth,
|
||||
contentHeight - nameRowHeight - UI_FRAME_PADDING_Y
|
||||
x + UI_FRAME_START_X,
|
||||
y + UI_FRAME_START_Y,
|
||||
width - (UI_FRAME_START_X * 2),
|
||||
height - (UI_FRAME_START_Y * 2)
|
||||
));
|
||||
|
||||
errorChain(spriteBatchFlush());
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include "error/error.h"
|
||||
#include "ui/widget/uimenu.h"
|
||||
|
||||
#define UI_GAME_MENU_ITEM_COUNT 3
|
||||
#define UI_GAME_MENU_ITEM_COUNT 4
|
||||
#define UI_GAME_MENU_WIDTH 150.0f
|
||||
#define UI_GAME_MENU_LABEL_MAX 32
|
||||
|
||||
@@ -19,6 +19,7 @@ typedef struct {
|
||||
char_t charactersLabel[UI_GAME_MENU_LABEL_MAX];
|
||||
char_t itemsLabel[UI_GAME_MENU_LABEL_MAX];
|
||||
char_t settingsLabel[UI_GAME_MENU_LABEL_MAX];
|
||||
char_t saveLabel[UI_GAME_MENU_LABEL_MAX];
|
||||
} uigamemenu_t;
|
||||
|
||||
extern uigamemenu_t UI_GAME_MENU;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
#include "util/memory.h"
|
||||
#include "locale/localemanager.h"
|
||||
#include "asset/loader/locale/assetlocaleloader.h"
|
||||
#include "input/input.h"
|
||||
#include "save/save.h"
|
||||
|
||||
void uiSettingsInputSelected(
|
||||
const uimenu_t *menu,
|
||||
@@ -39,7 +39,7 @@ errorret_t uiSettingsInputInit(uisettingsdata_t *data) {
|
||||
UI_SETTINGS_INPUT_LABEL_MAX
|
||||
));
|
||||
MENU_SLIDER_FLOAT(
|
||||
input->deadzoneLabel, INPUT_DEADZONE_DEFAULT, 0.0f, 1.0f, 0.05f
|
||||
input->deadzoneLabel, SAVE_DEADZONE_DEFAULT, 0.0f, 1.0f, 0.05f
|
||||
);
|
||||
#else
|
||||
MENU_LABEL("No input settings yet");
|
||||
@@ -55,14 +55,14 @@ void uiSettingsInputLoad(void) {
|
||||
#ifdef DUSK_INPUT_GAMEPAD
|
||||
uiSliderSetFloat(
|
||||
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider,
|
||||
INPUT.deadzone
|
||||
saveGet(SAVE_ACTIVE_SLOT)->deadzone
|
||||
);
|
||||
#endif
|
||||
}
|
||||
|
||||
void uiSettingsInputApply(void) {
|
||||
#ifdef DUSK_INPUT_GAMEPAD
|
||||
INPUT.deadzone = uiSliderGetFloat(
|
||||
saveGet(SAVE_ACTIVE_SLOT)->deadzone = uiSliderGetFloat(
|
||||
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider
|
||||
);
|
||||
#endif
|
||||
@@ -73,7 +73,7 @@ bool_t uiSettingsInputHasChanges(void) {
|
||||
#ifdef DUSK_INPUT_GAMEPAD
|
||||
if(uiSliderGetFloat(
|
||||
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider
|
||||
) != INPUT.deadzone) return true;
|
||||
) != saveGet(SAVE_ACTIVE_SLOT)->deadzone) return true;
|
||||
#endif
|
||||
|
||||
return false;
|
||||
|
||||
@@ -44,7 +44,7 @@ void uiItemListSetItems(
|
||||
) {
|
||||
assertNotNull(list, "Item list cannot be NULL");
|
||||
assertTrue(
|
||||
itemCount <= INVENTORY_CAPACITY_MAX, "Too many items for list"
|
||||
itemCount <= UI_ITEM_LIST_CAPACITY_MAX, "Too many items for list"
|
||||
);
|
||||
|
||||
memoryCopy(list->items, items, sizeof(uiitem_t) * itemCount);
|
||||
@@ -58,7 +58,7 @@ errorret_t uiItemListSetItemStacks(
|
||||
) {
|
||||
assertNotNull(list, "Item list cannot be NULL");
|
||||
assertTrue(
|
||||
stackCount <= INVENTORY_CAPACITY_MAX, "Too many items for list"
|
||||
stackCount <= UI_ITEM_LIST_CAPACITY_MAX, "Too many items for list"
|
||||
);
|
||||
|
||||
for(uint8_t i = 0; i < stackCount; i++) {
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include "ui/focus/uifocus.h"
|
||||
#include "rpg/item/inventory.h"
|
||||
|
||||
#define UI_ITEM_LIST_CAPACITY_MAX 40
|
||||
|
||||
typedef struct uiitemlist_s uiitemlist_t;
|
||||
|
||||
typedef void (*uiitemlistselectedcallback_t)(
|
||||
@@ -49,7 +51,7 @@ typedef errorret_t (*uiitemlistcolumncallback_t)(
|
||||
);
|
||||
|
||||
struct uiitemlist_s {
|
||||
uiitem_t items[INVENTORY_CAPACITY_MAX];
|
||||
uiitem_t items[UI_ITEM_LIST_CAPACITY_MAX];
|
||||
uint8_t itemCount;
|
||||
|
||||
// Grid layout: how many item slots wide/tall the list displays.
|
||||
@@ -103,7 +105,7 @@ void uiItemListInit(
|
||||
* @param list The item list to update.
|
||||
* @param items The items to display.
|
||||
* @param itemCount Number of entries in items. Must be <=
|
||||
* INVENTORY_CAPACITY_MAX.
|
||||
* UI_ITEM_LIST_CAPACITY_MAX.
|
||||
*/
|
||||
void uiItemListSetItems(
|
||||
uiitemlist_t *list,
|
||||
@@ -118,7 +120,7 @@ void uiItemListSetItems(
|
||||
* @param list The item list to update.
|
||||
* @param stacks The item stacks to display.
|
||||
* @param stackCount Number of entries in stacks. Must be <=
|
||||
* INVENTORY_CAPACITY_MAX.
|
||||
* UI_ITEM_LIST_CAPACITY_MAX.
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiItemListSetItemStacks(
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "assert/assert.h"
|
||||
#include "log/log.h"
|
||||
#include "util/string.h"
|
||||
#include "save/save.h"
|
||||
|
||||
inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
||||
#ifdef DUSK_INPUT_GAMEPAD
|
||||
@@ -187,5 +188,5 @@ float_t inputButtonGetValueDolphin(const inputbutton_t button) {
|
||||
}
|
||||
|
||||
float_t inputGetDeadzoneDolphin(const inputbutton_t button) {
|
||||
return 0.2f;
|
||||
return saveGet(SAVE_ACTIVE_SLOT)->deadzone;
|
||||
}
|
||||
@@ -9,23 +9,48 @@
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
|
||||
static void _saveGetFileName(
|
||||
const uint8_t slot, char_t *out, const size_t max
|
||||
) {
|
||||
snprintf(out, max, "%s_%u", SAVE_DOLPHIN_GAME_CODE, (uint32_t)slot);
|
||||
}
|
||||
|
||||
errorret_t saveInitDolphin(void) {
|
||||
SAVE.platform.mounted = false;
|
||||
|
||||
int32_t result = CARD_Mount(
|
||||
// Must run once before any other CARD_* call: sets up card_inited,
|
||||
// the per-channel control blocks (wait queues, alarms) CARD_Mount reads,
|
||||
// and initializes the DSP (needed for the card unlock sequence).
|
||||
// Skipping this leaves those structures unset, so CARD_Mount ends up
|
||||
// touching hardware state that was never brought up -- e.g. Dolphin's
|
||||
// "Trying to read 32 bits from an invalid MMIO" error -- rather than
|
||||
// failing cleanly with a CARD_ERROR_* code.
|
||||
int32_t result = CARD_Init(SAVE_DOLPHIN_GAME_CODE, NULL);
|
||||
if(result < 0) {
|
||||
errorThrow("Failed to initialize memory card subsystem: %s (%d)",
|
||||
saveCardErrorStringDolphin(result), result
|
||||
);
|
||||
}
|
||||
|
||||
do {
|
||||
result = CARD_Mount(
|
||||
SAVE_DOLPHIN_CHANNEL,
|
||||
SAVE.platform.cardBuffer,
|
||||
NULL
|
||||
);
|
||||
} while(result == CARD_ERROR_BUSY);
|
||||
|
||||
// Special-case the failures a player can actually act on; everything
|
||||
// else falls through to the generic, fully-enumerated message below.
|
||||
switch(result) {
|
||||
case CARD_ERROR_NOCARD:
|
||||
errorThrow("No memory card inserted in the slot");
|
||||
case CARD_ERROR_WRONGDEVICE:
|
||||
errorThrow("Unsupported device inserted in the memory card slot");
|
||||
case CARD_ERROR_BROKEN:
|
||||
errorThrow("Memory card is damaged or unformatted");
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if(result < 0) {
|
||||
errorThrow("Failed to mount memory card (error %d)", result);
|
||||
errorThrow("Failed to mount memory card: %s (%d)",
|
||||
saveCardErrorStringDolphin(result), result
|
||||
);
|
||||
}
|
||||
|
||||
SAVE.platform.mounted = true;
|
||||
@@ -42,19 +67,22 @@ errorret_t saveDisposeDolphin(void) {
|
||||
|
||||
errorret_t saveLoadDolphin(const uint8_t slot, savefile_t *file) {
|
||||
char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX];
|
||||
_saveGetFileName(slot, fileName, SAVE_DOLPHIN_FILE_NAME_MAX);
|
||||
saveGetFileNameDolphin(slot, fileName, SAVE_DOLPHIN_FILE_NAME_MAX);
|
||||
|
||||
int32_t result = CARD_Open(
|
||||
int32_t result;
|
||||
do {
|
||||
result = CARD_Open(
|
||||
SAVE_DOLPHIN_CHANNEL, fileName, &SAVE.platform.cardFile
|
||||
);
|
||||
} while(result == CARD_ERROR_BUSY);
|
||||
if(result == CARD_ERROR_NOFILE) {
|
||||
file->exists = false;
|
||||
errorOk();
|
||||
}
|
||||
if(result < 0) {
|
||||
file->exists = false;
|
||||
errorThrow("Failed to open memory card file for slot %u (error %d)",
|
||||
(uint32_t)slot, result
|
||||
errorThrow("Failed to open memory card file for slot %u: %s (%d)",
|
||||
(uint32_t)slot, saveCardErrorStringDolphin(result), result
|
||||
);
|
||||
}
|
||||
|
||||
@@ -64,16 +92,18 @@ errorret_t saveLoadDolphin(const uint8_t slot, savefile_t *file) {
|
||||
errorThrow("Failed to allocate memory card read buffer");
|
||||
}
|
||||
|
||||
do {
|
||||
result = CARD_Read(
|
||||
&SAVE.platform.cardFile, buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0
|
||||
);
|
||||
} while(result == CARD_ERROR_BUSY);
|
||||
CARD_Close(&SAVE.platform.cardFile);
|
||||
|
||||
if(result < 0) {
|
||||
memoryFree(buffer);
|
||||
file->exists = false;
|
||||
errorThrow("Failed to read memory card data for slot %u (error %d)",
|
||||
(uint32_t)slot, result
|
||||
errorThrow("Failed to read memory card data for slot %u: %s (%d)",
|
||||
(uint32_t)slot, saveCardErrorStringDolphin(result), result
|
||||
);
|
||||
}
|
||||
|
||||
@@ -86,7 +116,7 @@ errorret_t saveLoadDolphin(const uint8_t slot, savefile_t *file) {
|
||||
|
||||
errorret_t saveWriteDolphin(const uint8_t slot, const savefile_t *file) {
|
||||
char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX];
|
||||
_saveGetFileName(slot, fileName, SAVE_DOLPHIN_FILE_NAME_MAX);
|
||||
saveGetFileNameDolphin(slot, fileName, SAVE_DOLPHIN_FILE_NAME_MAX);
|
||||
|
||||
void *buffer = memoryAlign(32, SAVE_DOLPHIN_SECTOR_SIZE);
|
||||
if(!buffer) {
|
||||
@@ -96,34 +126,41 @@ errorret_t saveWriteDolphin(const uint8_t slot, const savefile_t *file) {
|
||||
memoryCopy(buffer, file, sizeof(savefile_t));
|
||||
|
||||
// Try open existing file first; create if absent.
|
||||
int32_t result = CARD_Open(
|
||||
int32_t result;
|
||||
do {
|
||||
result = CARD_Open(
|
||||
SAVE_DOLPHIN_CHANNEL, fileName, &SAVE.platform.cardFile
|
||||
);
|
||||
} while(result == CARD_ERROR_BUSY);
|
||||
if(result == CARD_ERROR_NOFILE) {
|
||||
do {
|
||||
result = CARD_Create(
|
||||
SAVE_DOLPHIN_CHANNEL,
|
||||
fileName,
|
||||
SAVE_DOLPHIN_SECTOR_SIZE,
|
||||
&SAVE.platform.cardFile
|
||||
);
|
||||
} while(result == CARD_ERROR_BUSY);
|
||||
}
|
||||
|
||||
if(result < 0) {
|
||||
memoryFree(buffer);
|
||||
errorThrow("Failed to open/create memory card file for slot %u (error %d)",
|
||||
(uint32_t)slot, result
|
||||
errorThrow("Failed to open/create memory card file for slot %u: %s (%d)",
|
||||
(uint32_t)slot, saveCardErrorStringDolphin(result), result
|
||||
);
|
||||
}
|
||||
|
||||
do {
|
||||
result = CARD_Write(
|
||||
&SAVE.platform.cardFile, buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0
|
||||
);
|
||||
} while(result == CARD_ERROR_BUSY);
|
||||
CARD_Close(&SAVE.platform.cardFile);
|
||||
memoryFree(buffer);
|
||||
|
||||
if(result < 0) {
|
||||
errorThrow("Failed to write memory card data for slot %u (error %d)",
|
||||
(uint32_t)slot, result
|
||||
errorThrow("Failed to write memory card data for slot %u: %s (%d)",
|
||||
(uint32_t)slot, saveCardErrorStringDolphin(result), result
|
||||
);
|
||||
}
|
||||
|
||||
@@ -132,14 +169,52 @@ errorret_t saveWriteDolphin(const uint8_t slot, const savefile_t *file) {
|
||||
|
||||
errorret_t saveDeleteDolphin(const uint8_t slot) {
|
||||
char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX];
|
||||
_saveGetFileName(slot, fileName, SAVE_DOLPHIN_FILE_NAME_MAX);
|
||||
saveGetFileNameDolphin(slot, fileName, SAVE_DOLPHIN_FILE_NAME_MAX);
|
||||
|
||||
int32_t result = CARD_Delete(SAVE_DOLPHIN_CHANNEL, fileName);
|
||||
int32_t result;
|
||||
do {
|
||||
result = CARD_Delete(SAVE_DOLPHIN_CHANNEL, fileName);
|
||||
} while(result == CARD_ERROR_BUSY);
|
||||
if(result < 0 && result != CARD_ERROR_NOFILE) {
|
||||
errorThrow("Failed to delete memory card file for slot %u (error %d)",
|
||||
(uint32_t)slot, result
|
||||
errorThrow("Failed to delete memory card file for slot %u: %s (%d)",
|
||||
(uint32_t)slot, saveCardErrorStringDolphin(result), result
|
||||
);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void saveGetFileNameDolphin(
|
||||
const uint8_t slot, char_t *out, const size_t max
|
||||
) {
|
||||
snprintf(out, max, "%s_%u", SAVE_DOLPHIN_GAME_CODE, (uint32_t)slot);
|
||||
}
|
||||
|
||||
const char_t *saveCardErrorStringDolphin(const int32_t result) {
|
||||
switch(result) {
|
||||
case CARD_ERROR_READY: return "card is ready";
|
||||
case CARD_ERROR_UNLOCKED:
|
||||
return "card is being unlocked or already unlocked";
|
||||
case CARD_ERROR_BUSY: return "card is busy";
|
||||
case CARD_ERROR_WRONGDEVICE: return "wrong device connected in slot";
|
||||
case CARD_ERROR_NOCARD: return "no memory card in slot";
|
||||
case CARD_ERROR_NOFILE: return "specified file not found";
|
||||
case CARD_ERROR_IOERROR: return "internal EXI I/O error";
|
||||
case CARD_ERROR_BROKEN:
|
||||
return "directory structure or file entry broken";
|
||||
case CARD_ERROR_EXIST:
|
||||
return "file already exists with the specified parameters";
|
||||
case CARD_ERROR_NOENT:
|
||||
return "no empty block available to create the file";
|
||||
case CARD_ERROR_INSSPACE:
|
||||
return "not enough space to write file to memory card";
|
||||
case CARD_ERROR_NOPERM:
|
||||
return "not enough permissions to operate on the file";
|
||||
case CARD_ERROR_LIMIT: return "card size limit reached";
|
||||
case CARD_ERROR_NAMETOOLONG: return "filename too long";
|
||||
case CARD_ERROR_ENCODING: return "font encoding PAL/SJIS mismatch";
|
||||
case CARD_ERROR_CANCELED: return "card operation canceled";
|
||||
case CARD_ERROR_FATAL_ERROR: return "fatal error, non-recoverable";
|
||||
default: return "unknown card error";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,3 +66,26 @@ errorret_t saveWriteDolphin(const uint8_t slot, const savefile_t *file);
|
||||
* @return An error code if the delete fails.
|
||||
*/
|
||||
errorret_t saveDeleteDolphin(const uint8_t slot);
|
||||
|
||||
/**
|
||||
* Builds the memory card file name for a given save slot, from
|
||||
* SAVE_DOLPHIN_GAME_CODE and the slot index.
|
||||
*
|
||||
* @param slot The save slot index.
|
||||
* @param out Destination buffer for the file name.
|
||||
* @param max Size of out, in bytes.
|
||||
*/
|
||||
void saveGetFileNameDolphin(
|
||||
const uint8_t slot, char_t *out, const size_t max
|
||||
);
|
||||
|
||||
/**
|
||||
* Describes a libogc CARD_ERROR_* result code (see
|
||||
* https://libogc.devkitpro.org/group__card__errors.html), for logging
|
||||
* alongside the raw numeric code.
|
||||
*
|
||||
* @param result The result code returned by a CARD_* libogc call.
|
||||
* @return A human-readable description of the result code, or
|
||||
* "unknown card error" if result doesn't match a known CARD_ERROR_* code.
|
||||
*/
|
||||
const char_t *saveCardErrorStringDolphin(const int32_t result);
|
||||
|
||||
@@ -19,12 +19,20 @@ static void _saveStreamGetFileName(
|
||||
errorret_t saveStreamOpenReadDolphin(
|
||||
savestreamdolphin_t *p, bool_t *found, const uint8_t slot
|
||||
) {
|
||||
if(!SAVE.platform.mounted) {
|
||||
*found = false;
|
||||
errorThrow("No memory card mounted");
|
||||
}
|
||||
|
||||
char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX];
|
||||
_saveStreamGetFileName(fileName, SAVE_DOLPHIN_FILE_NAME_MAX, slot);
|
||||
|
||||
int32_t result = CARD_Open(
|
||||
int32_t result;
|
||||
do {
|
||||
result = CARD_Open(
|
||||
SAVE_DOLPHIN_CHANNEL, fileName, &p->cardFile
|
||||
);
|
||||
} while(result == CARD_ERROR_BUSY);
|
||||
if(result == CARD_ERROR_NOFILE) {
|
||||
*found = false;
|
||||
p->position = 0;
|
||||
@@ -33,17 +41,19 @@ errorret_t saveStreamOpenReadDolphin(
|
||||
}
|
||||
if(result < 0) {
|
||||
*found = false;
|
||||
errorThrow("Failed to open memory card file for slot %u (error %d)",
|
||||
(uint32_t)slot, result
|
||||
errorThrow("Failed to open memory card file for slot %u: %s (%d)",
|
||||
(uint32_t)slot, saveCardErrorStringDolphin(result), result
|
||||
);
|
||||
}
|
||||
|
||||
do {
|
||||
result = CARD_Read(&p->cardFile, p->buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0);
|
||||
} while(result == CARD_ERROR_BUSY);
|
||||
CARD_Close(&p->cardFile);
|
||||
if(result < 0) {
|
||||
*found = false;
|
||||
errorThrow("Failed to read memory card data for slot %u (error %d)",
|
||||
(uint32_t)slot, result
|
||||
errorThrow("Failed to read memory card data for slot %u: %s (%d)",
|
||||
(uint32_t)slot, saveCardErrorStringDolphin(result), result
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,6 +67,8 @@ errorret_t saveStreamOpenReadDolphin(
|
||||
errorret_t saveStreamOpenWriteDolphin(
|
||||
savestreamdolphin_t *p, const uint8_t slot
|
||||
) {
|
||||
if(!SAVE.platform.mounted) errorThrow("No memory card mounted");
|
||||
|
||||
memoryZero(p->buffer, SAVE_DOLPHIN_SECTOR_SIZE);
|
||||
p->position = 0;
|
||||
p->writing = true;
|
||||
@@ -70,14 +82,21 @@ void saveStreamCloseDolphin(savestreamdolphin_t *p) {
|
||||
char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX];
|
||||
_saveStreamGetFileName(fileName, SAVE_DOLPHIN_FILE_NAME_MAX, p->slot);
|
||||
|
||||
int32_t result = CARD_Open(SAVE_DOLPHIN_CHANNEL, fileName, &p->cardFile);
|
||||
int32_t result;
|
||||
do {
|
||||
result = CARD_Open(SAVE_DOLPHIN_CHANNEL, fileName, &p->cardFile);
|
||||
} while(result == CARD_ERROR_BUSY);
|
||||
if(result == CARD_ERROR_NOFILE) {
|
||||
CARD_Create(
|
||||
do {
|
||||
result = CARD_Create(
|
||||
SAVE_DOLPHIN_CHANNEL, fileName, SAVE_DOLPHIN_SECTOR_SIZE, &p->cardFile
|
||||
);
|
||||
} while(result == CARD_ERROR_BUSY);
|
||||
}
|
||||
|
||||
CARD_Write(&p->cardFile, p->buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0);
|
||||
do {
|
||||
result = CARD_Write(&p->cardFile, p->buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0);
|
||||
} while(result == CARD_ERROR_BUSY);
|
||||
CARD_Close(&p->cardFile);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
#include "input/input.h"
|
||||
#include "save/save.h"
|
||||
|
||||
inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
||||
#ifdef DUSK_INPUT_GAMEPAD
|
||||
@@ -547,5 +548,5 @@ errorret_t inputInitLinux(void) {
|
||||
}
|
||||
|
||||
float_t inputGetDeadzoneSDL2(const inputbutton_t button) {
|
||||
return 0.17f;
|
||||
return saveGet(SAVE_ACTIVE_SLOT)->deadzone;
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
#include "input/input.h"
|
||||
#include "save/save.h"
|
||||
|
||||
// #define INPUT_PSP_GAMEPAD_BUTTON_ACCEPT INPUT_SDL2_GAMEPAD_BUTTON_CUSTOM
|
||||
// #define INPUT_PSP_GAMEPAD_BUTTON_CANCEL INPUT_SDL2_GAMEPAD_BUTTON_CUSTOM
|
||||
@@ -94,5 +95,5 @@ errorret_t inputInitPSP(void) {
|
||||
}
|
||||
|
||||
float_t inputGetDeadzoneSDL2(const inputbutton_t button) {
|
||||
return 0.2f;
|
||||
return saveGet(SAVE_ACTIVE_SLOT)->deadzone;
|
||||
}
|
||||
@@ -16,15 +16,22 @@ typedef savestreampsp_t saveplatformstream_t;
|
||||
#define saveDisposePlatform saveDisposePSP
|
||||
#define saveDeletePlatform saveDeletePSP
|
||||
|
||||
#define saveStreamOpenReadPlatform(stream, slot) \
|
||||
saveStreamOpenReadPSP(&(stream)->platform, &(stream)->found, slot)
|
||||
#define saveStreamOpenWritePlatform(stream, slot) \
|
||||
saveStreamOpenWritePSP(&(stream)->platform, slot)
|
||||
#define saveStreamClosePlatform(stream) \
|
||||
saveStreamClosePSP(&(stream)->platform)
|
||||
#define saveStreamReadBytesPlatform(stream, buf, len) \
|
||||
saveStreamReadBytesPSP(&(stream)->platform, buf, len)
|
||||
#define saveStreamWriteBytesPlatform(stream, buf, len) \
|
||||
saveStreamWriteBytesPSP(&(stream)->platform, buf, len)
|
||||
#define saveStreamSeekPlatform(stream, pos) \
|
||||
saveStreamSeekPSP(&(stream)->platform, pos)
|
||||
|
||||
// Save/load go entirely through the native sceUtilitySavedata dialog
|
||||
// (savePSPBeginSave/Load), which spans multiple frames - these bypass
|
||||
// save.c's normal synchronous open/write-fields/close flow above (that's
|
||||
// still used internally, just against an in-memory buffer, from within
|
||||
// savePSPBeginSave/Load themselves) and are what save.c's saveWrite()/
|
||||
// saveLoad() actually call on this platform.
|
||||
#define saveAsyncWritePlatform(slot, onComplete, user) \
|
||||
savePSPBeginSave(slot, onComplete, user)
|
||||
#define saveAsyncLoadPlatform(slot, onComplete, user) \
|
||||
savePSPBeginLoad(slot, onComplete, user)
|
||||
#define saveIsBusyPlatform() savePSPIsBusy()
|
||||
#define savePlatformUpdate() savePSPUpdate()
|
||||
|
||||
+262
-53
@@ -6,8 +6,38 @@
|
||||
*/
|
||||
|
||||
#include "save/save.h"
|
||||
#include "save/savepsp.h"
|
||||
#include "save/savestream.h"
|
||||
#include "system/systempsp.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
static void savePSPParamCommonInit(SceUtilitySavedataParam *param) {
|
||||
memoryZero(param, sizeof(SceUtilitySavedataParam));
|
||||
param->base.size = sizeof(SceUtilitySavedataParam);
|
||||
param->base.language = systemPSPGetLanguage();
|
||||
param->base.buttonSwap = systemPSPGetCrossButtonSetting();
|
||||
param->base.graphicsThread = 17;
|
||||
param->base.accessThread = 19;
|
||||
param->base.fontThread = 18;
|
||||
param->base.soundThread = 16;
|
||||
|
||||
stringCopy(param->gameName, SAVE_PSP_GAME_NAME, sizeof(param->gameName));
|
||||
stringCopy(param->fileName, SAVE_PSP_FILE_NAME, sizeof(param->fileName));
|
||||
}
|
||||
|
||||
static void savePSPSaveNameForSlot(
|
||||
char_t *out, const size_t max, const uint8_t slot
|
||||
) {
|
||||
stringFormat(out, max, "%02u", (uint32_t)slot);
|
||||
}
|
||||
|
||||
errorret_t saveInitPSP(void) {
|
||||
SceIoStat stat;
|
||||
if(sceIoGetstat(SAVE_PSP_ROOT, &stat) < 0) {
|
||||
errorThrow("No memory stick detected");
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -15,61 +45,11 @@ errorret_t saveDisposePSP(void) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveLoadPSP(const uint8_t slot, savefile_t *file) {
|
||||
char_t path[SAVE_PSP_PATH_MAX];
|
||||
snprintf(path, SAVE_PSP_PATH_MAX, SAVE_PSP_FILE_FORMAT,
|
||||
SAVE_PSP_TITLE_ID, (uint32_t)slot
|
||||
);
|
||||
|
||||
SceUID fd = sceIoOpen(path, PSP_O_RDONLY, 0);
|
||||
if(fd < 0) {
|
||||
file->exists = false;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
int32_t read = sceIoRead(fd, file, sizeof(savefile_t));
|
||||
sceIoClose(fd);
|
||||
|
||||
if(read != (int32_t)sizeof(savefile_t)) {
|
||||
file->exists = false;
|
||||
errorThrow("Failed to read save data for slot %u", (uint32_t)slot);
|
||||
}
|
||||
|
||||
file->exists = true;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveWritePSP(const uint8_t slot, const savefile_t *file) {
|
||||
char_t dir[SAVE_PSP_PATH_MAX];
|
||||
snprintf(dir, SAVE_PSP_PATH_MAX, SAVE_PSP_DIR_FORMAT,
|
||||
SAVE_PSP_TITLE_ID, (uint32_t)slot
|
||||
);
|
||||
sceIoMkdir(dir, 0777);
|
||||
|
||||
char_t path[SAVE_PSP_PATH_MAX];
|
||||
snprintf(path, SAVE_PSP_PATH_MAX, SAVE_PSP_FILE_FORMAT,
|
||||
SAVE_PSP_TITLE_ID, (uint32_t)slot
|
||||
);
|
||||
|
||||
SceUID fd = sceIoOpen(path, PSP_O_WRONLY | PSP_O_CREAT | PSP_O_TRUNC, 0777);
|
||||
if(fd < 0) {
|
||||
errorThrow("Failed to open save file for writing: slot %u", (uint32_t)slot);
|
||||
}
|
||||
|
||||
int32_t written = sceIoWrite(fd, file, sizeof(savefile_t));
|
||||
sceIoClose(fd);
|
||||
|
||||
if(written != (int32_t)sizeof(savefile_t)) {
|
||||
errorThrow("Failed to write save data for slot %u", (uint32_t)slot);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveDeletePSP(const uint8_t slot) {
|
||||
char_t path[SAVE_PSP_PATH_MAX];
|
||||
snprintf(path, SAVE_PSP_PATH_MAX, SAVE_PSP_FILE_FORMAT,
|
||||
SAVE_PSP_TITLE_ID, (uint32_t)slot
|
||||
stringFormat(
|
||||
path, sizeof(path), SAVE_PSP_FILE_FORMAT, SAVE_PSP_GAME_NAME,
|
||||
(uint32_t)slot
|
||||
);
|
||||
|
||||
int32_t result = sceIoRemove(path);
|
||||
@@ -77,5 +57,234 @@ errorret_t saveDeletePSP(const uint8_t slot) {
|
||||
errorThrow("Failed to delete save file for slot %u", (uint32_t)slot);
|
||||
}
|
||||
|
||||
char_t dir[SAVE_PSP_PATH_MAX];
|
||||
stringFormat(
|
||||
dir, sizeof(dir), "ms0:/PSP/SAVEDATA/%s%02u", SAVE_PSP_GAME_NAME,
|
||||
(uint32_t)slot
|
||||
);
|
||||
char_t sfoPath[SAVE_PSP_PATH_MAX];
|
||||
stringFormat(sfoPath, sizeof(sfoPath), "%s/PARAM.SFO", dir);
|
||||
// Best-effort - PARAM.SFO/the directory itself may not exist (e.g. this
|
||||
// slot was written by the old raw-file format, pre-dating this dialog-
|
||||
// based rewrite) or the directory may still contain other entries.
|
||||
sceIoRemove(sfoPath);
|
||||
sceIoRmdir(dir);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void savePSPBeginSave(
|
||||
const uint8_t slot, savecallback_t onComplete, void *user
|
||||
) {
|
||||
assertNotNull(onComplete, "onComplete cannot be NULL");
|
||||
assertTrue(SAVE.platform.op == SAVE_PSP_OP_NONE, "Save already in progress");
|
||||
|
||||
savefile_t *file = &SAVE.files[slot];
|
||||
|
||||
// Serialize into the buffer synchronously (plain memory writes, same
|
||||
// header/version/CRC framing as every other platform) before the dialog
|
||||
// ever starts - only the actual commit-to-storage step needs to wait on
|
||||
// the dialog.
|
||||
savestream_t stream;
|
||||
memoryZero(&stream, sizeof(savestream_t));
|
||||
stream.platform.buffer = SAVE.platform.dataBuffer;
|
||||
stream.platform.bufferSize = sizeof(SAVE.platform.dataBuffer);
|
||||
|
||||
errorret_t ret = saveFileWrite(&stream, file);
|
||||
if(errorIsOk(ret)) ret = saveStreamFinalizeWriteImpl(&stream);
|
||||
if(errorIsNotOk(ret)) {
|
||||
onComplete(ret, user);
|
||||
return;
|
||||
}
|
||||
SAVE.platform.dataLength = stream.platform.length;
|
||||
|
||||
SceUtilitySavedataParam *param = &SAVE.platform.param;
|
||||
savePSPParamCommonInit(param);
|
||||
// AUTOSAVE rather than SAVE - SAVE shows a "save to this data?" confirm
|
||||
// screen even for a slot with no existing data, which isn't the UX we
|
||||
// want for a menu-triggered "Save" action (that confirmation already
|
||||
// happened when the player chose to save). AUTOSAVE writes silently
|
||||
// (just a brief "saving" icon flash) while still generating the same
|
||||
// PARAM.SFO/title/description as any other mode.
|
||||
param->mode = PSP_UTILITY_SAVEDATA_AUTOSAVE;
|
||||
param->overwrite = 1;
|
||||
savePSPSaveNameForSlot(param->saveName, sizeof(param->saveName), slot);
|
||||
|
||||
param->dataBuf = SAVE.platform.dataBuffer;
|
||||
param->dataBufSize = sizeof(SAVE.platform.dataBuffer);
|
||||
param->dataSize = SAVE.platform.dataLength;
|
||||
|
||||
// No ICON0/PIC1/SND0 art exists in this project yet, so these are left
|
||||
// zeroed (bufSize 0) - the utility treats that as "no icon/background/
|
||||
// sound" rather than an error. title/savedataTitle/detail are still
|
||||
// fully functional and are what actually populates PARAM.SFO and the
|
||||
// save browser entry.
|
||||
stringCopy(param->sfoParam.title, "Dusk", sizeof(param->sfoParam.title));
|
||||
stringCopy(
|
||||
param->sfoParam.savedataTitle, file->playerName,
|
||||
sizeof(param->sfoParam.savedataTitle)
|
||||
);
|
||||
stringCopy(
|
||||
param->sfoParam.detail, "Dusk save file.", sizeof(param->sfoParam.detail)
|
||||
);
|
||||
|
||||
int32_t initRet = sceUtilitySavedataInitStart(param);
|
||||
if(initRet < 0) {
|
||||
onComplete(errorThrowImpl(
|
||||
&SAVE.errorState, ERROR_NOT_OK, __FILE__, __func__, __LINE__,
|
||||
"Failed to start save dialog: 0x%08X", initRet
|
||||
), user);
|
||||
return;
|
||||
}
|
||||
|
||||
SAVE.platform.op = SAVE_PSP_OP_SAVE;
|
||||
SAVE.platform.slot = slot;
|
||||
SAVE.platform.onComplete = onComplete;
|
||||
SAVE.platform.onCompleteUser = user;
|
||||
}
|
||||
|
||||
void savePSPBeginLoad(
|
||||
const uint8_t slot, savecallback_t onComplete, void *user
|
||||
) {
|
||||
assertNotNull(onComplete, "onComplete cannot be NULL");
|
||||
assertTrue(SAVE.platform.op == SAVE_PSP_OP_NONE, "Save already in progress");
|
||||
|
||||
char_t path[SAVE_PSP_PATH_MAX];
|
||||
stringFormat(
|
||||
path, sizeof(path), SAVE_PSP_FILE_FORMAT, SAVE_PSP_GAME_NAME,
|
||||
(uint32_t)slot
|
||||
);
|
||||
|
||||
SceIoStat stat;
|
||||
if(sceIoGetstat(path, &stat) < 0) {
|
||||
// No save data for this slot yet - not an error (matches every other
|
||||
// platform's "nothing to load yet" behavior), and deliberately skips
|
||||
// showing the dialog at all rather than surfacing an empty "no data"
|
||||
// native screen for a slot the player has never saved to.
|
||||
onComplete(errorOkImpl(), user);
|
||||
return;
|
||||
}
|
||||
|
||||
SceUtilitySavedataParam *param = &SAVE.platform.param;
|
||||
savePSPParamCommonInit(param);
|
||||
param->mode = PSP_UTILITY_SAVEDATA_AUTOLOAD;// See savePSPBeginSave().
|
||||
savePSPSaveNameForSlot(param->saveName, sizeof(param->saveName), slot);
|
||||
|
||||
param->dataBuf = SAVE.platform.dataBuffer;
|
||||
param->dataBufSize = sizeof(SAVE.platform.dataBuffer);
|
||||
|
||||
int32_t initRet = sceUtilitySavedataInitStart(param);
|
||||
if(initRet < 0) {
|
||||
onComplete(errorThrowImpl(
|
||||
&SAVE.errorState, ERROR_NOT_OK, __FILE__, __func__, __LINE__,
|
||||
"Failed to start load dialog: 0x%08X", initRet
|
||||
), user);
|
||||
return;
|
||||
}
|
||||
|
||||
SAVE.platform.op = SAVE_PSP_OP_LOAD;
|
||||
SAVE.platform.slot = slot;
|
||||
SAVE.platform.onComplete = onComplete;
|
||||
SAVE.platform.onCompleteUser = user;
|
||||
}
|
||||
|
||||
bool_t savePSPIsBusy(void) {
|
||||
return SAVE.platform.op != SAVE_PSP_OP_NONE;
|
||||
}
|
||||
|
||||
errorret_t savePSPUpdate(void) {
|
||||
if(SAVE.platform.op == SAVE_PSP_OP_NONE) errorOk();
|
||||
|
||||
int32_t status = sceUtilitySavedataGetStatus();
|
||||
switch(status) {
|
||||
case PSP_UTILITY_DIALOG_INIT:
|
||||
break;
|
||||
|
||||
// NOTE: unlike the netconf dialog, this does not replicate Dusk's own
|
||||
// GL state (blend/cull/depth + texture/color) before calling Update().
|
||||
// A prior fix for exactly that class of bug was documented for the
|
||||
// network dialog, but no longer exists in the current codebase to
|
||||
// copy from - if the save dialog's own text/icons don't render
|
||||
// correctly on real hardware (PPSSPP won't reproduce this - it doesn't
|
||||
// model pspGL's deferred state application), that state-priming
|
||||
// pattern is the fix to reach for. See the network dialog's git
|
||||
// history / the project's PSP dialog memory notes for the exact
|
||||
// technique (state flags + a forced flush via a degenerate triangle
|
||||
// draw).
|
||||
case PSP_UTILITY_DIALOG_VISIBLE:
|
||||
// sceUtilitySavedataUpdate() is void, unlike sceUtilityNetconfUpdate()
|
||||
// - nothing to check here, GetStatus() next frame reflects any
|
||||
// resulting state change.
|
||||
sceUtilitySavedataUpdate(1);
|
||||
break;
|
||||
|
||||
case PSP_UTILITY_DIALOG_QUIT:
|
||||
// The save/load operation itself has already finished (successfully
|
||||
// or not) - this just starts tearing the dialog down. The actual
|
||||
// result is read once that teardown settles, below - don't call
|
||||
// ShutdownStart more than once while waiting for it to.
|
||||
if(!SAVE.platform.shuttingDown) {
|
||||
SAVE.platform.shuttingDown = true;
|
||||
sceUtilitySavedataShutdownStart();
|
||||
}
|
||||
break;
|
||||
|
||||
// Confirmed under PPSSPP: status settles straight from QUIT to NONE,
|
||||
// without FINISHED ever being separately observed in between - so
|
||||
// both are treated identically here as "torn down, read the result",
|
||||
// and it's shuttingDown (not which of these two codes we saw) that
|
||||
// distinguishes that from a genuine disappearance.
|
||||
case PSP_UTILITY_DIALOG_FINISHED:
|
||||
case PSP_UTILITY_DIALOG_NONE: {
|
||||
savepspop_t op = SAVE.platform.op;
|
||||
uint8_t slot = SAVE.platform.slot;
|
||||
savecallback_t cb = SAVE.platform.onComplete;
|
||||
void *user = SAVE.platform.onCompleteUser;
|
||||
bool_t reachedQuit = SAVE.platform.shuttingDown;
|
||||
SAVE.platform.op = SAVE_PSP_OP_NONE;
|
||||
SAVE.platform.shuttingDown = false;
|
||||
|
||||
if(!reachedQuit) {
|
||||
cb(errorThrowImpl(
|
||||
&SAVE.errorState, ERROR_NOT_OK, __FILE__, __func__, __LINE__,
|
||||
"Save dialog disappeared without a result"
|
||||
), user);
|
||||
break;
|
||||
}
|
||||
|
||||
int32_t result = SAVE.platform.param.base.result;
|
||||
if(result != 0) {
|
||||
SAVE.available = false;
|
||||
cb(errorThrowImpl(
|
||||
&SAVE.errorState, ERROR_NOT_OK, __FILE__, __func__, __LINE__,
|
||||
"Save dialog failed: 0x%08X", result
|
||||
), user);
|
||||
break;
|
||||
}
|
||||
SAVE.available = true;
|
||||
|
||||
if(op == SAVE_PSP_OP_LOAD) {
|
||||
savefile_t *file = &SAVE.files[slot];
|
||||
savestream_t stream;
|
||||
memoryZero(&stream, sizeof(savestream_t));
|
||||
stream.platform.buffer = SAVE.platform.dataBuffer;
|
||||
stream.platform.bufferSize = sizeof(SAVE.platform.dataBuffer);
|
||||
stream.platform.length = SAVE.platform.param.dataSize;
|
||||
|
||||
errorret_t ret = saveFileLoad(&stream, file);
|
||||
if(errorIsOk(ret)) ret = saveStreamVerifyChecksumImpl(&stream, slot);
|
||||
file->exists = errorIsOk(ret);
|
||||
cb(ret, user);
|
||||
} else {
|
||||
SAVE.files[slot].exists = true;
|
||||
cb(errorOkImpl(), user);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
errorThrow("Unknown savedata dialog status: %d", status);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
+90
-26
@@ -9,23 +9,52 @@
|
||||
#include "error/error.h"
|
||||
#include "save/savefile.h"
|
||||
#include <pspiofilemgr.h>
|
||||
#include <psputility.h>
|
||||
|
||||
#define SAVE_PSP_PATH_MAX 256
|
||||
#define SAVE_PSP_FILE_FORMAT "ms0:/PSP/SAVEDATA/%s%02u/save.dat"
|
||||
#define SAVE_PSP_DIR_FORMAT "ms0:/PSP/SAVEDATA/%s%02u"
|
||||
#define SAVE_PSP_ROOT "ms0:/"
|
||||
#define SAVE_PSP_FILE_NAME "save.bin"
|
||||
#define SAVE_PSP_FILE_FORMAT "ms0:/PSP/SAVEDATA/%s%02u/" SAVE_PSP_FILE_NAME
|
||||
#define SAVE_PSP_DATA_BUFFER_SIZE 4096
|
||||
|
||||
#ifndef SAVE_PSP_TITLE_ID
|
||||
#define SAVE_PSP_TITLE_ID "DUSK00001"
|
||||
#ifndef SAVE_PSP_GAME_NAME
|
||||
#define SAVE_PSP_GAME_NAME "DUSK00001"
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
SAVE_PSP_OP_NONE,
|
||||
SAVE_PSP_OP_SAVE,
|
||||
SAVE_PSP_OP_LOAD
|
||||
} savepspop_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t unused;
|
||||
SceUtilitySavedataParam param;
|
||||
// Raw buffer sceUtilitySavedata reads/writes the whole save into/from -
|
||||
// populated by our own savestream_t serialization (see savestreampsp.h)
|
||||
// before a save starts, and deserialized from after a load finishes.
|
||||
uint8_t dataBuffer[SAVE_PSP_DATA_BUFFER_SIZE] __attribute__((aligned(64)));
|
||||
size_t dataLength;
|
||||
|
||||
savepspop_t op;
|
||||
// True once sceUtilitySavedataShutdownStart() has been requested (dialog
|
||||
// status PSP_UTILITY_DIALOG_QUIT seen) - distinguishes a normal "torn
|
||||
// down after finishing" NONE/FINISHED from a genuinely unexpected one
|
||||
// seen before ever reaching QUIT. Some implementations (confirmed on
|
||||
// PPSSPP) settle straight to NONE after shutdown without a separately
|
||||
// observable FINISHED step in between.
|
||||
bool_t shuttingDown;
|
||||
uint8_t slot;
|
||||
savecallback_t onComplete;
|
||||
void *onCompleteUser;
|
||||
} savepsp_t;
|
||||
|
||||
/**
|
||||
* Initializes the save system on PSP.
|
||||
* Initializes the save system on PSP. Confirms the memory stick is
|
||||
* actually reachable (sceIoGetstat on SAVE_PSP_ROOT) rather than assuming
|
||||
* so, since the savedata dialog otherwise only reports failure once a
|
||||
* save/load is actually attempted.
|
||||
*
|
||||
* @return An error code if initialization fails.
|
||||
* @return An error code if no memory stick is reachable.
|
||||
*/
|
||||
errorret_t saveInitPSP(void);
|
||||
|
||||
@@ -37,27 +66,62 @@ errorret_t saveInitPSP(void);
|
||||
errorret_t saveDisposePSP(void);
|
||||
|
||||
/**
|
||||
* Loads a save file from PSP save data for the given slot.
|
||||
*
|
||||
* @param slot The save slot index.
|
||||
* @param file Output save file data.
|
||||
* @return An error code if the load fails.
|
||||
*/
|
||||
errorret_t saveLoadPSP(const uint8_t slot, savefile_t *file);
|
||||
|
||||
/**
|
||||
* Writes a save file to PSP save data for the given slot.
|
||||
*
|
||||
* @param slot The save slot index.
|
||||
* @param file Save file data to write.
|
||||
* @return An error code if the write fails.
|
||||
*/
|
||||
errorret_t saveWritePSP(const uint8_t slot, const savefile_t *file);
|
||||
|
||||
/**
|
||||
* Deletes the save file for the given slot from PSP save data.
|
||||
* Deletes the save data folder for the given slot from the memory stick.
|
||||
*
|
||||
* @param slot The save slot index.
|
||||
* @return An error code if the delete fails.
|
||||
*/
|
||||
errorret_t saveDeletePSP(const uint8_t slot);
|
||||
|
||||
/**
|
||||
* Starts a save via the native sceUtilitySavedata dialog (mode AUTOSAVE -
|
||||
* writes silently with just a brief icon flash, no confirm screen, since
|
||||
* SAVE mode shows one even for a slot with no existing data - but
|
||||
* PARAM.SFO/title/description are generated identically regardless of
|
||||
* mode, and the OS handles the save browser entry either way) for the
|
||||
* given slot. Serializes SAVE.files[slot] into SAVE.platform.dataBuffer
|
||||
* first, synchronously, then kicks off the dialog and returns - completion
|
||||
* is reported later via onComplete, driven by savePSPUpdate() each frame.
|
||||
* If no save data exists yet for this slot, sceUtilitySavedataInitStart()
|
||||
* creates it.
|
||||
*
|
||||
* @param slot The save slot index.
|
||||
* @param onComplete Callback invoked once the dialog finishes.
|
||||
* @param user User data passed through to onComplete.
|
||||
*/
|
||||
void savePSPBeginSave(
|
||||
const uint8_t slot, savecallback_t onComplete, void *user
|
||||
);
|
||||
|
||||
/**
|
||||
* Starts a load via the native sceUtilitySavedata dialog (mode AUTOLOAD -
|
||||
* see savePSPBeginSave() for why not the plain LOAD mode) for the given
|
||||
* slot, unless a quick sceIoGetstat check finds no save data for this slot
|
||||
* yet - in which case onComplete is invoked immediately with
|
||||
* SAVE.files[slot].exists left false, matching the other platforms'
|
||||
* "no file yet" semantics, and no dialog is shown at all.
|
||||
*
|
||||
* @param slot The save slot index.
|
||||
* @param onComplete Callback invoked once the dialog (or immediate
|
||||
* not-found short-circuit) finishes.
|
||||
* @param user User data passed through to onComplete.
|
||||
*/
|
||||
void savePSPBeginLoad(
|
||||
const uint8_t slot, savecallback_t onComplete, void *user
|
||||
);
|
||||
|
||||
/**
|
||||
* Pumps the in-progress save/load dialog one step, if any - must be called
|
||||
* every engine frame (see saveUpdate()). No-op if no dialog is active.
|
||||
*
|
||||
* @return An error code indicating success or failure.
|
||||
*/
|
||||
errorret_t savePSPUpdate(void);
|
||||
|
||||
/**
|
||||
* True while a save/load dialog is in progress (see savePSPBeginSave()/
|
||||
* savePSPBeginLoad()).
|
||||
*
|
||||
* @return True if a save/load dialog is currently open.
|
||||
*/
|
||||
bool_t savePSPIsBusy(void);
|
||||
|
||||
@@ -7,71 +7,35 @@
|
||||
|
||||
#include "save/save.h"
|
||||
#include "save/savestreampsp.h"
|
||||
|
||||
errorret_t saveStreamOpenReadPSP(
|
||||
savestreampsp_t *p, bool_t *found, const uint8_t slot
|
||||
) {
|
||||
char_t path[SAVE_PSP_PATH_MAX];
|
||||
snprintf(path, SAVE_PSP_PATH_MAX, SAVE_PSP_FILE_FORMAT,
|
||||
SAVE_PSP_TITLE_ID, (uint32_t)slot
|
||||
);
|
||||
|
||||
p->fd = sceIoOpen(path, PSP_O_RDONLY, 0);
|
||||
*found = (p->fd >= 0);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamOpenWritePSP(savestreampsp_t *p, const uint8_t slot) {
|
||||
char_t dir[SAVE_PSP_PATH_MAX];
|
||||
snprintf(dir, SAVE_PSP_PATH_MAX, SAVE_PSP_DIR_FORMAT,
|
||||
SAVE_PSP_TITLE_ID, (uint32_t)slot
|
||||
);
|
||||
sceIoMkdir(dir, 0777);
|
||||
|
||||
char_t path[SAVE_PSP_PATH_MAX];
|
||||
snprintf(path, SAVE_PSP_PATH_MAX, SAVE_PSP_FILE_FORMAT,
|
||||
SAVE_PSP_TITLE_ID, (uint32_t)slot
|
||||
);
|
||||
|
||||
p->fd = sceIoOpen(path, PSP_O_WRONLY | PSP_O_CREAT | PSP_O_TRUNC, 0777);
|
||||
if(p->fd < 0) {
|
||||
errorThrow(
|
||||
"Failed to open PSP save file for writing: slot %u", (uint32_t)slot
|
||||
);
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void saveStreamClosePSP(savestreampsp_t *p) {
|
||||
if(p->fd >= 0) {
|
||||
sceIoClose(p->fd);
|
||||
p->fd = -1;
|
||||
}
|
||||
}
|
||||
#include "util/memory.h"
|
||||
|
||||
errorret_t saveStreamReadBytesPSP(
|
||||
savestreampsp_t *p, void *buf, const size_t len
|
||||
) {
|
||||
int32_t read = sceIoRead(p->fd, buf, (SceSize)len);
|
||||
if(read != (int32_t)len) {
|
||||
errorThrow("Unexpected end of PSP save file");
|
||||
if(p->position + len > p->length) {
|
||||
errorThrow("Save stream read exceeds buffer length");
|
||||
}
|
||||
memoryCopy(buf, p->buffer + p->position, len);
|
||||
p->position += len;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteBytesPSP(
|
||||
savestreampsp_t *p, const void *buf, const size_t len
|
||||
) {
|
||||
int32_t written = sceIoWrite(p->fd, buf, (SceSize)len);
|
||||
if(written != (int32_t)len) {
|
||||
errorThrow("Failed to write PSP save data");
|
||||
if(p->position + len > p->bufferSize) {
|
||||
errorThrow("Save stream write exceeds buffer size");
|
||||
}
|
||||
memoryCopy(p->buffer + p->position, buf, len);
|
||||
p->position += len;
|
||||
if(p->position > p->length) p->length = p->position;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamSeekPSP(savestreampsp_t *p, const size_t pos) {
|
||||
if(sceIoLseek(p->fd, (SceOff)pos, PSP_SEEK_SET) < 0) {
|
||||
errorThrow("Failed to seek in PSP save file");
|
||||
if(pos > p->bufferSize) {
|
||||
errorThrow("Save stream seek out of range");
|
||||
}
|
||||
p->position = pos;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -7,71 +7,49 @@
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include <pspiofilemgr.h>
|
||||
#include <stddef.h>
|
||||
|
||||
// Backed by SAVE.platform.dataBuffer (see savepsp.h) rather than owning its
|
||||
// own memory - the buffer has to outlive a single saveFileWrite()/Load()
|
||||
// call, since the actual save/load dialog it's handed to only completes
|
||||
// several frames later.
|
||||
typedef struct {
|
||||
SceUID fd;
|
||||
uint8_t *buffer;
|
||||
size_t bufferSize;
|
||||
size_t position;
|
||||
size_t length;
|
||||
} savestreampsp_t;
|
||||
|
||||
/**
|
||||
* Opens a PSP save data file for reading.
|
||||
*
|
||||
* @param p Stream to initialize.
|
||||
* @param found Set to true if the file exists, false if it does not.
|
||||
* @param slot Save slot index.
|
||||
* @return An error if the open fails for a reason other than missing file.
|
||||
*/
|
||||
errorret_t saveStreamOpenReadPSP(
|
||||
savestreampsp_t *p, bool_t *found, const uint8_t slot
|
||||
);
|
||||
|
||||
/**
|
||||
* Opens a PSP save data file for writing, creating or truncating it.
|
||||
* Creates the save data directory if it does not already exist.
|
||||
*
|
||||
* @param p Stream to initialize.
|
||||
* @param slot Save slot index.
|
||||
* @return An error if the file cannot be opened for writing.
|
||||
*/
|
||||
errorret_t saveStreamOpenWritePSP(savestreampsp_t *p, const uint8_t slot);
|
||||
|
||||
/**
|
||||
* Closes the file descriptor held by the stream.
|
||||
*
|
||||
* @param p Stream to close.
|
||||
*/
|
||||
void saveStreamClosePSP(savestreampsp_t *p);
|
||||
|
||||
/**
|
||||
* Reads len bytes from the stream into buf.
|
||||
* Copies len bytes from the buffer at the current position into buf.
|
||||
*
|
||||
* @param p Active stream.
|
||||
* @param buf Destination buffer.
|
||||
* @param len Number of bytes to read.
|
||||
* @return An error if fewer than len bytes are available.
|
||||
* @return An error if the read would exceed the populated data length.
|
||||
*/
|
||||
errorret_t saveStreamReadBytesPSP(
|
||||
savestreampsp_t *p, void *buf, const size_t len
|
||||
);
|
||||
|
||||
/**
|
||||
* Writes len bytes from buf into the stream.
|
||||
* Copies len bytes from buf into the buffer at the current position,
|
||||
* growing p->length if this write extends past it.
|
||||
*
|
||||
* @param p Active stream.
|
||||
* @param buf Source buffer.
|
||||
* @param len Number of bytes to write.
|
||||
* @return An error if the write fails.
|
||||
* @return An error if the write would exceed bufferSize.
|
||||
*/
|
||||
errorret_t saveStreamWriteBytesPSP(
|
||||
savestreampsp_t *p, const void *buf, const size_t len
|
||||
);
|
||||
|
||||
/**
|
||||
* Seeks to an absolute byte position within the stream.
|
||||
* Sets the current read/write position within the buffer.
|
||||
*
|
||||
* @param p Active stream.
|
||||
* @param pos Target byte offset from the start of the file.
|
||||
* @return An error if the seek fails.
|
||||
* @param pos Target byte offset from the start of the buffer.
|
||||
* @return An error if pos is out of range.
|
||||
*/
|
||||
errorret_t saveStreamSeekPSP(savestreampsp_t *p, const size_t pos);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
#include "input/input.h"
|
||||
#include "save/save.h"
|
||||
|
||||
inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
||||
{ .name = "triangle", {
|
||||
@@ -83,5 +84,5 @@ inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
||||
};
|
||||
|
||||
float_t inputGetDeadzoneSDL2(const inputbutton_t button) {
|
||||
return 0.17f;
|
||||
return saveGet(SAVE_ACTIVE_SLOT)->deadzone;
|
||||
}
|
||||
|
||||
+275
-60
@@ -4,44 +4,78 @@
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
"""
|
||||
Generates chunk JSON + companion DMF mesh files from raw chunk JSON files.
|
||||
Generates DCF + companion DMF files from raw chunk JSON files, or upgrades
|
||||
legacy DCF files (version 1 or 2) to the current version.
|
||||
|
||||
JSON input (assetsraw/<map>/chunks/chunk_X_Y_Z.json, one subdirectory per
|
||||
map - e.g. assetsraw/overworld/chunks/):
|
||||
JSON input (assetsraw/chunks/chunk_X_Y_Z.json):
|
||||
{
|
||||
"tiles": [
|
||||
{ "pos": [x, y, z], "type": <tile_shape_int>, "tile": <uv_tile_int> }
|
||||
],
|
||||
"meshes": [
|
||||
{ "file": "house_5_3.dmf", "pos": [x, y, z] }
|
||||
],
|
||||
"entities": [
|
||||
{ "type": "global", "globalId": <int>, "pos": [x, y, z] },
|
||||
{ "type": "item", "itemId": <int>, "quantity": <int>, "pos": [x, y, z] }
|
||||
],
|
||||
"areas": [
|
||||
{
|
||||
"min": [x, y, z], "max": [x, y, z],
|
||||
"callbackId": <int>, "notify": <int>, "trigger": <int>
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Tiles absent from the array default to TILE_SHAPE_NULL. Each (x, y)
|
||||
column may only have ONE real tile - chunks store one tile per column,
|
||||
not one per (x, y, z), and that tile records its own local z (see the
|
||||
chunk JSON format below). If two entries share the same (x, y) with
|
||||
version 4 tile format below). If two entries share the same (x, y) with
|
||||
different z, the later one in the array wins and a warning is printed.
|
||||
Mesh files are located by searching under assets/meshes/ and referenced by
|
||||
path from the assets root in the output chunk JSON.
|
||||
path from the assets root in the DCF.
|
||||
|
||||
Output chunk JSON is derived automatically, preserving the map
|
||||
subdirectory:
|
||||
assetsraw/<map>/chunks/chunk_X_Y_Z.json -> assets/<map>/chunks/X_Y_Z.json
|
||||
"entities" spawns things into the world when this chunk loads. A "global"
|
||||
entity is spawned via mapSpawnEntity() - globalId indexes
|
||||
ENTITY_GLOBAL_LIST (src/dusk/rpg/entity/global/entitygloballist.h) and is
|
||||
deduped automatically if already spawned, so it's safe to declare on a
|
||||
chunk that streams in more than once. An "item" entity has no persistent
|
||||
identity - it respawns fresh every time this chunk (re)loads, including
|
||||
after being picked up, since nothing tracks "already collected" yet.
|
||||
itemId is a raw ITEM_ID_* value (see src/dusk/rpg/item/item.json for the
|
||||
name -> id mapping, same convention as the tile "type" ints above).
|
||||
|
||||
Chunk JSON format (loaded at runtime by assetChunkLoaderSync):
|
||||
{
|
||||
"tiles": [ [shape, z], ... ] // exactly CHUNK_WIDTH * CHUNK_HEIGHT
|
||||
// entries, one per (x, y) column,
|
||||
// x-major (matches tile_t: { shape, z })
|
||||
"meshes": [
|
||||
{ "model": "<path to .json model, relative to assets/>",
|
||||
"offset": [x, y, z] }
|
||||
]
|
||||
}
|
||||
By convention mesh index 0 (when present) is the chunk's auto-generated
|
||||
terrain and the rest are props (see sceneOverworldDrawChunksBase/Props
|
||||
on the C side).
|
||||
"areas" declares map trigger regions (see rpg/overworld/maparea.h) owned
|
||||
by this chunk - they're removed when the chunk unloads and re-added if it
|
||||
streams back in. callbackId indexes MAP_AREA_CALLBACK_LIST
|
||||
(src/dusk/rpg/overworld/global/mapareagloballist.h; 0 is reserved and
|
||||
invalid). notify is bitwise MAP_AREA_NOTIFY_PLAYER(1)|NOTIFY_NPC(2).
|
||||
trigger is bitwise MAP_TRIGGER_STEP(1)|ENTER(2)|EXIT(4).
|
||||
|
||||
Output DCF is derived automatically:
|
||||
assetsraw/chunks/chunk_X_Y_Z.json -> assets/chunks/X_Y_Z.dcf
|
||||
|
||||
Version 5 DCF format (after 8-byte header):
|
||||
tile_t tiles[CHUNK_WIDTH * CHUNK_HEIGHT] (one per x/y column)
|
||||
each tile: uint32_t shape, uint8_t z, 3 padding bytes (8 bytes total,
|
||||
matching the C tile_t struct's layout: { tileshape_t shape; uint8_t z; })
|
||||
uint8_t meshCount
|
||||
for each model:
|
||||
null-terminated string (relative asset path to .json model)
|
||||
float32[3] (x, y, z offset)
|
||||
uint8_t entitySpawnCount
|
||||
for each entity spawn:
|
||||
uint8_t kind (0 = global entity, 1 = item entity)
|
||||
uint16_t a (globalId if kind 0, itemId if kind 1)
|
||||
uint8_t b (unused if kind 0, quantity if kind 1)
|
||||
int16_t x, y, z (world position, 3 fields)
|
||||
uint8_t areaSpawnCount
|
||||
for each area spawn:
|
||||
int16_t minX, minY, minZ (3 fields)
|
||||
int16_t maxX, maxY, maxZ (3 fields)
|
||||
uint16_t callbackId
|
||||
uint8_t notify
|
||||
uint8_t trigger
|
||||
|
||||
DMF format:
|
||||
Bytes 0-3: DMF\\x00
|
||||
@@ -51,8 +85,9 @@ DMF format:
|
||||
Each vertex: uv[2] + pos[3] = 5 x float32 LE = 20 bytes
|
||||
|
||||
Usage:
|
||||
python3 -m tools.asset.chunk # process all assetsraw/*/chunks/*.json
|
||||
python3 -m tools.asset.chunk # process all assetsraw/chunks/*.json
|
||||
python3 -m tools.asset.chunk <file.json> # process one JSON file
|
||||
python3 -m tools.asset.chunk <file.dcf> # upgrade legacy (v1/v2) DCF in-place
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -79,14 +114,26 @@ WORLD_LAYER_HEIGHT = 1.0 / math.sqrt(2)
|
||||
|
||||
CHUNK_MESH_COUNT_MAX = 10
|
||||
CHUNK_MESH_NAME_MAX = 64
|
||||
CHUNK_ENTITY_SPAWN_COUNT_MAX = 8
|
||||
CHUNK_AREA_COUNT_MAX = 4
|
||||
|
||||
# Internal working format for a column's (shape, z) pair, packed/unpacked
|
||||
# with struct purely so build_terrain_verts can index into it uniformly -
|
||||
# unrelated to the on-disk format, which is JSON.
|
||||
ENTITY_SPAWN_KIND_GLOBAL = 0
|
||||
ENTITY_SPAWN_KIND_ITEM = 1
|
||||
|
||||
# Matches sizeof(tile_t) on the C side: uint32_t shape + uint8_t z, padded
|
||||
# to 8 bytes ({ tileshape_t shape; uint8_t z; } with 4-byte enum alignment).
|
||||
TILE_STRUCT_FORMAT = '<IB3x'
|
||||
TILE_SIZE = struct.calcsize(TILE_STRUCT_FORMAT)
|
||||
VERTEX_SIZE = 20
|
||||
|
||||
# Legacy (v1/v2) DCFs stored one uint32 shape per (x, y, z) triple across
|
||||
# the full 3D grid, at whatever CHUNK_DEPTH was in effect when they were
|
||||
# written (4). Frozen here, independent of the live CHUNK_DEPTH above, so
|
||||
# a future depth change can't corrupt parsing of genuinely old files.
|
||||
_LEGACY_CHUNK_DEPTH = 4
|
||||
_LEGACY_TILE_SIZE = 4
|
||||
_LEGACY_CHUNK_TILE_COUNT = CHUNK_WIDTH * CHUNK_HEIGHT * _LEGACY_CHUNK_DEPTH
|
||||
|
||||
TILE_SHAPE_NULL = 0
|
||||
TILE_SHAPE_GROUND = 1
|
||||
TILE_SHAPE_RAMP_NORTH = 2
|
||||
@@ -102,7 +149,9 @@ TILE_SHAPE_RAMP_NORTHWEST_INNER = 11
|
||||
TILE_SHAPE_RAMP_SOUTHEAST_INNER = 12
|
||||
TILE_SHAPE_RAMP_SOUTHWEST_INNER = 13
|
||||
|
||||
FILE_MAGIC = b'DCF'
|
||||
DMF_MAGIC = b'DMF\x00'
|
||||
VERSION_OUT = 5
|
||||
DMF_VERSION = 1
|
||||
|
||||
|
||||
@@ -138,13 +187,13 @@ def write_model_json(path, mesh_rel, texture_rel, color):
|
||||
print(f' Wrote model JSON {path}')
|
||||
|
||||
|
||||
def derive_chunk_json_path(json_path):
|
||||
"""assetsraw/<map>/chunks/chunk_X_Y_Z.json -> assets/<map>/chunks/X_Y_Z.json"""
|
||||
def derive_dcf_path(json_path):
|
||||
"""assetsraw/chunks/chunk_X_Y_Z.json -> assets/chunks/X_Y_Z.dcf"""
|
||||
base = os.path.splitext(os.path.basename(json_path))[0]
|
||||
if base.startswith('chunk_'):
|
||||
base = base[len('chunk_'):]
|
||||
rel_dir = os.path.relpath(os.path.dirname(os.path.abspath(json_path)), ASSETSRAW_DIR)
|
||||
return os.path.join(ASSETS_DIR, rel_dir, base + '.json')
|
||||
return os.path.join(ASSETS_DIR, rel_dir, base + '.dcf')
|
||||
|
||||
|
||||
def write_dmf(path, vertex_bytes):
|
||||
@@ -159,36 +208,77 @@ def write_dmf(path, vertex_bytes):
|
||||
print(f' Wrote DMF {path}: {vert_count} vertices, {len(buf)} bytes')
|
||||
|
||||
|
||||
def write_chunk_json(chunk_json_path, tiles, mesh_names, mesh_offsets=None):
|
||||
"""Write a chunk JSON file referencing the given model asset paths."""
|
||||
def write_dcf(
|
||||
dcf_path, tiles, mesh_names, mesh_offsets=None,
|
||||
entity_spawns=None, area_spawns=None
|
||||
):
|
||||
"""Write a current-version DCF referencing the given DMF asset paths."""
|
||||
mesh_count = len(mesh_names)
|
||||
if mesh_offsets is None:
|
||||
mesh_offsets = [(0.0, 0.0, 0.0)] * mesh_count
|
||||
if entity_spawns is None:
|
||||
entity_spawns = []
|
||||
if area_spawns is None:
|
||||
area_spawns = []
|
||||
|
||||
tile_list = []
|
||||
for i in range(CHUNK_TILE_COUNT):
|
||||
shape, z = struct.unpack_from(TILE_STRUCT_FORMAT, tiles, i * TILE_SIZE)
|
||||
tile_list.append([shape, z])
|
||||
if len(entity_spawns) > CHUNK_ENTITY_SPAWN_COUNT_MAX:
|
||||
raise ValueError(
|
||||
f"Too many entity spawns ({len(entity_spawns)}) - max "
|
||||
f"{CHUNK_ENTITY_SPAWN_COUNT_MAX}"
|
||||
)
|
||||
if len(area_spawns) > CHUNK_AREA_COUNT_MAX:
|
||||
raise ValueError(
|
||||
f"Too many area spawns ({len(area_spawns)}) - max "
|
||||
f"{CHUNK_AREA_COUNT_MAX}"
|
||||
)
|
||||
|
||||
meshes = []
|
||||
buf = bytearray()
|
||||
buf += FILE_MAGIC
|
||||
buf += b'\x00'
|
||||
buf += struct.pack('<I', VERSION_OUT)
|
||||
buf += tiles
|
||||
buf += struct.pack('<B', mesh_count)
|
||||
for name, offset in zip(mesh_names, mesh_offsets):
|
||||
if len(name) >= CHUNK_MESH_NAME_MAX:
|
||||
encoded = name.encode('ascii')
|
||||
if len(encoded) >= CHUNK_MESH_NAME_MAX:
|
||||
raise ValueError(
|
||||
f"Mesh name too long (>= {CHUNK_MESH_NAME_MAX}): {name}"
|
||||
)
|
||||
meshes.append({'model': name, 'offset': list(offset)})
|
||||
buf += encoded + b'\x00'
|
||||
buf += struct.pack('<3f', offset[0], offset[1], offset[2])
|
||||
|
||||
obj = {'tiles': tile_list, 'meshes': meshes}
|
||||
os.makedirs(os.path.dirname(chunk_json_path), exist_ok=True)
|
||||
with open(chunk_json_path, 'w') as f:
|
||||
json.dump(obj, f)
|
||||
buf += struct.pack('<B', len(entity_spawns))
|
||||
for spawn in entity_spawns:
|
||||
kind = spawn['kind']
|
||||
x, y, z = spawn['pos']
|
||||
if kind == ENTITY_SPAWN_KIND_GLOBAL:
|
||||
a, b = spawn['globalId'], 0
|
||||
else:
|
||||
a, b = spawn['itemId'], spawn['quantity']
|
||||
buf += struct.pack('<BHB3h', kind, a, b, x, y, z)
|
||||
|
||||
buf += struct.pack('<B', len(area_spawns))
|
||||
for area in area_spawns:
|
||||
minX, minY, minZ = area['min']
|
||||
maxX, maxY, maxZ = area['max']
|
||||
buf += struct.pack(
|
||||
'<6hHBB',
|
||||
minX, minY, minZ, maxX, maxY, maxZ,
|
||||
area['callbackId'], area['notify'], area['trigger']
|
||||
)
|
||||
|
||||
with open(dcf_path, 'wb') as f:
|
||||
f.write(buf)
|
||||
print(
|
||||
f' Wrote chunk JSON {chunk_json_path}: {mesh_count} mesh(es)'
|
||||
f' Wrote DCF {dcf_path}: '
|
||||
f'version {VERSION_OUT}, {mesh_count} mesh(es), '
|
||||
f'{len(entity_spawns)} entity spawn(s), {len(area_spawns)} '
|
||||
f'area(s), {len(buf)} bytes'
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# JSON -> chunk JSON + DMF
|
||||
# JSON -> DCF + DMF
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _tile_quad(u0, u1, v0, v1, fx, fy, sw_z, se_z, ne_z, nw_z):
|
||||
@@ -259,7 +349,7 @@ def build_terrain_verts(tiles_bytes):
|
||||
return bytes(buf)
|
||||
|
||||
|
||||
def from_json(json_path, chunk_json_path):
|
||||
def from_json(json_path, dcf_path):
|
||||
with open(json_path) as f:
|
||||
data = json.load(f)
|
||||
|
||||
@@ -284,7 +374,7 @@ def from_json(json_path, chunk_json_path):
|
||||
model_names = []
|
||||
mesh_offsets = []
|
||||
|
||||
chunk_base = os.path.splitext(os.path.basename(chunk_json_path))[0]
|
||||
dcf_base = os.path.splitext(os.path.basename(dcf_path))[0]
|
||||
|
||||
# Terrain mesh + model (only written if non-empty)
|
||||
terrain_dir = os.path.join(ASSETS_DIR, 'meshes', 'chunks')
|
||||
@@ -292,10 +382,10 @@ def from_json(json_path, chunk_json_path):
|
||||
os.makedirs(terrain_dir, exist_ok=True)
|
||||
os.makedirs(models_dir, exist_ok=True)
|
||||
if terrain_verts:
|
||||
dmf_name = f'chunk_{chunk_base}_0.dmf'
|
||||
dmf_name = f'chunk_{dcf_base}_0.dmf'
|
||||
write_dmf(os.path.join(terrain_dir, dmf_name), terrain_verts)
|
||||
mesh_rel = f'meshes/chunks/{dmf_name}'
|
||||
json_name = f'chunk_{chunk_base}_0.json'
|
||||
json_name = f'chunk_{dcf_base}_0.json'
|
||||
write_model_json(
|
||||
os.path.join(models_dir, json_name),
|
||||
mesh_rel,
|
||||
@@ -319,14 +409,137 @@ def from_json(json_path, chunk_json_path):
|
||||
mesh_offsets.append((float(pos[0]), float(pos[1]), float(pos[2])))
|
||||
print(f' Resolved {filename} -> {rel}')
|
||||
|
||||
write_chunk_json(chunk_json_path, bytes(tiles), model_names, mesh_offsets)
|
||||
entity_spawns = []
|
||||
for spawn in data.get('entities', []):
|
||||
pos = tuple(int(v) for v in spawn['pos'])
|
||||
if spawn['type'] == 'global':
|
||||
entity_spawns.append({
|
||||
'kind': ENTITY_SPAWN_KIND_GLOBAL,
|
||||
'globalId': int(spawn['globalId']),
|
||||
'pos': pos,
|
||||
})
|
||||
elif spawn['type'] == 'item':
|
||||
entity_spawns.append({
|
||||
'kind': ENTITY_SPAWN_KIND_ITEM,
|
||||
'itemId': int(spawn['itemId']),
|
||||
'quantity': int(spawn['quantity']),
|
||||
'pos': pos,
|
||||
})
|
||||
else:
|
||||
raise ValueError(f"Unknown entity spawn type: {spawn['type']}")
|
||||
|
||||
area_spawns = []
|
||||
for area in data.get('areas', []):
|
||||
area_spawns.append({
|
||||
'min': tuple(int(v) for v in area['min']),
|
||||
'max': tuple(int(v) for v in area['max']),
|
||||
'callbackId': int(area['callbackId']),
|
||||
'notify': int(area['notify']),
|
||||
'trigger': int(area['trigger']),
|
||||
})
|
||||
|
||||
write_dcf(
|
||||
dcf_path, bytes(tiles), model_names, mesh_offsets,
|
||||
entity_spawns, area_spawns
|
||||
)
|
||||
|
||||
|
||||
def process_json(json_path):
|
||||
chunk_json_path = derive_chunk_json_path(json_path)
|
||||
os.makedirs(os.path.dirname(chunk_json_path), exist_ok=True)
|
||||
print(f"{json_path} -> {chunk_json_path}")
|
||||
from_json(json_path, chunk_json_path)
|
||||
dcf_path = derive_dcf_path(json_path)
|
||||
os.makedirs(os.path.dirname(dcf_path), exist_ok=True)
|
||||
print(f"{json_path} -> {dcf_path}")
|
||||
from_json(json_path, dcf_path)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Legacy DCF (v1/v2) -> current version
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def collapse_legacy_tiles(legacy_tiles, path):
|
||||
"""Collapse the old one-tile-per-(x,y,z) grid into the current
|
||||
one-tile-per-(x,y) format. If a column has more than one non-null tile
|
||||
across its Z layers, the highest Z wins and a warning is printed."""
|
||||
tiles = bytearray(CHUNK_TILE_COUNT * TILE_SIZE)
|
||||
for y in range(CHUNK_HEIGHT):
|
||||
for x in range(CHUNK_WIDTH):
|
||||
found = []
|
||||
for z in range(_LEGACY_CHUNK_DEPTH):
|
||||
legacy_idx = x + y * CHUNK_WIDTH + z * CHUNK_WIDTH * CHUNK_HEIGHT
|
||||
shape = struct.unpack_from(
|
||||
'<I', legacy_tiles, legacy_idx * _LEGACY_TILE_SIZE
|
||||
)[0]
|
||||
if shape != TILE_SHAPE_NULL:
|
||||
found.append((z, shape))
|
||||
if not found:
|
||||
continue
|
||||
if len(found) > 1:
|
||||
print(
|
||||
f' WARNING: {path}: column ({x}, {y}) has tiles at '
|
||||
f'z={[z for z, _ in found]} - only one tile per column '
|
||||
f'is kept, z={found[-1][0]} wins (highest Z)'
|
||||
)
|
||||
z, shape = found[-1]
|
||||
struct.pack_into(
|
||||
TILE_STRUCT_FORMAT, tiles, tile_index(x, y) * TILE_SIZE,
|
||||
shape, z
|
||||
)
|
||||
return bytes(tiles)
|
||||
|
||||
|
||||
def read_legacy_dcf(path):
|
||||
with open(path, 'rb') as f:
|
||||
data = f.read()
|
||||
if data[:3] != FILE_MAGIC:
|
||||
raise ValueError(f"{path}: not a DCF file")
|
||||
version = struct.unpack_from('<I', data, 4)[0]
|
||||
if version not in (1, 2):
|
||||
raise ValueError(f"{path}: expected version 1 or 2, got {version}")
|
||||
|
||||
offset = 8
|
||||
tiles_size = _LEGACY_CHUNK_TILE_COUNT * _LEGACY_TILE_SIZE
|
||||
tiles = collapse_legacy_tiles(data[offset:offset + tiles_size], path)
|
||||
offset += tiles_size
|
||||
|
||||
meshes = []
|
||||
if version == 1:
|
||||
vert_count = struct.unpack_from('<I', data, offset)[0]
|
||||
offset += 4
|
||||
verts = data[offset:offset + vert_count * VERTEX_SIZE]
|
||||
if len(verts) != vert_count * VERTEX_SIZE:
|
||||
raise ValueError(f"{path}: truncated vertex data")
|
||||
if vert_count > 0:
|
||||
meshes.append(verts)
|
||||
else:
|
||||
mesh_count = data[offset]
|
||||
offset += 1
|
||||
for _ in range(mesh_count):
|
||||
vert_count = struct.unpack_from('<I', data, offset)[0]
|
||||
offset += 4
|
||||
verts = data[offset:offset + vert_count * VERTEX_SIZE]
|
||||
if len(verts) != vert_count * VERTEX_SIZE:
|
||||
raise ValueError(f"{path}: truncated vertex data")
|
||||
offset += vert_count * VERTEX_SIZE
|
||||
if vert_count > 0:
|
||||
meshes.append(verts)
|
||||
return tiles, meshes
|
||||
|
||||
|
||||
def upgrade_dcf(path):
|
||||
print(f"Upgrading legacy DCF {path} ...")
|
||||
tiles, meshes = read_legacy_dcf(path)
|
||||
print(
|
||||
f" tiles={CHUNK_TILE_COUNT}, meshes={len(meshes)}, "
|
||||
f"total_verts={sum(len(m) // VERTEX_SIZE for m in meshes)}"
|
||||
)
|
||||
base = os.path.splitext(os.path.basename(path))[0]
|
||||
terrain_dir = os.path.join(ASSETS_DIR, 'meshes', 'chunks')
|
||||
os.makedirs(terrain_dir, exist_ok=True)
|
||||
mesh_names = []
|
||||
for idx, verts in enumerate(meshes):
|
||||
dmf_name = f'chunk_{base}_{idx}.dmf'
|
||||
write_dmf(os.path.join(terrain_dir, dmf_name), verts)
|
||||
mesh_names.append(f'meshes/chunks/{dmf_name}')
|
||||
write_dcf(path, tiles, mesh_names)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -337,25 +550,27 @@ def main():
|
||||
args = sys.argv[1:]
|
||||
|
||||
if not args:
|
||||
chunks_dir = os.path.join(ASSETSRAW_DIR, 'chunks')
|
||||
if not os.path.isdir(chunks_dir):
|
||||
print(f"No directory found: {chunks_dir}")
|
||||
sys.exit(1)
|
||||
json_files = sorted(
|
||||
os.path.join(dirpath, f)
|
||||
for dirpath, _, filenames in os.walk(ASSETSRAW_DIR)
|
||||
if os.path.basename(dirpath) == 'chunks'
|
||||
for f in filenames
|
||||
os.path.join(chunks_dir, f)
|
||||
for f in os.listdir(chunks_dir)
|
||||
if f.endswith('.json')
|
||||
)
|
||||
if not json_files:
|
||||
print(f"No chunk JSON files found under {ASSETSRAW_DIR}")
|
||||
print(f"No JSON files found in {chunks_dir}")
|
||||
sys.exit(0)
|
||||
for p in json_files:
|
||||
process_json(p)
|
||||
return
|
||||
|
||||
src = args[0]
|
||||
if os.path.splitext(src)[1].lower() != '.json':
|
||||
print(f"Expected a .json input file, got: {src}")
|
||||
sys.exit(1)
|
||||
if os.path.splitext(src)[1].lower() == '.json':
|
||||
process_json(src)
|
||||
else:
|
||||
upgrade_dcf(src)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
|
||||
parser = argparse.ArgumentParser(description="Item JSON to .h defines")
|
||||
parser.add_argument("--json", required=True, help="Path to item JSON file")
|
||||
parser.add_argument("--output", required=True, help="Path to output .h file")
|
||||
args = parser.parse_args()
|
||||
|
||||
def type_enum(name):
|
||||
return "ITEM_TYPE_" + name.upper()
|
||||
|
||||
def id_enum(name):
|
||||
return "ITEM_ID_" + name.upper()
|
||||
|
||||
# Load JSON
|
||||
item_ids = []
|
||||
item_types = []
|
||||
rows = {}
|
||||
|
||||
with open(args.json, encoding="utf-8") as f:
|
||||
entries = json.load(f)
|
||||
|
||||
if not all(
|
||||
"id" in row and "type" in row and "name" in row for row in entries
|
||||
):
|
||||
raise ValueError("Each item must have 'id', 'type', and 'name' fields")
|
||||
|
||||
for row in entries:
|
||||
item_id, item_type = row["id"], row["type"]
|
||||
if item_id not in item_ids:
|
||||
item_ids.append(item_id)
|
||||
if item_type not in item_types:
|
||||
item_types.append(item_type)
|
||||
rows[item_id] = row
|
||||
|
||||
# Assign enum values: types and IDs each start from 1 with NULL = 0.
|
||||
type_values = {}
|
||||
type_count = 1
|
||||
for t in item_types:
|
||||
type_values[t] = type_count
|
||||
type_count += 1
|
||||
|
||||
id_values = {}
|
||||
id_count = 1
|
||||
for i in item_ids:
|
||||
id_values[i] = id_count
|
||||
id_count += 1
|
||||
|
||||
# Count items per type
|
||||
type_item_counts = { t: 0 for t in item_types }
|
||||
for i in item_ids:
|
||||
type_item_counts[rows[i]["type"]] += 1
|
||||
|
||||
# Build output
|
||||
out = [
|
||||
"#pragma once",
|
||||
'#include "dusk.h"',
|
||||
"",
|
||||
"typedef enum {",
|
||||
" ITEM_TYPE_NULL = 0,",
|
||||
]
|
||||
for t in item_types:
|
||||
out.append(f" {type_enum(t)} = {type_values[t]},")
|
||||
out += [
|
||||
f" ITEM_TYPE_COUNT = {type_count}",
|
||||
"} itemtype_t;",
|
||||
"",
|
||||
"typedef enum {",
|
||||
" ITEM_ID_NULL = 0,",
|
||||
]
|
||||
for i in item_ids:
|
||||
out.append(f" {id_enum(i)} = {id_values[i]},")
|
||||
out += [
|
||||
f" ITEM_ID_COUNT = {id_count}",
|
||||
"} itemid_t;",
|
||||
"",
|
||||
"typedef struct {",
|
||||
" itemid_t id;",
|
||||
" itemtype_t type;",
|
||||
" const char_t *name;",
|
||||
"} item_t;",
|
||||
"",
|
||||
"static const item_t ITEMS[] = {",
|
||||
]
|
||||
for i in item_ids:
|
||||
row = rows[i]
|
||||
out += [
|
||||
f" [{id_enum(i)}] = {{",
|
||||
f" .id = {id_enum(i)},",
|
||||
f" .type = {type_enum(row['type'])},",
|
||||
f" .name = \"item.{row['name']}.name\",",
|
||||
" },",
|
||||
]
|
||||
out += [
|
||||
"};",
|
||||
"",
|
||||
"static const uint8_t ITEM_TYPE_COUNTS[] = {",
|
||||
]
|
||||
for t in item_types:
|
||||
out.append(f" [{type_enum(t)}] = {type_item_counts[t]},")
|
||||
out += [
|
||||
"};",
|
||||
"",
|
||||
]
|
||||
max_type_count = max(type_item_counts.values()) if type_item_counts else 0
|
||||
out += [
|
||||
f"#define ITEM_TYPE_COUNT_MAX {max_type_count}",
|
||||
"",
|
||||
]
|
||||
|
||||
os.makedirs(os.path.dirname(args.output), exist_ok=True)
|
||||
with open(args.output, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(out))
|
||||
+5
-1
@@ -38,7 +38,11 @@ out += [
|
||||
" STORY_FLAG_COUNT",
|
||||
"} storyflag_t;",
|
||||
"",
|
||||
"static storyflagvalue_t STORY_FLAG_VALUES[STORY_FLAG_COUNT] = {",
|
||||
"// Stamped onto a save file's storyFlags the first time it's used (see",
|
||||
"// storyFlagInitDefaults()) - not a live value array. Live flag state",
|
||||
"// lives entirely in the save file (savefile_t.storyFlags), read/written",
|
||||
"// via storyFlagGet()/storyFlagSet() - see storyflag.h.",
|
||||
"static const storyflagvalue_t STORY_FLAG_DEFAULTS[STORY_FLAG_COUNT] = {",
|
||||
]
|
||||
for flag in flags:
|
||||
out.append(f" [{flag_enum(flag['id'])}] = {flag['initial']},")
|
||||
|
||||
Reference in New Issue
Block a user