Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8fb1c9fb42 | |||
| 5f08337726 | |||
| 0bc80d5df3 | |||
| 2432443ea6 | |||
| fa138971e7 | |||
| d7982599d1 | |||
| f16c80aa0f | |||
| ef5adf8274 | |||
| 2ffc65b1b1 | |||
| d4a17bd98d | |||
| dcf5f434c5 | |||
| 78cd973a33 |
@@ -0,0 +1,453 @@
|
|||||||
|
# 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 |
|
||||||
|
| `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)
|
||||||
|
src/dusksdl2/ SDL2 window + input (Linux, Knulli, PSP)
|
||||||
|
src/dusklinux/ Linux + Knulli platform impl
|
||||||
|
src/duskpsp/ PSP platform impl
|
||||||
|
src/duskdolphin/ GameCube / Wii platform impl (no SDL2/OpenGL)
|
||||||
|
```
|
||||||
|
|
||||||
|
Dolphin is the only target that bypasses SDL2 and OpenGL entirely —
|
||||||
|
it uses native GameCube/Wii rendering and input APIs.
|
||||||
|
|
||||||
|
### Platform guards
|
||||||
|
Use the compile-time macros for platform-specific code:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#ifdef DUSK_PSP
|
||||||
|
// PSP-only path
|
||||||
|
#elif defined(DUSK_GAMECUBE) || defined(DUSK_WII)
|
||||||
|
// GameCube / Wii path
|
||||||
|
#else
|
||||||
|
// Generic / Linux fallback
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
|
||||||
|
Additional capability macros set per-target:
|
||||||
|
`DUSK_SDL2`, `DUSK_OPENGL`, `DUSK_OPENGL_ES`, `DUSK_OPENGL_LEGACY`,
|
||||||
|
`DUSK_INPUT_GAMEPAD`, `DUSK_INPUT_KEYBOARD`, `DUSK_INPUT_POINTER`,
|
||||||
|
`DUSK_PLATFORM_ENDIAN_BIG` / `DUSK_PLATFORM_ENDIAN_LITTLE`.
|
||||||
|
|
||||||
|
### Abstraction pattern
|
||||||
|
Platform-specific implementations are wired in via `#define` macros in
|
||||||
|
each platform's `displayplatform.h` / `inputplatform.h` etc., which
|
||||||
|
the core calls through. Functions that a platform does not support are
|
||||||
|
simply left undefined — the core guards calls with `#ifdef`.
|
||||||
|
|
||||||
|
### Adding platform-specific code
|
||||||
|
- Put it under `src/dusk<platform>/` in the matching subsystem folder.
|
||||||
|
- Gate any core call-site with the appropriate `#ifdef DUSK_<PLATFORM>`
|
||||||
|
or capability macro.
|
||||||
|
- Keep the `src/dusk/` core free of platform ifdefs — delegate through
|
||||||
|
the platform header macros instead.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adding a new asset loader type
|
||||||
|
1. Add an enum value to `assetloadertype_t` (before `_COUNT`) in
|
||||||
|
`src/dusk/asset/loader/assetloader.h`.
|
||||||
|
2. Add fields to the input/loading/output unions in `assetloader.h`.
|
||||||
|
3. Implement `assetXxxLoaderSync`, `assetXxxLoaderAsync`, and
|
||||||
|
`assetXxxDispose` in a new `src/dusk/asset/loader/xxx/` directory.
|
||||||
|
4. Register the three callbacks in `ASSET_LOADER_CALLBACKS[]` in
|
||||||
|
`src/dusk/asset/loader/assetloader.c`.
|
||||||
|
5. If user-facing, create a JS module (see below) and a `.d.ts` file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adding a new entity component
|
||||||
|
1. Create `src/dusk/entity/component/<category>/entityMyComp.h/.c` with
|
||||||
|
struct `entityMyComp_t`, `entityMyCompInit()`, and optionally
|
||||||
|
`entityMyCompDispose()`.
|
||||||
|
2. Add the include to `src/dusk/entity/componentlist.h` header block.
|
||||||
|
3. Add a row to `src/dusk/entity/componentlist.h`:
|
||||||
|
```c
|
||||||
|
X(MYCOMP, entityMyComp_t, myComp, entityMyCompInit, NULL, NULL)
|
||||||
|
```
|
||||||
|
This auto-generates the enum, union field, and definition entry.
|
||||||
|
4. If JS-facing, create the script module and `.d.ts` (see below).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adding a new script (JS) module
|
||||||
|
1. Create `src/dusk/script/module/<category>/moduleMyMod.h/.c`.
|
||||||
|
- Declare `extern scriptproto_t MODULE_MYMOD_PROTO;` in the header.
|
||||||
|
- Use `moduleBaseFunction(name)` to define JS-callable functions.
|
||||||
|
- Register props/funcs in `moduleMyModInit()` with
|
||||||
|
`scriptProtoDefineProp` / `scriptProtoDefineFunc` /
|
||||||
|
`scriptProtoDefineStaticFunc`.
|
||||||
|
2. `#include` the header in
|
||||||
|
`src/dusk/script/module/modulelist.c` and call
|
||||||
|
`moduleMyModInit()` in `moduleListInit()` (and `Dispose` in
|
||||||
|
`moduleListDispose()`).
|
||||||
|
3. For component modules also register in
|
||||||
|
`src/dusk/script/module/entity/component/modulecomponentlist.c`
|
||||||
|
so `entity.add()` returns the typed wrapper.
|
||||||
|
4. Create `types/<category>/mymod.d.ts` and add a
|
||||||
|
`/// <reference path="..." />` line to `types/index.d.ts`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Script module type declarations
|
||||||
|
Whenever a `src/dusk/script/module/**/*.c` file is created or modified,
|
||||||
|
check whether the corresponding `types/**/*.d.ts` needs updating and
|
||||||
|
apply any changes before finishing the task.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## JavaScript (asset scripts)
|
||||||
|
- Use `var` for module-level state; `const` for values that never
|
||||||
|
change.
|
||||||
|
- Always use semicolons.
|
||||||
|
- Scene objects are plain objects (`var scene = {}`) with assigned
|
||||||
|
methods.
|
||||||
|
- Export via `module.exports = scene`.
|
||||||
|
- Async scene init should use `async function` and `await`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Coding style
|
||||||
|
|
||||||
|
### ASCII only
|
||||||
|
Source files (`.c`, `.h`, `.js`) must contain only ASCII characters (U+0000–U+007F).
|
||||||
|
Non-ASCII characters are banned even in comments and string literals.
|
||||||
|
Use ASCII-only substitutes instead:
|
||||||
|
- `--` or `-` instead of `—` (em dash)
|
||||||
|
- `->` instead of `→` (arrow)
|
||||||
|
- `x` or `*` instead of `×` (multiplication)
|
||||||
|
|
||||||
|
Only non-script asset files (e.g. `.po` locale files) may contain non-ASCII text.
|
||||||
|
|
||||||
|
### Indentation
|
||||||
|
2 spaces. No tabs.
|
||||||
|
|
||||||
|
### Keyword and operator spacing
|
||||||
|
No space between a keyword or function name and its opening parenthesis:
|
||||||
|
|
||||||
|
```c
|
||||||
|
if(!ptr) return;
|
||||||
|
for(uint8_t i = 0; i < count; i++) {
|
||||||
|
while(entry->state != DONE) {
|
||||||
|
switch(type) {
|
||||||
|
sizeof(assetbatch_t)
|
||||||
|
memoryZero(ptr, size)
|
||||||
|
```
|
||||||
|
|
||||||
|
Spaces around all binary operators and after every comma:
|
||||||
|
|
||||||
|
```c
|
||||||
|
pos->flags |= ENTITY_POSITION_FLAG_WORLD_DIRTY;
|
||||||
|
(size_t)end - (size_t)start
|
||||||
|
foo(a, b, c)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Braces
|
||||||
|
Opening brace on the **same line** as the statement (K&R style) for all
|
||||||
|
constructs — functions, `if`, `else`, `for`, `while`, `switch`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
void assetEntryLock(assetentry_t *entry) {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
if(dirty) {
|
||||||
|
...
|
||||||
|
} else {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Guard returns
|
||||||
|
Short guards go on one line with no braces:
|
||||||
|
|
||||||
|
```c
|
||||||
|
if(!ptr) return;
|
||||||
|
if(!b || !b->batch) return jerry_undefined();
|
||||||
|
if(!(flags & DIRTY)) return;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Blank lines
|
||||||
|
- One blank line between functions; no blank line at the start or end of
|
||||||
|
a function body.
|
||||||
|
- One blank line between logical blocks inside a function body.
|
||||||
|
- No trailing blank lines at the end of a file.
|
||||||
|
|
||||||
|
### Pointer placement
|
||||||
|
`*` is attached to the variable name, not the type:
|
||||||
|
|
||||||
|
```c
|
||||||
|
assetentry_t *entry
|
||||||
|
const char_t *name
|
||||||
|
void *ptr
|
||||||
|
uint8_t *d = (uint8_t *)dest;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Casts
|
||||||
|
Space between cast and operand:
|
||||||
|
|
||||||
|
```c
|
||||||
|
(assetbatch_t *)user
|
||||||
|
(uint8_t *)dest
|
||||||
|
(textureformat_t)v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return
|
||||||
|
No parentheses around the return value:
|
||||||
|
|
||||||
|
```c
|
||||||
|
return ptr;
|
||||||
|
return MEMORY_POINTERS_IN_USE;
|
||||||
|
```
|
||||||
|
|
||||||
|
### switch / case
|
||||||
|
`case` indented 2 spaces from `switch`; body indented 2 more from `case`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
switch(type) {
|
||||||
|
case ASSET_LOADER_TYPE_TEXTURE:
|
||||||
|
descs[i].input.texture = (textureformat_t)v;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-line function signatures
|
||||||
|
When parameters don't fit on one line, put each on its own line indented
|
||||||
|
2 spaces; the closing `) {` (definition) or `);` (declaration) goes on
|
||||||
|
its own line at column 0:
|
||||||
|
|
||||||
|
```c
|
||||||
|
void assetEntryInit(
|
||||||
|
assetentry_t *entry,
|
||||||
|
const char_t *name,
|
||||||
|
const assetloadertype_t type,
|
||||||
|
assetloaderinput_t *input
|
||||||
|
) {
|
||||||
|
|
||||||
|
errorret_t memoryCompare(
|
||||||
|
const void *a,
|
||||||
|
const void *b,
|
||||||
|
const size_t size
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Structs and enums
|
||||||
|
Anonymous inner struct or enum with a `typedef`, `_t` suffix, closing
|
||||||
|
brace and name on the same line:
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
errorcode_t code;
|
||||||
|
char_t *message;
|
||||||
|
} errorstate_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
ASSET_LOADER_TYPE_NULL,
|
||||||
|
ASSET_LOADER_TYPE_COUNT
|
||||||
|
} assetloadertype_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Designated initialisers
|
||||||
|
Spaces inside braces; `.field = value`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
jsassetentry_t e = { .entry = entry };
|
||||||
|
assetbatchloadedpend_t init = { .batch = batch };
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ternary operator
|
||||||
|
Spaces around `?` and `:`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
const float val = psx > 0.0f ? pt[0][0] / psx : 0.0f;
|
||||||
|
```
|
||||||
|
|
||||||
|
### const placement
|
||||||
|
`const` before the type, `*` attached to the variable:
|
||||||
|
|
||||||
|
```c
|
||||||
|
const char_t *name
|
||||||
|
const void *src
|
||||||
|
const size_t size
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comments in `.c` files
|
||||||
|
- Do not use section dividers (`/* ---- ... ---- */`). Just let the
|
||||||
|
functions follow one another with a single blank line between them.
|
||||||
|
- Multi-line explanatory comments inside function bodies use `//` lines:
|
||||||
|
```c
|
||||||
|
// Script modules are freed; orphaned JS wrapper objects now get GC'd
|
||||||
|
// so their finalizers fire before assetDispose() checks ref counts.
|
||||||
|
jerry_heap_gc(JERRY_GC_PRESSURE_HIGH);
|
||||||
|
```
|
||||||
|
- Do not use `/* */` for inline or inline-block comments inside `.c`
|
||||||
|
function bodies.
|
||||||
|
|
||||||
|
### Comments in `.h` files
|
||||||
|
Every public declaration gets a Javadoc block (`/** … */`) with
|
||||||
|
`@param` and `@returns` where relevant. Keep it on the lines immediately
|
||||||
|
above the declaration with no blank line in between.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Color system
|
||||||
|
|
||||||
|
Colors are defined in `src/dusk/display/color.csv` and code-generated
|
||||||
|
into a `color.h` header by `tools/color/csv/__main__.py`.
|
||||||
|
|
||||||
|
Each row in the CSV has `name,r,g,b,a` with channel values in `[0.0, 1.0]`.
|
||||||
|
The script emits four `#define` variants per color plus a bare alias:
|
||||||
|
|
||||||
|
```
|
||||||
|
COLOR_<NAME>_4B color4b(r8, g8, b8, a8) // default alias target
|
||||||
|
COLOR_<NAME>_3B color3b(r8, g8, b8)
|
||||||
|
COLOR_<NAME>_3F color3f(rf, gf, bf)
|
||||||
|
COLOR_<NAME>_4F color4f(rf, gf, bf, af)
|
||||||
|
COLOR_<NAME> COLOR_<NAME>_4B
|
||||||
|
```
|
||||||
|
|
||||||
|
`color_t` is `color4b_t` (four `uint8_t` channels).
|
||||||
|
|
||||||
|
To add a new color, append a row to `color.csv` and rebuild — do not
|
||||||
|
hand-edit the generated header.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
- Tests live in `test/` mirroring `src/dusk/` structure.
|
||||||
|
- Use cmocka; include `dusktest.h`.
|
||||||
|
- Test functions: `static void test_something(void **state)`.
|
||||||
|
- After each test, assert `memoryGetAllocatedCount() == 0` to catch
|
||||||
|
leaks.
|
||||||
|
- Build with `-DDUSK_BUILD_TESTS=ON`.
|
||||||
@@ -13,7 +13,6 @@ cmake_policy(SET CMP0079 NEW)
|
|||||||
# set(FETCHCONTENT_UPDATES_DISCONNECTED ON)
|
# set(FETCHCONTENT_UPDATES_DISCONNECTED ON)
|
||||||
|
|
||||||
option(DUSK_BUILD_TESTS "Enable tests" OFF)
|
option(DUSK_BUILD_TESTS "Enable tests" OFF)
|
||||||
option(DUSK_NETWORK "Enable network support" ON)
|
|
||||||
|
|
||||||
set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name")
|
set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name")
|
||||||
set(DUSK_GAME_AUTHOR "YourWishes" CACHE STRING "Game author / coder")
|
set(DUSK_GAME_AUTHOR "YourWishes" CACHE STRING "Game author / coder")
|
||||||
@@ -91,12 +90,6 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
DUSK_VERSION="${DUSK_VERSION}"
|
DUSK_VERSION="${DUSK_VERSION}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if(DUSK_NETWORK)
|
|
||||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
|
||||||
DUSK_NETWORK
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# Toolchains
|
# Toolchains
|
||||||
include(cmake/targets/${DUSK_TARGET_SYSTEM}.cmake)
|
include(cmake/targets/${DUSK_TARGET_SYSTEM}.cmake)
|
||||||
|
|
||||||
|
|||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
# Dusk Roadmap
|
||||||
|
|
||||||
|
Tracking upcoming milestones for the engine.
|
||||||
|
|
||||||
|
## Upcoming milestones
|
||||||
|
|
||||||
|
1. Add a very basic physics engine, moving away from the current
|
||||||
|
tile-based movement.
|
||||||
|
2. Give entities full freedom of movement (no longer locked to tile
|
||||||
|
grid positions).
|
||||||
|
3. Update entity interaction, triggers, chunk management, and other
|
||||||
|
systems that currently assume tile-based positioning so they work
|
||||||
|
with the new 3D positioning/movement code.
|
||||||
|
4. Investigate and fix poor UI rendering performance. Rendering the
|
||||||
|
console alone tanks framerate despite the existing mesh
|
||||||
|
optimizations, so there is likely more headroom to find in the
|
||||||
|
vertex/text rendering path.
|
||||||
|
5. Create UI elements for displaying status indicators, e.g. network
|
||||||
|
connection state and save-in-progress.
|
||||||
|
6. Fully test saving end-to-end on all supported platforms.
|
||||||
|
7. Remove the tile system from chunks in favor of meshes, with
|
||||||
|
dynamic hitboxes per chunk loaded in from the chunk file data.
|
||||||
|
8. Create UI elements for network status: a connecting modal, an
|
||||||
|
error state, and a connected flag. Retire the test HTTP request
|
||||||
|
once these are in place.
|
||||||
|
9. Build the socket server and client implementation, including
|
||||||
|
handlers for the different packet types.
|
||||||
|
10. Add a dedicated multiplayer entity type, `clientplayer`, alongside
|
||||||
|
the existing `npc` and `player` types. Limit to 8 (defined
|
||||||
|
constant) for now.
|
||||||
|
11. Send and receive `clientplayer` position over the network.
|
||||||
|
12. Create a UI menu for creating a server and joining a server. For
|
||||||
|
now, join IPs are hard-coded (testing against a fixed IP of
|
||||||
|
10.0.0.94).
|
||||||
|
13. Create "handshake" packets. For now, just send the username,
|
||||||
|
enforced to be under 10 characters long.
|
||||||
|
14. Server tracks all players' positions and broadcasts them to all
|
||||||
|
connected clients.
|
||||||
|
15. Server sends disconnect packets for users who leave.
|
||||||
|
16. Server assigns each client a UUID; all clients know every other
|
||||||
|
client's UUID (used to reference them across position updates,
|
||||||
|
disconnect packets, etc).
|
||||||
|
17. Server notifies all clients (by UUID) when a user joins, leaves,
|
||||||
|
or is disconnected, so clients can spawn or remove the
|
||||||
|
corresponding `clientplayer` entity in the world.
|
||||||
|
|
||||||
|
## Principles
|
||||||
|
|
||||||
|
- Never trust the network implicitly. Neither side (server or client)
|
||||||
|
should assume the other's packets are well-formed or benign --
|
||||||
|
validate all incoming packet data defensively, since either side
|
||||||
|
may send garbage or malicious data. Use `errorret_t` /
|
||||||
|
`errorThrow()` for these runtime checks, not assert macros --
|
||||||
|
asserts are debug-only and won't guard release builds against
|
||||||
|
malformed or malicious packet data.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -10,9 +10,6 @@ msgid "ui.title"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Welcome"
|
"Welcome"
|
||||||
|
|
||||||
msgid "save.linux.mkdirp_failed"
|
|
||||||
msgstr "Failed to create save directory, check the disk is not full or write-protected."
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettings.c
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
msgid "ui.settings.tabs.general"
|
msgid "ui.settings.tabs.general"
|
||||||
msgstr "General"
|
msgstr "General"
|
||||||
@@ -59,130 +56,6 @@ msgstr "Items"
|
|||||||
msgid "ui.game_menu.settings"
|
msgid "ui.game_menu.settings"
|
||||||
msgstr "Settings"
|
msgstr "Settings"
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save"
|
|
||||||
msgstr "Save"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_success"
|
|
||||||
msgstr "Game saved."
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_cancelled"
|
|
||||||
msgstr "Save cancelled."
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_unavailable"
|
|
||||||
msgstr "Can't save - no save device found."
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_temporary"
|
|
||||||
msgstr "This session is temporary - no save device was found, so saving is disabled."
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_create_confirm"
|
|
||||||
msgstr "No save data found. Create a new save?"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_failed_format"
|
|
||||||
msgstr "Save failed: %s"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_check_failed_format"
|
|
||||||
msgstr "Can't save: %s"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
|
||||||
msgid "ui.initial.no_card.message"
|
|
||||||
msgstr "No save device found. You can continue, but\nprogress will not be saved."
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
|
||||||
msgid "ui.initial.no_card.retry"
|
|
||||||
msgstr "Retry"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
|
||||||
msgid "ui.initial.no_card.continue"
|
|
||||||
msgstr "Continue Anyway"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
|
||||||
msgid "ui.initial.create_save.message"
|
|
||||||
msgstr "No save data found. Create a new save?"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
|
||||||
msgid "ui.initial.create_save.yes"
|
|
||||||
msgstr "Yes"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
|
||||||
msgid "ui.initial.create_save.no"
|
|
||||||
msgstr "No"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
|
||||||
msgid "ui.main_menu.new_game"
|
|
||||||
msgstr "New Game"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
|
||||||
msgid "ui.main_menu.load_game"
|
|
||||||
msgstr "Load Game"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
|
||||||
msgid "ui.main_menu.options"
|
|
||||||
msgstr "Options"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
|
||||||
msgid "ui.main_menu.quit"
|
|
||||||
msgstr "Quit Game"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/uiconfirm.c
|
|
||||||
msgid "ui.confirm.confirm"
|
|
||||||
msgstr "Confirm"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/uiconfirm.c
|
|
||||||
msgid "ui.confirm.cancel"
|
|
||||||
msgstr "Cancel"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
|
||||||
msgid "ui.battle.menu.attack"
|
|
||||||
msgstr "Attack"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
|
||||||
msgid "ui.battle.menu.flee"
|
|
||||||
msgstr "Flee"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
|
||||||
msgid "ui.battle.menu.target_format"
|
|
||||||
msgstr "Enemy %u (%u/%u HP)"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/battle/uibattlehud.c
|
|
||||||
msgid "ui.battle.hud.hp_format"
|
|
||||||
msgstr "HP %u/%u"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/battle/uibattlehud.c
|
|
||||||
msgid "ui.battle.hud.mp_format"
|
|
||||||
msgstr "MP %u/%u"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettingsaudio.c
|
|
||||||
msgid "ui.settings.audio.placeholder"
|
|
||||||
msgstr "No audio settings yet"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettingsdisplay.c
|
|
||||||
msgid "ui.settings.display.placeholder"
|
|
||||||
msgstr "No display settings yet"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettingsinput.c
|
|
||||||
msgid "ui.settings.input.placeholder"
|
|
||||||
msgstr "No input settings yet"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/backpack/uibackpack.c
|
|
||||||
msgid "ui.backpack.category_format"
|
|
||||||
msgstr "Category %u"
|
|
||||||
|
|
||||||
#: src/dusk/ui/overlay/uiloading.c
|
|
||||||
msgid "ui.loading.text"
|
|
||||||
msgstr "loading"
|
|
||||||
|
|
||||||
#: src/dusk/ui/overlay/uiautosave.c
|
|
||||||
msgid "ui.autosave.saving"
|
|
||||||
msgstr "SAVING"
|
|
||||||
|
|
||||||
msgid "item.potion.name"
|
msgid "item.potion.name"
|
||||||
msgstr "Potion"
|
msgstr "Potion"
|
||||||
|
|
||||||
|
|||||||
@@ -57,130 +57,6 @@ msgstr "Objetos"
|
|||||||
msgid "ui.game_menu.settings"
|
msgid "ui.game_menu.settings"
|
||||||
msgstr "Configuración"
|
msgstr "Configuración"
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save"
|
|
||||||
msgstr "Guardar"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_success"
|
|
||||||
msgstr "Partida guardada."
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_cancelled"
|
|
||||||
msgstr "Guardado cancelado."
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_unavailable"
|
|
||||||
msgstr "No se puede guardar: no se encontró ningún dispositivo de guardado."
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_temporary"
|
|
||||||
msgstr "Esta sesión es temporal - no se encontró ningún dispositivo de guardado, por lo que guardar está deshabilitado."
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_create_confirm"
|
|
||||||
msgstr "No se encontraron datos guardados. ¿Crear una partida nueva?"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_failed_format"
|
|
||||||
msgstr "Error al guardar: %s"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_check_failed_format"
|
|
||||||
msgstr "No se puede guardar: %s"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
|
||||||
msgid "ui.initial.no_card.message"
|
|
||||||
msgstr "No se encontró ningún dispositivo de guardado. Puedes continuar, pero\nel progreso no se guardará."
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
|
||||||
msgid "ui.initial.no_card.retry"
|
|
||||||
msgstr "Reintentar"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
|
||||||
msgid "ui.initial.no_card.continue"
|
|
||||||
msgstr "Continuar de todos modos"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
|
||||||
msgid "ui.initial.create_save.message"
|
|
||||||
msgstr "No se encontraron datos guardados. ¿Crear una partida nueva?"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
|
||||||
msgid "ui.initial.create_save.yes"
|
|
||||||
msgstr "Sí"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
|
||||||
msgid "ui.initial.create_save.no"
|
|
||||||
msgstr "No"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
|
||||||
msgid "ui.main_menu.new_game"
|
|
||||||
msgstr "Nueva Partida"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
|
||||||
msgid "ui.main_menu.load_game"
|
|
||||||
msgstr "Cargar Partida"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
|
||||||
msgid "ui.main_menu.options"
|
|
||||||
msgstr "Opciones"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
|
||||||
msgid "ui.main_menu.quit"
|
|
||||||
msgstr "Salir del Juego"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/uiconfirm.c
|
|
||||||
msgid "ui.confirm.confirm"
|
|
||||||
msgstr "Confirmar"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/uiconfirm.c
|
|
||||||
msgid "ui.confirm.cancel"
|
|
||||||
msgstr "Cancelar"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
|
||||||
msgid "ui.battle.menu.attack"
|
|
||||||
msgstr "Atacar"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
|
||||||
msgid "ui.battle.menu.flee"
|
|
||||||
msgstr "Huir"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
|
||||||
msgid "ui.battle.menu.target_format"
|
|
||||||
msgstr "Enemigo %u (%u/%u PS)"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/battle/uibattlehud.c
|
|
||||||
msgid "ui.battle.hud.hp_format"
|
|
||||||
msgstr "PS %u/%u"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/battle/uibattlehud.c
|
|
||||||
msgid "ui.battle.hud.mp_format"
|
|
||||||
msgstr "PM %u/%u"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettingsaudio.c
|
|
||||||
msgid "ui.settings.audio.placeholder"
|
|
||||||
msgstr "Aún no hay opciones de audio"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettingsdisplay.c
|
|
||||||
msgid "ui.settings.display.placeholder"
|
|
||||||
msgstr "Aún no hay opciones de pantalla"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettingsinput.c
|
|
||||||
msgid "ui.settings.input.placeholder"
|
|
||||||
msgstr "Aún no hay opciones de entrada"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/backpack/uibackpack.c
|
|
||||||
msgid "ui.backpack.category_format"
|
|
||||||
msgstr "Categoría %u"
|
|
||||||
|
|
||||||
#: src/dusk/ui/overlay/uiloading.c
|
|
||||||
msgid "ui.loading.text"
|
|
||||||
msgstr "cargando"
|
|
||||||
|
|
||||||
#: src/dusk/ui/overlay/uiautosave.c
|
|
||||||
msgid "ui.autosave.saving"
|
|
||||||
msgstr "GUARDANDO"
|
|
||||||
|
|
||||||
#: src/dusk/rpg/item/item.json
|
#: src/dusk/rpg/item/item.json
|
||||||
msgid "item.potion.name"
|
msgid "item.potion.name"
|
||||||
msgstr "Poción"
|
msgstr "Poción"
|
||||||
|
|||||||
@@ -57,130 +57,6 @@ msgstr "アイテム"
|
|||||||
msgid "ui.game_menu.settings"
|
msgid "ui.game_menu.settings"
|
||||||
msgstr "設定"
|
msgstr "設定"
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save"
|
|
||||||
msgstr "セーブ"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_success"
|
|
||||||
msgstr "セーブしました。"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_cancelled"
|
|
||||||
msgstr "セーブをキャンセルしました。"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_unavailable"
|
|
||||||
msgstr "セーブできません - セーブデバイスが見つかりません。"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_temporary"
|
|
||||||
msgstr "このセッションは一時的です - セーブデバイスが見つからなかったため、セーブは無効になっています。"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_create_confirm"
|
|
||||||
msgstr "セーブデータが見つかりません。新しいセーブを作成しますか?"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_failed_format"
|
|
||||||
msgstr "セーブに失敗しました: %s"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_check_failed_format"
|
|
||||||
msgstr "セーブできません: %s"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
|
||||||
msgid "ui.initial.no_card.message"
|
|
||||||
msgstr "セーブデバイスが見つかりません。続行できますが、\n進行状況は保存されません。"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
|
||||||
msgid "ui.initial.no_card.retry"
|
|
||||||
msgstr "再試行"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
|
||||||
msgid "ui.initial.no_card.continue"
|
|
||||||
msgstr "続行する"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
|
||||||
msgid "ui.initial.create_save.message"
|
|
||||||
msgstr "セーブデータが見つかりません。新しいセーブを作成しますか?"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
|
||||||
msgid "ui.initial.create_save.yes"
|
|
||||||
msgstr "はい"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
|
||||||
msgid "ui.initial.create_save.no"
|
|
||||||
msgstr "いいえ"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
|
||||||
msgid "ui.main_menu.new_game"
|
|
||||||
msgstr "ニューゲーム"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
|
||||||
msgid "ui.main_menu.load_game"
|
|
||||||
msgstr "ロードゲーム"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
|
||||||
msgid "ui.main_menu.options"
|
|
||||||
msgstr "オプション"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
|
||||||
msgid "ui.main_menu.quit"
|
|
||||||
msgstr "ゲームを終了"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/uiconfirm.c
|
|
||||||
msgid "ui.confirm.confirm"
|
|
||||||
msgstr "確認"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/uiconfirm.c
|
|
||||||
msgid "ui.confirm.cancel"
|
|
||||||
msgstr "キャンセル"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
|
||||||
msgid "ui.battle.menu.attack"
|
|
||||||
msgstr "攻撃"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
|
||||||
msgid "ui.battle.menu.flee"
|
|
||||||
msgstr "逃げる"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
|
||||||
msgid "ui.battle.menu.target_format"
|
|
||||||
msgstr "敵%u (%u/%u HP)"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/battle/uibattlehud.c
|
|
||||||
msgid "ui.battle.hud.hp_format"
|
|
||||||
msgstr "HP %u/%u"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/battle/uibattlehud.c
|
|
||||||
msgid "ui.battle.hud.mp_format"
|
|
||||||
msgstr "MP %u/%u"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettingsaudio.c
|
|
||||||
msgid "ui.settings.audio.placeholder"
|
|
||||||
msgstr "オーディオ設定はまだありません"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettingsdisplay.c
|
|
||||||
msgid "ui.settings.display.placeholder"
|
|
||||||
msgstr "表示設定はまだありません"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettingsinput.c
|
|
||||||
msgid "ui.settings.input.placeholder"
|
|
||||||
msgstr "入力設定はまだありません"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/backpack/uibackpack.c
|
|
||||||
msgid "ui.backpack.category_format"
|
|
||||||
msgstr "カテゴリー%u"
|
|
||||||
|
|
||||||
#: src/dusk/ui/overlay/uiloading.c
|
|
||||||
msgid "ui.loading.text"
|
|
||||||
msgstr "読み込み中"
|
|
||||||
|
|
||||||
#: src/dusk/ui/overlay/uiautosave.c
|
|
||||||
msgid "ui.autosave.saving"
|
|
||||||
msgstr "保存中"
|
|
||||||
|
|
||||||
#: src/dusk/rpg/item/item.json
|
#: src/dusk/rpg/item/item.json
|
||||||
msgid "item.potion.name"
|
msgid "item.potion.name"
|
||||||
msgstr "ポーション"
|
msgstr "ポーション"
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -2179,27 +2179,5 @@
|
|||||||
0
|
0
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"type": "global",
|
|
||||||
"globalId": 3,
|
|
||||||
"pos": [8, 8, 1]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "item",
|
|
||||||
"itemId": 1,
|
|
||||||
"quantity": 1,
|
|
||||||
"pos": [12, 2, 0]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"areas": [
|
|
||||||
{
|
|
||||||
"min": [11, 3, 0],
|
|
||||||
"max": [16, 9, 10],
|
|
||||||
"callbackId": 1,
|
|
||||||
"notify": 3,
|
|
||||||
"trigger": 6
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
# Copyright (c) 2026 Dominic Masters
|
|
||||||
#
|
|
||||||
# This software is released under the MIT License.
|
|
||||||
# https://opensource.org/licenses/MIT
|
|
||||||
|
|
||||||
if(NOT DEFINED ENV{VITASDK})
|
|
||||||
message(FATAL_ERROR "VITASDK environment variable is not set.")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
include("$ENV{VITASDK}/share/vita.cmake" REQUIRED)
|
|
||||||
|
|
||||||
set(VITA_APP_NAME "Dusk")
|
|
||||||
set(VITA_TITLEID "DUSK00001")
|
|
||||||
set(VITA_VERSION "01.00")
|
|
||||||
|
|
||||||
find_package(SDL2 REQUIRED)
|
|
||||||
|
|
||||||
# Custom flags for cglm
|
|
||||||
set(CGLM_SHARED OFF CACHE BOOL "Build cglm shared" FORCE)
|
|
||||||
set(CGLM_STATIC ON CACHE BOOL "Build cglm static" FORCE)
|
|
||||||
find_package(cglm REQUIRED)
|
|
||||||
|
|
||||||
# Link libraries
|
|
||||||
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
|
||||||
${SDL2_LIBRARIES}
|
|
||||||
cglm
|
|
||||||
SDL2
|
|
||||||
SDL2main
|
|
||||||
zip
|
|
||||||
bz2
|
|
||||||
z
|
|
||||||
zstd
|
|
||||||
crypto
|
|
||||||
lzma
|
|
||||||
m
|
|
||||||
pthread
|
|
||||||
stdc++
|
|
||||||
vitaGL
|
|
||||||
mathneon
|
|
||||||
vitashark
|
|
||||||
kubridge_stub
|
|
||||||
SceAppMgr_stub
|
|
||||||
SceAudio_stub
|
|
||||||
SceCtrl_stub
|
|
||||||
SceCommonDialog_stub
|
|
||||||
SceDisplay_stub
|
|
||||||
SceKernelDmacMgr_stub
|
|
||||||
SceGxm_stub
|
|
||||||
SceShaccCg_stub
|
|
||||||
SceSysmodule_stub
|
|
||||||
ScePower_stub
|
|
||||||
SceTouch_stub
|
|
||||||
SceVshBridge_stub
|
|
||||||
SceIofilemgr_stub
|
|
||||||
SceShaccCgExt
|
|
||||||
libtaihen_stub.a
|
|
||||||
|
|
||||||
|
|
||||||
# SceKernel_stub
|
|
||||||
SceAppUtil_stub
|
|
||||||
SceHid_stub
|
|
||||||
SceRtc_stub
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
|
|
||||||
${SDL2_INCLUDE_DIRS}
|
|
||||||
)
|
|
||||||
|
|
||||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
|
||||||
DUSK_SDL2
|
|
||||||
DUSK_OPENGL
|
|
||||||
DUSK_VITA
|
|
||||||
DUSK_INPUT_GAMEPAD
|
|
||||||
DUSK_PLATFORM_ENDIAN_LITTLE
|
|
||||||
DUSK_OPENGL_LEGACY
|
|
||||||
DUSK_DISPLAY_WIDTH=960
|
|
||||||
DUSK_DISPLAY_HEIGHT=544
|
|
||||||
)
|
|
||||||
|
|
||||||
# Post-build: create SELF from the ELF binary (UNSAFE = homebrew, no signing)
|
|
||||||
vita_create_self(${DUSK_BINARY_TARGET_NAME}.self ${DUSK_BINARY_TARGET_NAME} UNSAFE)
|
|
||||||
|
|
||||||
# Post-build: package SELF + assets into a .vpk installable on the Vita
|
|
||||||
vita_create_vpk(${DUSK_BINARY_TARGET_NAME}.vpk ${VITA_TITLEID} ${DUSK_BINARY_TARGET_NAME}.self
|
|
||||||
VERSION ${VITA_VERSION}
|
|
||||||
NAME ${VITA_APP_NAME}
|
|
||||||
FILE ${DUSK_ASSETS_ZIP} dusk.dsk
|
|
||||||
)
|
|
||||||
@@ -4,17 +4,6 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
|||||||
DUSK_WII
|
DUSK_WII
|
||||||
)
|
)
|
||||||
|
|
||||||
# Wii save storage method - see src/duskdolphin/save/savedeviceplatform.h.
|
|
||||||
set(DUSK_SAVE_WII_METHOD "NAND" CACHE STRING
|
|
||||||
"Wii save storage: NAND (internal storage via ISFS), CARD (GameCube-\
|
|
||||||
compatible memory card emulation), or SD (SD card via libfat)"
|
|
||||||
)
|
|
||||||
set_property(CACHE DUSK_SAVE_WII_METHOD PROPERTY STRINGS "NAND" "CARD" "SD")
|
|
||||||
|
|
||||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
|
||||||
DUSK_SAVE_WII_METHOD_${DUSK_SAVE_WII_METHOD}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Generate Homebrew Channel meta.xml from project identity variables
|
# Generate Homebrew Channel meta.xml from project identity variables
|
||||||
string(TIMESTAMP DUSK_BUILD_DATE "%Y%m%d000000" UTC)
|
string(TIMESTAMP DUSK_BUILD_DATE "%Y%m%d000000" UTC)
|
||||||
configure_file(
|
configure_file(
|
||||||
|
|||||||
@@ -5,10 +5,6 @@
|
|||||||
|
|
||||||
add_subdirectory(dusk)
|
add_subdirectory(dusk)
|
||||||
|
|
||||||
if(DUSK_NETWORK)
|
|
||||||
add_subdirectory(dusknetwork)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "knulli")
|
if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "knulli")
|
||||||
add_subdirectory(dusklinux)
|
add_subdirectory(dusklinux)
|
||||||
add_subdirectory(dusksdl2)
|
add_subdirectory(dusksdl2)
|
||||||
@@ -19,11 +15,6 @@ elseif(DUSK_TARGET_SYSTEM STREQUAL "psp")
|
|||||||
add_subdirectory(dusksdl2)
|
add_subdirectory(dusksdl2)
|
||||||
add_subdirectory(duskgl)
|
add_subdirectory(duskgl)
|
||||||
|
|
||||||
elseif(DUSK_TARGET_SYSTEM STREQUAL "vita")
|
|
||||||
add_subdirectory(duskvita)
|
|
||||||
add_subdirectory(dusksdl2)
|
|
||||||
add_subdirectory(duskgl)
|
|
||||||
|
|
||||||
elseif(DUSK_TARGET_SYSTEM STREQUAL "wii" OR DUSK_TARGET_SYSTEM STREQUAL "gamecube")
|
elseif(DUSK_TARGET_SYSTEM STREQUAL "wii" OR DUSK_TARGET_SYSTEM STREQUAL "gamecube")
|
||||||
add_subdirectory(duskdolphin)
|
add_subdirectory(duskdolphin)
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ add_subdirectory(scene)
|
|||||||
add_subdirectory(system)
|
add_subdirectory(system)
|
||||||
add_subdirectory(time)
|
add_subdirectory(time)
|
||||||
add_subdirectory(ui)
|
add_subdirectory(ui)
|
||||||
|
add_subdirectory(network)
|
||||||
add_subdirectory(save)
|
add_subdirectory(save)
|
||||||
add_subdirectory(util)
|
add_subdirectory(util)
|
||||||
add_subdirectory(thread)
|
add_subdirectory(thread)
|
||||||
@@ -7,5 +7,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
PUBLIC
|
PUBLIC
|
||||||
easing.c
|
easing.c
|
||||||
animation.c
|
animation.c
|
||||||
keyframe.c
|
|
||||||
)
|
)
|
||||||
|
|||||||
+25
-105
@@ -11,122 +11,42 @@
|
|||||||
void animationInit(
|
void animationInit(
|
||||||
animation_t *anim,
|
animation_t *anim,
|
||||||
keyframe_t *keyframes,
|
keyframe_t *keyframes,
|
||||||
uint16_t *keyframeCounts,
|
uint16_t keyframeCount
|
||||||
const uint16_t layerCount
|
|
||||||
) {
|
) {
|
||||||
assertNotNull(anim, "Animation pointer cannot be null.");
|
assertNotNull(anim, "Animation pointer cannot be null.");
|
||||||
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
|
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
|
||||||
assertNotNull(keyframeCounts, "Keyframe counts pointer cannot be null.");
|
assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
|
||||||
assertTrue(layerCount > 0, "Layer count must be greater than zero.");
|
|
||||||
|
|
||||||
memoryZero(anim, sizeof(animation_t));
|
|
||||||
anim->keyframes = keyframes;
|
anim->keyframes = keyframes;
|
||||||
anim->keyframeCounts = keyframeCounts;
|
anim->keyframeCount = keyframeCount;
|
||||||
anim->layerCount = layerCount;
|
|
||||||
|
|
||||||
// Determine duration
|
|
||||||
float_t duration = 0.0f;
|
|
||||||
for(uint16_t layer = 0; layer < layerCount; layer++) {
|
|
||||||
uint16_t keyframeCount = keyframeCounts[layer];
|
|
||||||
assertTrue(keyframeCount > 0, "Keyframe count invalid.");
|
|
||||||
keyframe_t *layerKeyframes = keyframes + layer * keyframeCount;
|
|
||||||
|
|
||||||
#ifdef DUSK_ASSERTIONS
|
|
||||||
// Check that the keyframes are sorted by time.
|
|
||||||
for(uint16_t i = 1; i < keyframeCount; i++) {
|
|
||||||
assertTrue(
|
|
||||||
layerKeyframes[i].time >= layerKeyframes[i - 1].time,
|
|
||||||
"Keyframes must be sorted by time."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
keyframe_t *lastKeyframe = layerKeyframes + keyframeCount - 1;
|
|
||||||
duration = mathMax(duration, lastKeyframe->time);
|
|
||||||
}
|
|
||||||
assertTrue(duration > 0, "Animation duration must be greater than 0.");
|
|
||||||
anim->duration = duration;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
float_t animationGetLayerValue(const animation_t *anim, const uint16_t layer) {
|
float_t animationGetValue(animation_t *anim, const float_t time) {
|
||||||
assertNotNull(anim, "Animation pointer cannot be null.");
|
assertNotNull(anim, "Animation pointer cannot be null.");
|
||||||
assertTrue(layer < anim->layerCount, "Layer index out of bounds.");
|
assertNotNull(anim->keyframes, "Keyframes pointer cannot be null.");
|
||||||
|
assertTrue(anim->keyframeCount > 0, "Keyframe count invalid.");
|
||||||
|
assertTrue(time >= 0, "Time must be non-negative.");
|
||||||
|
|
||||||
|
keyframe_t *start;
|
||||||
|
keyframe_t *end;
|
||||||
|
keyframe_t *last = anim->keyframes + anim->keyframeCount - 1;
|
||||||
|
keyframe_t *current = anim->keyframes;
|
||||||
|
start = current;
|
||||||
|
|
||||||
uint16_t keyframeCount = anim->keyframeCounts[layer];
|
do {
|
||||||
keyframe_t *layerKeyframes = anim->keyframes + layer * keyframeCount;
|
if(current->time > time) {
|
||||||
return keyframeGetValue(layerKeyframes, keyframeCount, anim->time);
|
end = current;
|
||||||
}
|
break;
|
||||||
|
|
||||||
void animationUpdate(
|
|
||||||
animation_t *anim,
|
|
||||||
const float_t deltaTime
|
|
||||||
) {
|
|
||||||
assertNotNull(anim, "Animation pointer cannot be null.");
|
|
||||||
assertTrue(deltaTime >= 0, "Delta time must be non-negative.");
|
|
||||||
|
|
||||||
bool_t justCompleted = false;
|
|
||||||
|
|
||||||
if(!(anim->flags & ANIMATION_FLAG_INTERNAL_COMPLETED)) {
|
|
||||||
bool_t loop = (anim->flags & ANIMATION_FLAG_LOOP) != 0;
|
|
||||||
bool_t pingpong = (anim->flags & ANIMATION_FLAG_PINGPONG) != 0;
|
|
||||||
assertFalse(
|
|
||||||
loop && pingpong,
|
|
||||||
"Cannot set both ANIMATION_FLAG_LOOP and ANIMATION_FLAG_PINGPONG."
|
|
||||||
);
|
|
||||||
|
|
||||||
bool_t backward;
|
|
||||||
if(pingpong) {
|
|
||||||
backward = (anim->flags & ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD) != 0;
|
|
||||||
} else {
|
|
||||||
backward = (anim->flags & ANIMATION_FLAG_REVERSE) != 0;
|
|
||||||
}
|
}
|
||||||
|
start = current;
|
||||||
|
current++;
|
||||||
|
|
||||||
// Resolve boundary crossings one at a time, so a single large deltaTime
|
if(current > last) {
|
||||||
// can correctly loop/pingpong across multiple boundaries in one call.
|
end = start;
|
||||||
float_t remaining = deltaTime;
|
break;
|
||||||
while(remaining > 0.0f) {
|
|
||||||
float_t toBoundary = (
|
|
||||||
backward ? anim->time : (anim->duration - anim->time)
|
|
||||||
);
|
|
||||||
if(remaining < toBoundary) {
|
|
||||||
anim->time += backward ? -remaining : remaining;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
remaining -= toBoundary;
|
|
||||||
anim->time = backward ? 0.0f : anim->duration;
|
|
||||||
|
|
||||||
bool_t stopHere;
|
|
||||||
if(backward) {
|
|
||||||
stopHere = (anim->flags & ANIMATION_FLAG_STOP_BEGINNING) != 0;
|
|
||||||
} else {
|
|
||||||
stopHere = (anim->flags & ANIMATION_FLAG_STOP_END) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(stopHere) {
|
|
||||||
justCompleted = true;
|
|
||||||
break;
|
|
||||||
} else if(pingpong) {
|
|
||||||
backward = !backward;
|
|
||||||
if(backward) anim->flags |= ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD;
|
|
||||||
else anim->flags &= ~ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD;
|
|
||||||
} else if(loop) {
|
|
||||||
anim->time = backward ? anim->duration : 0.0f;
|
|
||||||
if(anim->onLoop) anim->onLoop(anim->user);
|
|
||||||
} else {
|
|
||||||
justCompleted = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} while(true);
|
||||||
|
|
||||||
if(justCompleted) anim->flags |= ANIMATION_FLAG_INTERNAL_COMPLETED;
|
float_t t = (time - start->time) / (end->time - start->time);
|
||||||
}
|
return mathLerp(start->value, end->value, easingApply(start->easing, t));
|
||||||
|
|
||||||
// Call onUpdate for each layer.
|
|
||||||
for(uint16_t layer = 0; layer < anim->layerCount; layer++) {
|
|
||||||
float_t value = animationGetLayerValue(anim, layer);
|
|
||||||
if(anim->onUpdate) anim->onUpdate(layer, value, anim->user);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(justCompleted && anim->onComplete) anim->onComplete(anim->user);
|
|
||||||
}
|
}
|
||||||
@@ -6,89 +6,29 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "keyframe.h"
|
#include "keyframe.h"
|
||||||
|
|
||||||
#define ANIMATION_FLAG_LOOP (1 << 0)
|
|
||||||
#define ANIMATION_FLAG_REVERSE (1 << 1)
|
|
||||||
#define ANIMATION_FLAG_PINGPONG (1 << 2)
|
|
||||||
#define ANIMATION_FLAG_STOP_BEGINNING (1 << 3)
|
|
||||||
#define ANIMATION_FLAG_STOP_END (1 << 4)
|
|
||||||
|
|
||||||
// Internal - tracks which direction a pingponging animation is currently
|
|
||||||
// travelling. Do not set this manually, it is managed by animationUpdate().
|
|
||||||
#define ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD (1 << 7)
|
|
||||||
|
|
||||||
// Internal - set once the animation has stopped advancing (see
|
|
||||||
// animationUpdate()). Do not set this manually. There is currently no way to
|
|
||||||
// restart a completed animation short of clearing this bit and resetting
|
|
||||||
// anim->time by hand.
|
|
||||||
#define ANIMATION_FLAG_INTERNAL_COMPLETED (1 << 6)
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
keyframe_t *keyframes;
|
keyframe_t *keyframes;
|
||||||
uint16_t *keyframeCounts;
|
uint16_t keyframeCount;
|
||||||
uint16_t layerCount;
|
|
||||||
float_t time;
|
|
||||||
float_t duration;
|
|
||||||
uint8_t flags;
|
|
||||||
|
|
||||||
void *user;
|
|
||||||
void (*onUpdate)(const uint16_t layer, const float_t value, void *user);
|
|
||||||
void (*onComplete)(void *user);
|
|
||||||
void (*onLoop)(void *user);
|
|
||||||
} animation_t;
|
} animation_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes an animation with the given keyframes and layer count.
|
* Initializes an animation.
|
||||||
*
|
*
|
||||||
* @param anim Pointer to the animation to initialize.
|
* @param anim The animation to initialize.
|
||||||
* @param keyframes Pointer to the array of keyframes for each layer.
|
* @param keyframes The keyframes to use for the animation.
|
||||||
* @param keyframeCount Number of keyframes in each layer.
|
* @param keyframeCount The number of keyframes in the animation.
|
||||||
* @param layerCount Number of layers in the animation.
|
|
||||||
*/
|
*/
|
||||||
void animationInit(
|
void animationInit(
|
||||||
animation_t *anim,
|
animation_t *anim,
|
||||||
keyframe_t *keyframes,
|
keyframe_t *keyframes,
|
||||||
uint16_t *keyframeCounts,
|
uint16_t keyframeCount
|
||||||
const uint16_t layerCount
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the current time of the animation, clamping it to the valid range.
|
* Gets the value of the animation at a given time.
|
||||||
* This will call the onUpdate callback but none of the other callbacks.
|
|
||||||
*
|
*
|
||||||
* @param anim Pointer to the animation to set the time for.
|
* @param anim The animation to get the value from.
|
||||||
* @param time The new time to set for the animation.
|
* @param time The time at which to get the value, in seconds.
|
||||||
|
* @return The value of the animation at the given time.
|
||||||
*/
|
*/
|
||||||
void animationSetTime(animation_t *anim, const float_t time);
|
float_t animationGetValue(animation_t *anim, const float_t time);
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current value of a specific layer in the animation based on the
|
|
||||||
* current animation time.
|
|
||||||
*/
|
|
||||||
float_t animationGetLayerValue(const animation_t *anim, const uint16_t layer);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the animation state based on the elapsed time. Advances anim->time
|
|
||||||
* by deltaTime (or against it, if ANIMATION_FLAG_REVERSE is set), then
|
|
||||||
* resolves whatever happens when it reaches the 0 or duration boundary:
|
|
||||||
*
|
|
||||||
* - ANIMATION_FLAG_PINGPONG: reflects off the boundary and continues playing
|
|
||||||
* in the opposite direction, forever, unless stopped (see below).
|
|
||||||
* - ANIMATION_FLAG_LOOP: wraps back around to the other boundary and keeps
|
|
||||||
* playing in the same direction, forever, unless stopped (see below).
|
|
||||||
* - ANIMATION_FLAG_STOP_BEGINNING / ANIMATION_FLAG_STOP_END: when the
|
|
||||||
* animation reaches that specific boundary, it clamps there and stops
|
|
||||||
* (firing onComplete) instead of looping/pingponging past it.
|
|
||||||
* - If none of the above apply at a boundary, the animation clamps there and
|
|
||||||
* stops, firing onComplete.
|
|
||||||
*
|
|
||||||
* onUpdate is called for every layer on every call. onLoop is called each
|
|
||||||
* time a loop wraps around. onComplete is called at most once, the moment
|
|
||||||
* the animation stops advancing.
|
|
||||||
*
|
|
||||||
* @param anim Pointer to the animation to update.
|
|
||||||
* @param deltaTime Time elapsed since the last update (in seconds).
|
|
||||||
*/
|
|
||||||
void animationUpdate(
|
|
||||||
animation_t *anim,
|
|
||||||
const float_t deltaTime
|
|
||||||
);
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "keyframe.h"
|
|
||||||
#include "assert/assert.h"
|
|
||||||
#include "util/math.h"
|
|
||||||
|
|
||||||
float_t keyframeGetValue(
|
|
||||||
const keyframe_t *keyframes,
|
|
||||||
const uint32_t keyframeCount,
|
|
||||||
const float_t time
|
|
||||||
) {
|
|
||||||
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
|
|
||||||
assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
|
|
||||||
assertTrue(time >= 0, "Time must be non-negative.");
|
|
||||||
#ifdef DUSK_ASSERTIONS
|
|
||||||
// Checks that the keyframes are sorted by time.
|
|
||||||
for(uint32_t i = 1; i < keyframeCount; i++) {
|
|
||||||
assertTrue(
|
|
||||||
keyframes[i].time >= keyframes[i - 1].time,
|
|
||||||
"Keyframes must be sorted by time."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
keyframe_t *last = (keyframe_t *)(keyframes + keyframeCount - 1);
|
|
||||||
if(time >= last->time) return last->value;
|
|
||||||
|
|
||||||
// Since time < last->time (checked above), current is guaranteed to stop
|
|
||||||
// at or before reaching last, so no separate end-of-array check is needed.
|
|
||||||
keyframe_t *current = (keyframe_t *)keyframes;
|
|
||||||
keyframe_t *start = current;
|
|
||||||
while(current->time <= time) {
|
|
||||||
start = current;
|
|
||||||
current++;
|
|
||||||
}
|
|
||||||
keyframe_t *end = current;
|
|
||||||
|
|
||||||
float_t t = (time - start->time) / (end->time - start->time);
|
|
||||||
return mathLerp(start->value, end->value, easingApply(start->easing, t));
|
|
||||||
}
|
|
||||||
@@ -10,18 +10,4 @@ typedef struct {
|
|||||||
float_t time;
|
float_t time;
|
||||||
float_t value;
|
float_t value;
|
||||||
easingtype_t easing;
|
easingtype_t easing;
|
||||||
} keyframe_t;
|
} keyframe_t;
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the value of a keyframe at a given time.
|
|
||||||
*
|
|
||||||
* @param keyframes The keyframes to get the value from.
|
|
||||||
* @param keyframeCount The number of keyframes in the array.
|
|
||||||
* @param time The time at which to get the value, in seconds.
|
|
||||||
* @return The value of the keyframe at the given time.
|
|
||||||
*/
|
|
||||||
float_t keyframeGetValue(
|
|
||||||
const keyframe_t *keyframes,
|
|
||||||
const uint32_t keyframeCount,
|
|
||||||
const float_t time
|
|
||||||
);
|
|
||||||
@@ -22,8 +22,6 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef DUSK_ASSERTIONS_FAKED
|
#ifndef DUSK_ASSERTIONS_FAKED
|
||||||
#define DUSK_ASSERTIONS 1
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the assert system. Must be the very first call in engine
|
* Initializes the assert system. Must be the very first call in engine
|
||||||
* startup.
|
* startup.
|
||||||
|
|||||||
@@ -14,26 +14,6 @@
|
|||||||
#include "asset/loader/assetloader.h"
|
#include "asset/loader/assetloader.h"
|
||||||
#include "asset/asset.h"
|
#include "asset/asset.h"
|
||||||
|
|
||||||
// Reads a little-endian int16 from a potentially-unaligned offset into a
|
|
||||||
// worldunit_t, advancing *offset past it.
|
|
||||||
static worldunit_t assetChunkReadWorldUnit(
|
|
||||||
const uint8_t *data,
|
|
||||||
size_t *offset
|
|
||||||
) {
|
|
||||||
int16_t value;
|
|
||||||
memoryCopy(&value, data + *offset, sizeof(int16_t));
|
|
||||||
*offset += sizeof(int16_t);
|
|
||||||
return (worldunit_t)endianLittleToHost16((uint16_t)value);
|
|
||||||
}
|
|
||||||
|
|
||||||
static worldpos_t assetChunkReadWorldPos(const uint8_t *data, size_t *offset) {
|
|
||||||
worldpos_t pos;
|
|
||||||
pos.x = assetChunkReadWorldUnit(data, offset);
|
|
||||||
pos.y = assetChunkReadWorldUnit(data, offset);
|
|
||||||
pos.z = assetChunkReadWorldUnit(data, offset);
|
|
||||||
return pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
|
errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
|
||||||
assertNotNull(loading, "Loading cannot be NULL");
|
assertNotNull(loading, "Loading cannot be NULL");
|
||||||
assertNotMainThread("Should be called from an async thread.");
|
assertNotMainThread("Should be called from an async thread.");
|
||||||
@@ -166,62 +146,6 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
|||||||
out->meshOffsets[m][2] = endianLittleToHostFloat(out->meshOffsets[m][2]);
|
out->meshOffsets[m][2] = endianLittleToHostFloat(out->meshOffsets[m][2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
out->entitySpawnCount = data[offset];
|
|
||||||
offset += sizeof(uint8_t);
|
|
||||||
assertTrue(
|
|
||||||
out->entitySpawnCount <= CHUNK_ENTITY_SPAWN_COUNT_MAX,
|
|
||||||
"Chunk entity spawn count exceeds maximum."
|
|
||||||
);
|
|
||||||
|
|
||||||
for(uint8_t s = 0; s < out->entitySpawnCount; s++) {
|
|
||||||
chunkentityspawn_t *spawn = &out->entitySpawns[s];
|
|
||||||
spawn->kind = (chunkentityspawnkind_t)data[offset];
|
|
||||||
offset += sizeof(uint8_t);
|
|
||||||
|
|
||||||
uint16_t a;
|
|
||||||
memoryCopy(&a, data + offset, sizeof(uint16_t));
|
|
||||||
a = endianLittleToHost16(a);
|
|
||||||
offset += sizeof(uint16_t);
|
|
||||||
|
|
||||||
uint8_t b = data[offset];
|
|
||||||
offset += sizeof(uint8_t);
|
|
||||||
|
|
||||||
if(spawn->kind == CHUNK_ENTITY_SPAWN_KIND_ITEM) {
|
|
||||||
spawn->globalId = 0;
|
|
||||||
spawn->itemId = a;
|
|
||||||
spawn->itemQuantity = b;
|
|
||||||
} else {
|
|
||||||
spawn->globalId = a;
|
|
||||||
spawn->itemId = 0;
|
|
||||||
spawn->itemQuantity = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
spawn->position = assetChunkReadWorldPos(data, &offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
out->areaSpawnCount = data[offset];
|
|
||||||
offset += sizeof(uint8_t);
|
|
||||||
assertTrue(
|
|
||||||
out->areaSpawnCount <= CHUNK_AREA_COUNT_MAX,
|
|
||||||
"Chunk area spawn count exceeds maximum."
|
|
||||||
);
|
|
||||||
|
|
||||||
for(uint8_t s = 0; s < out->areaSpawnCount; s++) {
|
|
||||||
chunkareaspawn_t *area = &out->areaSpawns[s];
|
|
||||||
area->min = assetChunkReadWorldPos(data, &offset);
|
|
||||||
area->max = assetChunkReadWorldPos(data, &offset);
|
|
||||||
|
|
||||||
uint16_t callbackId;
|
|
||||||
memoryCopy(&callbackId, data + offset, sizeof(uint16_t));
|
|
||||||
area->callbackId = endianLittleToHost16(callbackId);
|
|
||||||
offset += sizeof(uint16_t);
|
|
||||||
|
|
||||||
area->notify = data[offset];
|
|
||||||
offset += sizeof(uint8_t);
|
|
||||||
area->trigger = data[offset];
|
|
||||||
offset += sizeof(uint8_t);
|
|
||||||
}
|
|
||||||
|
|
||||||
memoryFree(data);
|
memoryFree(data);
|
||||||
loading->loading.chunk.data = NULL;
|
loading->loading.chunk.data = NULL;
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
#include "asset/assetfile.h"
|
#include "asset/assetfile.h"
|
||||||
#include "rpg/overworld/chunk.h"
|
#include "rpg/overworld/chunk.h"
|
||||||
|
|
||||||
#define ASSET_CHUNK_FILE_VERSION 5
|
#define ASSET_CHUNK_FILE_VERSION 4
|
||||||
|
|
||||||
typedef struct assetloading_s assetloading_t;
|
typedef struct assetloading_s assetloading_t;
|
||||||
typedef struct assetentry_s assetentry_t;
|
typedef struct assetentry_s assetentry_t;
|
||||||
@@ -33,39 +33,12 @@ typedef struct {
|
|||||||
uint8_t modelIndex;
|
uint8_t modelIndex;
|
||||||
} assetchunkloaderloading_t;
|
} assetchunkloaderloading_t;
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
CHUNK_ENTITY_SPAWN_KIND_GLOBAL,
|
|
||||||
CHUNK_ENTITY_SPAWN_KIND_ITEM
|
|
||||||
} chunkentityspawnkind_t;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
chunkentityspawnkind_t kind;
|
|
||||||
uint16_t globalId; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_GLOBAL.
|
|
||||||
uint16_t itemId; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_ITEM.
|
|
||||||
uint8_t itemQuantity; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_ITEM.
|
|
||||||
worldpos_t position;
|
|
||||||
} chunkentityspawn_t;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
worldpos_t min;
|
|
||||||
worldpos_t max;
|
|
||||||
uint16_t callbackId; // Index into MAP_AREA_CALLBACK_LIST.
|
|
||||||
uint8_t notify;
|
|
||||||
uint8_t trigger;
|
|
||||||
} chunkareaspawn_t;
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
tile_t *tiles;
|
tile_t *tiles;
|
||||||
uint8_t meshCount;
|
uint8_t meshCount;
|
||||||
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
|
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
|
||||||
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
|
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
|
||||||
assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX];
|
assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX];
|
||||||
|
|
||||||
uint8_t entitySpawnCount;
|
|
||||||
chunkentityspawn_t entitySpawns[CHUNK_ENTITY_SPAWN_COUNT_MAX];
|
|
||||||
|
|
||||||
uint8_t areaSpawnCount;
|
|
||||||
chunkareaspawn_t areaSpawns[CHUNK_AREA_COUNT_MAX];
|
|
||||||
} assetchunkoutput_t;
|
} assetchunkoutput_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ console_t CONSOLE;
|
|||||||
void consoleInit(void) {
|
void consoleInit(void) {
|
||||||
memoryZero(&CONSOLE, sizeof(console_t));
|
memoryZero(&CONSOLE, sizeof(console_t));
|
||||||
CONSOLE.visible = false;
|
CONSOLE.visible = false;
|
||||||
threadMutexInit(&CONSOLE.printMutex);
|
|
||||||
|
#ifdef DUSK_CONSOLE_POSIX
|
||||||
|
threadMutexInit(&CONSOLE.printMutex);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void consolePrint(const char_t *message, ...) {
|
void consolePrint(const char_t *message, ...) {
|
||||||
@@ -29,14 +32,21 @@ void consolePrint(const char_t *message, ...) {
|
|||||||
int32_t len = stringFormatVA(buffer, CONSOLE_LINE_MAX, message, args);
|
int32_t len = stringFormatVA(buffer, CONSOLE_LINE_MAX, message, args);
|
||||||
va_end(args);
|
va_end(args);
|
||||||
|
|
||||||
threadMutexLock(&CONSOLE.printMutex);
|
#ifdef DUSK_CONSOLE_POSIX
|
||||||
|
threadMutexLock(&CONSOLE.printMutex);
|
||||||
|
#endif
|
||||||
|
|
||||||
memoryMove(
|
memoryMove(
|
||||||
CONSOLE.line[0],
|
CONSOLE.line[0],
|
||||||
CONSOLE.line[1],
|
CONSOLE.line[1],
|
||||||
(CONSOLE_HISTORY_MAX - 1) * CONSOLE_LINE_MAX
|
(CONSOLE_HISTORY_MAX - 1) * CONSOLE_LINE_MAX
|
||||||
);
|
);
|
||||||
memoryCopy(CONSOLE.line[CONSOLE_HISTORY_MAX - 1], buffer, len + 1);
|
memoryCopy(CONSOLE.line[CONSOLE_HISTORY_MAX - 1], buffer, len + 1);
|
||||||
threadMutexUnlock(&CONSOLE.printMutex);
|
CONSOLE.dirty = true;
|
||||||
|
|
||||||
|
#ifdef DUSK_CONSOLE_POSIX
|
||||||
|
threadMutexUnlock(&CONSOLE.printMutex);
|
||||||
|
#endif
|
||||||
|
|
||||||
logDebug("%s\n", buffer);
|
logDebug("%s\n", buffer);
|
||||||
}
|
}
|
||||||
@@ -52,5 +62,7 @@ void consoleUpdate(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void consoleDispose(void) {
|
void consoleDispose(void) {
|
||||||
threadMutexDispose(&CONSOLE.printMutex);
|
#ifdef DUSK_CONSOLE_POSIX
|
||||||
|
threadMutexDispose(&CONSOLE.printMutex);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
@@ -6,18 +6,29 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include "consoledefs.h"
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "dusk.h"
|
#include "dusk.h"
|
||||||
#include "thread/thread.h"
|
|
||||||
|
|
||||||
#define CONSOLE_LINE_MAX 512
|
#ifdef DUSK_CONSOLE_POSIX
|
||||||
#define CONSOLE_HISTORY_MAX 16
|
#include "thread/thread.h"
|
||||||
#define CONSOLE_EXEC_BUFFER_MAX 32
|
#include <poll.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#define CONSOLE_POSIX_POLL_RATE 75
|
||||||
|
#endif
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char_t line[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX];
|
char_t line[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX];
|
||||||
bool_t visible;
|
bool_t visible;
|
||||||
threadmutex_t printMutex;
|
|
||||||
|
// Set whenever the history changes; consumers rendering the console
|
||||||
|
// (e.g. uiConsoleDraw) check and clear this to know when their own
|
||||||
|
// cached representation of the history needs rebuilding.
|
||||||
|
bool_t dirty;
|
||||||
|
|
||||||
|
#ifdef DUSK_CONSOLE_POSIX
|
||||||
|
threadmutex_t printMutex;
|
||||||
|
#endif
|
||||||
} console_t;
|
} console_t;
|
||||||
|
|
||||||
extern console_t CONSOLE;
|
extern console_t CONSOLE;
|
||||||
|
|||||||
+3
-2
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "moduleplatformvita.h"
|
|
||||||
|
|
||||||
#define modulePlatformPlatform modulePlatformVita
|
#define CONSOLE_LINE_MAX 512
|
||||||
|
#define CONSOLE_HISTORY_MAX 16
|
||||||
|
#define CONSOLE_EXEC_BUFFER_MAX 32
|
||||||
@@ -33,17 +33,13 @@ errorret_t displayInit(void) {
|
|||||||
#ifdef displayPlatformInit
|
#ifdef displayPlatformInit
|
||||||
errorChain(displayPlatformInit());
|
errorChain(displayPlatformInit());
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Set initial state
|
|
||||||
errorChain(displaySetState((displaystate_t){ .flags = 0 }));
|
errorChain(displaySetState((displaystate_t){ .flags = 0 }));
|
||||||
|
|
||||||
// Init the fixed textures
|
|
||||||
errorChain(textureInit(
|
errorChain(textureInit(
|
||||||
&TEXTURE_WHITE, TEXTURE_FIXED_WIDTH, TEXTURE_FIXED_HEIGHT,
|
&TEXTURE_WHITE, 4, 4,
|
||||||
TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_WHITE_PIXELS }
|
TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_WHITE_PIXELS }
|
||||||
));
|
));
|
||||||
errorChain(textureInit(
|
errorChain(textureInit(
|
||||||
&TEXTURE_TEST, TEXTURE_FIXED_WIDTH, TEXTURE_FIXED_HEIGHT,
|
&TEXTURE_TEST, 4, 4,
|
||||||
TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_TEST_PIXELS }
|
TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_TEST_PIXELS }
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -54,14 +50,14 @@ errorret_t displayInit(void) {
|
|||||||
errorChain(planeInit());
|
errorChain(planeInit());
|
||||||
errorChain(capsuleInit());
|
errorChain(capsuleInit());
|
||||||
errorChain(triPrismInit());
|
errorChain(triPrismInit());
|
||||||
|
|
||||||
// Init the subsystems
|
|
||||||
errorChain(frameBufferInitBackBuffer());
|
errorChain(frameBufferInitBackBuffer());
|
||||||
errorChain(spriteBatchInit());
|
errorChain(spriteBatchInit());
|
||||||
errorChain(textInit());
|
errorChain(textInit());
|
||||||
errorChain(screenInit());
|
errorChain(screenInit());
|
||||||
|
|
||||||
// Setup initial shader with default values
|
// Setup initial shader with default values
|
||||||
|
|
||||||
errorChain(shaderListInit());
|
errorChain(shaderListInit());
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ errorret_t shaderInit(shader_t *shader, const shaderdefinition_t *def) {
|
|||||||
|
|
||||||
errorret_t shaderBind(shader_t *shader) {
|
errorret_t shaderBind(shader_t *shader) {
|
||||||
assertNotNull(shader, "Shader cannot be null");
|
assertNotNull(shader, "Shader cannot be null");
|
||||||
|
if(bound == shader) errorOk();
|
||||||
errorChain(shaderBindPlatform(shader));
|
errorChain(shaderBindPlatform(shader));
|
||||||
bound = shader;
|
bound = shader;
|
||||||
errorOk();
|
errorOk();
|
||||||
|
|||||||
@@ -7,5 +7,4 @@
|
|||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
text.c
|
text.c
|
||||||
font.c
|
|
||||||
)
|
)
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "font.h"
|
|
||||||
#include "util/memory.h"
|
|
||||||
#include "util/math.h"
|
|
||||||
#include "display/color.h"
|
|
||||||
|
|
||||||
font_t FONT_DEFAULT;
|
|
||||||
static texture_t FONT_DEFAULT_TEXTURE;
|
|
||||||
static tileset_t FONT_DEFAULT_TILESET;
|
|
||||||
|
|
||||||
const uint8_t FONT_DEFAULT_GLYPHS[FONT_DEFAULT_TILE_COUNT][
|
|
||||||
FONT_DEFAULT_TILE_HEIGHT
|
|
||||||
] = {
|
|
||||||
{ 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x10, 0x00, 0x00 }, // !
|
|
||||||
{ 0x00, 0x14, 0x14, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // "
|
|
||||||
{ 0x00, 0x14, 0x14, 0x3E, 0x14, 0x3E, 0x14, 0x14, 0x00, 0x00 }, // #
|
|
||||||
{ 0x00, 0x08, 0x1E, 0x28, 0x1C, 0x0A, 0x3C, 0x08, 0x00, 0x00 }, // $
|
|
||||||
{ 0x00, 0x00, 0x22, 0x24, 0x08, 0x12, 0x22, 0x00, 0x00, 0x00 }, // %
|
|
||||||
{ 0x00, 0x08, 0x14, 0x14, 0x1A, 0x24, 0x24, 0x1A, 0x00, 0x00 }, // &
|
|
||||||
{ 0x00, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // '
|
|
||||||
{ 0x00, 0x04, 0x08, 0x08, 0x08, 0x08, 0x08, 0x04, 0x00, 0x00 }, // (
|
|
||||||
{ 0x00, 0x10, 0x08, 0x08, 0x08, 0x08, 0x08, 0x10, 0x00, 0x00 }, // )
|
|
||||||
{ 0x00, 0x00, 0x08, 0x2A, 0x1C, 0x2A, 0x08, 0x00, 0x00, 0x00 }, // *
|
|
||||||
{ 0x00, 0x00, 0x08, 0x08, 0x3E, 0x08, 0x08, 0x00, 0x00, 0x00 }, // +
|
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x20, 0x00 }, // ,
|
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00 }, // -
|
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00 }, // .
|
|
||||||
{ 0x00, 0x04, 0x04, 0x08, 0x08, 0x08, 0x10, 0x10, 0x00, 0x00 }, // /
|
|
||||||
{ 0x00, 0x1C, 0x22, 0x26, 0x2A, 0x32, 0x22, 0x1C, 0x00, 0x00 }, // 0
|
|
||||||
{ 0x00, 0x08, 0x18, 0x08, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // 1
|
|
||||||
{ 0x00, 0x1C, 0x22, 0x02, 0x04, 0x08, 0x10, 0x3E, 0x00, 0x00 }, // 2
|
|
||||||
{ 0x00, 0x1C, 0x22, 0x02, 0x0C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 3
|
|
||||||
{ 0x00, 0x22, 0x22, 0x22, 0x3E, 0x02, 0x02, 0x02, 0x00, 0x00 }, // 4
|
|
||||||
{ 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 5
|
|
||||||
{ 0x00, 0x1C, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // 6
|
|
||||||
{ 0x00, 0x3E, 0x02, 0x02, 0x04, 0x08, 0x08, 0x08, 0x00, 0x00 }, // 7
|
|
||||||
{ 0x00, 0x1C, 0x22, 0x22, 0x1C, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // 8
|
|
||||||
{ 0x00, 0x1C, 0x22, 0x22, 0x1E, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 9
|
|
||||||
{ 0x00, 0x00, 0x10, 0x10, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00 }, // :
|
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ;
|
|
||||||
{ 0x00, 0x04, 0x08, 0x10, 0x20, 0x10, 0x08, 0x04, 0x00, 0x00 }, // <
|
|
||||||
{ 0x00, 0x00, 0x00, 0x3E, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00 }, // =
|
|
||||||
{ 0x00, 0x10, 0x08, 0x04, 0x02, 0x04, 0x08, 0x10, 0x00, 0x00 }, // >
|
|
||||||
{ 0x00, 0x1C, 0x22, 0x02, 0x04, 0x08, 0x00, 0x08, 0x00, 0x00 }, // ?
|
|
||||||
{ 0x00, 0x1C, 0x26, 0x2A, 0x2A, 0x26, 0x20, 0x1C, 0x00, 0x00 }, // @
|
|
||||||
{ 0x00, 0x1C, 0x22, 0x22, 0x22, 0x3E, 0x22, 0x22, 0x00, 0x00 }, // A
|
|
||||||
{ 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // B
|
|
||||||
{ 0x00, 0x1C, 0x22, 0x20, 0x20, 0x20, 0x22, 0x1C, 0x00, 0x00 }, // C
|
|
||||||
{ 0x00, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // D
|
|
||||||
{ 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x20, 0x20, 0x3E, 0x00, 0x00 }, // E
|
|
||||||
{ 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x20, 0x20, 0x20, 0x00, 0x00 }, // F
|
|
||||||
{ 0x00, 0x1C, 0x22, 0x20, 0x2E, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // G
|
|
||||||
{ 0x00, 0x22, 0x22, 0x22, 0x3E, 0x22, 0x22, 0x22, 0x00, 0x00 }, // H
|
|
||||||
{ 0x00, 0x3E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // I
|
|
||||||
{ 0x00, 0x02, 0x02, 0x02, 0x02, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // J
|
|
||||||
{ 0x00, 0x22, 0x24, 0x28, 0x30, 0x28, 0x24, 0x22, 0x00, 0x00 }, // K
|
|
||||||
{ 0x00, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3E, 0x00, 0x00 }, // L
|
|
||||||
{ 0x00, 0x22, 0x36, 0x2A, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // M
|
|
||||||
{ 0x00, 0x22, 0x22, 0x32, 0x2A, 0x26, 0x22, 0x22, 0x00, 0x00 }, // N
|
|
||||||
{ 0x00, 0x1C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // O
|
|
||||||
{ 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x20, 0x20, 0x20, 0x00, 0x00 }, // P
|
|
||||||
{ 0x00, 0x1C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x06, 0x00 }, // Q
|
|
||||||
{ 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x22, 0x22, 0x22, 0x00, 0x00 }, // R
|
|
||||||
{ 0x00, 0x1C, 0x22, 0x20, 0x1C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // S
|
|
||||||
{ 0x00, 0x3E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00 }, // T
|
|
||||||
{ 0x00, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // U
|
|
||||||
{ 0x00, 0x22, 0x22, 0x22, 0x22, 0x14, 0x14, 0x08, 0x00, 0x00 }, // V
|
|
||||||
{ 0x00, 0x22, 0x22, 0x22, 0x22, 0x2A, 0x36, 0x22, 0x00, 0x00 }, // W
|
|
||||||
{ 0x00, 0x22, 0x22, 0x14, 0x08, 0x14, 0x22, 0x22, 0x00, 0x00 }, // X
|
|
||||||
{ 0x00, 0x22, 0x22, 0x14, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00 }, // Y
|
|
||||||
{ 0x00, 0x3E, 0x02, 0x04, 0x08, 0x10, 0x20, 0x3E, 0x00, 0x00 }, // Z
|
|
||||||
{ 0x00, 0x0C, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0C, 0x00, 0x00 }, // [
|
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // backslash (not drawn in source font)
|
|
||||||
{ 0x00, 0x18, 0x08, 0x08, 0x08, 0x08, 0x08, 0x18, 0x00, 0x00 }, // ]
|
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ^ (not drawn in source font)
|
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // _ (not drawn in source font)
|
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ` (not drawn in source font)
|
|
||||||
{ 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // a
|
|
||||||
{ 0x00, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // b
|
|
||||||
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x20, 0x22, 0x1C, 0x00, 0x00 }, // c
|
|
||||||
{ 0x00, 0x02, 0x02, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // d
|
|
||||||
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x3E, 0x20, 0x1C, 0x00, 0x00 }, // e
|
|
||||||
{ 0x00, 0x0C, 0x12, 0x10, 0x3C, 0x10, 0x10, 0x10, 0x00, 0x00 }, // f
|
|
||||||
{ 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x1C }, // g
|
|
||||||
{ 0x00, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // h
|
|
||||||
{ 0x00, 0x08, 0x00, 0x18, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // i
|
|
||||||
{ 0x00, 0x02, 0x00, 0x06, 0x02, 0x02, 0x02, 0x02, 0x22, 0x1C }, // j
|
|
||||||
{ 0x00, 0x20, 0x20, 0x22, 0x24, 0x38, 0x24, 0x22, 0x00, 0x00 }, // k
|
|
||||||
{ 0x00, 0x30, 0x10, 0x10, 0x10, 0x10, 0x10, 0x0E, 0x00, 0x00 }, // l
|
|
||||||
{ 0x00, 0x00, 0x00, 0x3C, 0x2A, 0x2A, 0x2A, 0x2A, 0x00, 0x00 }, // m
|
|
||||||
{ 0x00, 0x00, 0x00, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // n
|
|
||||||
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // o
|
|
||||||
{ 0x00, 0x00, 0x00, 0x3C, 0x22, 0x22, 0x22, 0x3C, 0x20, 0x20 }, // p
|
|
||||||
{ 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x02 }, // q
|
|
||||||
{ 0x00, 0x00, 0x00, 0x2C, 0x32, 0x20, 0x20, 0x20, 0x00, 0x00 }, // r
|
|
||||||
{ 0x00, 0x00, 0x00, 0x1E, 0x20, 0x1C, 0x02, 0x3C, 0x00, 0x00 }, // s
|
|
||||||
{ 0x00, 0x10, 0x10, 0x3C, 0x10, 0x10, 0x10, 0x0E, 0x00, 0x00 }, // t
|
|
||||||
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // u
|
|
||||||
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x14, 0x08, 0x00, 0x00 }, // v
|
|
||||||
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x2A, 0x2A, 0x14, 0x00, 0x00 }, // w
|
|
||||||
{ 0x00, 0x00, 0x00, 0x22, 0x14, 0x08, 0x14, 0x22, 0x00, 0x00 }, // x
|
|
||||||
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x1C }, // y
|
|
||||||
{ 0x00, 0x00, 0x00, 0x3E, 0x04, 0x08, 0x10, 0x3E, 0x00, 0x00 }, // z
|
|
||||||
{ 0x00, 0x04, 0x08, 0x08, 0x10, 0x08, 0x08, 0x04, 0x00, 0x00 }, // {
|
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // | (not drawn in source font)
|
|
||||||
{ 0x00, 0x10, 0x08, 0x08, 0x04, 0x08, 0x08, 0x10, 0x00, 0x00 }, // }
|
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ~ (not drawn in source font)
|
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // unused trailing tile
|
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // unused trailing tile
|
|
||||||
};
|
|
||||||
|
|
||||||
errorret_t fontDefaultInit(void) {
|
|
||||||
const int32_t width = (int32_t)mathNextPowTwo(
|
|
||||||
FONT_DEFAULT_COLUMNS * FONT_DEFAULT_TILE_WIDTH
|
|
||||||
);
|
|
||||||
const int32_t height = (int32_t)mathNextPowTwo(
|
|
||||||
FONT_DEFAULT_ROWS * FONT_DEFAULT_TILE_HEIGHT
|
|
||||||
);
|
|
||||||
|
|
||||||
color_t *pixels = memoryAllocate(sizeof(color_t) * width * height);
|
|
||||||
memoryZero(pixels, sizeof(color_t) * width * height);
|
|
||||||
|
|
||||||
for(uint16_t i = 0; i < FONT_DEFAULT_TILE_COUNT; i++) {
|
|
||||||
const uint16_t tileX = (i % FONT_DEFAULT_COLUMNS) * FONT_DEFAULT_TILE_WIDTH;
|
|
||||||
const uint16_t tileY = (i / FONT_DEFAULT_COLUMNS) * FONT_DEFAULT_TILE_HEIGHT;
|
|
||||||
|
|
||||||
for(uint8_t row = 0; row < FONT_DEFAULT_TILE_HEIGHT; row++) {
|
|
||||||
const uint8_t bits = FONT_DEFAULT_GLYPHS[i][row];
|
|
||||||
|
|
||||||
for(uint8_t col = 0; col < FONT_DEFAULT_TILE_WIDTH; col++) {
|
|
||||||
if(!((bits >> (FONT_DEFAULT_TILE_WIDTH - 1 - col)) & 1)) continue;
|
|
||||||
pixels[((tileY + row) * width) + (tileX + col)] = COLOR_WHITE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FONT_DEFAULT_TILESET.tileWidth = FONT_DEFAULT_TILE_WIDTH;
|
|
||||||
FONT_DEFAULT_TILESET.tileHeight = FONT_DEFAULT_TILE_HEIGHT;
|
|
||||||
FONT_DEFAULT_TILESET.columns = FONT_DEFAULT_COLUMNS;
|
|
||||||
FONT_DEFAULT_TILESET.rows = FONT_DEFAULT_ROWS;
|
|
||||||
FONT_DEFAULT_TILESET.tileCount = FONT_DEFAULT_TILE_COUNT;
|
|
||||||
FONT_DEFAULT_TILESET.uv[0] = (float_t)FONT_DEFAULT_TILE_WIDTH / (float_t)width;
|
|
||||||
FONT_DEFAULT_TILESET.uv[1] = (float_t)FONT_DEFAULT_TILE_HEIGHT / (float_t)height;
|
|
||||||
|
|
||||||
const texturedata_t data = { .rgbaColors = pixels };
|
|
||||||
errorret_t textureResult = textureInit(
|
|
||||||
&FONT_DEFAULT_TEXTURE, width, height, TEXTURE_FORMAT_RGBA, data
|
|
||||||
);
|
|
||||||
memoryFree(pixels);
|
|
||||||
errorChain(textureResult);
|
|
||||||
|
|
||||||
FONT_DEFAULT.texture = &FONT_DEFAULT_TEXTURE;
|
|
||||||
FONT_DEFAULT.tileset = &FONT_DEFAULT_TILESET;
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t fontDefaultDispose(void) {
|
|
||||||
errorChain(textureDispose(&FONT_DEFAULT_TEXTURE));
|
|
||||||
FONT_DEFAULT.texture = NULL;
|
|
||||||
FONT_DEFAULT.tileset = NULL;
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
|
||||||
#include "display/texture/texture.h"
|
#include "display/texture/texture.h"
|
||||||
#include "display/texture/tileset.h"
|
#include "display/texture/tileset.h"
|
||||||
|
|
||||||
@@ -14,51 +13,3 @@ typedef struct {
|
|||||||
texture_t *texture;
|
texture_t *texture;
|
||||||
tileset_t *tileset;
|
tileset_t *tileset;
|
||||||
} font_t;
|
} font_t;
|
||||||
|
|
||||||
/**
|
|
||||||
* Pixel width/height of a single default-font glyph tile.
|
|
||||||
*/
|
|
||||||
#define FONT_DEFAULT_TILE_WIDTH 6
|
|
||||||
#define FONT_DEFAULT_TILE_HEIGHT 10
|
|
||||||
|
|
||||||
/** Grid layout of the generated default-font texture, in tiles. */
|
|
||||||
#define FONT_DEFAULT_COLUMNS 16
|
|
||||||
#define FONT_DEFAULT_ROWS 6
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Number of glyphs defined in FONT_DEFAULT_GLYPHS (FONT_DEFAULT_COLUMNS *
|
|
||||||
* FONT_DEFAULT_ROWS), covering the printable ASCII range starting at
|
|
||||||
* TEXT_CHAR_START ('!') plus a couple of unused trailing tiles.
|
|
||||||
*/
|
|
||||||
#define FONT_DEFAULT_TILE_COUNT (FONT_DEFAULT_COLUMNS * FONT_DEFAULT_ROWS)
|
|
||||||
|
|
||||||
extern font_t FONT_DEFAULT;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hard coded bitmap data for the built-in default font. Indexed
|
|
||||||
* [glyph][row], where glyph 0 corresponds to TEXT_CHAR_START ('!') and
|
|
||||||
* glyphs run consecutively through the printable ASCII range. Each row
|
|
||||||
* byte holds FONT_DEFAULT_TILE_WIDTH bit flags, one per pixel column:
|
|
||||||
* bit (FONT_DEFAULT_TILE_WIDTH - 1) is the leftmost pixel and bit 0 is
|
|
||||||
* the rightmost; 1 means the pixel is set, 0 means it is not.
|
|
||||||
*/
|
|
||||||
extern const uint8_t FONT_DEFAULT_GLYPHS[FONT_DEFAULT_TILE_COUNT][
|
|
||||||
FONT_DEFAULT_TILE_HEIGHT
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds the default font's texture + tileset directly from
|
|
||||||
* FONT_DEFAULT_GLYPHS, without going through the asset system - so the
|
|
||||||
* engine always has a usable font to render with regardless of whether
|
|
||||||
* asset loading (e.g. the packed .dsk archive) succeeds.
|
|
||||||
*
|
|
||||||
* @return Either an error or success result.
|
|
||||||
*/
|
|
||||||
errorret_t fontDefaultInit(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disposes of the default font created by fontDefaultInit().
|
|
||||||
*
|
|
||||||
* @return Either an error or success result.
|
|
||||||
*/
|
|
||||||
errorret_t fontDefaultDispose(void);
|
|
||||||
|
|||||||
@@ -9,15 +9,34 @@
|
|||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "display/spritebatch/spritebatch.h"
|
#include "display/spritebatch/spritebatch.h"
|
||||||
|
#include "asset/asset.h"
|
||||||
|
#include "asset/loader/display/assettextureloader.h"
|
||||||
|
#include "asset/loader/display/assettilesetloader.h"
|
||||||
#include "display/shader/shaderunlit.h"
|
#include "display/shader/shaderunlit.h"
|
||||||
|
|
||||||
|
font_t FONT_DEFAULT;
|
||||||
|
|
||||||
errorret_t textInit(void) {
|
errorret_t textInit(void) {
|
||||||
errorChain(fontDefaultInit());
|
assetloaderinput_t input = { .texture = TEXTURE_FORMAT_RGBA };
|
||||||
|
assetentry_t *entryTexture = assetLock(
|
||||||
|
"ui/minogram.png", ASSET_LOADER_TYPE_TEXTURE, &input
|
||||||
|
);
|
||||||
|
assetentry_t *entryTileset = assetLock(
|
||||||
|
"ui/minogram.dtf", ASSET_LOADER_TYPE_TILESET, NULL
|
||||||
|
);
|
||||||
|
errorChain(assetRequireLoaded(entryTexture));
|
||||||
|
errorChain(assetRequireLoaded(entryTileset));
|
||||||
|
|
||||||
|
FONT_DEFAULT.texture = &entryTexture->data.texture;
|
||||||
|
FONT_DEFAULT.tileset = &entryTileset->data.tileset;
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t textDispose(void) {
|
errorret_t textDispose(void) {
|
||||||
errorChain(fontDefaultDispose());
|
FONT_DEFAULT.texture = NULL;
|
||||||
|
FONT_DEFAULT.tileset = NULL;
|
||||||
|
assetUnlock("ui/minogram.png");
|
||||||
|
assetUnlock("ui/minogram.dtf");
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
|
|
||||||
#define TEXT_CHAR_START '!'
|
#define TEXT_CHAR_START '!'
|
||||||
|
|
||||||
|
extern font_t FONT_DEFAULT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the text system.
|
* Initializes the text system.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
#include "display/display.h"
|
#include "display/display.h"
|
||||||
|
|
||||||
texture_t TEXTURE_WHITE;
|
texture_t TEXTURE_WHITE;
|
||||||
color_t TEXTURE_WHITE_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT] = {
|
color_t TEXTURE_WHITE_PIXELS[4*4] = {
|
||||||
COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE,
|
COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE,
|
||||||
COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE,
|
COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE,
|
||||||
COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE,
|
COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE,
|
||||||
@@ -20,7 +20,7 @@ color_t TEXTURE_WHITE_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
texture_t TEXTURE_TEST;
|
texture_t TEXTURE_TEST;
|
||||||
color_t TEXTURE_TEST_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT] = {
|
color_t TEXTURE_TEST_PIXELS[4*4] = {
|
||||||
COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA,
|
COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA,
|
||||||
COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK,
|
COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK,
|
||||||
COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA,
|
COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA,
|
||||||
|
|||||||
@@ -17,9 +17,6 @@
|
|||||||
#error "textureDisposePlatform should not be defined."
|
#error "textureDisposePlatform should not be defined."
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define TEXTURE_FIXED_WIDTH 4
|
|
||||||
#define TEXTURE_FIXED_HEIGHT 4
|
|
||||||
|
|
||||||
typedef textureformatplatform_t textureformat_t;
|
typedef textureformatplatform_t textureformat_t;
|
||||||
typedef textureplatform_t texture_t;
|
typedef textureplatform_t texture_t;
|
||||||
|
|
||||||
@@ -32,9 +29,9 @@ typedef union texturedata_u {
|
|||||||
} texturedata_t;
|
} texturedata_t;
|
||||||
|
|
||||||
extern texture_t TEXTURE_WHITE;
|
extern texture_t TEXTURE_WHITE;
|
||||||
extern color_t TEXTURE_WHITE_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT];
|
extern color_t TEXTURE_WHITE_PIXELS[4*4];
|
||||||
extern texture_t TEXTURE_TEST;
|
extern texture_t TEXTURE_TEST;
|
||||||
extern color_t TEXTURE_TEST_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT];
|
extern color_t TEXTURE_TEST_PIXELS[4*4];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes a texture.
|
* Initializes a texture.
|
||||||
|
|||||||
+102
-24
@@ -16,12 +16,12 @@
|
|||||||
#include "asset/asset.h"
|
#include "asset/asset.h"
|
||||||
#include "ui/ui.h"
|
#include "ui/ui.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#ifdef DUSK_NETWORK
|
#include "network/network.h"
|
||||||
#include "network/network.h"
|
#include "network/http/networkhttprequest.h"
|
||||||
#endif
|
|
||||||
#include "system/system.h"
|
#include "system/system.h"
|
||||||
#include "console/console.h"
|
#include "console/console.h"
|
||||||
#include "save/savemanager.h"\
|
#include "save/save.h"
|
||||||
|
#include "save/savesettings.h"
|
||||||
|
|
||||||
engine_t ENGINE;
|
engine_t ENGINE;
|
||||||
|
|
||||||
@@ -39,16 +39,22 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
|||||||
errorChain(systemInit());
|
errorChain(systemInit());
|
||||||
errorChain(inputInit());
|
errorChain(inputInit());
|
||||||
errorChain(assetInit());
|
errorChain(assetInit());
|
||||||
errorChain(saveManagerInit());
|
errorChain(saveInit());
|
||||||
|
errorChain(saveSettingsLoad());
|
||||||
errorChain(localeManagerInit());
|
errorChain(localeManagerInit());
|
||||||
errorChain(displayInit());
|
errorChain(displayInit());
|
||||||
errorChain(uiInit());
|
errorChain(uiInit());
|
||||||
errorChain(rpgInit());
|
errorChain(rpgInit());
|
||||||
#ifdef DUSK_NETWORK
|
errorChain(networkInit());
|
||||||
errorChain(networkInit());
|
|
||||||
#endif
|
|
||||||
errorChain(sceneInit());
|
errorChain(sceneInit());
|
||||||
|
|
||||||
|
networkRequestConnection(
|
||||||
|
engineNetworkOnConnected,
|
||||||
|
engineNetworkOnFailed,
|
||||||
|
engineNetworkOnDisconnect,
|
||||||
|
NULL
|
||||||
|
);
|
||||||
|
|
||||||
consolePrint("Engine initialized");
|
consolePrint("Engine initialized");
|
||||||
|
|
||||||
#ifdef DUSK_ASSERTIONS_FAKED
|
#ifdef DUSK_ASSERTIONS_FAKED
|
||||||
@@ -57,28 +63,42 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
|||||||
consolePrint("Assertions real");
|
consolePrint("Assertions real");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
sceneSet(SCENE_TYPE_INITIAL);
|
sceneSet(SCENE_TYPE_OVERWORLD);
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t engineUpdate(void) {
|
errorret_t engineUpdate(void) {
|
||||||
// Order here is important.
|
// Order here is important.
|
||||||
#ifdef DUSK_NETWORK
|
errorChain(networkUpdate());
|
||||||
errorChain(networkUpdate());
|
|
||||||
#endif
|
|
||||||
errorChain(saveManagerUpdate());
|
|
||||||
timeUpdate();
|
timeUpdate();
|
||||||
inputUpdate();
|
|
||||||
consoleUpdate();
|
if(
|
||||||
errorChain(rpgUpdate());
|
ENGINE.networkDisconnectTestPending &&
|
||||||
errorChain(sceneUpdate());
|
TIME.time >= ENGINE.networkDisconnectTestAt
|
||||||
errorChain(assetUpdate());
|
) {
|
||||||
errorChain(uiUpdate());
|
ENGINE.networkDisconnectTestPending = false;
|
||||||
|
networkRequestDisconnection(engineNetworkDisconnectTestOnComplete, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
const systemdialogtype_t dialogType = systemGetActiveDialogType();
|
||||||
|
if(dialogType == SYSTEM_DIALOG_TYPE_NONE) {
|
||||||
|
inputUpdate();
|
||||||
|
consoleUpdate();
|
||||||
|
errorChain(rpgUpdate());
|
||||||
|
errorChain(sceneUpdate());
|
||||||
|
errorChain(assetUpdate());
|
||||||
|
errorChain(uiUpdate());
|
||||||
|
}
|
||||||
|
|
||||||
// Render
|
// Render
|
||||||
errorChain(displayUpdate());
|
errorChain(displayUpdate());
|
||||||
if(inputPressed(INPUT_ACTION_RAGEQUIT)) ENGINE.running = false;
|
if(
|
||||||
|
dialogType == SYSTEM_DIALOG_TYPE_NONE &&
|
||||||
|
inputPressed(INPUT_ACTION_RAGEQUIT)
|
||||||
|
) {
|
||||||
|
ENGINE.running = false;
|
||||||
|
}
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,16 +108,74 @@ void engineExit(void) {
|
|||||||
|
|
||||||
errorret_t engineDispose(void) {
|
errorret_t engineDispose(void) {
|
||||||
errorChain(sceneDispose());
|
errorChain(sceneDispose());
|
||||||
#ifdef DUSK_NETWORK
|
errorChain(networkDispose());
|
||||||
errorChain(networkDispose());
|
|
||||||
#endif
|
|
||||||
errorChain(rpgDispose());
|
errorChain(rpgDispose());
|
||||||
localeManagerDispose();
|
localeManagerDispose();
|
||||||
errorChain(uiDispose());
|
errorChain(uiDispose());
|
||||||
consoleDispose();
|
consoleDispose();
|
||||||
errorChain(displayDispose());
|
errorChain(displayDispose());
|
||||||
errorChain(saveManagerDispose());
|
errorChain(saveDispose());
|
||||||
errorChain(assetDispose());
|
errorChain(assetDispose());
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void engineHttpTestOnComplete(void *params, void *user) {
|
||||||
|
networkhttprequest_t *request = (networkhttprequest_t *)params;
|
||||||
|
const networkhttpresponse_t *response = &request->response;
|
||||||
|
|
||||||
|
consolePrint("frogfind.com response status: %d", response->status);
|
||||||
|
|
||||||
|
for(uint32_t i = 0; i < response->headers.count; i++) {
|
||||||
|
const networkhttpheader_t *header = &response->headers.headers[i];
|
||||||
|
consolePrint("frogfind.com header: %s: %s", header->name, header->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
consolePrint("frogfind.com body length: %d", (int32_t)response->bodyLength);
|
||||||
|
|
||||||
|
const size_t chunkMax = CONSOLE_LINE_MAX - 32;
|
||||||
|
for(size_t offset = 0; offset < response->bodyLength; offset += chunkMax) {
|
||||||
|
size_t chunkLength = response->bodyLength - offset;
|
||||||
|
if(chunkLength > chunkMax) chunkLength = chunkMax;
|
||||||
|
|
||||||
|
consolePrint("frogfind.com body: %.*s",
|
||||||
|
(int32_t)chunkLength, (const char_t *)response->body + offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void engineHttpTestOnError(void *params, void *user) {
|
||||||
|
consolePrint("frogfind.com request failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
void engineNetworkOnConnected(void *user) {
|
||||||
|
consolePrint("Network connected");
|
||||||
|
|
||||||
|
errorret_t ret = networkHttpRequest(
|
||||||
|
NETWORK_HTTP_METHOD_GET,
|
||||||
|
"http://frogfind.com",
|
||||||
|
NULL, 0,
|
||||||
|
NULL, 0,
|
||||||
|
NULL, 0,
|
||||||
|
engineHttpTestOnComplete,
|
||||||
|
engineHttpTestOnError,
|
||||||
|
NULL
|
||||||
|
);
|
||||||
|
errorCatch(errorPrint(ret));
|
||||||
|
|
||||||
|
ENGINE.networkDisconnectTestAt = TIME.time + 10.0f;
|
||||||
|
ENGINE.networkDisconnectTestPending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void engineNetworkOnFailed(errorret_t error, void *user) {
|
||||||
|
consolePrint("Network connection failed");
|
||||||
|
errorCatch(errorPrint(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
void engineNetworkOnDisconnect(errorret_t error, void *user) {
|
||||||
|
consolePrint("Network disconnected");
|
||||||
|
errorCatch(errorPrint(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
void engineNetworkDisconnectTestOnComplete(void *user) {
|
||||||
|
consolePrint("Network disconnect test complete");
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ typedef struct {
|
|||||||
int32_t argc;
|
int32_t argc;
|
||||||
const char_t **argv;
|
const char_t **argv;
|
||||||
const char_t *version;
|
const char_t *version;
|
||||||
|
|
||||||
|
// Test: disconnects the network 10 seconds after it connects.
|
||||||
|
bool_t networkDisconnectTestPending;
|
||||||
|
float_t networkDisconnectTestAt;
|
||||||
} engine_t;
|
} engine_t;
|
||||||
|
|
||||||
extern engine_t ENGINE;
|
extern engine_t ENGINE;
|
||||||
@@ -38,3 +42,51 @@ errorret_t engineUpdate(void);
|
|||||||
*/
|
*/
|
||||||
errorret_t engineDispose(void);
|
errorret_t engineDispose(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs the response status, headers and body of the frogfind.com test
|
||||||
|
* request to the console.
|
||||||
|
*
|
||||||
|
* @param params The completed networkhttprequest_t.
|
||||||
|
* @param user Unused.
|
||||||
|
*/
|
||||||
|
void engineHttpTestOnComplete(void *params, void *user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs that the frogfind.com test request failed.
|
||||||
|
*
|
||||||
|
* @param params The failed networkhttprequest_t.
|
||||||
|
* @param user Unused.
|
||||||
|
*/
|
||||||
|
void engineHttpTestOnError(void *params, void *user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires the frogfind.com test request once the network connection is
|
||||||
|
* up (some platforms, such as PSP, only bring the network stack up
|
||||||
|
* asynchronously after networkRequestConnection is called).
|
||||||
|
*
|
||||||
|
* @param user Unused.
|
||||||
|
*/
|
||||||
|
void engineNetworkOnConnected(void *user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs that the network connection could not be established.
|
||||||
|
*
|
||||||
|
* @param error The error describing why the connection failed.
|
||||||
|
* @param user Unused.
|
||||||
|
*/
|
||||||
|
void engineNetworkOnFailed(errorret_t error, void *user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs that the network connection was lost after having connected.
|
||||||
|
*
|
||||||
|
* @param error The error describing why the connection was lost.
|
||||||
|
* @param user Unused.
|
||||||
|
*/
|
||||||
|
void engineNetworkOnDisconnect(errorret_t error, void *user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs that the 10-second test disconnect completed.
|
||||||
|
*
|
||||||
|
* @param user Unused.
|
||||||
|
*/
|
||||||
|
void engineNetworkDisconnectTestOnComplete(void *user);
|
||||||
|
|||||||
+1
-11
@@ -11,12 +11,12 @@
|
|||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
#include "util/math.h"
|
#include "util/math.h"
|
||||||
#include "time/time.h"
|
#include "time/time.h"
|
||||||
#include "event/event.h"
|
|
||||||
|
|
||||||
input_t INPUT;
|
input_t INPUT;
|
||||||
|
|
||||||
errorret_t inputInit(void) {
|
errorret_t inputInit(void) {
|
||||||
memoryZero(&INPUT, sizeof(input_t));
|
memoryZero(&INPUT, sizeof(input_t));
|
||||||
|
INPUT.deadzone = INPUT_DEADZONE_DEFAULT;
|
||||||
|
|
||||||
for(uint8_t i = 0; i < INPUT_ACTION_COUNT; i++) {
|
for(uint8_t i = 0; i < INPUT_ACTION_COUNT; i++) {
|
||||||
INPUT.actions[i].action = (inputaction_t)i;
|
INPUT.actions[i].action = (inputaction_t)i;
|
||||||
@@ -83,16 +83,6 @@ void inputUpdate(void) {
|
|||||||
|
|
||||||
cur++;
|
cur++;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef DUSK_TIME_DYNAMIC
|
|
||||||
if(TIME.dynamicUpdate) return;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
for(uint8_t i = INPUT_ACTION_NULL + 1; i < INPUT_ACTION_COUNT; i++) {
|
|
||||||
inputactiondata_t *act = &INPUT.actions[i];
|
|
||||||
bool_t isDown = act->currentValue > 0.0f;
|
|
||||||
bool_t wasDown = act->lastValue > 0.0f;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
float_t inputGetCurrentValue(const inputaction_t action) {
|
float_t inputGetCurrentValue(const inputaction_t action) {
|
||||||
|
|||||||
@@ -10,9 +10,17 @@
|
|||||||
#include "inputbutton.h"
|
#include "inputbutton.h"
|
||||||
#include "inputaction.h"
|
#include "inputaction.h"
|
||||||
|
|
||||||
|
#define INPUT_LISTENER_PRESSED_MAX 16
|
||||||
|
#define INPUT_LISTENER_RELEASED_MAX INPUT_LISTENER_PRESSED_MAX
|
||||||
|
#define INPUT_DEADZONE_DEFAULT 0.1f
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
inputactiondata_t actions[INPUT_ACTION_COUNT];
|
inputactiondata_t actions[INPUT_ACTION_COUNT];
|
||||||
|
|
||||||
inputplatform_t platform;
|
inputplatform_t platform;
|
||||||
|
|
||||||
|
/** User-configured gamepad axis deadzone (0.0f to 1.0f). */
|
||||||
|
float_t deadzone;
|
||||||
} input_t;
|
} input_t;
|
||||||
|
|
||||||
extern input_t INPUT;
|
extern input_t INPUT;
|
||||||
|
|||||||
@@ -9,8 +9,6 @@
|
|||||||
#include "time/time.h"
|
#include "time/time.h"
|
||||||
#include "input/inputactiondefs.h"
|
#include "input/inputactiondefs.h"
|
||||||
|
|
||||||
#define INPUT_ACTION_CALLBACK_COUNT_MAX 4
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
inputaction_t action;
|
inputaction_t action;
|
||||||
float_t lastValue;
|
float_t lastValue;
|
||||||
|
|||||||
@@ -13,31 +13,17 @@ typedef struct {
|
|||||||
const char_t *file;
|
const char_t *file;
|
||||||
} localeinfo_t;
|
} localeinfo_t;
|
||||||
|
|
||||||
static const localeinfo_t LOCALE_INFO_EN_US = {
|
static const localeinfo_t LOCALE_EN_US = {
|
||||||
.name = "en-US",
|
.name = "en-US",
|
||||||
.file = "locale/en_US.po",
|
.file = "locale/en_US.po",
|
||||||
};
|
};
|
||||||
|
|
||||||
static const localeinfo_t LOCALE_INFO_JP_JP = {
|
static const localeinfo_t LOCALE_JP_JP = {
|
||||||
.name = "ja-JP",
|
.name = "ja-JP",
|
||||||
.file = "locale/jp_JP.po",
|
.file = "locale/jp_JP.po",
|
||||||
};
|
};
|
||||||
|
|
||||||
static const localeinfo_t LOCALE_INFO_ES_MX = {
|
static const localeinfo_t LOCALE_ES_MX = {
|
||||||
.name = "es-MX",
|
.name = "es-MX",
|
||||||
.file = "locale/es_MX.po",
|
.file = "locale/es_MX.po",
|
||||||
};
|
};
|
||||||
|
|
||||||
static const localeinfo_t * const LOCALE_INFO_LIST[] = {
|
|
||||||
&LOCALE_INFO_EN_US,
|
|
||||||
&LOCALE_INFO_JP_JP,
|
|
||||||
&LOCALE_INFO_ES_MX
|
|
||||||
};
|
|
||||||
|
|
||||||
#define LOCALE_INFO_LIST_COUNT ( \
|
|
||||||
sizeof(LOCALE_INFO_LIST) / sizeof(LOCALE_INFO_LIST[0]) \
|
|
||||||
)
|
|
||||||
|
|
||||||
#define LOCALE_DEFAULT LOCALE_INFO_EN_US
|
|
||||||
|
|
||||||
// EOF
|
|
||||||
@@ -7,22 +7,13 @@
|
|||||||
|
|
||||||
#include "localemanager.h"
|
#include "localemanager.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "util/string.h"
|
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "ui/ui.h"
|
|
||||||
#include "system/system.h"
|
|
||||||
#include "console/console.h"
|
|
||||||
|
|
||||||
localemanager_t LOCALE;
|
localemanager_t LOCALE;
|
||||||
|
|
||||||
errorret_t localeManagerInit() {
|
errorret_t localeManagerInit() {
|
||||||
memoryZero(&LOCALE, sizeof(localemanager_t));
|
memoryZero(&LOCALE, sizeof(localemanager_t));
|
||||||
|
errorChain(localeManagerSetLocale(&LOCALE_EN_US));
|
||||||
// TODO: Set locale based on system locale.
|
|
||||||
const localeinfo_t *locale = systemGetLocale();
|
|
||||||
errorChain(localeManagerSetLocale(locale));
|
|
||||||
consolePrint("Locale set to: %s", locale->name);
|
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,9 +30,6 @@ errorret_t localeManagerSetLocale(const localeinfo_t *locale) {
|
|||||||
assetEntryLock(LOCALE.entry);
|
assetEntryLock(LOCALE.entry);
|
||||||
errorChain(assetRequireLoaded(LOCALE.entry));
|
errorChain(assetRequireLoaded(LOCALE.entry));
|
||||||
|
|
||||||
// TODO : Trigger UI update.
|
|
||||||
errorChain(uiUpdateTranslations());
|
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "locale/localemanager.h"
|
#include "localemanager.h"
|
||||||
#include "locale/localeinfo.h"
|
#include "locale/localeinfo.h"
|
||||||
#include "asset/asset.h"
|
#include "asset/asset.h"
|
||||||
|
|
||||||
@@ -20,14 +20,14 @@ extern localemanager_t LOCALE;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the locale system.
|
* Initialize the locale system.
|
||||||
*
|
*
|
||||||
* @return An error code if a failure occurs.
|
* @return An error code if a failure occurs.
|
||||||
*/
|
*/
|
||||||
errorret_t localeManagerInit();
|
errorret_t localeManagerInit();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the current locale.
|
* Set the current locale.
|
||||||
*
|
*
|
||||||
* @param locale The locale to set.
|
* @param locale The locale to set.
|
||||||
* @return An error code if a failure occurs.
|
* @return An error code if a failure occurs.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -8,3 +8,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
network.c
|
network.c
|
||||||
networkinfo.c
|
networkinfo.c
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Subdirs
|
||||||
|
add_subdirectory(http)
|
||||||
+6
-3
@@ -5,7 +5,10 @@
|
|||||||
|
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
cutscenestartbattle.c
|
networkhttpheader.c
|
||||||
cutscenebattlewaitstate.c
|
networkhttpurl.c
|
||||||
cutscenebattleforceaction.c
|
networkhttprequest.c
|
||||||
|
networkhttpprocess.c
|
||||||
|
networkhttpthread.c
|
||||||
|
networkhttp.c
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "networkhttp.h"
|
||||||
|
#include "networkhttprequest.h"
|
||||||
|
#include "networkhttpthread.h"
|
||||||
|
|
||||||
|
networkhttp_t NETWORK_HTTP;
|
||||||
|
|
||||||
|
errorret_t networkHttpInit(void) {
|
||||||
|
networkHttpRequestPoolInit();
|
||||||
|
|
||||||
|
threadInit(&NETWORK_HTTP.thread, networkHttpThreadRun);
|
||||||
|
threadStart(&NETWORK_HTTP.thread);
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkHttpUpdate(void) {
|
||||||
|
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
|
||||||
|
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
|
||||||
|
|
||||||
|
threadMutexLock(&request->mutex);
|
||||||
|
const networkhttprequeststate_t state = request->state;
|
||||||
|
threadMutexUnlock(&request->mutex);
|
||||||
|
|
||||||
|
if(state == NETWORK_HTTP_REQUEST_STATE_DONE) {
|
||||||
|
eventInvoke(&request->onComplete, request);
|
||||||
|
} else if(state == NETWORK_HTTP_REQUEST_STATE_ERROR) {
|
||||||
|
eventInvoke(&request->onError, request);
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
networkHttpRequestReset(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkHttpDispose(void) {
|
||||||
|
threadStop(&NETWORK_HTTP.thread);
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -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 "thread/thread.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
thread_t thread;
|
||||||
|
} networkhttp_t;
|
||||||
|
|
||||||
|
extern networkhttp_t NETWORK_HTTP;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the HTTP client: prepares the request pool and starts
|
||||||
|
* the background worker thread. Called once during engine startup.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t networkHttpInit(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches any requests that finished on the background thread since
|
||||||
|
* the last call: fires onComplete/onError on the main thread, frees
|
||||||
|
* the response, and returns the slot to the pool. Call once per frame.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t networkHttpUpdate(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the background worker thread and disposes of the HTTP client.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t networkHttpDispose(void);
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "networkhttpheader.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
|
||||||
|
void networkHttpHeaderListClear(networkhttpheaderlist_t *list) {
|
||||||
|
memoryZero(list, sizeof(networkhttpheaderlist_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t networkHttpHeaderIsReserved(const char_t *name) {
|
||||||
|
return
|
||||||
|
stringCompareInsensitive(name, "Host") == 0 ||
|
||||||
|
stringCompareInsensitive(name, "Connection") == 0 ||
|
||||||
|
stringCompareInsensitive(name, "Content-Length") == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void networkHttpHeaderListAdd(
|
||||||
|
networkhttpheaderlist_t *list,
|
||||||
|
const char_t *name,
|
||||||
|
const char_t *value,
|
||||||
|
const bool_t skipReserved
|
||||||
|
) {
|
||||||
|
if(list->count >= NETWORK_HTTP_HEADER_COUNT_MAX) return;
|
||||||
|
if(skipReserved && networkHttpHeaderIsReserved(name)) return;
|
||||||
|
|
||||||
|
networkhttpheader_t *header = &list->headers[list->count];
|
||||||
|
stringCopy(header->name, name, NETWORK_HTTP_HEADER_NAME_MAX - 1);
|
||||||
|
stringCopy(header->value, value, NETWORK_HTTP_HEADER_VALUE_MAX - 1);
|
||||||
|
list->count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const networkhttpheader_t * networkHttpHeaderListFind(
|
||||||
|
const networkhttpheaderlist_t *list,
|
||||||
|
const char_t *name
|
||||||
|
) {
|
||||||
|
for(uint32_t i = 0; i < list->count; i++) {
|
||||||
|
if(stringCompareInsensitive(list->headers[i].name, name) == 0) {
|
||||||
|
return &list->headers[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "dusk.h"
|
||||||
|
|
||||||
|
#define NETWORK_HTTP_HEADER_COUNT_MAX 16
|
||||||
|
#define NETWORK_HTTP_HEADER_NAME_MAX 64
|
||||||
|
#define NETWORK_HTTP_HEADER_VALUE_MAX 256
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char_t name[NETWORK_HTTP_HEADER_NAME_MAX];
|
||||||
|
char_t value[NETWORK_HTTP_HEADER_VALUE_MAX];
|
||||||
|
} networkhttpheader_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
networkhttpheader_t headers[NETWORK_HTTP_HEADER_COUNT_MAX];
|
||||||
|
uint32_t count;
|
||||||
|
} networkhttpheaderlist_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears a header list, removing all entries.
|
||||||
|
*
|
||||||
|
* @param list The header list to clear.
|
||||||
|
*/
|
||||||
|
void networkHttpHeaderListClear(networkhttpheaderlist_t *list);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether a header name is reserved, meaning it is computed
|
||||||
|
* internally by the request writer and must not be set by the caller.
|
||||||
|
* This is Host, Connection and Content-Length.
|
||||||
|
*
|
||||||
|
* @param name The header name to check, compared case-insensitively.
|
||||||
|
* @return true if the header name is reserved.
|
||||||
|
*/
|
||||||
|
bool_t networkHttpHeaderIsReserved(const char_t *name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a header to the list, silently doing nothing if the list is
|
||||||
|
* already full or if skipReserved is true and the name is reserved (see
|
||||||
|
* networkHttpHeaderIsReserved).
|
||||||
|
*
|
||||||
|
* @param list The header list to append to.
|
||||||
|
* @param name The header name.
|
||||||
|
* @param value The header value.
|
||||||
|
* @param skipReserved If true, reserved header names are ignored.
|
||||||
|
*/
|
||||||
|
void networkHttpHeaderListAdd(
|
||||||
|
networkhttpheaderlist_t *list,
|
||||||
|
const char_t *name,
|
||||||
|
const char_t *value,
|
||||||
|
const bool_t skipReserved
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the first header in the list matching the given name, compared
|
||||||
|
* case-insensitively.
|
||||||
|
*
|
||||||
|
* @param list The header list to search.
|
||||||
|
* @param name The header name to search for.
|
||||||
|
* @return Pointer to the matching header, or NULL if not found.
|
||||||
|
*/
|
||||||
|
const networkhttpheader_t * networkHttpHeaderListFind(
|
||||||
|
const networkhttpheaderlist_t *list,
|
||||||
|
const char_t *name
|
||||||
|
);
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "networkhttpprocess.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
|
||||||
|
const char_t * networkHttpProcessMethodName(
|
||||||
|
const networkhttpmethod_t method
|
||||||
|
) {
|
||||||
|
switch(method) {
|
||||||
|
case NETWORK_HTTP_METHOD_POST: return "POST";
|
||||||
|
case NETWORK_HTTP_METHOD_PUT: return "PUT";
|
||||||
|
case NETWORK_HTTP_METHOD_GET:
|
||||||
|
default:
|
||||||
|
return "GET";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkHttpProcessBuildHead(
|
||||||
|
const networkhttprequest_t *request,
|
||||||
|
const networkhttpurl_t *target,
|
||||||
|
char_t *dest,
|
||||||
|
const size_t destSize,
|
||||||
|
size_t *outLength
|
||||||
|
) {
|
||||||
|
size_t cursor = 0;
|
||||||
|
|
||||||
|
errorChain(networkHttpUrlAppend(
|
||||||
|
dest, destSize, &cursor, networkHttpProcessMethodName(request->method)
|
||||||
|
));
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, " "));
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, target->path));
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, " HTTP/1.1\r\n"));
|
||||||
|
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "Host: "));
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, target->host));
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
|
||||||
|
|
||||||
|
errorChain(networkHttpUrlAppend(
|
||||||
|
dest, destSize, &cursor, "Connection: close\r\n"
|
||||||
|
));
|
||||||
|
|
||||||
|
for(uint32_t i = 0; i < request->requestHeaders.count; i++) {
|
||||||
|
const networkhttpheader_t *header = &request->requestHeaders.headers[i];
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, header->name));
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, ": "));
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, header->value));
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(request->bodyLength > 0) {
|
||||||
|
char_t lengthStr[24];
|
||||||
|
stringFormat(lengthStr, sizeof(lengthStr) - 1, "%zu", request->bodyLength);
|
||||||
|
|
||||||
|
errorChain(networkHttpUrlAppend(
|
||||||
|
dest, destSize, &cursor, "Content-Length: "
|
||||||
|
));
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, lengthStr));
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
|
||||||
|
|
||||||
|
*outLength = cursor;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkHttpProcessSendAll(
|
||||||
|
networksocketplatform_t *sock,
|
||||||
|
const uint8_t *data,
|
||||||
|
const size_t length
|
||||||
|
) {
|
||||||
|
size_t sentTotal = 0;
|
||||||
|
while(sentTotal < length) {
|
||||||
|
size_t sent = 0;
|
||||||
|
errorChain(networkSocketPlatformSend(
|
||||||
|
sock, data + sentTotal, length - sentTotal, &sent
|
||||||
|
));
|
||||||
|
if(sent == 0) errorThrow("Connection closed while sending data");
|
||||||
|
sentTotal += sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkHttpProcessReceiveHeaders(
|
||||||
|
networksocketplatform_t *sock,
|
||||||
|
char_t *headerBuf,
|
||||||
|
const size_t headerBufSize,
|
||||||
|
uint8_t *leftover,
|
||||||
|
const size_t leftoverBufSize,
|
||||||
|
size_t *outLeftoverLength
|
||||||
|
) {
|
||||||
|
size_t headerLength = 0;
|
||||||
|
headerBuf[0] = '\0';
|
||||||
|
|
||||||
|
uint8_t chunk[NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE];
|
||||||
|
|
||||||
|
for(;;) {
|
||||||
|
size_t received = 0;
|
||||||
|
errorChain(networkSocketPlatformReceive(
|
||||||
|
sock, chunk, sizeof(chunk), &received
|
||||||
|
));
|
||||||
|
if(received == 0) {
|
||||||
|
errorThrow("Connection closed before response headers completed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(headerLength + received >= headerBufSize) {
|
||||||
|
errorThrow("Response headers too large");
|
||||||
|
}
|
||||||
|
|
||||||
|
memoryCopy(headerBuf + headerLength, chunk, received);
|
||||||
|
headerLength += received;
|
||||||
|
headerBuf[headerLength] = '\0';
|
||||||
|
|
||||||
|
char_t *terminator = strstr(headerBuf, "\r\n\r\n");
|
||||||
|
if(terminator == NULL) continue;
|
||||||
|
|
||||||
|
const size_t terminatorOffset = (size_t)(terminator - headerBuf);
|
||||||
|
const size_t bodyStart = terminatorOffset + 4;
|
||||||
|
const size_t extra = headerLength - bodyStart;
|
||||||
|
|
||||||
|
if(extra > leftoverBufSize) errorThrow("Too much body data buffered");
|
||||||
|
if(extra > 0) memoryCopy(leftover, headerBuf + bodyStart, extra);
|
||||||
|
*outLeftoverLength = extra;
|
||||||
|
|
||||||
|
// Keep the trailing "\r\n" of the last header line, drop the blank
|
||||||
|
// line so callers can split on "\r\n" without a special case.
|
||||||
|
headerBuf[terminatorOffset + 2] = '\0';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkHttpProcessParseStatusLine(
|
||||||
|
const char_t *line,
|
||||||
|
uint16_t *outStatus
|
||||||
|
) {
|
||||||
|
const char_t *space = strchr(line, ' ');
|
||||||
|
if(space == NULL) errorThrow("Malformed status line: %s", line);
|
||||||
|
|
||||||
|
char_t statusStr[4];
|
||||||
|
if(strlen(space + 1) < 3) errorThrow("Malformed status line: %s", line);
|
||||||
|
memoryCopy(statusStr, space + 1, 3);
|
||||||
|
statusStr[3] = '\0';
|
||||||
|
|
||||||
|
if(!stringToU16(statusStr, outStatus)) {
|
||||||
|
errorThrow("Malformed status code: %s", line);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
void networkHttpProcessParseHeaderLine(
|
||||||
|
const char_t *line,
|
||||||
|
networkhttpheaderlist_t *outHeaders
|
||||||
|
) {
|
||||||
|
const char_t *colon = strchr(line, ':');
|
||||||
|
if(colon == NULL) return;
|
||||||
|
|
||||||
|
const size_t nameLength = (size_t)(colon - line);
|
||||||
|
if(nameLength == 0 || nameLength >= NETWORK_HTTP_HEADER_NAME_MAX) return;
|
||||||
|
|
||||||
|
char_t name[NETWORK_HTTP_HEADER_NAME_MAX];
|
||||||
|
memoryCopy(name, line, nameLength);
|
||||||
|
name[nameLength] = '\0';
|
||||||
|
|
||||||
|
const char_t *valueStart = colon + 1;
|
||||||
|
while(*valueStart == ' ') valueStart++;
|
||||||
|
|
||||||
|
const size_t valueLength = strlen(valueStart);
|
||||||
|
if(valueLength >= NETWORK_HTTP_HEADER_VALUE_MAX) return;
|
||||||
|
|
||||||
|
char_t value[NETWORK_HTTP_HEADER_VALUE_MAX];
|
||||||
|
memoryCopy(value, valueStart, valueLength + 1);
|
||||||
|
|
||||||
|
networkHttpHeaderListAdd(outHeaders, name, value, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkHttpProcessParseHeaders(
|
||||||
|
char_t *headerBuf,
|
||||||
|
uint16_t *outStatus,
|
||||||
|
networkhttpheaderlist_t *outHeaders
|
||||||
|
) {
|
||||||
|
char_t *cursor = headerBuf;
|
||||||
|
bool_t first = true;
|
||||||
|
|
||||||
|
for(;;) {
|
||||||
|
char_t *lineEnd = strstr(cursor, "\r\n");
|
||||||
|
const bool_t isLast = lineEnd == NULL;
|
||||||
|
if(lineEnd != NULL) *lineEnd = '\0';
|
||||||
|
|
||||||
|
if(first) {
|
||||||
|
errorChain(networkHttpProcessParseStatusLine(cursor, outStatus));
|
||||||
|
first = false;
|
||||||
|
} else if(cursor[0] != '\0') {
|
||||||
|
networkHttpProcessParseHeaderLine(cursor, outHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isLast) break;
|
||||||
|
cursor = lineEnd + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkHttpProcessReadBody(
|
||||||
|
networksocketplatform_t *sock,
|
||||||
|
const networkhttpheaderlist_t *headers,
|
||||||
|
const uint8_t *leftover,
|
||||||
|
const size_t leftoverLength,
|
||||||
|
uint8_t **outBody,
|
||||||
|
size_t *outBodyLength
|
||||||
|
) {
|
||||||
|
const networkhttpheader_t *contentLength =
|
||||||
|
networkHttpHeaderListFind(headers, "Content-Length");
|
||||||
|
|
||||||
|
if(contentLength != NULL) {
|
||||||
|
int64_t expected = 0;
|
||||||
|
if(!stringToI64(contentLength->value, &expected) || expected < 0) {
|
||||||
|
errorThrow("Malformed Content-Length header: %s", contentLength->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(expected == 0) {
|
||||||
|
*outBody = NULL;
|
||||||
|
*outBodyLength = 0;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t *body = memoryAllocate((size_t)expected);
|
||||||
|
const size_t initialCopy =
|
||||||
|
leftoverLength < (size_t)expected ? leftoverLength : (size_t)expected;
|
||||||
|
if(initialCopy > 0) memoryCopy(body, leftover, initialCopy);
|
||||||
|
size_t received = initialCopy;
|
||||||
|
|
||||||
|
while(received < (size_t)expected) {
|
||||||
|
size_t got = 0;
|
||||||
|
errorret_t ret = networkSocketPlatformReceive(
|
||||||
|
sock, body + received, (size_t)expected - received, &got
|
||||||
|
);
|
||||||
|
if(errorIsNotOk(ret)) {
|
||||||
|
memoryFree(body);
|
||||||
|
return errorChainImpl(ret, __FILE__, __func__, __LINE__);
|
||||||
|
}
|
||||||
|
if(got == 0) {
|
||||||
|
memoryFree(body);
|
||||||
|
errorThrow("Connection closed before full body was received");
|
||||||
|
}
|
||||||
|
received += got;
|
||||||
|
}
|
||||||
|
|
||||||
|
*outBody = body;
|
||||||
|
*outBodyLength = (size_t)expected;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
// No Content-Length: read until the connection closes.
|
||||||
|
size_t capacity = NETWORK_HTTP_PROCESS_BODY_INITIAL_SIZE;
|
||||||
|
if(leftoverLength > capacity) capacity = leftoverLength;
|
||||||
|
|
||||||
|
uint8_t *body = memoryAllocate(capacity);
|
||||||
|
size_t length = 0;
|
||||||
|
if(leftoverLength > 0) {
|
||||||
|
memoryCopy(body, leftover, leftoverLength);
|
||||||
|
length = leftoverLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(;;) {
|
||||||
|
if(length == capacity) {
|
||||||
|
const size_t newCapacity = capacity * 2;
|
||||||
|
memoryResize((void **)&body, capacity, newCapacity);
|
||||||
|
capacity = newCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t got = 0;
|
||||||
|
errorret_t ret = networkSocketPlatformReceive(
|
||||||
|
sock, body + length, capacity - length, &got
|
||||||
|
);
|
||||||
|
if(errorIsNotOk(ret)) {
|
||||||
|
memoryFree(body);
|
||||||
|
return errorChainImpl(ret, __FILE__, __func__, __LINE__);
|
||||||
|
}
|
||||||
|
if(got == 0) break;
|
||||||
|
length += got;
|
||||||
|
}
|
||||||
|
|
||||||
|
*outBody = body;
|
||||||
|
*outBodyLength = length;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkHttpProcessAttempt(
|
||||||
|
const networkhttprequest_t *request,
|
||||||
|
const networkhttpurl_t *target,
|
||||||
|
networkhttpresponse_t *outResponse
|
||||||
|
) {
|
||||||
|
memoryZero(outResponse, sizeof(networkhttpresponse_t));
|
||||||
|
|
||||||
|
networksocketplatform_t sock;
|
||||||
|
errorChain(networkSocketPlatformConnect(&sock, target->host, target->port));
|
||||||
|
|
||||||
|
char_t head[NETWORK_HTTP_PROCESS_HEAD_BUF_SIZE];
|
||||||
|
size_t headLength = 0;
|
||||||
|
networkHttpProcessErrorChain(&sock, networkHttpProcessBuildHead(
|
||||||
|
request, target, head, NETWORK_HTTP_PROCESS_HEAD_BUF_SIZE, &headLength
|
||||||
|
));
|
||||||
|
|
||||||
|
networkHttpProcessErrorChain(&sock, networkHttpProcessSendAll(
|
||||||
|
&sock, (const uint8_t *)head, headLength
|
||||||
|
));
|
||||||
|
|
||||||
|
if(request->bodyLength > 0) {
|
||||||
|
networkHttpProcessErrorChain(&sock, networkHttpProcessSendAll(
|
||||||
|
&sock, request->body, request->bodyLength
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
char_t headerBuf[NETWORK_HTTP_PROCESS_HEADER_BUF_SIZE];
|
||||||
|
uint8_t leftover[NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE];
|
||||||
|
size_t leftoverLength = 0;
|
||||||
|
|
||||||
|
networkHttpProcessErrorChain(&sock, networkHttpProcessReceiveHeaders(
|
||||||
|
&sock, headerBuf, NETWORK_HTTP_PROCESS_HEADER_BUF_SIZE,
|
||||||
|
leftover, NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE, &leftoverLength
|
||||||
|
));
|
||||||
|
|
||||||
|
networkHttpProcessErrorChain(&sock, networkHttpProcessParseHeaders(
|
||||||
|
headerBuf, &outResponse->status, &outResponse->headers
|
||||||
|
));
|
||||||
|
|
||||||
|
errorret_t bodyRet = networkHttpProcessReadBody(
|
||||||
|
&sock, &outResponse->headers, leftover, leftoverLength,
|
||||||
|
&outResponse->body, &outResponse->bodyLength
|
||||||
|
);
|
||||||
|
networkSocketPlatformClose(&sock);
|
||||||
|
errorChain(bodyRet);
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
/**
|
||||||
|
* 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 "network/http/networkhttprequest.h"
|
||||||
|
#include "network/http/networkhttpurl.h"
|
||||||
|
#include "network/networksocketplatform.h"
|
||||||
|
|
||||||
|
#define NETWORK_HTTP_PROCESS_HEAD_BUF_SIZE 4096
|
||||||
|
#define NETWORK_HTTP_PROCESS_HEADER_BUF_SIZE 8192
|
||||||
|
#define NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE 2048
|
||||||
|
#define NETWORK_HTTP_PROCESS_BODY_INITIAL_SIZE 4096
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand to chain an error, closing sock first if expr failed. Used
|
||||||
|
* for every step of an attempt after the socket has connected, since
|
||||||
|
* failing partway through must not leak the socket.
|
||||||
|
*/
|
||||||
|
#define networkHttpProcessErrorChain(sock, _expr) { \
|
||||||
|
errorret_t _nhpErr = (_expr); \
|
||||||
|
if(errorIsNotOk(_nhpErr)) { \
|
||||||
|
networkSocketPlatformClose(sock); \
|
||||||
|
return errorChainImpl(_nhpErr, __FILE__, __func__, __LINE__); \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the wire method name ("GET", "POST", "PUT") for method.
|
||||||
|
*
|
||||||
|
* @param method The method to name.
|
||||||
|
* @return A static string naming the method.
|
||||||
|
*/
|
||||||
|
const char_t * networkHttpProcessMethodName(
|
||||||
|
const networkhttpmethod_t method
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the request line, headers and blank line terminator (but not
|
||||||
|
* the body) for request, targeting target, into dest.
|
||||||
|
*
|
||||||
|
* @param request The request being sent.
|
||||||
|
* @param target The resolved host/port/path for this attempt.
|
||||||
|
* @param dest The destination buffer.
|
||||||
|
* @param destSize The size of dest, including the null terminator.
|
||||||
|
* @param outLength The number of bytes written is written here.
|
||||||
|
* @return An error if the result would not fit in dest.
|
||||||
|
*/
|
||||||
|
errorret_t networkHttpProcessBuildHead(
|
||||||
|
const networkhttprequest_t *request,
|
||||||
|
const networkhttpurl_t *target,
|
||||||
|
char_t *dest,
|
||||||
|
const size_t destSize,
|
||||||
|
size_t *outLength
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends length bytes of data over sock, looping until all of it has
|
||||||
|
* been written.
|
||||||
|
*
|
||||||
|
* @param sock The connected socket.
|
||||||
|
* @param data The data to send.
|
||||||
|
* @param length The number of bytes in data.
|
||||||
|
* @return An error if the send failed or the connection closed early.
|
||||||
|
*/
|
||||||
|
errorret_t networkHttpProcessSendAll(
|
||||||
|
networksocketplatform_t *sock,
|
||||||
|
const uint8_t *data,
|
||||||
|
const size_t length
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads from sock until the blank line ("\r\n\r\n") terminating the
|
||||||
|
* response headers has been seen, writing the status line and header
|
||||||
|
* lines (without the blank line) into headerBuf as a null-terminated
|
||||||
|
* string. Any body bytes read past the terminator in the same receive
|
||||||
|
* are written to leftover.
|
||||||
|
*
|
||||||
|
* @param sock The connected socket.
|
||||||
|
* @param headerBuf The destination buffer for the header text.
|
||||||
|
* @param headerBufSize The size of headerBuf, including the null
|
||||||
|
* terminator.
|
||||||
|
* @param leftover Destination buffer for any body bytes read early.
|
||||||
|
* @param leftoverBufSize The size of leftover.
|
||||||
|
* @param outLeftoverLength The number of bytes written to leftover is
|
||||||
|
* written here.
|
||||||
|
* @return An error if the headers could not be read, were malformed,
|
||||||
|
* or did not fit within the given buffers.
|
||||||
|
*/
|
||||||
|
errorret_t networkHttpProcessReceiveHeaders(
|
||||||
|
networksocketplatform_t *sock,
|
||||||
|
char_t *headerBuf,
|
||||||
|
const size_t headerBufSize,
|
||||||
|
uint8_t *leftover,
|
||||||
|
const size_t leftoverBufSize,
|
||||||
|
size_t *outLeftoverLength
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a "HTTP/1.1 <status> <reason>" status line.
|
||||||
|
*
|
||||||
|
* @param line The status line, null-terminated.
|
||||||
|
* @param outStatus The parsed status code is written here.
|
||||||
|
* @return An error if the status line is malformed.
|
||||||
|
*/
|
||||||
|
errorret_t networkHttpProcessParseStatusLine(
|
||||||
|
const char_t *line,
|
||||||
|
uint16_t *outStatus
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a single "Name: Value" response header line and appends it to
|
||||||
|
* outHeaders. Silently does nothing if the line has no colon or the
|
||||||
|
* name/value are too long to fit a networkhttpheader_t.
|
||||||
|
*
|
||||||
|
* @param line The header line, null-terminated.
|
||||||
|
* @param outHeaders The header list to append to.
|
||||||
|
*/
|
||||||
|
void networkHttpProcessParseHeaderLine(
|
||||||
|
const char_t *line,
|
||||||
|
networkhttpheaderlist_t *outHeaders
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the status line and every header line out of headerBuf (as
|
||||||
|
* produced by networkHttpProcessReceiveHeaders). Mutates headerBuf in
|
||||||
|
* place to split it into lines.
|
||||||
|
*
|
||||||
|
* @param headerBuf The header text to parse.
|
||||||
|
* @param outStatus The parsed status code is written here.
|
||||||
|
* @param outHeaders The header list to populate.
|
||||||
|
* @return An error if the status line is malformed.
|
||||||
|
*/
|
||||||
|
errorret_t networkHttpProcessParseHeaders(
|
||||||
|
char_t *headerBuf,
|
||||||
|
uint16_t *outStatus,
|
||||||
|
networkhttpheaderlist_t *outHeaders
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the response body from sock. If headers contains a
|
||||||
|
* Content-Length, reads exactly that many bytes; otherwise reads until
|
||||||
|
* the connection closes. Either way, leftover/leftoverLength (body
|
||||||
|
* bytes already read while looking for the header terminator) are
|
||||||
|
* included at the start of the result.
|
||||||
|
*
|
||||||
|
* @param sock The connected socket.
|
||||||
|
* @param headers The parsed response headers.
|
||||||
|
* @param leftover Body bytes already read past the header terminator.
|
||||||
|
* @param leftoverLength The number of bytes in leftover.
|
||||||
|
* @param outBody A memoryAllocate'd buffer holding the body is written
|
||||||
|
* here, or NULL if the body is empty. Owned by the caller.
|
||||||
|
* @param outBodyLength The number of bytes in *outBody is written here.
|
||||||
|
* @return An error if the body could not be fully read.
|
||||||
|
*/
|
||||||
|
errorret_t networkHttpProcessReadBody(
|
||||||
|
networksocketplatform_t *sock,
|
||||||
|
const networkhttpheaderlist_t *headers,
|
||||||
|
const uint8_t *leftover,
|
||||||
|
const size_t leftoverLength,
|
||||||
|
uint8_t **outBody,
|
||||||
|
size_t *outBodyLength
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a single blocking HTTP exchange: connects to target, writes
|
||||||
|
* the request head + body, and reads back one parsed response. Does
|
||||||
|
* not follow redirects -- that is the caller's responsibility.
|
||||||
|
*
|
||||||
|
* @param request The request to send (method, headers, body).
|
||||||
|
* @param target The resolved host/port/path to connect to for this
|
||||||
|
* attempt (may differ from the request's own URL if this is a
|
||||||
|
* redirected attempt).
|
||||||
|
* @param outResponse The parsed response is written here on success.
|
||||||
|
* @return An error if the exchange could not be completed.
|
||||||
|
*/
|
||||||
|
errorret_t networkHttpProcessAttempt(
|
||||||
|
const networkhttprequest_t *request,
|
||||||
|
const networkhttpurl_t *target,
|
||||||
|
networkhttpresponse_t *outResponse
|
||||||
|
);
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "networkhttprequest.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
|
||||||
|
networkhttprequest_t NETWORK_HTTP_REQUESTS[NETWORK_HTTP_REQUEST_COUNT_MAX];
|
||||||
|
|
||||||
|
void networkHttpRequestPoolInit(void) {
|
||||||
|
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
|
||||||
|
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
|
||||||
|
|
||||||
|
memoryZero(request, sizeof(networkhttprequest_t));
|
||||||
|
threadMutexInit(&request->mutex);
|
||||||
|
eventInit(
|
||||||
|
&request->onComplete, request->onCompleteCallbacks,
|
||||||
|
request->onCompleteUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
|
||||||
|
);
|
||||||
|
eventInit(
|
||||||
|
&request->onError, request->onErrorCallbacks,
|
||||||
|
request->onErrorUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
|
||||||
|
);
|
||||||
|
request->state = NETWORK_HTTP_REQUEST_STATE_FREE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
networkhttprequest_t * networkHttpRequestClaim(void) {
|
||||||
|
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
|
||||||
|
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
|
||||||
|
|
||||||
|
threadMutexLock(&request->mutex);
|
||||||
|
if(request->state != NETWORK_HTTP_REQUEST_STATE_FREE) {
|
||||||
|
threadMutexUnlock(&request->mutex);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
threadMutexUnlock(&request->mutex);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
assertUnreachable("No available HTTP request slots.");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void networkHttpRequestReset(networkhttprequest_t *request) {
|
||||||
|
assertNotNull(request, "request must not be NULL");
|
||||||
|
|
||||||
|
threadMutexLock(&request->mutex);
|
||||||
|
|
||||||
|
if(request->body != NULL) memoryFree(request->body);
|
||||||
|
request->body = NULL;
|
||||||
|
request->bodyLength = 0;
|
||||||
|
|
||||||
|
if(request->response.body != NULL) memoryFree(request->response.body);
|
||||||
|
memoryZero(&request->response, sizeof(networkhttpresponse_t));
|
||||||
|
|
||||||
|
networkHttpHeaderListClear(&request->requestHeaders);
|
||||||
|
|
||||||
|
eventInit(
|
||||||
|
&request->onComplete, request->onCompleteCallbacks,
|
||||||
|
request->onCompleteUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
|
||||||
|
);
|
||||||
|
eventInit(
|
||||||
|
&request->onError, request->onErrorCallbacks,
|
||||||
|
request->onErrorUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
|
||||||
|
);
|
||||||
|
|
||||||
|
request->state = NETWORK_HTTP_REQUEST_STATE_FREE;
|
||||||
|
|
||||||
|
threadMutexUnlock(&request->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkHttpRequest(
|
||||||
|
const networkhttpmethod_t method,
|
||||||
|
const char_t *url,
|
||||||
|
const networkhttpheader_t *headers,
|
||||||
|
const uint32_t headerCount,
|
||||||
|
const networkhttpheader_t *queryParams,
|
||||||
|
const uint32_t queryParamCount,
|
||||||
|
const uint8_t *body,
|
||||||
|
const size_t bodyLength,
|
||||||
|
const eventcallback_t onComplete,
|
||||||
|
const eventcallback_t onError,
|
||||||
|
void *user
|
||||||
|
) {
|
||||||
|
assertNotNull(url, "url must not be NULL");
|
||||||
|
|
||||||
|
networkhttprequest_t *request = networkHttpRequestClaim();
|
||||||
|
request->method = method;
|
||||||
|
|
||||||
|
errorChain(networkHttpUrlBuild(
|
||||||
|
request->url, NETWORK_HTTP_URL_MAX, url, queryParams, queryParamCount
|
||||||
|
));
|
||||||
|
|
||||||
|
networkHttpHeaderListClear(&request->requestHeaders);
|
||||||
|
for(uint32_t i = 0; i < headerCount; i++) {
|
||||||
|
networkHttpHeaderListAdd(
|
||||||
|
&request->requestHeaders, headers[i].name, headers[i].value, true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
request->bodyLength = bodyLength;
|
||||||
|
if(bodyLength > 0) {
|
||||||
|
assertNotNull(body, "body must not be NULL when bodyLength > 0");
|
||||||
|
request->body = memoryAllocate(bodyLength);
|
||||||
|
memoryCopy(request->body, body, bodyLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(onComplete != NULL) eventSubscribe(&request->onComplete, onComplete, user);
|
||||||
|
if(onError != NULL) eventSubscribe(&request->onError, onError, user);
|
||||||
|
|
||||||
|
threadMutexLock(&request->mutex);
|
||||||
|
request->state = NETWORK_HTTP_REQUEST_STATE_PENDING;
|
||||||
|
threadMutexUnlock(&request->mutex);
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
/**
|
||||||
|
* 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 "event/event.h"
|
||||||
|
#include "thread/threadmutex.h"
|
||||||
|
#include "network/http/networkhttpheader.h"
|
||||||
|
#include "network/http/networkhttpurl.h"
|
||||||
|
|
||||||
|
#define NETWORK_HTTP_REQUEST_COUNT_MAX 4
|
||||||
|
#define NETWORK_HTTP_REQUEST_EVENT_MAX 1
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
NETWORK_HTTP_METHOD_GET,
|
||||||
|
NETWORK_HTTP_METHOD_POST,
|
||||||
|
NETWORK_HTTP_METHOD_PUT
|
||||||
|
} networkhttpmethod_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
NETWORK_HTTP_REQUEST_STATE_FREE,
|
||||||
|
NETWORK_HTTP_REQUEST_STATE_PENDING,
|
||||||
|
NETWORK_HTTP_REQUEST_STATE_ACTIVE,
|
||||||
|
NETWORK_HTTP_REQUEST_STATE_DONE,
|
||||||
|
NETWORK_HTTP_REQUEST_STATE_ERROR
|
||||||
|
} networkhttprequeststate_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint16_t status;
|
||||||
|
networkhttpheaderlist_t headers;
|
||||||
|
|
||||||
|
// Owned, memoryAllocate'd. Only valid until the onComplete/onError
|
||||||
|
// event has finished firing, at which point it is freed.
|
||||||
|
uint8_t *body;
|
||||||
|
size_t bodyLength;
|
||||||
|
} networkhttpresponse_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
threadmutex_t mutex;
|
||||||
|
networkhttprequeststate_t state;
|
||||||
|
|
||||||
|
networkhttpmethod_t method;
|
||||||
|
|
||||||
|
// Fully built, including any query params, by networkHttpRequest.
|
||||||
|
char_t url[NETWORK_HTTP_URL_MAX];
|
||||||
|
networkhttpheaderlist_t requestHeaders;
|
||||||
|
|
||||||
|
// Owned copy, memoryAllocate'd.
|
||||||
|
uint8_t *body;
|
||||||
|
size_t bodyLength;
|
||||||
|
|
||||||
|
/** Fired on the main thread once a response has been received. */
|
||||||
|
event_t onComplete;
|
||||||
|
eventcallback_t onCompleteCallbacks[NETWORK_HTTP_REQUEST_EVENT_MAX];
|
||||||
|
void *onCompleteUsers[NETWORK_HTTP_REQUEST_EVENT_MAX];
|
||||||
|
|
||||||
|
/** Fired on the main thread if the request could not be completed. */
|
||||||
|
event_t onError;
|
||||||
|
eventcallback_t onErrorCallbacks[NETWORK_HTTP_REQUEST_EVENT_MAX];
|
||||||
|
void *onErrorUsers[NETWORK_HTTP_REQUEST_EVENT_MAX];
|
||||||
|
|
||||||
|
networkhttpresponse_t response;
|
||||||
|
} networkhttprequest_t;
|
||||||
|
|
||||||
|
extern networkhttprequest_t
|
||||||
|
NETWORK_HTTP_REQUESTS[NETWORK_HTTP_REQUEST_COUNT_MAX];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the request pool: prepares every slot's mutex and events
|
||||||
|
* and marks them all FREE. Called once by networkHttpInit.
|
||||||
|
*/
|
||||||
|
void networkHttpRequestPoolInit(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds and returns a FREE slot in the request pool. The slot is not
|
||||||
|
* marked as in-use by this call; the caller must populate it and then
|
||||||
|
* set its state to PENDING.
|
||||||
|
*
|
||||||
|
* @return A free request slot.
|
||||||
|
*/
|
||||||
|
networkhttprequest_t * networkHttpRequestClaim(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets a request slot back to FREE, freeing any owned buffers
|
||||||
|
* (request body, response body) and clearing its event subscribers.
|
||||||
|
*
|
||||||
|
* @param request The request slot to reset.
|
||||||
|
*/
|
||||||
|
void networkHttpRequestReset(networkhttprequest_t *request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an HTTP request asynchronously. The request runs on a
|
||||||
|
* background thread; onComplete or onError is invoked on the main
|
||||||
|
* thread (from networkHttpUpdate) once it finishes. 301 redirects are
|
||||||
|
* followed automatically.
|
||||||
|
*
|
||||||
|
* @param method The HTTP method to use.
|
||||||
|
* @param url The "http://host[:port]/path" URL to request. Must not
|
||||||
|
* use any scheme other than http.
|
||||||
|
* @param headers Request headers to send, may be NULL if headerCount
|
||||||
|
* is 0. The Host, Connection and Content-Length headers are
|
||||||
|
* reserved and computed internally; any of those passed here
|
||||||
|
* are ignored.
|
||||||
|
* @param headerCount Number of entries in headers.
|
||||||
|
* @param queryParams Query string parameters to append to url, may be
|
||||||
|
* NULL if queryParamCount is 0.
|
||||||
|
* @param queryParamCount Number of entries in queryParams.
|
||||||
|
* @param body Request body to send for POST/PUT, may be NULL if
|
||||||
|
* bodyLength is 0. Ignored for GET.
|
||||||
|
* @param bodyLength Number of bytes in body.
|
||||||
|
* @param onComplete Invoked with (networkhttprequest_t *, user) once a
|
||||||
|
* response has been received, whatever its status code. May be
|
||||||
|
* NULL to ignore.
|
||||||
|
* @param onError Invoked with (networkhttprequest_t *, user) if the
|
||||||
|
* request could not be completed (DNS/connect/send/receive
|
||||||
|
* failure, or a malformed response). May be NULL to ignore.
|
||||||
|
* @param user Arbitrary pointer forwarded to onComplete/onError.
|
||||||
|
* @return An error if the request could not be queued (e.g. a
|
||||||
|
* malformed url).
|
||||||
|
*/
|
||||||
|
errorret_t networkHttpRequest(
|
||||||
|
const networkhttpmethod_t method,
|
||||||
|
const char_t *url,
|
||||||
|
const networkhttpheader_t *headers,
|
||||||
|
const uint32_t headerCount,
|
||||||
|
const networkhttpheader_t *queryParams,
|
||||||
|
const uint32_t queryParamCount,
|
||||||
|
const uint8_t *body,
|
||||||
|
const size_t bodyLength,
|
||||||
|
const eventcallback_t onComplete,
|
||||||
|
const eventcallback_t onError,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "networkhttpthread.h"
|
||||||
|
#include "networkhttpprocess.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
void networkHttpThreadRun(thread_t *thread) {
|
||||||
|
assertNotMainThread("networkHttpThreadRun must not run on the main thread.");
|
||||||
|
|
||||||
|
while(!threadShouldStop(thread)) {
|
||||||
|
bool_t didWork = false;
|
||||||
|
|
||||||
|
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
|
||||||
|
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
|
||||||
|
|
||||||
|
threadMutexLock(&request->mutex);
|
||||||
|
if(request->state != NETWORK_HTTP_REQUEST_STATE_PENDING) {
|
||||||
|
threadMutexUnlock(&request->mutex);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
request->state = NETWORK_HTTP_REQUEST_STATE_ACTIVE;
|
||||||
|
threadMutexUnlock(&request->mutex);
|
||||||
|
|
||||||
|
didWork = true;
|
||||||
|
networkHttpThreadProcessRequest(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(threadShouldStop(thread)) break;
|
||||||
|
if(!didWork) usleep(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void networkHttpThreadProcessRequest(networkhttprequest_t *request) {
|
||||||
|
networkhttpurl_t target;
|
||||||
|
errorret_t parseRet = networkHttpUrlParse(request->url, &target);
|
||||||
|
|
||||||
|
networkhttprequeststate_t finalState = NETWORK_HTTP_REQUEST_STATE_DONE;
|
||||||
|
|
||||||
|
if(errorIsNotOk(parseRet)) {
|
||||||
|
errorCatch(errorPrint(parseRet));
|
||||||
|
finalState = NETWORK_HTTP_REQUEST_STATE_ERROR;
|
||||||
|
} else {
|
||||||
|
for(uint32_t redirect = 0; ; redirect++) {
|
||||||
|
errorret_t ret = networkHttpProcessAttempt(
|
||||||
|
request, &target, &request->response
|
||||||
|
);
|
||||||
|
|
||||||
|
if(errorIsNotOk(ret)) {
|
||||||
|
errorCatch(errorPrint(ret));
|
||||||
|
finalState = NETWORK_HTTP_REQUEST_STATE_ERROR;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool_t isRedirect = request->response.status == 301;
|
||||||
|
const networkhttpheader_t *location = isRedirect ?
|
||||||
|
networkHttpHeaderListFind(&request->response.headers, "Location") :
|
||||||
|
NULL;
|
||||||
|
|
||||||
|
if(location == NULL || redirect >= NETWORK_HTTP_REDIRECT_COUNT_MAX) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
networkhttpurl_t nextTarget;
|
||||||
|
errorret_t nextRet = networkHttpUrlParse(location->value, &nextTarget);
|
||||||
|
if(errorIsNotOk(nextRet)) {
|
||||||
|
// Not an absolute URL we can follow -- treat the 301 itself as
|
||||||
|
// the final response instead of failing the whole request.
|
||||||
|
errorCatch(errorPrint(nextRet));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free the intermediate response body before following further.
|
||||||
|
if(request->response.body != NULL) memoryFree(request->response.body);
|
||||||
|
target = nextTarget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
threadMutexLock(&request->mutex);
|
||||||
|
request->state = finalState;
|
||||||
|
threadMutexUnlock(&request->mutex);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "thread/thread.h"
|
||||||
|
#include "network/http/networkhttprequest.h"
|
||||||
|
|
||||||
|
#define NETWORK_HTTP_REDIRECT_COUNT_MAX 5
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The background worker thread body: repeatedly scans the request
|
||||||
|
* pool for PENDING slots, processes each to completion, and marks it
|
||||||
|
* DONE or ERROR. Idles briefly when there is nothing to do. Runs
|
||||||
|
* until the thread is stopped.
|
||||||
|
*
|
||||||
|
* @param thread The thread runner.
|
||||||
|
*/
|
||||||
|
void networkHttpThreadRun(thread_t *thread);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes a single ACTIVE request slot to completion, following up
|
||||||
|
* to NETWORK_HTTP_REDIRECT_COUNT_MAX HTTP 301 redirects, and leaves it
|
||||||
|
* in the DONE or ERROR state. Only called from the background thread.
|
||||||
|
*
|
||||||
|
* @param request The request slot to process.
|
||||||
|
*/
|
||||||
|
void networkHttpThreadProcessRequest(networkhttprequest_t *request);
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "networkhttpurl.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
|
||||||
|
errorret_t networkHttpUrlParse(const char_t *url, networkhttpurl_t *out) {
|
||||||
|
assertNotNull(url, "url must not be NULL");
|
||||||
|
assertNotNull(out, "out must not be NULL");
|
||||||
|
|
||||||
|
if(strlen(url) <= 7 || strncasecmp(url, "http://", 7) != 0) {
|
||||||
|
errorThrow("Unsupported URL scheme (only http:// is supported): %s", url);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char_t *rest = url + 7;
|
||||||
|
|
||||||
|
const size_t hostLen = strcspn(rest, ":/");
|
||||||
|
if(hostLen == 0 || hostLen >= NETWORK_HTTP_HOST_MAX) {
|
||||||
|
errorThrow("Invalid host in URL: %s", url);
|
||||||
|
}
|
||||||
|
memoryCopy(out->host, rest, hostLen);
|
||||||
|
out->host[hostLen] = '\0';
|
||||||
|
rest += hostLen;
|
||||||
|
|
||||||
|
if(*rest == ':') {
|
||||||
|
rest++;
|
||||||
|
|
||||||
|
const size_t portLen = strcspn(rest, "/");
|
||||||
|
char_t portStr[8];
|
||||||
|
if(portLen == 0 || portLen >= sizeof(portStr)) {
|
||||||
|
errorThrow("Invalid port in URL: %s", url);
|
||||||
|
}
|
||||||
|
memoryCopy(portStr, rest, portLen);
|
||||||
|
portStr[portLen] = '\0';
|
||||||
|
|
||||||
|
if(!stringToU16(portStr, &out->port)) {
|
||||||
|
errorThrow("Invalid port in URL: %s", url);
|
||||||
|
}
|
||||||
|
rest += portLen;
|
||||||
|
} else {
|
||||||
|
out->port = NETWORK_HTTP_PORT_DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char_t *path = *rest == '\0' ? "/" : rest;
|
||||||
|
const size_t pathLen = strlen(path);
|
||||||
|
if(pathLen >= NETWORK_HTTP_PATH_MAX) {
|
||||||
|
errorThrow("Path too long in URL: %s", url);
|
||||||
|
}
|
||||||
|
memoryCopy(out->path, path, pathLen + 1);
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkHttpUrlBuild(
|
||||||
|
char_t *dest,
|
||||||
|
const size_t destSize,
|
||||||
|
const char_t *baseUrl,
|
||||||
|
const networkhttpheader_t *queryParams,
|
||||||
|
const uint32_t queryParamCount
|
||||||
|
) {
|
||||||
|
assertNotNull(dest, "dest must not be NULL");
|
||||||
|
assertNotNull(baseUrl, "baseUrl must not be NULL");
|
||||||
|
|
||||||
|
const size_t baseLen = strlen(baseUrl);
|
||||||
|
if(baseLen >= destSize) errorThrow("URL too long: %s", baseUrl);
|
||||||
|
memoryCopy(dest, baseUrl, baseLen + 1);
|
||||||
|
|
||||||
|
size_t cursor = baseLen;
|
||||||
|
bool_t hasQuery = stringIncludesString(baseUrl, "?");
|
||||||
|
|
||||||
|
for(uint32_t i = 0; i < queryParamCount; i++) {
|
||||||
|
errorChain(networkHttpUrlAppend(
|
||||||
|
dest, destSize, &cursor, hasQuery ? "&" : "?"
|
||||||
|
));
|
||||||
|
hasQuery = true;
|
||||||
|
|
||||||
|
errorChain(networkHttpUrlEncodeComponent(
|
||||||
|
dest, destSize, &cursor, queryParams[i].name
|
||||||
|
));
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "="));
|
||||||
|
errorChain(networkHttpUrlEncodeComponent(
|
||||||
|
dest, destSize, &cursor, queryParams[i].value
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkHttpUrlAppend(
|
||||||
|
char_t *dest,
|
||||||
|
const size_t destSize,
|
||||||
|
size_t *cursor,
|
||||||
|
const char_t *str
|
||||||
|
) {
|
||||||
|
const size_t len = strlen(str);
|
||||||
|
if(*cursor + len >= destSize) errorThrow("URL buffer too small");
|
||||||
|
|
||||||
|
memoryCopy(dest + *cursor, str, len + 1);
|
||||||
|
*cursor += len;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkHttpUrlEncodeComponent(
|
||||||
|
char_t *dest,
|
||||||
|
const size_t destSize,
|
||||||
|
size_t *cursor,
|
||||||
|
const char_t *src
|
||||||
|
) {
|
||||||
|
const char_t *hex = "0123456789ABCDEF";
|
||||||
|
|
||||||
|
for(const char_t *p = src; *p != '\0'; p++) {
|
||||||
|
const uint8_t c = (uint8_t)*p;
|
||||||
|
const bool_t unreserved =
|
||||||
|
(c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
||||||
|
(c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~';
|
||||||
|
|
||||||
|
if(unreserved) {
|
||||||
|
if(*cursor + 1 >= destSize) errorThrow("URL buffer too small");
|
||||||
|
dest[(*cursor)++] = (char_t)c;
|
||||||
|
} else {
|
||||||
|
if(*cursor + 3 >= destSize) errorThrow("URL buffer too small");
|
||||||
|
dest[(*cursor)++] = '%';
|
||||||
|
dest[(*cursor)++] = hex[(c >> 4) & 0xF];
|
||||||
|
dest[(*cursor)++] = hex[c & 0xF];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dest[*cursor] = '\0';
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "error/error.h"
|
||||||
|
#include "network/http/networkhttpheader.h"
|
||||||
|
|
||||||
|
#define NETWORK_HTTP_URL_MAX 1024
|
||||||
|
#define NETWORK_HTTP_HOST_MAX 256
|
||||||
|
#define NETWORK_HTTP_PATH_MAX 768
|
||||||
|
#define NETWORK_HTTP_PORT_DEFAULT 80
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char_t host[NETWORK_HTTP_HOST_MAX];
|
||||||
|
uint16_t port;
|
||||||
|
|
||||||
|
// Includes the leading slash and, if present, the query string.
|
||||||
|
char_t path[NETWORK_HTTP_PATH_MAX];
|
||||||
|
} networkhttpurl_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a "http://host[:port]/path[?query]" URL. Any other scheme is
|
||||||
|
* rejected, since this client does not support TLS.
|
||||||
|
*
|
||||||
|
* @param url The URL to parse.
|
||||||
|
* @param out The parsed URL is written here.
|
||||||
|
* @return An error if the URL is malformed or uses an unsupported scheme.
|
||||||
|
*/
|
||||||
|
errorret_t networkHttpUrlParse(const char_t *url, networkhttpurl_t *out);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies baseUrl into dest, then appends each queryParam as a
|
||||||
|
* percent-encoded "key=value" pair, joined with "&" (or "?" for the
|
||||||
|
* first one, unless baseUrl already contains a "?").
|
||||||
|
*
|
||||||
|
* @param dest The destination buffer.
|
||||||
|
* @param destSize The size of dest, including the null terminator.
|
||||||
|
* @param baseUrl The URL to copy before appending query params.
|
||||||
|
* @param queryParams Array of query parameters to append, may be NULL if
|
||||||
|
* queryParamCount is 0.
|
||||||
|
* @param queryParamCount Number of entries in queryParams.
|
||||||
|
* @return An error if the result would not fit in dest.
|
||||||
|
*/
|
||||||
|
errorret_t networkHttpUrlBuild(
|
||||||
|
char_t *dest,
|
||||||
|
const size_t destSize,
|
||||||
|
const char_t *baseUrl,
|
||||||
|
const networkhttpheader_t *queryParams,
|
||||||
|
const uint32_t queryParamCount
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a raw (not percent-encoded) string to dest at *cursor,
|
||||||
|
* advancing *cursor and keeping dest null-terminated.
|
||||||
|
*
|
||||||
|
* @param dest The destination buffer.
|
||||||
|
* @param destSize The size of dest, including the null terminator.
|
||||||
|
* @param cursor The current write offset into dest, updated in place.
|
||||||
|
* @param str The string to append.
|
||||||
|
* @return An error if the result would not fit in dest.
|
||||||
|
*/
|
||||||
|
errorret_t networkHttpUrlAppend(
|
||||||
|
char_t *dest,
|
||||||
|
const size_t destSize,
|
||||||
|
size_t *cursor,
|
||||||
|
const char_t *str
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Percent-encodes src and appends it to dest at *cursor, advancing
|
||||||
|
* *cursor and keeping dest null-terminated. Unreserved characters
|
||||||
|
* (letters, digits, "-", "_", ".", "~") are copied as-is.
|
||||||
|
*
|
||||||
|
* @param dest The destination buffer.
|
||||||
|
* @param destSize The size of dest, including the null terminator.
|
||||||
|
* @param cursor The current write offset into dest, updated in place.
|
||||||
|
* @param src The string to percent-encode and append.
|
||||||
|
* @return An error if the result would not fit in dest.
|
||||||
|
*/
|
||||||
|
errorret_t networkHttpUrlEncodeComponent(
|
||||||
|
char_t *dest,
|
||||||
|
const size_t destSize,
|
||||||
|
size_t *cursor,
|
||||||
|
const char_t *src
|
||||||
|
);
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "network.h"
|
#include "network.h"
|
||||||
|
#include "network/http/networkhttp.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "log/log.h"
|
#include "log/log.h"
|
||||||
@@ -18,11 +19,13 @@ errorret_t networkInit() {
|
|||||||
NETWORK.errorState.code = ERROR_OK;
|
NETWORK.errorState.code = ERROR_OK;
|
||||||
NETWORK.onDisconnect = NULL;
|
NETWORK.onDisconnect = NULL;
|
||||||
|
|
||||||
return networkPlatformInit();
|
errorChain(networkPlatformInit());
|
||||||
|
return networkHttpInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t networkUpdate() {
|
errorret_t networkUpdate() {
|
||||||
errorChain(networkPlatformUpdate());
|
errorChain(networkPlatformUpdate());
|
||||||
|
errorChain(networkHttpUpdate());
|
||||||
|
|
||||||
if(NETWORK.state == NETWORK_STATE_CONNECTED && !networkIsConnected()) {
|
if(NETWORK.state == NETWORK_STATE_CONNECTED && !networkIsConnected()) {
|
||||||
NETWORK.state = NETWORK_STATE_DISCONNECTED;
|
NETWORK.state = NETWORK_STATE_DISCONNECTED;
|
||||||
@@ -112,5 +115,5 @@ errorret_t networkDispose() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
errorChain(networkPlatformDispose());
|
errorChain(networkPlatformDispose());
|
||||||
errorOk();
|
return networkHttpDispose();
|
||||||
}
|
}
|
||||||
@@ -11,10 +11,8 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Subdirs
|
# Subdirs
|
||||||
add_subdirectory(battle)
|
|
||||||
add_subdirectory(cutscene)
|
add_subdirectory(cutscene)
|
||||||
add_subdirectory(entity)
|
add_subdirectory(entity)
|
||||||
add_subdirectory(overworld)
|
add_subdirectory(overworld)
|
||||||
|
add_subdirectory(item)
|
||||||
add_subdirectory(story)
|
add_subdirectory(physics)
|
||||||
add_subdirectory(item)
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# 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
|
|
||||||
battle.c
|
|
||||||
battlefighter.c
|
|
||||||
party.c
|
|
||||||
)
|
|
||||||
|
|
||||||
# Subdirs
|
|
||||||
add_subdirectory(testbattle)
|
|
||||||
@@ -1,326 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "battle.h"
|
|
||||||
#include "assert/assert.h"
|
|
||||||
#include "util/memory.h"
|
|
||||||
#include "rpg/cutscene/cutscenesystem.h"
|
|
||||||
|
|
||||||
battle_t BATTLE;
|
|
||||||
|
|
||||||
void battleInit(void) {
|
|
||||||
memoryZero(&BATTLE, sizeof(battle_t));
|
|
||||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
|
||||||
BATTLE.fighters[i].id = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t battleGetAvailableFighter(void) {
|
|
||||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
|
||||||
if(BATTLE.fighters[i].status == BATTLE_FIGHTER_STATUS_NULL) return i;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0xFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
battlefighter_t *battleAddFighter(
|
|
||||||
const battlefighterteam_t team,
|
|
||||||
const battlefightercontroller_t controller,
|
|
||||||
const battlefighterstats_t stats,
|
|
||||||
const uint16_t healthMax,
|
|
||||||
const uint16_t mpMax
|
|
||||||
) {
|
|
||||||
const uint8_t index = battleGetAvailableFighter();
|
|
||||||
if(index == 0xFF) return NULL;
|
|
||||||
|
|
||||||
battlefighter_t *fighter = &BATTLE.fighters[index];
|
|
||||||
battleFighterInit(fighter, team, controller, stats, healthMax, mpMax);
|
|
||||||
return fighter;
|
|
||||||
}
|
|
||||||
|
|
||||||
void battleStart(
|
|
||||||
const battleencountertype_t encounterType,
|
|
||||||
const bool_t fleeAvailable
|
|
||||||
) {
|
|
||||||
assertTrue(encounterType < BATTLE_ENCOUNTER_COUNT, "Invalid encounter type");
|
|
||||||
|
|
||||||
BATTLE.encounterType = encounterType;
|
|
||||||
BATTLE.fleeAvailable = fleeAvailable;
|
|
||||||
BATTLE.result = BATTLE_RESULT_NONE;
|
|
||||||
BATTLE.round = 1;
|
|
||||||
|
|
||||||
battleSetState(BATTLE_STATE_OPENING);
|
|
||||||
}
|
|
||||||
|
|
||||||
void battleDispose(void) {
|
|
||||||
battleInit();
|
|
||||||
}
|
|
||||||
|
|
||||||
battlefighter_t *battleGetCurrentFighter(void) {
|
|
||||||
if(BATTLE.state != BATTLE_STATE_PLAYER_SELECTION) return NULL;
|
|
||||||
if(BATTLE.selectionIndex >= BATTLE.executionCount) return NULL;
|
|
||||||
return &BATTLE.fighters[BATTLE.executionOrder[BATTLE.selectionIndex]];
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t battleGetAliveCount(const battlefighterteam_t team) {
|
|
||||||
uint8_t count = 0;
|
|
||||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
|
||||||
if(BATTLE.fighters[i].team != team) continue;
|
|
||||||
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
void battleResolveAttack(
|
|
||||||
battlefighter_t *attacker,
|
|
||||||
battlefighter_t *defender
|
|
||||||
) {
|
|
||||||
assertNotNull(attacker, "Attacker cannot be NULL");
|
|
||||||
assertNotNull(defender, "Defender cannot be NULL");
|
|
||||||
|
|
||||||
const int32_t rawDamage =
|
|
||||||
(int32_t)attacker->stats.attack - (int32_t)defender->stats.defense;
|
|
||||||
const uint16_t damage = rawDamage > 0 ? (uint16_t)rawDamage : 1;
|
|
||||||
|
|
||||||
defender->health = damage >= defender->health ? 0 : defender->health - damage;
|
|
||||||
if(defender->health == 0) defender->status = BATTLE_FIGHTER_STATUS_DEAD;
|
|
||||||
}
|
|
||||||
|
|
||||||
battleresult_t battleCheckResult(void) {
|
|
||||||
if(BATTLE.result != BATTLE_RESULT_NONE) return BATTLE.result;
|
|
||||||
|
|
||||||
if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ALLY) == 0) {
|
|
||||||
battleSetResult(BATTLE_RESULT_LOSS);
|
|
||||||
} else if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ENEMY) == 0) {
|
|
||||||
battleSetResult(BATTLE_RESULT_WIN);
|
|
||||||
}
|
|
||||||
|
|
||||||
return BATTLE.result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void battleQueueAction(
|
|
||||||
const uint8_t fighterIndex,
|
|
||||||
const battleactiontype_t type,
|
|
||||||
const uint8_t targetIndex
|
|
||||||
) {
|
|
||||||
battleaction_t *action = &BATTLE.actions[fighterIndex];
|
|
||||||
action->type = type;
|
|
||||||
action->targetIndex = targetIndex;
|
|
||||||
|
|
||||||
if(BATTLE.onActionDecided != NULL) {
|
|
||||||
BATTLE.onActionDecided(&BATTLE.fighters[fighterIndex], action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void battlePlayerAttack(const uint8_t targetIndex) {
|
|
||||||
battlefighter_t *fighter = battleGetCurrentFighter();
|
|
||||||
if(fighter == NULL) return;
|
|
||||||
if(targetIndex >= BATTLE_FIGHTER_COUNT_MAX) return;
|
|
||||||
if(!battleFighterIsAlive(&BATTLE.fighters[targetIndex])) return;
|
|
||||||
|
|
||||||
battleQueueAction(fighter->id, BATTLE_ACTION_ATTACK, targetIndex);
|
|
||||||
BATTLE.selectionIndex++;
|
|
||||||
battleAdvanceSelection();
|
|
||||||
}
|
|
||||||
|
|
||||||
void battlePlayerFlee(void) {
|
|
||||||
battlefighter_t *fighter = battleGetCurrentFighter();
|
|
||||||
if(fighter == NULL) return;
|
|
||||||
if(!BATTLE.fleeAvailable) return;
|
|
||||||
|
|
||||||
battleSetResult(BATTLE_RESULT_FLED);
|
|
||||||
}
|
|
||||||
|
|
||||||
void battleUpdate(void) {
|
|
||||||
if(BATTLE.state == BATTLE_STATE_NONE) return;
|
|
||||||
if(BATTLE.state == BATTLE_STATE_ENDED) return;
|
|
||||||
if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_BATTLE) return;
|
|
||||||
|
|
||||||
switch(BATTLE.state) {
|
|
||||||
case BATTLE_STATE_OPENING:
|
|
||||||
battleSetState(BATTLE_STATE_PRE_ROUND);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case BATTLE_STATE_PRE_ROUND:
|
|
||||||
battleUpdatePreRound();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case BATTLE_STATE_AI_SELECTION:
|
|
||||||
battleUpdateAiSelection();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case BATTLE_STATE_MOVES_EXECUTING:
|
|
||||||
battleUpdateMovesExecuting();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case BATTLE_STATE_POST_ROUND:
|
|
||||||
battleUpdatePostRound();
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
// BATTLE_STATE_PLAYER_SELECTION: waits on battlePlayerAttack/Flee.
|
|
||||||
// BATTLE_STATE_NONE/ENDED: handled above.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void battleBuildExecutionOrder(const bool_t applyEncounterBias) {
|
|
||||||
BATTLE.executionCount = 0;
|
|
||||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
|
||||||
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
|
|
||||||
BATTLE.executionOrder[BATTLE.executionCount++] = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insertion sort by speed descending -- fine for BATTLE_FIGHTER_COUNT_MAX.
|
|
||||||
for(uint8_t i = 1; i < BATTLE.executionCount; i++) {
|
|
||||||
const uint8_t key = BATTLE.executionOrder[i];
|
|
||||||
const uint16_t keySpeed = BATTLE.fighters[key].stats.speed;
|
|
||||||
|
|
||||||
int8_t j = (int8_t)i - 1;
|
|
||||||
while(
|
|
||||||
j >= 0 &&
|
|
||||||
BATTLE.fighters[BATTLE.executionOrder[j]].stats.speed < keySpeed
|
|
||||||
) {
|
|
||||||
BATTLE.executionOrder[j + 1] = BATTLE.executionOrder[j];
|
|
||||||
j--;
|
|
||||||
}
|
|
||||||
BATTLE.executionOrder[j + 1] = key;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!applyEncounterBias) return;
|
|
||||||
|
|
||||||
if(BATTLE.encounterType == BATTLE_ENCOUNTER_PLAYER_ADVANTAGE) {
|
|
||||||
battleMoveTeamFirst(BATTLE_FIGHTER_TEAM_ALLY);
|
|
||||||
} else if(BATTLE.encounterType == BATTLE_ENCOUNTER_BACK_ATTACK) {
|
|
||||||
battleMoveTeamFirst(BATTLE_FIGHTER_TEAM_ENEMY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void battleMoveTeamFirst(const battlefighterteam_t team) {
|
|
||||||
uint8_t sorted[BATTLE_FIGHTER_COUNT_MAX];
|
|
||||||
uint8_t count = 0;
|
|
||||||
|
|
||||||
for(uint8_t i = 0; i < BATTLE.executionCount; i++) {
|
|
||||||
if(BATTLE.fighters[BATTLE.executionOrder[i]].team != team) continue;
|
|
||||||
sorted[count++] = BATTLE.executionOrder[i];
|
|
||||||
}
|
|
||||||
for(uint8_t i = 0; i < BATTLE.executionCount; i++) {
|
|
||||||
if(BATTLE.fighters[BATTLE.executionOrder[i]].team == team) continue;
|
|
||||||
sorted[count++] = BATTLE.executionOrder[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
memoryCopy(BATTLE.executionOrder, sorted, sizeof(uint8_t) * BATTLE.executionCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter) {
|
|
||||||
const battlefighterteam_t enemyTeam =
|
|
||||||
fighter->team == BATTLE_FIGHTER_TEAM_ALLY ?
|
|
||||||
BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY;
|
|
||||||
|
|
||||||
battlefighter_t *weakest = NULL;
|
|
||||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
|
||||||
battlefighter_t *candidate = &BATTLE.fighters[i];
|
|
||||||
if(candidate->team != enemyTeam) continue;
|
|
||||||
if(!battleFighterIsAlive(candidate)) continue;
|
|
||||||
if(weakest == NULL || candidate->health < weakest->health) {
|
|
||||||
weakest = candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return weakest;
|
|
||||||
}
|
|
||||||
|
|
||||||
void battleSetState(const battlestate_t next) {
|
|
||||||
const battlestate_t previous = BATTLE.state;
|
|
||||||
BATTLE.state = next;
|
|
||||||
if(BATTLE.onStateChanged != NULL) BATTLE.onStateChanged(previous, next);
|
|
||||||
}
|
|
||||||
|
|
||||||
void battleSetResult(const battleresult_t result) {
|
|
||||||
BATTLE.result = result;
|
|
||||||
battleSetState(BATTLE_STATE_ENDED);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool_t battleFighterNeedsDecision(
|
|
||||||
const uint8_t fighterIndex,
|
|
||||||
const battlefightercontroller_t controller
|
|
||||||
) {
|
|
||||||
return battleFighterIsAlive(&BATTLE.fighters[fighterIndex])
|
|
||||||
&& BATTLE.fighters[fighterIndex].controller == controller
|
|
||||||
&& BATTLE.actions[fighterIndex].type == BATTLE_ACTION_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
void battleAdvanceSelection(void) {
|
|
||||||
while(BATTLE.selectionIndex < BATTLE.executionCount) {
|
|
||||||
const uint8_t fighterIndex = BATTLE.executionOrder[BATTLE.selectionIndex];
|
|
||||||
if(
|
|
||||||
battleFighterNeedsDecision(fighterIndex, BATTLE_FIGHTER_CONTROLLER_PLAYER)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
BATTLE.selectionIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
battleSetState(BATTLE_STATE_AI_SELECTION);
|
|
||||||
}
|
|
||||||
|
|
||||||
void battleUpdatePreRound(void) {
|
|
||||||
// No need to clear BATTLE.actions here: every living fighter's action is
|
|
||||||
// unconditionally reset to BATTLE_ACTION_NONE as it's processed in
|
|
||||||
// battleUpdateMovesExecuting, and round 1 starts pre-zeroed by
|
|
||||||
// battleInit(). Clearing it here would also wipe out any action a
|
|
||||||
// cutscene force-queued while parked at BATTLE_STATE_PRE_ROUND.
|
|
||||||
battleBuildExecutionOrder(BATTLE.round == 1);
|
|
||||||
|
|
||||||
BATTLE.selectionIndex = 0;
|
|
||||||
battleSetState(BATTLE_STATE_PLAYER_SELECTION);
|
|
||||||
battleAdvanceSelection();
|
|
||||||
}
|
|
||||||
|
|
||||||
void battleUpdateAiSelection(void) {
|
|
||||||
for(uint8_t i = 0; i < BATTLE.executionCount; i++) {
|
|
||||||
const uint8_t fighterIndex = BATTLE.executionOrder[i];
|
|
||||||
if(
|
|
||||||
!battleFighterNeedsDecision(fighterIndex, BATTLE_FIGHTER_CONTROLLER_AI)
|
|
||||||
) continue;
|
|
||||||
|
|
||||||
battlefighter_t *target =
|
|
||||||
battleAIChooseTarget(&BATTLE.fighters[fighterIndex]);
|
|
||||||
if(target == NULL) continue;
|
|
||||||
|
|
||||||
battleQueueAction(fighterIndex, BATTLE_ACTION_ATTACK, target->id);
|
|
||||||
}
|
|
||||||
|
|
||||||
BATTLE.executionIndex = 0;
|
|
||||||
battleSetState(BATTLE_STATE_MOVES_EXECUTING);
|
|
||||||
}
|
|
||||||
|
|
||||||
void battleUpdateMovesExecuting(void) {
|
|
||||||
if(BATTLE.executionIndex >= BATTLE.executionCount) {
|
|
||||||
battleSetState(BATTLE_STATE_POST_ROUND);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uint8_t fighterIndex = BATTLE.executionOrder[BATTLE.executionIndex++];
|
|
||||||
battlefighter_t *fighter = &BATTLE.fighters[fighterIndex];
|
|
||||||
if(!battleFighterIsAlive(fighter)) return;
|
|
||||||
|
|
||||||
battleaction_t *action = &BATTLE.actions[fighterIndex];
|
|
||||||
if(action->type == BATTLE_ACTION_ATTACK) {
|
|
||||||
battlefighter_t *target = &BATTLE.fighters[action->targetIndex];
|
|
||||||
if(battleFighterIsAlive(target)) battleResolveAttack(fighter, target);
|
|
||||||
}
|
|
||||||
action->type = BATTLE_ACTION_NONE;
|
|
||||||
|
|
||||||
battleCheckResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
void battleUpdatePostRound(void) {
|
|
||||||
BATTLE.round++;
|
|
||||||
battleSetState(BATTLE_STATE_PRE_ROUND);
|
|
||||||
}
|
|
||||||
@@ -1,313 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "battlefighter.h"
|
|
||||||
|
|
||||||
#define BATTLE_FIGHTER_COUNT_MAX 8
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
BATTLE_ENCOUNTER_REGULAR,
|
|
||||||
BATTLE_ENCOUNTER_PLAYER_ADVANTAGE,
|
|
||||||
BATTLE_ENCOUNTER_BACK_ATTACK,
|
|
||||||
|
|
||||||
BATTLE_ENCOUNTER_COUNT
|
|
||||||
} battleencountertype_t;
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
BATTLE_RESULT_NONE,
|
|
||||||
BATTLE_RESULT_WIN,
|
|
||||||
BATTLE_RESULT_LOSS,
|
|
||||||
BATTLE_RESULT_FLED,
|
|
||||||
|
|
||||||
BATTLE_RESULT_COUNT
|
|
||||||
} battleresult_t;
|
|
||||||
|
|
||||||
// Where BATTLE currently is within a round. A cutscene can pause progression
|
|
||||||
// (CUTSCENE_PAUSE_BATTLE) and use CUTSCENE_BATTLE_WAIT_STATE to synchronize
|
|
||||||
// with any of these, or CUTSCENE_BATTLE_FORCE_ACTION to decide a fighter's
|
|
||||||
// action ahead of PLAYER_SELECTION/AI_SELECTION reaching them.
|
|
||||||
typedef enum {
|
|
||||||
BATTLE_STATE_NONE, // Battle inactive.
|
|
||||||
|
|
||||||
BATTLE_STATE_OPENING, // Entered once by battleStart().
|
|
||||||
BATTLE_STATE_PRE_ROUND, // Rebuilds execution order, clears the action queue.
|
|
||||||
BATTLE_STATE_PLAYER_SELECTION, // Waits on battlePlayerAttack/Flee.
|
|
||||||
BATTLE_STATE_AI_SELECTION, // Auto-queues every undecided AI fighter.
|
|
||||||
BATTLE_STATE_MOVES_EXECUTING, // Resolves one queued action per update.
|
|
||||||
BATTLE_STATE_POST_ROUND, // Round wrap-up; loops back to PRE_ROUND.
|
|
||||||
|
|
||||||
BATTLE_STATE_ENDED, // Terminal for WIN/LOSS/FLED alike -- see BATTLE.result.
|
|
||||||
|
|
||||||
BATTLE_STATE_COUNT
|
|
||||||
} battlestate_t;
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
BATTLE_ACTION_NONE, // No action decided yet for this fighter this round.
|
|
||||||
BATTLE_ACTION_ATTACK,
|
|
||||||
|
|
||||||
BATTLE_ACTION_COUNT
|
|
||||||
} battleactiontype_t;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
battleactiontype_t type;
|
|
||||||
uint8_t targetIndex; // Meaningful for BATTLE_ACTION_ATTACK.
|
|
||||||
} battleaction_t;
|
|
||||||
|
|
||||||
typedef void (*battlestatechangedcallback_t)(
|
|
||||||
const battlestate_t previous,
|
|
||||||
const battlestate_t next
|
|
||||||
);
|
|
||||||
|
|
||||||
typedef void (*battleactiondecidedcallback_t)(
|
|
||||||
const battlefighter_t *fighter,
|
|
||||||
const battleaction_t *action
|
|
||||||
);
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
battlestate_t state;
|
|
||||||
battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX];
|
|
||||||
battleaction_t actions[BATTLE_FIGHTER_COUNT_MAX];
|
|
||||||
|
|
||||||
battleencountertype_t encounterType;
|
|
||||||
bool_t fleeAvailable;
|
|
||||||
battleresult_t result;
|
|
||||||
|
|
||||||
// Fighter indices (into fighters[]), sorted for the current round.
|
|
||||||
uint8_t executionOrder[BATTLE_FIGHTER_COUNT_MAX];
|
|
||||||
uint8_t executionCount;
|
|
||||||
uint8_t executionIndex;
|
|
||||||
uint16_t round;
|
|
||||||
|
|
||||||
// Cursor into executionOrder used by BATTLE_STATE_PLAYER_SELECTION to find
|
|
||||||
// the next player-controlled fighter that still needs a decision.
|
|
||||||
uint8_t selectionIndex;
|
|
||||||
|
|
||||||
battlestatechangedcallback_t onStateChanged;
|
|
||||||
battleactiondecidedcallback_t onActionDecided;
|
|
||||||
} battle_t;
|
|
||||||
|
|
||||||
extern battle_t BATTLE;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the battle system. Marks it as inactive with no fighters.
|
|
||||||
*/
|
|
||||||
void battleInit(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets an available (unused) fighter slot index.
|
|
||||||
*
|
|
||||||
* @return The index of an available fighter slot, or 0xFF if none are
|
|
||||||
* available.
|
|
||||||
*/
|
|
||||||
uint8_t battleGetAvailableFighter(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a fighter to the battle in the next available slot.
|
|
||||||
*
|
|
||||||
* @param team The team the fighter belongs to.
|
|
||||||
* @param controller Who makes decisions for the fighter.
|
|
||||||
* @param stats The fighter's base combat stats.
|
|
||||||
* @param healthMax The fighter's maximum health.
|
|
||||||
* @param mpMax The fighter's maximum mp.
|
|
||||||
* @return Pointer to the newly added fighter, or NULL if the battle is
|
|
||||||
* already full.
|
|
||||||
*/
|
|
||||||
battlefighter_t *battleAddFighter(
|
|
||||||
const battlefighterteam_t team,
|
|
||||||
const battlefightercontroller_t controller,
|
|
||||||
const battlefighterstats_t stats,
|
|
||||||
const uint16_t healthMax,
|
|
||||||
const uint16_t mpMax
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the battle: enters BATTLE_STATE_OPENING and marks the battle
|
|
||||||
* active. Call once every fighter has been added via battleAddFighter.
|
|
||||||
*
|
|
||||||
* @param encounterType Determines the opening round's execution order.
|
|
||||||
* @param fleeAvailable Whether the party may attempt to flee this battle.
|
|
||||||
*/
|
|
||||||
void battleStart(
|
|
||||||
const battleencountertype_t encounterType,
|
|
||||||
const bool_t fleeAvailable
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disposes of the battle, clearing all fighters and marking it inactive.
|
|
||||||
*/
|
|
||||||
void battleDispose(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the fighter currently awaiting a player decision.
|
|
||||||
*
|
|
||||||
* @return Pointer to the fighter awaiting a decision, or NULL if the battle
|
|
||||||
* isn't in BATTLE_STATE_PLAYER_SELECTION or every player-controlled
|
|
||||||
* fighter has already decided.
|
|
||||||
*/
|
|
||||||
battlefighter_t *battleGetCurrentFighter(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of living fighters on a team.
|
|
||||||
*
|
|
||||||
* @param team The team to count.
|
|
||||||
* @return Count of living fighters on that team.
|
|
||||||
*/
|
|
||||||
uint8_t battleGetAliveCount(const battlefighterteam_t team);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves a physical attack from attacker onto defender: damage is the
|
|
||||||
* attacker's attack stat minus the defender's defense stat (minimum 1),
|
|
||||||
* subtracted from the defender's health. The defender is marked dead
|
|
||||||
* once health reaches 0.
|
|
||||||
*
|
|
||||||
* @param attacker The attacking fighter.
|
|
||||||
* @param defender The defending fighter.
|
|
||||||
*/
|
|
||||||
void battleResolveAttack(
|
|
||||||
battlefighter_t *attacker,
|
|
||||||
battlefighter_t *defender
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the battle has been won or lost, transitioning to
|
|
||||||
* BATTLE_STATE_ENDED and updating BATTLE.result if so. Does nothing if a
|
|
||||||
* result has already been set (e.g. by a successful flee).
|
|
||||||
*
|
|
||||||
* @return The battle's current result.
|
|
||||||
*/
|
|
||||||
battleresult_t battleCheckResult(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Queues an action for a fighter to perform once BATTLE_STATE_MOVES_EXECUTING
|
|
||||||
* reaches them this round, overwriting any action already queued for that
|
|
||||||
* fighter. Fires BATTLE.onActionDecided.
|
|
||||||
*
|
|
||||||
* @param fighterIndex Index into BATTLE.fighters of the deciding fighter.
|
|
||||||
* @param type The type of action to perform.
|
|
||||||
* @param targetIndex Index into BATTLE.fighters of the target, meaningful
|
|
||||||
* for BATTLE_ACTION_ATTACK.
|
|
||||||
*/
|
|
||||||
void battleQueueAction(
|
|
||||||
const uint8_t fighterIndex,
|
|
||||||
const battleactiontype_t type,
|
|
||||||
const uint8_t targetIndex
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submits the currently-selecting fighter's attack against a target, if the
|
|
||||||
* battle is in BATTLE_STATE_PLAYER_SELECTION and awaiting a decision.
|
|
||||||
* Queues the action and advances the selection cursor.
|
|
||||||
*
|
|
||||||
* @param targetIndex Index into BATTLE.fighters of the target.
|
|
||||||
*/
|
|
||||||
void battlePlayerAttack(const uint8_t targetIndex);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submits a flee attempt for the currently-selecting fighter, if the battle
|
|
||||||
* is in BATTLE_STATE_PLAYER_SELECTION and fleeing is available for this
|
|
||||||
* battle. Always succeeds, ending the battle with BATTLE_RESULT_FLED.
|
|
||||||
*/
|
|
||||||
void battlePlayerFlee(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the battle simulation for one frame, dispatching on BATTLE.state.
|
|
||||||
* No-op if the battle isn't active, has already ended, or
|
|
||||||
* CUTSCENE_PAUSE_BATTLE is set.
|
|
||||||
*/
|
|
||||||
void battleUpdate(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rebuilds BATTLE.executionOrder/executionCount from every currently living
|
|
||||||
* fighter, sorted by speed descending.
|
|
||||||
*
|
|
||||||
* @param applyEncounterBias If true, reorders the freshly speed-sorted
|
|
||||||
* queue so BATTLE.encounterType's favoured team goes first (used only
|
|
||||||
* for the opening round).
|
|
||||||
*/
|
|
||||||
void battleBuildExecutionOrder(const bool_t applyEncounterBias);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stably partitions BATTLE.executionOrder so every fighter on the given team
|
|
||||||
* comes first, preserving each side's relative (speed-sorted) order.
|
|
||||||
*
|
|
||||||
* @param team The team to move to the front of the execution order.
|
|
||||||
*/
|
|
||||||
void battleMoveTeamFirst(const battlefighterteam_t team);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Picks an AI target for fighter: the lowest-health living fighter on
|
|
||||||
* the opposing team.
|
|
||||||
*
|
|
||||||
* @param fighter The AI-controlled fighter choosing a target.
|
|
||||||
* @return The chosen target, or NULL if the opposing team has no
|
|
||||||
* living fighters.
|
|
||||||
*/
|
|
||||||
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets BATTLE.state and fires BATTLE.onStateChanged with the previous and
|
|
||||||
* new state.
|
|
||||||
*
|
|
||||||
* @param next The state to transition to.
|
|
||||||
*/
|
|
||||||
void battleSetState(const battlestate_t next);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets BATTLE.result and transitions to BATTLE_STATE_ENDED.
|
|
||||||
*
|
|
||||||
* @param result The result to end the battle with.
|
|
||||||
*/
|
|
||||||
void battleSetResult(const battleresult_t result);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether a fighter is a live, undecided candidate for the given
|
|
||||||
* controller -- i.e. whether PLAYER_SELECTION or AI_SELECTION should still
|
|
||||||
* be deciding an action for it this round.
|
|
||||||
*
|
|
||||||
* @param fighterIndex Index into BATTLE.fighters to check.
|
|
||||||
* @param controller The controller PLAYER_SELECTION/AI_SELECTION is
|
|
||||||
* currently deciding for.
|
|
||||||
* @return True if the fighter is alive, matches controller, and has no
|
|
||||||
* action queued yet.
|
|
||||||
*/
|
|
||||||
bool_t battleFighterNeedsDecision(
|
|
||||||
const uint8_t fighterIndex,
|
|
||||||
const battlefightercontroller_t controller
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Advances BATTLE.selectionIndex to the next player-controlled fighter that
|
|
||||||
* still needs a decision, or transitions to BATTLE_STATE_AI_SELECTION once
|
|
||||||
* none remain.
|
|
||||||
*/
|
|
||||||
void battleAdvanceSelection(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles BATTLE_STATE_PRE_ROUND: rebuilds the execution order and moves on
|
|
||||||
* to BATTLE_STATE_PLAYER_SELECTION, positioning the selection cursor.
|
|
||||||
*/
|
|
||||||
void battleUpdatePreRound(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles BATTLE_STATE_AI_SELECTION: queues an attack for every undecided
|
|
||||||
* AI-controlled fighter, then moves on to BATTLE_STATE_MOVES_EXECUTING.
|
|
||||||
*/
|
|
||||||
void battleUpdateAiSelection(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles BATTLE_STATE_MOVES_EXECUTING: resolves one queued action from
|
|
||||||
* BATTLE.executionOrder per call, or transitions to BATTLE_STATE_POST_ROUND
|
|
||||||
* once every fighter this round has been processed.
|
|
||||||
*/
|
|
||||||
void battleUpdateMovesExecuting(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles BATTLE_STATE_POST_ROUND: advances BATTLE.round and transitions
|
|
||||||
* back to BATTLE_STATE_PRE_ROUND.
|
|
||||||
*/
|
|
||||||
void battleUpdatePostRound(void);
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "battlefighter.h"
|
|
||||||
#include "assert/assert.h"
|
|
||||||
#include "util/memory.h"
|
|
||||||
|
|
||||||
void battleFighterInit(
|
|
||||||
battlefighter_t *fighter,
|
|
||||||
const battlefighterteam_t team,
|
|
||||||
const battlefightercontroller_t controller,
|
|
||||||
const battlefighterstats_t stats,
|
|
||||||
const uint16_t healthMax,
|
|
||||||
const uint16_t mpMax
|
|
||||||
) {
|
|
||||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
|
||||||
assertTrue(team < BATTLE_FIGHTER_TEAM_COUNT, "Invalid fighter team");
|
|
||||||
assertTrue(
|
|
||||||
controller < BATTLE_FIGHTER_CONTROLLER_COUNT,
|
|
||||||
"Invalid fighter controller"
|
|
||||||
);
|
|
||||||
|
|
||||||
const uint8_t id = fighter->id;
|
|
||||||
memoryZero(fighter, sizeof(battlefighter_t));
|
|
||||||
fighter->id = id;
|
|
||||||
fighter->status = BATTLE_FIGHTER_STATUS_NORMAL;
|
|
||||||
fighter->team = team;
|
|
||||||
fighter->controller = controller;
|
|
||||||
fighter->stats = stats;
|
|
||||||
fighter->healthMax = healthMax;
|
|
||||||
fighter->health = healthMax;
|
|
||||||
fighter->mpMax = mpMax;
|
|
||||||
fighter->mp = mpMax;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool_t battleFighterIsAlive(const battlefighter_t *fighter) {
|
|
||||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
|
||||||
return fighter->status == BATTLE_FIGHTER_STATUS_NORMAL;
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "error/error.h"
|
|
||||||
|
|
||||||
// An empty status means the slot in BATTLE.fighters is unused.
|
|
||||||
typedef enum {
|
|
||||||
BATTLE_FIGHTER_STATUS_NULL,
|
|
||||||
|
|
||||||
BATTLE_FIGHTER_STATUS_NORMAL,
|
|
||||||
BATTLE_FIGHTER_STATUS_DEAD,
|
|
||||||
|
|
||||||
BATTLE_FIGHTER_STATUS_COUNT
|
|
||||||
} battlefighterstatus_t;
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
BATTLE_FIGHTER_TEAM_ALLY,
|
|
||||||
BATTLE_FIGHTER_TEAM_ENEMY,
|
|
||||||
|
|
||||||
BATTLE_FIGHTER_TEAM_COUNT
|
|
||||||
} battlefighterteam_t;
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
|
||||||
BATTLE_FIGHTER_CONTROLLER_AI,
|
|
||||||
|
|
||||||
BATTLE_FIGHTER_CONTROLLER_COUNT
|
|
||||||
} battlefightercontroller_t;
|
|
||||||
|
|
||||||
// Base combat stats, kept separate from the resource pools (health/mp) on
|
|
||||||
// battlefighter_t so that equipment/buffs can later modify them without
|
|
||||||
// touching current health/mp state.
|
|
||||||
typedef struct {
|
|
||||||
uint16_t attack;
|
|
||||||
uint16_t defense;
|
|
||||||
uint16_t magic;
|
|
||||||
uint16_t speed;
|
|
||||||
uint16_t luck;
|
|
||||||
} battlefighterstats_t;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
uint8_t id;
|
|
||||||
battlefighterstatus_t status;
|
|
||||||
battlefighterteam_t team;
|
|
||||||
battlefightercontroller_t controller;
|
|
||||||
|
|
||||||
uint16_t health;
|
|
||||||
uint16_t healthMax;
|
|
||||||
uint16_t mp;
|
|
||||||
uint16_t mpMax;
|
|
||||||
|
|
||||||
battlefighterstats_t stats;
|
|
||||||
} battlefighter_t;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes a battle fighter in place, filling health/mp to their maximum
|
|
||||||
* values and setting its status to normal.
|
|
||||||
*
|
|
||||||
* @param fighter Pointer to the fighter to initialize.
|
|
||||||
* @param team The team the fighter belongs to.
|
|
||||||
* @param controller Who makes decisions for the fighter.
|
|
||||||
* @param stats The fighter's base combat stats.
|
|
||||||
* @param healthMax The fighter's maximum health.
|
|
||||||
* @param mpMax The fighter's maximum mp.
|
|
||||||
*/
|
|
||||||
void battleFighterInit(
|
|
||||||
battlefighter_t *fighter,
|
|
||||||
const battlefighterteam_t team,
|
|
||||||
const battlefightercontroller_t controller,
|
|
||||||
const battlefighterstats_t stats,
|
|
||||||
const uint16_t healthMax,
|
|
||||||
const uint16_t mpMax
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the fighter is in a state where it can still act (i.e.
|
|
||||||
* is not dead).
|
|
||||||
*
|
|
||||||
* @param fighter Pointer to the fighter to check.
|
|
||||||
* @returns True if the fighter can act.
|
|
||||||
*/
|
|
||||||
bool_t battleFighterIsAlive(const battlefighter_t *fighter);
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "party.h"
|
|
||||||
#include "assert/assert.h"
|
|
||||||
#include "util/memory.h"
|
|
||||||
|
|
||||||
party_t PARTY;
|
|
||||||
|
|
||||||
void partyInit(void) {
|
|
||||||
memoryZero(&PARTY, sizeof(party_t));
|
|
||||||
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
|
|
||||||
PARTY.members[i].id = i;
|
|
||||||
}
|
|
||||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
|
||||||
PARTY.order[i] = PARTY_ORDER_EMPTY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t partyGetAvailableMember(void) {
|
|
||||||
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
|
|
||||||
if(PARTY.members[i].status == BATTLE_FIGHTER_STATUS_NULL) return i;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0xFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
battlefighter_t *partyAddMember(
|
|
||||||
const battlefighterstats_t stats,
|
|
||||||
const uint16_t healthMax,
|
|
||||||
const uint16_t mpMax
|
|
||||||
) {
|
|
||||||
const uint8_t index = partyGetAvailableMember();
|
|
||||||
if(index == 0xFF) return NULL;
|
|
||||||
|
|
||||||
battlefighter_t *member = &PARTY.members[index];
|
|
||||||
battleFighterInit(
|
|
||||||
member, BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
|
||||||
stats, healthMax, mpMax
|
|
||||||
);
|
|
||||||
|
|
||||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
|
||||||
if(PARTY.order[i] != PARTY_ORDER_EMPTY) continue;
|
|
||||||
PARTY.order[i] = index;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return member;
|
|
||||||
}
|
|
||||||
|
|
||||||
battlefighter_t *partyGetOrderMember(const uint8_t slot) {
|
|
||||||
assertTrue(slot < PARTY_ACTIVE_SIZE_MAX, "Invalid party order slot");
|
|
||||||
|
|
||||||
const uint8_t index = PARTY.order[slot];
|
|
||||||
if(index == PARTY_ORDER_EMPTY) return NULL;
|
|
||||||
return &PARTY.members[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
void partySetOrder(const uint8_t slot, const uint8_t memberIndex) {
|
|
||||||
assertTrue(slot < PARTY_ACTIVE_SIZE_MAX, "Invalid party order slot");
|
|
||||||
assertTrue(
|
|
||||||
memberIndex == PARTY_ORDER_EMPTY || memberIndex < PARTY_MEMBER_COUNT_MAX,
|
|
||||||
"Invalid party member index"
|
|
||||||
);
|
|
||||||
|
|
||||||
PARTY.order[slot] = memberIndex;
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "battlefighter.h"
|
|
||||||
|
|
||||||
#define PARTY_MEMBER_COUNT_MAX 4
|
|
||||||
#define PARTY_ACTIVE_SIZE_MAX 3
|
|
||||||
#define PARTY_ORDER_EMPTY 0xFF
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
battlefighter_t members[PARTY_MEMBER_COUNT_MAX];
|
|
||||||
|
|
||||||
// Maps an active battle slot to the roster member filling it, or
|
|
||||||
// PARTY_ORDER_EMPTY if the slot is unfilled. Only the first
|
|
||||||
// PARTY_ACTIVE_SIZE_MAX of the PARTY_MEMBER_COUNT_MAX roster members
|
|
||||||
// can be in the active lineup at once.
|
|
||||||
uint8_t order[PARTY_ACTIVE_SIZE_MAX];
|
|
||||||
} party_t;
|
|
||||||
|
|
||||||
extern party_t PARTY;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the party system with an empty roster and order.
|
|
||||||
*/
|
|
||||||
void partyInit(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets an available (unused) party member slot index.
|
|
||||||
*
|
|
||||||
* @return The index of an available slot, or 0xFF if the party is full.
|
|
||||||
*/
|
|
||||||
uint8_t partyGetAvailableMember(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a member to the party roster in the next available slot. Party
|
|
||||||
* members are always allies controlled by the player. If there is a
|
|
||||||
* free active order slot, the new member is placed into it.
|
|
||||||
*
|
|
||||||
* @param stats The member's base combat stats.
|
|
||||||
* @param healthMax The member's maximum health.
|
|
||||||
* @param mpMax The member's maximum mp.
|
|
||||||
* @return Pointer to the newly added party member, or NULL if the party is
|
|
||||||
* already full.
|
|
||||||
*/
|
|
||||||
battlefighter_t *partyAddMember(
|
|
||||||
const battlefighterstats_t stats,
|
|
||||||
const uint16_t healthMax,
|
|
||||||
const uint16_t mpMax
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the roster member currently occupying an active order slot.
|
|
||||||
*
|
|
||||||
* @param slot The active order slot to query.
|
|
||||||
* @return Pointer to the member in that slot, or NULL if the slot is
|
|
||||||
* empty.
|
|
||||||
*/
|
|
||||||
battlefighter_t *partyGetOrderMember(const uint8_t slot);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assigns a roster member to an active order slot, replacing whatever
|
|
||||||
* was there. Use PARTY_ORDER_EMPTY to clear a slot.
|
|
||||||
*
|
|
||||||
* @param slot The active order slot to assign.
|
|
||||||
* @param memberIndex The roster member index to place there, or
|
|
||||||
* PARTY_ORDER_EMPTY to clear the slot.
|
|
||||||
*/
|
|
||||||
void partySetOrder(const uint8_t slot, const uint8_t memberIndex);
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "testbattle.h"
|
|
||||||
#include "rpg/battle/battle.h"
|
|
||||||
#include "scene/scene.h"
|
|
||||||
|
|
||||||
void testBattleStart(void) {
|
|
||||||
battleInit();
|
|
||||||
|
|
||||||
const battlefighterstats_t allyOneStats =
|
|
||||||
{ .attack = 10, .defense = 5, .magic = 0, .speed = 10, .luck = 0 };
|
|
||||||
const battlefighterstats_t allyTwoStats =
|
|
||||||
{ .attack = 8, .defense = 4, .magic = 0, .speed = 8, .luck = 0 };
|
|
||||||
const battlefighterstats_t enemyOneStats =
|
|
||||||
{ .attack = 6, .defense = 3, .magic = 0, .speed = 6, .luck = 0 };
|
|
||||||
const battlefighterstats_t enemyTwoStats =
|
|
||||||
{ .attack = 7, .defense = 3, .magic = 0, .speed = 5, .luck = 0 };
|
|
||||||
|
|
||||||
battleAddFighter(
|
|
||||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
|
||||||
allyOneStats, 30, 10
|
|
||||||
);
|
|
||||||
battleAddFighter(
|
|
||||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
|
||||||
allyTwoStats, 25, 10
|
|
||||||
);
|
|
||||||
battleAddFighter(
|
|
||||||
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
|
|
||||||
enemyOneStats, 20, 5
|
|
||||||
);
|
|
||||||
battleAddFighter(
|
|
||||||
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
|
|
||||||
enemyTwoStats, 20, 5
|
|
||||||
);
|
|
||||||
|
|
||||||
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
|
|
||||||
sceneSet(SCENE_TYPE_BATTLE);
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "error/error.h"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TEMPORARY test hook: sets up a hardcoded mock battle and switches to the
|
|
||||||
* battle scene, so the battle scene (camera/fighters/HUD) can be seen and
|
|
||||||
* played without a real encounter trigger yet.
|
|
||||||
*/
|
|
||||||
void testBattleStart(void);
|
|
||||||
@@ -162,28 +162,6 @@ typedef struct cutscene_s {
|
|||||||
.shake = { .amount = AMOUNT, .duration = DURATION } \
|
.shake = { .amount = AMOUNT, .duration = DURATION } \
|
||||||
}
|
}
|
||||||
|
|
||||||
// Waits until BATTLE.state reaches STATE. Put this BEFORE
|
|
||||||
// CUTSCENE_SET_PAUSE(CUTSCENE_PAUSE_BATTLE), not after -- pausing first
|
|
||||||
// freezes BATTLE.state wherever it already is, so it would never reach
|
|
||||||
// STATE on its own to satisfy the wait. Waiting unpaused, then pausing the
|
|
||||||
// moment it's satisfied, catches the battle right at STATE before it can
|
|
||||||
// advance further.
|
|
||||||
#define CUTSCENE_BATTLE_WAIT_STATE(STATE) \
|
|
||||||
{ \
|
|
||||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE, \
|
|
||||||
.battleWaitState = { .state = STATE } \
|
|
||||||
}
|
|
||||||
|
|
||||||
// Immediately queues an attack for FIGHTER_INDEX against TARGET_INDEX,
|
|
||||||
// bypassing normal player/AI selection for that fighter this round.
|
|
||||||
#define CUTSCENE_BATTLE_FORCE_ACTION(FIGHTER_INDEX, TARGET_INDEX) \
|
|
||||||
{ \
|
|
||||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION, \
|
|
||||||
.battleForceAction = { \
|
|
||||||
.fighterIndex = FIGHTER_INDEX, .targetIndex = TARGET_INDEX \
|
|
||||||
} \
|
|
||||||
}
|
|
||||||
|
|
||||||
#define CUTSCENE_SET_PAUSE(FLAGS) \
|
#define CUTSCENE_SET_PAUSE(FLAGS) \
|
||||||
{ .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = (FLAGS) }
|
{ .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = (FLAGS) }
|
||||||
|
|
||||||
|
|||||||
@@ -14,13 +14,11 @@ typedef uint8_t cutscenepause_t;
|
|||||||
#define CUTSCENE_PAUSE_NPC ((cutscenepause_t)(1 << 0))
|
#define CUTSCENE_PAUSE_NPC ((cutscenepause_t)(1 << 0))
|
||||||
#define CUTSCENE_PAUSE_PLAYER ((cutscenepause_t)(1 << 1))
|
#define CUTSCENE_PAUSE_PLAYER ((cutscenepause_t)(1 << 1))
|
||||||
#define CUTSCENE_PAUSE_WORLD ((cutscenepause_t)(1 << 2))
|
#define CUTSCENE_PAUSE_WORLD ((cutscenepause_t)(1 << 2))
|
||||||
#define CUTSCENE_PAUSE_BATTLE ((cutscenepause_t)(1 << 3))
|
|
||||||
|
|
||||||
#define CUTSCENE_PAUSE_DEFAULT ((cutscenepause_t)( \
|
#define CUTSCENE_PAUSE_DEFAULT ((cutscenepause_t)( \
|
||||||
CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER \
|
CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER \
|
||||||
))
|
))
|
||||||
|
|
||||||
#define CUTSCENE_PAUSE_ALL ((cutscenepause_t)( \
|
#define CUTSCENE_PAUSE_ALL ((cutscenepause_t)( \
|
||||||
CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER | CUTSCENE_PAUSE_WORLD | \
|
CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER | CUTSCENE_PAUSE_WORLD \
|
||||||
CUTSCENE_PAUSE_BATTLE \
|
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -14,4 +14,3 @@ add_subdirectory(entity)
|
|||||||
add_subdirectory(item)
|
add_subdirectory(item)
|
||||||
add_subdirectory(maparea)
|
add_subdirectory(maparea)
|
||||||
add_subdirectory(ui)
|
add_subdirectory(ui)
|
||||||
add_subdirectory(battle)
|
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
|
||||||
|
|
||||||
void cutsceneBattleForceActionStart(
|
|
||||||
const cutsceneitem_t *item,
|
|
||||||
cutsceneitemdata_t *data
|
|
||||||
) {
|
|
||||||
battleQueueAction(
|
|
||||||
item->battleForceAction.fighterIndex, BATTLE_ACTION_ATTACK,
|
|
||||||
item->battleForceAction.targetIndex
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool_t cutsceneBattleForceActionUpdate(
|
|
||||||
const cutsceneitem_t *item,
|
|
||||||
cutsceneitemdata_t *data
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "rpg/battle/battle.h"
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
uint8_t fighterIndex;
|
|
||||||
uint8_t targetIndex;
|
|
||||||
} cutscenebattleforceaction_t;
|
|
||||||
|
|
||||||
typedef struct cutsceneitem_s cutsceneitem_t;
|
|
||||||
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts a battle force-action step: immediately queues an attack for the
|
|
||||||
* given fighter against the given target, bypassing normal player/AI
|
|
||||||
* selection for that fighter this round.
|
|
||||||
*
|
|
||||||
* @param item The cutscene item.
|
|
||||||
* @param data Runtime data storage.
|
|
||||||
*/
|
|
||||||
void cutsceneBattleForceActionStart(
|
|
||||||
const cutsceneitem_t *item,
|
|
||||||
cutsceneitemdata_t *data
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates a battle force-action step (always completes immediately).
|
|
||||||
*
|
|
||||||
* @param item The cutscene item.
|
|
||||||
* @param data Runtime data storage.
|
|
||||||
* @returns true always.
|
|
||||||
*/
|
|
||||||
bool_t cutsceneBattleForceActionUpdate(
|
|
||||||
const cutsceneitem_t *item,
|
|
||||||
cutsceneitemdata_t *data
|
|
||||||
);
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
|
||||||
|
|
||||||
bool_t cutsceneBattleWaitStateUpdate(
|
|
||||||
const cutsceneitem_t *item,
|
|
||||||
cutsceneitemdata_t *data
|
|
||||||
) {
|
|
||||||
return BATTLE.state == item->battleWaitState.state;
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "rpg/battle/battle.h"
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
battlestate_t state;
|
|
||||||
} cutscenebattlewaitstate_t;
|
|
||||||
|
|
||||||
typedef struct cutsceneitem_s cutsceneitem_t;
|
|
||||||
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates a battle wait-state step, completing once BATTLE.state reaches
|
|
||||||
* the watched state. Has no Start callback -- there's nothing to do until
|
|
||||||
* the state is actually reached.
|
|
||||||
*
|
|
||||||
* @param item The cutscene item.
|
|
||||||
* @param data Runtime data storage.
|
|
||||||
* @returns true once BATTLE.state equals the watched state.
|
|
||||||
*/
|
|
||||||
bool_t cutsceneBattleWaitStateUpdate(
|
|
||||||
const cutsceneitem_t *item,
|
|
||||||
cutsceneitemdata_t *data
|
|
||||||
);
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
|
||||||
#include "rpg/battle/party.h"
|
|
||||||
#include "scene/scene.h"
|
|
||||||
|
|
||||||
void cutsceneStartBattleStart(
|
|
||||||
const cutsceneitem_t *item,
|
|
||||||
cutsceneitemdata_t *data
|
|
||||||
) {
|
|
||||||
const cutscenestartbattle_t *config = &item->startBattle;
|
|
||||||
|
|
||||||
battleInit();
|
|
||||||
|
|
||||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
|
||||||
battlefighter_t *member = partyGetOrderMember(i);
|
|
||||||
if(member == NULL) continue;
|
|
||||||
|
|
||||||
battlefighter_t *fighter = battleAddFighter(
|
|
||||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
|
||||||
member->stats, member->healthMax, member->mpMax
|
|
||||||
);
|
|
||||||
if(fighter == NULL) continue;
|
|
||||||
|
|
||||||
fighter->health = member->health;
|
|
||||||
fighter->mp = member->mp;
|
|
||||||
fighter->status = member->status;
|
|
||||||
}
|
|
||||||
|
|
||||||
for(uint8_t i = 0; i < config->enemyCount; i++) {
|
|
||||||
const cutscenestartbattleenemy_t *enemy = &config->enemies[i];
|
|
||||||
battleAddFighter(
|
|
||||||
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
|
|
||||||
enemy->stats, enemy->healthMax, enemy->mpMax
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
battleStart(config->encounterType, config->fleeAvailable);
|
|
||||||
sceneSet(SCENE_TYPE_BATTLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool_t cutsceneStartBattleUpdate(
|
|
||||||
const cutsceneitem_t *item,
|
|
||||||
cutsceneitemdata_t *data
|
|
||||||
) {
|
|
||||||
if(BATTLE.result == BATTLE_RESULT_NONE) return false;
|
|
||||||
|
|
||||||
// Sync ally HP/MP back to the persistent party roster. Relies on
|
|
||||||
// ally fighters having been added to BATTLE.fighters in the same
|
|
||||||
// order partyGetOrderMember() iterates, starting at index 0 (see
|
|
||||||
// cutsceneStartBattleStart).
|
|
||||||
uint8_t allySlot = 0;
|
|
||||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
|
||||||
battlefighter_t *member = partyGetOrderMember(i);
|
|
||||||
if(member == NULL) continue;
|
|
||||||
|
|
||||||
battlefighter_t *fighter = &BATTLE.fighters[allySlot++];
|
|
||||||
member->health = fighter->health;
|
|
||||||
member->mp = fighter->mp;
|
|
||||||
member->status = fighter->status;
|
|
||||||
}
|
|
||||||
|
|
||||||
sceneSet(SCENE_TYPE_OVERWORLD);
|
|
||||||
battleDispose();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "rpg/battle/battle.h"
|
|
||||||
|
|
||||||
#define CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX 4
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
battlefighterstats_t stats;
|
|
||||||
uint16_t healthMax;
|
|
||||||
uint16_t mpMax;
|
|
||||||
} cutscenestartbattleenemy_t;
|
|
||||||
|
|
||||||
typedef struct cutsceneitem_s cutsceneitem_t;
|
|
||||||
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
battleencountertype_t encounterType;
|
|
||||||
bool_t fleeAvailable;
|
|
||||||
uint8_t enemyCount;
|
|
||||||
cutscenestartbattleenemy_t enemies[CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX];
|
|
||||||
} cutscenestartbattle_t;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts a battle: seeds BATTLE with the party's active order members
|
|
||||||
* and the item's configured enemies, then switches to the battle
|
|
||||||
* scene.
|
|
||||||
*
|
|
||||||
* @param item The cutscene item.
|
|
||||||
* @param data Runtime data storage.
|
|
||||||
*/
|
|
||||||
void cutsceneStartBattleStart(
|
|
||||||
const cutsceneitem_t *item,
|
|
||||||
cutsceneitemdata_t *data
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Waits for the battle to produce a result, syncs ally HP/MP back to
|
|
||||||
* the party roster, then returns to the overworld scene.
|
|
||||||
*
|
|
||||||
* @param item The cutscene item.
|
|
||||||
* @param data Runtime data storage.
|
|
||||||
* @returns true once the battle has ended.
|
|
||||||
*/
|
|
||||||
bool_t cutsceneStartBattleUpdate(
|
|
||||||
const cutsceneitem_t *item,
|
|
||||||
cutsceneitemdata_t *data
|
|
||||||
);
|
|
||||||
@@ -105,11 +105,6 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
|
|||||||
.update = cutsceneMapAreaWaitUpdate
|
.update = cutsceneMapAreaWaitUpdate
|
||||||
},
|
},
|
||||||
|
|
||||||
[CUTSCENE_ITEM_TYPE_START_BATTLE] = {
|
|
||||||
.init = cutsceneStartBattleStart,
|
|
||||||
.update = cutsceneStartBattleUpdate
|
|
||||||
},
|
|
||||||
|
|
||||||
[CUTSCENE_ITEM_TYPE_EMOJI] = {
|
[CUTSCENE_ITEM_TYPE_EMOJI] = {
|
||||||
.init = cutsceneEmojiStart,
|
.init = cutsceneEmojiStart,
|
||||||
.update = cutsceneEmojiUpdate
|
.update = cutsceneEmojiUpdate
|
||||||
@@ -118,15 +113,6 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
|
|||||||
[CUTSCENE_ITEM_TYPE_SHAKE] = {
|
[CUTSCENE_ITEM_TYPE_SHAKE] = {
|
||||||
.init = cutsceneShakeStart,
|
.init = cutsceneShakeStart,
|
||||||
.update = cutsceneShakeUpdate
|
.update = cutsceneShakeUpdate
|
||||||
},
|
|
||||||
|
|
||||||
[CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE] = {
|
|
||||||
.update = cutsceneBattleWaitStateUpdate
|
|
||||||
},
|
|
||||||
|
|
||||||
[CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION] = {
|
|
||||||
.init = cutsceneBattleForceActionStart,
|
|
||||||
.update = cutsceneBattleForceActionUpdate
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -26,9 +26,6 @@
|
|||||||
#include "maparea/cutscenemapareaadd.h"
|
#include "maparea/cutscenemapareaadd.h"
|
||||||
#include "maparea/cutscenemaparearemove.h"
|
#include "maparea/cutscenemaparearemove.h"
|
||||||
#include "maparea/cutscenemapareawait.h"
|
#include "maparea/cutscenemapareawait.h"
|
||||||
#include "battle/cutscenestartbattle.h"
|
|
||||||
#include "battle/cutscenebattlewaitstate.h"
|
|
||||||
#include "battle/cutscenebattleforceaction.h"
|
|
||||||
|
|
||||||
typedef struct cutscene_s cutscene_t;
|
typedef struct cutscene_s cutscene_t;
|
||||||
|
|
||||||
@@ -54,11 +51,8 @@ typedef enum {
|
|||||||
CUTSCENE_ITEM_TYPE_MAP_AREA_ADD,
|
CUTSCENE_ITEM_TYPE_MAP_AREA_ADD,
|
||||||
CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE,
|
CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE,
|
||||||
CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT,
|
CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT,
|
||||||
CUTSCENE_ITEM_TYPE_START_BATTLE,
|
|
||||||
CUTSCENE_ITEM_TYPE_EMOJI,
|
CUTSCENE_ITEM_TYPE_EMOJI,
|
||||||
CUTSCENE_ITEM_TYPE_SHAKE,
|
CUTSCENE_ITEM_TYPE_SHAKE,
|
||||||
CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE,
|
|
||||||
CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION,
|
|
||||||
|
|
||||||
CUTSCENE_ITEM_TYPE_COUNT
|
CUTSCENE_ITEM_TYPE_COUNT
|
||||||
} cutsceneitemtype_t;
|
} cutsceneitemtype_t;
|
||||||
@@ -86,11 +80,8 @@ struct cutsceneitem_s {
|
|||||||
cutscenemapareaadd_t mapAreaAdd;
|
cutscenemapareaadd_t mapAreaAdd;
|
||||||
cutscenemaparearemove_t mapAreaRemove;
|
cutscenemaparearemove_t mapAreaRemove;
|
||||||
cutscenemapareawait_t mapAreaWait;
|
cutscenemapareawait_t mapAreaWait;
|
||||||
cutscenestartbattle_t startBattle;
|
|
||||||
cutsceneemoji_t emoji;
|
cutsceneemoji_t emoji;
|
||||||
cutsceneshake_t shake;
|
cutsceneshake_t shake;
|
||||||
cutscenebattlewaitstate_t battleWaitState;
|
|
||||||
cutscenebattleforceaction_t battleForceAction;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
player.c
|
player.c
|
||||||
)
|
)
|
||||||
|
|
||||||
add_subdirectory(anim)
|
|
||||||
add_subdirectory(interact)
|
add_subdirectory(interact)
|
||||||
add_subdirectory(npc)
|
add_subdirectory(npc)
|
||||||
add_subdirectory(item)
|
add_subdirectory(item)
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "rpg/entity/entity.h"
|
|
||||||
#include "rpg/overworld/map.h"
|
|
||||||
#include "rpg/overworld/tile.h"
|
|
||||||
#include "time/time.h"
|
|
||||||
#include "entityanimwalk.h"
|
|
||||||
|
|
||||||
const entityanimcallback_t ENTITY_ANIM_CALLBACKS[ENTITY_ANIM_COUNT] = {
|
|
||||||
[ENTITY_ANIM_IDLE] = { entityAnimIdleUpdate },
|
|
||||||
[ENTITY_ANIM_TURN] = { entityAnimTurnUpdate },
|
|
||||||
[ENTITY_ANIM_WALK] = { entityAnimWalkUpdate },
|
|
||||||
[ENTITY_ANIM_RUN] = { entityAnimRunUpdate },
|
|
||||||
};
|
|
||||||
|
|
||||||
float_t entityAnimTileZOffset(const worldpos_t pos) {
|
|
||||||
return tileShapeIsRamp(mapGetTile(pos).shape) ? 0.5f : 0.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
void entityAnimUpdate(entity_t *entity) {
|
|
||||||
if(entity->animation != ENTITY_ANIM_IDLE) {
|
|
||||||
entity->animTime -= TIME.delta;
|
|
||||||
if(entity->animTime <= 0) {
|
|
||||||
if(
|
|
||||||
entity->animation == ENTITY_ANIM_WALK ||
|
|
||||||
entity->animation == ENTITY_ANIM_RUN
|
|
||||||
) {
|
|
||||||
entity->walkEndCooldown = ENTITY_ANIM_WALK_TURN_COOLDOWN;
|
|
||||||
}
|
|
||||||
entity->animation = ENTITY_ANIM_IDLE;
|
|
||||||
entity->animTime = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(entity->walkEndCooldown > 0) {
|
|
||||||
entity->walkEndCooldown -= TIME.delta;
|
|
||||||
if(entity->walkEndCooldown < 0) entity->walkEndCooldown = 0;
|
|
||||||
}
|
|
||||||
ENTITY_ANIM_CALLBACKS[entity->animation].update(entity);
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "dusk.h"
|
|
||||||
#include "entityanimidle.h"
|
|
||||||
#include "entityanimturn.h"
|
|
||||||
#include "entityanimwalk.h"
|
|
||||||
#include "entityanimrun.h"
|
|
||||||
|
|
||||||
typedef struct entity_s entity_t;
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
ENTITY_ANIM_IDLE,
|
|
||||||
ENTITY_ANIM_TURN,
|
|
||||||
ENTITY_ANIM_WALK,
|
|
||||||
ENTITY_ANIM_RUN,
|
|
||||||
ENTITY_ANIM_COUNT
|
|
||||||
} entityanim_t;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
/** Updates the render position for this animation state. */
|
|
||||||
void (*update)(entity_t *entity);
|
|
||||||
} entityanimcallback_t;
|
|
||||||
|
|
||||||
extern const entityanimcallback_t ENTITY_ANIM_CALLBACKS[ENTITY_ANIM_COUNT];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the entity animation timer and render position.
|
|
||||||
*
|
|
||||||
* @param entity Pointer to the entity to update.
|
|
||||||
*/
|
|
||||||
void entityAnimUpdate(entity_t *entity);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns 0.5 if the tile at pos is a ramp, 0.0 otherwise.
|
|
||||||
* Used to lift entity render position to mid-ramp height.
|
|
||||||
*
|
|
||||||
* @param pos World position to sample.
|
|
||||||
* @returns float_t The Z offset to apply.
|
|
||||||
*/
|
|
||||||
float_t entityAnimTileZOffset(const worldpos_t pos);
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "rpg/entity/entity.h"
|
|
||||||
#include "entityanim.h"
|
|
||||||
|
|
||||||
void entityAnimIdleUpdate(entity_t *entity) {
|
|
||||||
entity->renderPosition[0] = (float_t)entity->position.x;
|
|
||||||
entity->renderPosition[1] = (float_t)entity->position.y;
|
|
||||||
entity->renderPosition[2] = (
|
|
||||||
(float_t)entity->position.z + entityAnimTileZOffset(entity->position)
|
|
||||||
) * WORLD_LAYER_HEIGHT;
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "dusk.h"
|
|
||||||
|
|
||||||
typedef struct entity_s entity_t;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates render position for the idle animation state.
|
|
||||||
*
|
|
||||||
* @param entity Pointer to the entity to update.
|
|
||||||
*/
|
|
||||||
void entityAnimIdleUpdate(entity_t *entity);
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "rpg/entity/entity.h"
|
|
||||||
#include "entityanim.h"
|
|
||||||
|
|
||||||
void entityAnimRunUpdate(entity_t *entity) {
|
|
||||||
float_t t = 1.0f - (entity->animTime / ENTITY_ANIM_RUN_DURATION);
|
|
||||||
float_t zFrom = (float_t)entity->lastPosition.z
|
|
||||||
+ entityAnimTileZOffset(entity->lastPosition);
|
|
||||||
float_t zTo = (float_t)entity->position.z
|
|
||||||
+ entityAnimTileZOffset(entity->position);
|
|
||||||
entity->renderPosition[0] = (float_t)entity->lastPosition.x + t * (
|
|
||||||
(float_t)entity->position.x - (float_t)entity->lastPosition.x
|
|
||||||
);
|
|
||||||
entity->renderPosition[1] = (float_t)entity->lastPosition.y + t * (
|
|
||||||
(float_t)entity->position.y - (float_t)entity->lastPosition.y
|
|
||||||
);
|
|
||||||
entity->renderPosition[2] = (zFrom + t * (zTo - zFrom)) * WORLD_LAYER_HEIGHT;
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "dusk.h"
|
|
||||||
#include "time/time.h"
|
|
||||||
|
|
||||||
typedef struct entity_s entity_t;
|
|
||||||
|
|
||||||
#define ENTITY_ANIM_RUN_DURATION TIME_TICKS_TO_TIME(6)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates render position for the run animation state.
|
|
||||||
*
|
|
||||||
* @param entity Pointer to the entity to update.
|
|
||||||
*/
|
|
||||||
void entityAnimRunUpdate(entity_t *entity);
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "rpg/entity/entity.h"
|
|
||||||
#include "entityanim.h"
|
|
||||||
|
|
||||||
void entityAnimTurnUpdate(entity_t *entity) {
|
|
||||||
entity->renderPosition[0] = (float_t)entity->position.x;
|
|
||||||
entity->renderPosition[1] = (float_t)entity->position.y;
|
|
||||||
entity->renderPosition[2] = (
|
|
||||||
(float_t)entity->position.z + entityAnimTileZOffset(entity->position)
|
|
||||||
) * WORLD_LAYER_HEIGHT;
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "dusk.h"
|
|
||||||
#include "time/time.h"
|
|
||||||
|
|
||||||
typedef struct entity_s entity_t;
|
|
||||||
|
|
||||||
#define ENTITY_ANIM_TURN_DURATION TIME_TICKS_TO_TIME(4)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates render position for the turn animation state.
|
|
||||||
*
|
|
||||||
* @param entity Pointer to the entity to update.
|
|
||||||
*/
|
|
||||||
void entityAnimTurnUpdate(entity_t *entity);
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "rpg/entity/entity.h"
|
|
||||||
#include "entityanim.h"
|
|
||||||
|
|
||||||
void entityAnimWalkUpdate(entity_t *entity) {
|
|
||||||
float_t t = 1.0f - (entity->animTime / ENTITY_ANIM_WALK_DURATION);
|
|
||||||
float_t zFrom = (float_t)entity->lastPosition.z
|
|
||||||
+ entityAnimTileZOffset(entity->lastPosition);
|
|
||||||
float_t zTo = (float_t)entity->position.z
|
|
||||||
+ entityAnimTileZOffset(entity->position);
|
|
||||||
entity->renderPosition[0] = (float_t)entity->lastPosition.x + t * (
|
|
||||||
(float_t)entity->position.x - (float_t)entity->lastPosition.x
|
|
||||||
);
|
|
||||||
entity->renderPosition[1] = (float_t)entity->lastPosition.y + t * (
|
|
||||||
(float_t)entity->position.y - (float_t)entity->lastPosition.y
|
|
||||||
);
|
|
||||||
entity->renderPosition[2] = (zFrom + t * (zTo - zFrom)) * WORLD_LAYER_HEIGHT;
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "dusk.h"
|
|
||||||
#include "time/time.h"
|
|
||||||
|
|
||||||
typedef struct entity_s entity_t;
|
|
||||||
|
|
||||||
#define ENTITY_ANIM_WALK_DURATION TIME_TICKS_TO_TIME(12)
|
|
||||||
#define ENTITY_ANIM_WALK_TURN_COOLDOWN TIME_TICKS_TO_TIME(4)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates render position for the walk animation state.
|
|
||||||
*
|
|
||||||
* @param entity Pointer to the entity to update.
|
|
||||||
*/
|
|
||||||
void entityAnimWalkUpdate(entity_t *entity);
|
|
||||||
+119
-203
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2025 Dominic Masters
|
* Copyright (c) 2025 Dominic Masters
|
||||||
*
|
*
|
||||||
* This software is released under the MIT License.
|
* This software is released under the MIT License.
|
||||||
* https://opensource.org/licenses/MIT
|
* https://opensource.org/licenses/MIT
|
||||||
*/
|
*/
|
||||||
@@ -8,15 +8,14 @@
|
|||||||
#include "entity.h"
|
#include "entity.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "time/time.h"
|
|
||||||
#include "util/math.h"
|
#include "util/math.h"
|
||||||
#include "console/console.h"
|
#include "time/time.h"
|
||||||
#include "rpg/overworld/map.h"
|
#include "rpg/overworld/map.h"
|
||||||
#include "rpg/overworld/maparea.h"
|
#include "rpg/overworld/maparea.h"
|
||||||
#include "rpg/overworld/chunk.h"
|
#include "rpg/overworld/chunk.h"
|
||||||
#include "rpg/overworld/tile.h"
|
|
||||||
|
|
||||||
entity_t ENTITIES[ENTITY_COUNT];
|
entity_t ENTITIES[ENTITY_COUNT];
|
||||||
|
physicsworld_t ENTITY_PHYSICS_WORLD;
|
||||||
|
|
||||||
void entityInit(entity_t *entity, const entitytype_t type) {
|
void entityInit(entity_t *entity, const entitytype_t type) {
|
||||||
assertNotNull(entity, "Entity pointer cannot be NULL");
|
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||||
@@ -33,6 +32,9 @@ void entityInit(entity_t *entity, const entitytype_t type) {
|
|||||||
entity->type = type;
|
entity->type = type;
|
||||||
entity->chunkIndex = 0xFF;
|
entity->chunkIndex = 0xFF;
|
||||||
|
|
||||||
|
const vec3 extents = ENTITY_PHYSICS_EXTENTS_DEFAULT;
|
||||||
|
physicsBodyInit(&entity->body, (vec3){ 0.0f, 0.0f, 0.0f }, extents);
|
||||||
|
|
||||||
if(ENTITY_CALLBACKS[type].init != NULL) ENTITY_CALLBACKS[type].init(entity);
|
if(ENTITY_CALLBACKS[type].init != NULL) ENTITY_CALLBACKS[type].init(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,212 +43,115 @@ void entityUpdate(entity_t *entity) {
|
|||||||
assertTrue(entity->type < ENTITY_TYPE_COUNT, "Invalid entity type");
|
assertTrue(entity->type < ENTITY_TYPE_COUNT, "Invalid entity type");
|
||||||
assertTrue(entity->type != ENTITY_TYPE_NULL, "Cannot have NULL entity type");
|
assertTrue(entity->type != ENTITY_TYPE_NULL, "Cannot have NULL entity type");
|
||||||
|
|
||||||
// What state is the entity in?
|
|
||||||
entityAnimUpdate(entity);
|
|
||||||
|
|
||||||
// Movement code.
|
|
||||||
if(ENTITY_CALLBACKS[entity->type].movement != NULL) {
|
if(ENTITY_CALLBACKS[entity->type].movement != NULL) {
|
||||||
ENTITY_CALLBACKS[entity->type].movement(entity);
|
ENTITY_CALLBACKS[entity->type].movement(entity);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
bool_t entityCanTurn(entity_t *entity) {
|
physicsbody_t *others[ENTITY_COUNT];
|
||||||
return entity->animation == ENTITY_ANIM_IDLE &&
|
uint32_t othersCount = 0;
|
||||||
entity->walkEndCooldown <= 0;
|
for(uint8_t i = 0; i < ENTITY_COUNT; i++) {
|
||||||
}
|
if(ENTITIES[i].type == ENTITY_TYPE_NULL) continue;
|
||||||
|
if(&ENTITIES[i] == entity) continue;
|
||||||
|
others[othersCount++] = &ENTITIES[i].body;
|
||||||
|
}
|
||||||
|
|
||||||
bool_t entityCanWalk(entity_t *entity) {
|
physicsWorldStep(
|
||||||
return entity->animation == ENTITY_ANIM_IDLE;
|
&ENTITY_PHYSICS_WORLD, &entity->body, TIME.delta, others, othersCount
|
||||||
}
|
);
|
||||||
|
entitySyncFromPhysics(entity);
|
||||||
bool_t entityCanRun(entity_t *entity) {
|
|
||||||
return entity->animation == ENTITY_ANIM_IDLE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool_t entityCanUnload(entity_t *entity) {
|
bool_t entityCanUnload(entity_t *entity) {
|
||||||
return entity->globalId < ENTITY_GLOBAL_ID_START;
|
return entity->globalId < ENTITY_GLOBAL_ID_START;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void entityMove(
|
||||||
|
entity_t *entity, const vec2 direction, const bool_t running
|
||||||
|
) {
|
||||||
|
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||||
|
|
||||||
|
const float_t magSq =
|
||||||
|
direction[0] * direction[0] + direction[1] * direction[1];
|
||||||
|
if(magSq <= ENTITY_MOVE_DEADZONE * ENTITY_MOVE_DEADZONE) {
|
||||||
|
entity->body.velocity[0] = 0.0f;
|
||||||
|
entity->body.velocity[1] = 0.0f;
|
||||||
|
entity->animation = ENTITY_ANIM_IDLE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float_t mag = sqrtf(magSq);
|
||||||
|
const float_t clampedMag = mathMin(mag, 1.0f);
|
||||||
|
const float_t speed = running ? ENTITY_MOVE_RUN_SPEED : ENTITY_MOVE_WALK_SPEED;
|
||||||
|
|
||||||
|
entity->body.velocity[0] = (direction[0] / mag) * clampedMag * speed;
|
||||||
|
entity->body.velocity[1] = (direction[1] / mag) * clampedMag * speed;
|
||||||
|
entity->direction = entityDirFromVec2(direction);
|
||||||
|
entity->animation = running ? ENTITY_ANIM_RUN : ENTITY_ANIM_WALK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityStop(entity_t *entity) {
|
||||||
|
const vec2 zero = { 0.0f, 0.0f };
|
||||||
|
entityMove(entity, zero, false);
|
||||||
|
}
|
||||||
|
|
||||||
void entityTurn(entity_t *entity, const entitydir_t direction) {
|
void entityTurn(entity_t *entity, const entitydir_t direction) {
|
||||||
if(!entityCanTurn(entity)) return;
|
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||||
entity->direction = direction;
|
entity->direction = direction;
|
||||||
entity->animation = ENTITY_ANIM_TURN;
|
|
||||||
entity->animTime = ENTITY_ANIM_TURN_DURATION;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityWalk(entity_t *entity, const entitydir_t direction) {
|
void entityWalk(entity_t *entity, const entitydir_t direction) {
|
||||||
if(!entityCanWalk(entity)) return;
|
vec2 dirVec;
|
||||||
// TODO: Animation, delay, etc.
|
entityDirToVec2(direction, dirVec);
|
||||||
entity->direction = direction;
|
entityMove(entity, dirVec, false);
|
||||||
|
|
||||||
// Where are we moving?
|
|
||||||
worldpos_t newPos = entity->position;
|
|
||||||
worldunits_t relX, relY;
|
|
||||||
{
|
|
||||||
entityDirGetRelative(direction, &relX, &relY);
|
|
||||||
newPos.x += relX;
|
|
||||||
newPos.y += relY;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get tile under foot
|
|
||||||
tile_t tileCurrent = mapGetTile(entity->position);
|
|
||||||
tile_t tileNew = mapGetTile(newPos);
|
|
||||||
bool_t fall = false;
|
|
||||||
bool_t raise = false;
|
|
||||||
|
|
||||||
// Are we walking up a ramp?
|
|
||||||
if(
|
|
||||||
tileShapeIsRamp(tileCurrent.shape) &&
|
|
||||||
(
|
|
||||||
// Can only walk UP the direction the ramp faces.
|
|
||||||
(direction+TILE_SHAPE_RAMP_NORTH) == tileCurrent.shape ||
|
|
||||||
// If diagonal ramp, can go up one of two ways only. Inner ramps
|
|
||||||
// share the same allowed directions as their outer counterparts.
|
|
||||||
(
|
|
||||||
(
|
|
||||||
(
|
|
||||||
tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHEAST ||
|
|
||||||
tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHEAST_INNER
|
|
||||||
) &&
|
|
||||||
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST)
|
|
||||||
) ||
|
|
||||||
(
|
|
||||||
(
|
|
||||||
tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHWEST ||
|
|
||||||
tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHWEST_INNER
|
|
||||||
) &&
|
|
||||||
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST)
|
|
||||||
) ||
|
|
||||||
(
|
|
||||||
(
|
|
||||||
tileCurrent.shape == TILE_SHAPE_RAMP_NORTHEAST ||
|
|
||||||
tileCurrent.shape == TILE_SHAPE_RAMP_NORTHEAST_INNER
|
|
||||||
) &&
|
|
||||||
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST)
|
|
||||||
) ||
|
|
||||||
(
|
|
||||||
(
|
|
||||||
tileCurrent.shape == TILE_SHAPE_RAMP_NORTHWEST ||
|
|
||||||
tileCurrent.shape == TILE_SHAPE_RAMP_NORTHWEST_INNER
|
|
||||||
) &&
|
|
||||||
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
// Must be able to walk up.
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
tile_t tileNewSaved = tileNew;
|
|
||||||
tileNew = TILE_NULL;
|
|
||||||
worldpos_t abovePos = newPos;
|
|
||||||
abovePos.z += 1;
|
|
||||||
tile_t tileAbove = mapGetTile(abovePos);
|
|
||||||
|
|
||||||
if(
|
|
||||||
tileAbove.shape != TILE_SHAPE_NULL &&
|
|
||||||
tileShapeIsWalkable(tileAbove.shape)
|
|
||||||
) {
|
|
||||||
raise = true;
|
|
||||||
} else {
|
|
||||||
tileNew = tileNewSaved;
|
|
||||||
}
|
|
||||||
} else if(tileNew.shape == TILE_SHAPE_NULL && newPos.z > 0) {
|
|
||||||
// Falling down?
|
|
||||||
worldpos_t belowPos = newPos;
|
|
||||||
belowPos.z -= 1;
|
|
||||||
tile_t tileBelow = mapGetTile(belowPos);
|
|
||||||
if(
|
|
||||||
tileBelow.shape != TILE_SHAPE_NULL &&
|
|
||||||
tileShapeIsRamp(tileBelow.shape) &&
|
|
||||||
(
|
|
||||||
// This handles regular cardinal ramps
|
|
||||||
(
|
|
||||||
entityDirGetOpposite(direction)+TILE_SHAPE_RAMP_NORTH
|
|
||||||
) == tileBelow.shape ||
|
|
||||||
// This handles diagonal ramps. Inner ramps share the same
|
|
||||||
// allowed directions as their outer counterparts.
|
|
||||||
(
|
|
||||||
(
|
|
||||||
(
|
|
||||||
tileBelow.shape == TILE_SHAPE_RAMP_SOUTHEAST ||
|
|
||||||
tileBelow.shape == TILE_SHAPE_RAMP_SOUTHEAST_INNER
|
|
||||||
) &&
|
|
||||||
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST)
|
|
||||||
) ||
|
|
||||||
(
|
|
||||||
(
|
|
||||||
tileBelow.shape == TILE_SHAPE_RAMP_SOUTHWEST ||
|
|
||||||
tileBelow.shape == TILE_SHAPE_RAMP_SOUTHWEST_INNER
|
|
||||||
) &&
|
|
||||||
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST)
|
|
||||||
) ||
|
|
||||||
(
|
|
||||||
(
|
|
||||||
tileBelow.shape == TILE_SHAPE_RAMP_NORTHEAST ||
|
|
||||||
tileBelow.shape == TILE_SHAPE_RAMP_NORTHEAST_INNER
|
|
||||||
) &&
|
|
||||||
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST)
|
|
||||||
) ||
|
|
||||||
(
|
|
||||||
(
|
|
||||||
tileBelow.shape == TILE_SHAPE_RAMP_NORTHWEST ||
|
|
||||||
tileBelow.shape == TILE_SHAPE_RAMP_NORTHWEST_INNER
|
|
||||||
) &&
|
|
||||||
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
// We will fall to this tile.
|
|
||||||
fall = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Can we walk here?
|
|
||||||
if(!raise && !fall && !tileShapeIsWalkable(tileNew.shape)) return;// Blocked
|
|
||||||
|
|
||||||
// Raise/fall must be applied before checking for blocking entities,
|
|
||||||
// otherwise the check compares against the wrong z-level.
|
|
||||||
if(raise) {
|
|
||||||
newPos.z += 1;
|
|
||||||
} else if(fall) {
|
|
||||||
newPos.z -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Entity in way?
|
|
||||||
entity_t *other = ENTITIES;
|
|
||||||
do {
|
|
||||||
if(other == entity) continue;
|
|
||||||
if(other->type == ENTITY_TYPE_NULL) continue;
|
|
||||||
if(!worldPosIsEqual(other->position, newPos)) continue;
|
|
||||||
return;// Blocked
|
|
||||||
} while(++other, other < &ENTITIES[ENTITY_COUNT]);
|
|
||||||
|
|
||||||
entity->lastPosition = entity->position;
|
|
||||||
entity->position = newPos;
|
|
||||||
entity->animation = ENTITY_ANIM_WALK;
|
|
||||||
entity->animTime = ENTITY_ANIM_WALK_DURATION;// TODO: Running vs walking
|
|
||||||
entityUpdateChunk(entity);
|
|
||||||
mapAreaCheckEntity(entity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityRun(entity_t *entity, const entitydir_t direction) {
|
void entityRun(entity_t *entity, const entitydir_t direction) {
|
||||||
if(!entityCanRun(entity)) return;
|
vec2 dirVec;
|
||||||
entityWalk(entity, direction);
|
entityDirToVec2(direction, dirVec);
|
||||||
if(entity->animation == ENTITY_ANIM_WALK) {
|
entityMove(entity, dirVec, true);
|
||||||
entity->animation = ENTITY_ANIM_RUN;
|
|
||||||
entity->animTime = ENTITY_ANIM_RUN_DURATION;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entity_t * entityGetAt(const worldpos_t position) {
|
entity_t * entityGetFacing(entity_t *entity, const float_t range) {
|
||||||
|
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||||
|
|
||||||
|
vec2 dir;
|
||||||
|
entityDirToVec2(entity->direction, dir);
|
||||||
|
|
||||||
|
vec3 min, max;
|
||||||
|
physicsBodyGetBounds(&entity->body, min, max);
|
||||||
|
|
||||||
|
const vec3 probeMin = {
|
||||||
|
min[0] + dir[0] * range, min[1] + dir[1] * range, min[2]
|
||||||
|
};
|
||||||
|
const vec3 probeMax = {
|
||||||
|
max[0] + dir[0] * range, max[1] + dir[1] * range, max[2]
|
||||||
|
};
|
||||||
|
|
||||||
|
entity_t *best = NULL;
|
||||||
|
float_t bestDistSq = 0.0f;
|
||||||
|
|
||||||
entity_t *ent = ENTITIES;
|
entity_t *ent = ENTITIES;
|
||||||
do {
|
do {
|
||||||
if(ent->type == ENTITY_TYPE_NULL) continue;
|
if(ent->type == ENTITY_TYPE_NULL) continue;
|
||||||
if(!worldPosIsEqual(ent->position, position)) continue;
|
if(ent == entity) continue;
|
||||||
return ent;
|
|
||||||
|
vec3 oMin, oMax;
|
||||||
|
physicsBodyGetBounds(&ent->body, oMin, oMax);
|
||||||
|
|
||||||
|
if(probeMin[0] >= oMax[0] || probeMax[0] <= oMin[0]) continue;
|
||||||
|
if(probeMin[1] >= oMax[1] || probeMax[1] <= oMin[1]) continue;
|
||||||
|
if(probeMin[2] >= oMax[2] || probeMax[2] <= oMin[2]) continue;
|
||||||
|
|
||||||
|
const float_t dx = ent->body.position[0] - entity->body.position[0];
|
||||||
|
const float_t dy = ent->body.position[1] - entity->body.position[1];
|
||||||
|
const float_t distSq = dx * dx + dy * dy;
|
||||||
|
|
||||||
|
if(best != NULL && distSq >= bestDistSq) continue;
|
||||||
|
best = ent;
|
||||||
|
bestDistSq = distSq;
|
||||||
} while(++ent, ent < &ENTITIES[ENTITY_COUNT]);
|
} while(++ent, ent < &ENTITIES[ENTITY_COUNT]);
|
||||||
|
|
||||||
return NULL;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
entity_t * entityGetByGlobalId(const entityglobalid_t globalId) {
|
entity_t * entityGetByGlobalId(const entityglobalid_t globalId) {
|
||||||
@@ -271,12 +176,14 @@ uint8_t entityGetAvailable() {
|
|||||||
|
|
||||||
void entityPositionSet(entity_t *entity, const worldpos_t pos) {
|
void entityPositionSet(entity_t *entity, const worldpos_t pos) {
|
||||||
assertNotNull(entity, "Entity pointer cannot be NULL");
|
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||||
entity->lastPosition = pos;
|
|
||||||
entity->position = pos;
|
const vec3 floatPos = {
|
||||||
|
(float_t)pos.x, (float_t)pos.y, (float_t)pos.z
|
||||||
|
};
|
||||||
|
const vec3 extents = ENTITY_PHYSICS_EXTENTS_DEFAULT;
|
||||||
|
physicsBodyInit(&entity->body, floatPos, extents);
|
||||||
entity->animation = ENTITY_ANIM_IDLE;
|
entity->animation = ENTITY_ANIM_IDLE;
|
||||||
entity->animTime = 0;
|
entitySyncFromPhysics(entity);
|
||||||
entity->walkEndCooldown = 0;
|
|
||||||
entityUpdateChunk(entity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
|
void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
|
||||||
@@ -293,27 +200,19 @@ void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only claim the new chunk once actually inserted into one of its slots -
|
entity->chunkIndex = chunkIndex;
|
||||||
// otherwise entity->chunkIndex would point at a chunk that doesn't know
|
|
||||||
// about this entity, so it would never be torn down on unload.
|
|
||||||
entity->chunkIndex = 0xFF;
|
|
||||||
|
|
||||||
if(chunkIndex != 0xFF) {
|
if(chunkIndex != 0xFF) {
|
||||||
chunk_t *next = mapGetChunk(chunkIndex);
|
chunk_t *next = mapGetChunk(chunkIndex);
|
||||||
if(next != NULL) {
|
if(next != NULL) {
|
||||||
|
bool_t inserted = false;
|
||||||
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
|
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
|
||||||
if(next->entities[i] != 0xFF) continue;
|
if(next->entities[i] != 0xFF) continue;
|
||||||
next->entities[i] = entity->id;
|
next->entities[i] = entity->id;
|
||||||
entity->chunkIndex = chunkIndex;
|
inserted = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if(entity->chunkIndex != chunkIndex) {
|
assertTrue(inserted, "Chunk entity slot overflow");
|
||||||
consolePrint(
|
|
||||||
"entitySetChunk: chunk %u has no free entity slots, entity %u "
|
|
||||||
"left untracked",
|
|
||||||
chunkIndex, entity->id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -324,5 +223,22 @@ void entityUpdateChunk(entity_t *entity) {
|
|||||||
chunkpos_t cp;
|
chunkpos_t cp;
|
||||||
worldPosToChunkPos(&entity->position, &cp);
|
worldPosToChunkPos(&entity->position, &cp);
|
||||||
chunkindex_t ci = mapGetChunkIndexAt(cp);
|
chunkindex_t ci = mapGetChunkIndexAt(cp);
|
||||||
if(ci != -1) entitySetChunk(entity, (uint8_t)ci);
|
if(ci == -1 || ci == entity->chunkIndex) return;
|
||||||
}
|
entitySetChunk(entity, (uint8_t)ci);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entitySyncFromPhysics(entity_t *entity) {
|
||||||
|
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||||
|
|
||||||
|
entity->position = (worldpos_t){
|
||||||
|
(worldunit_t)floorf(entity->body.position[0]),
|
||||||
|
(worldunit_t)floorf(entity->body.position[1]),
|
||||||
|
(worldunit_t)floorf(entity->body.position[2])
|
||||||
|
};
|
||||||
|
|
||||||
|
glm_vec3_copy(entity->body.position, entity->renderPosition);
|
||||||
|
entity->renderPosition[2] *= WORLD_LAYER_HEIGHT;
|
||||||
|
|
||||||
|
entityUpdateChunk(entity);
|
||||||
|
mapAreaCheckEntity(entity);
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,10 +7,11 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "entitydir.h"
|
#include "entitydir.h"
|
||||||
#include "anim/entityanim.h"
|
|
||||||
#include "interact/entityinteract.h"
|
#include "interact/entityinteract.h"
|
||||||
#include "entitytype.h"
|
#include "entitytype.h"
|
||||||
#include "npc/npc.h"
|
#include "npc/npc.h"
|
||||||
|
#include "rpg/physics/physicsbody.h"
|
||||||
|
#include "rpg/physics/physicsworld.h"
|
||||||
|
|
||||||
typedef struct map_s map_t;
|
typedef struct map_s map_t;
|
||||||
|
|
||||||
@@ -20,6 +21,29 @@ typedef uint16_t entityglobalid_t;
|
|||||||
#define ENTITY_GLOBAL_ID_START 1
|
#define ENTITY_GLOBAL_ID_START 1
|
||||||
#define ENTITY_GLOBAL_ID_PLAYER 1
|
#define ENTITY_GLOBAL_ID_PLAYER 1
|
||||||
|
|
||||||
|
// Default collision box for every entity's physics body - matches the old
|
||||||
|
// system's exact one-tile footprint.
|
||||||
|
#define ENTITY_PHYSICS_EXTENTS_DEFAULT { 1.0f, 1.0f, 1.0f }
|
||||||
|
|
||||||
|
// Movement speeds, in grid units per second - chosen to match the feel of
|
||||||
|
// the old fixed-duration one-tile-per-step system (12 ticks/tile walking,
|
||||||
|
// 6 ticks/tile running, at DUSK_TIME_STEP = 16ms).
|
||||||
|
#define ENTITY_MOVE_WALK_SPEED 5.2083f
|
||||||
|
#define ENTITY_MOVE_RUN_SPEED 10.4167f
|
||||||
|
|
||||||
|
// Movement vectors below this magnitude are treated as no movement.
|
||||||
|
#define ENTITY_MOVE_DEADZONE 0.1f
|
||||||
|
|
||||||
|
// How far in front of an entity entityGetFacing probes for a target.
|
||||||
|
#define ENTITY_INTERACT_RANGE 1.0f
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
ENTITY_ANIM_IDLE,
|
||||||
|
ENTITY_ANIM_WALK,
|
||||||
|
ENTITY_ANIM_RUN,
|
||||||
|
ENTITY_ANIM_COUNT
|
||||||
|
} entityanim_t;
|
||||||
|
|
||||||
typedef struct entity_s {
|
typedef struct entity_s {
|
||||||
uint8_t id;
|
uint8_t id;
|
||||||
entityglobalid_t globalId;
|
entityglobalid_t globalId;
|
||||||
@@ -28,13 +52,18 @@ typedef struct entity_s {
|
|||||||
|
|
||||||
// Movement
|
// Movement
|
||||||
entitydir_t direction;
|
entitydir_t direction;
|
||||||
|
physicsbody_t body;
|
||||||
|
|
||||||
|
// Derived each frame from body.position (floored) - kept for systems
|
||||||
|
// that still assume an integer grid position (chunk membership, map
|
||||||
|
// area triggers, entity-at-position queries).
|
||||||
worldpos_t position;
|
worldpos_t position;
|
||||||
worldpos_t lastPosition;
|
|
||||||
|
// Derived each frame from body.position - mirrors the physics position
|
||||||
|
// into render/world-float space (z scaled by WORLD_LAYER_HEIGHT).
|
||||||
vec3 renderPosition;
|
vec3 renderPosition;
|
||||||
|
|
||||||
entityanim_t animation;
|
entityanim_t animation;
|
||||||
float_t animTime;
|
|
||||||
float_t walkEndCooldown;
|
|
||||||
|
|
||||||
entityinteract_t interact;
|
entityinteract_t interact;
|
||||||
|
|
||||||
@@ -43,6 +72,9 @@ typedef struct entity_s {
|
|||||||
|
|
||||||
extern entity_t ENTITIES[ENTITY_COUNT];
|
extern entity_t ENTITIES[ENTITY_COUNT];
|
||||||
|
|
||||||
|
// Shared physics world every entity's body steps against.
|
||||||
|
extern physicsworld_t ENTITY_PHYSICS_WORLD;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes an entity structure.
|
* Initializes an entity structure.
|
||||||
*
|
*
|
||||||
@@ -58,30 +90,6 @@ void entityInit(entity_t *entity, const entitytype_t type);
|
|||||||
*/
|
*/
|
||||||
void entityUpdate(entity_t *entity);
|
void entityUpdate(entity_t *entity);
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the entity is in a state where it can turn.
|
|
||||||
*
|
|
||||||
* @param entity Pointer to the entity to check.
|
|
||||||
* @returns True if the entity can turn.
|
|
||||||
*/
|
|
||||||
bool_t entityCanTurn(entity_t *entity);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the entity is in a state where it can walk.
|
|
||||||
*
|
|
||||||
* @param entity Pointer to the entity to check.
|
|
||||||
* @returns True if the entity can walk.
|
|
||||||
*/
|
|
||||||
bool_t entityCanWalk(entity_t *entity);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the entity is in a state where it can run.
|
|
||||||
*
|
|
||||||
* @param entity Pointer to the entity to check.
|
|
||||||
* @returns True if the entity can run.
|
|
||||||
*/
|
|
||||||
bool_t entityCanRun(entity_t *entity);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the entity is allowed to be unloaded. By default this is
|
* Returns true if the entity is allowed to be unloaded. By default this is
|
||||||
* true for entities whose global ID falls within the randomly assigned
|
* true for entities whose global ID falls within the randomly assigned
|
||||||
@@ -93,7 +101,29 @@ bool_t entityCanRun(entity_t *entity);
|
|||||||
bool_t entityCanUnload(entity_t *entity);
|
bool_t entityCanUnload(entity_t *entity);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turn an entity to face a new direction.
|
* Moves an entity continuously in a direction, at walking or running
|
||||||
|
* speed. Sets the entity's facing to the nearest cardinal direction that
|
||||||
|
* matches the movement vector. Must be called every tick the entity
|
||||||
|
* should keep moving - unlike the old tile-stepping system this does not
|
||||||
|
* complete a move on its own; call entityStop to stop.
|
||||||
|
*
|
||||||
|
* @param entity Pointer to the entity to move.
|
||||||
|
* @param direction Movement vector, magnitude 0-1 (values longer than 1
|
||||||
|
* are clamped to 1, so diagonals aren't faster than cardinals).
|
||||||
|
* @param running Whether to move at running speed instead of walking.
|
||||||
|
*/
|
||||||
|
void entityMove(entity_t *entity, const vec2 direction, const bool_t running);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops an entity's horizontal movement (equivalent to
|
||||||
|
* entityMove(entity, {0, 0}, false)).
|
||||||
|
*
|
||||||
|
* @param entity Pointer to the entity to stop.
|
||||||
|
*/
|
||||||
|
void entityStop(entity_t *entity);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn an entity to face a new direction, instantly, without moving it.
|
||||||
*
|
*
|
||||||
* @param entity Pointer to the entity to turn.
|
* @param entity Pointer to the entity to turn.
|
||||||
* @param direction The direction to face.
|
* @param direction The direction to face.
|
||||||
@@ -101,7 +131,10 @@ bool_t entityCanUnload(entity_t *entity);
|
|||||||
void entityTurn(entity_t *entity, const entitydir_t direction);
|
void entityTurn(entity_t *entity, const entitydir_t direction);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make an entity walk in a direction.
|
* Makes an entity walk continuously in a cardinal direction, at walking
|
||||||
|
* speed. Convenience wrapper over entityMove for callers that only think
|
||||||
|
* in terms of the 4 cardinal directions. Must be called every tick the
|
||||||
|
* entity should keep moving.
|
||||||
*
|
*
|
||||||
* @param entity Pointer to the entity to make walk.
|
* @param entity Pointer to the entity to make walk.
|
||||||
* @param direction The direction to walk in.
|
* @param direction The direction to walk in.
|
||||||
@@ -109,7 +142,8 @@ void entityTurn(entity_t *entity, const entitydir_t direction);
|
|||||||
void entityWalk(entity_t *entity, const entitydir_t direction);
|
void entityWalk(entity_t *entity, const entitydir_t direction);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make an entity run in a direction.
|
* Makes an entity walk continuously in a cardinal direction, at running
|
||||||
|
* speed. See entityWalk.
|
||||||
*
|
*
|
||||||
* @param entity Pointer to the entity to make run.
|
* @param entity Pointer to the entity to make run.
|
||||||
* @param direction The direction to run in.
|
* @param direction The direction to run in.
|
||||||
@@ -117,13 +151,16 @@ void entityWalk(entity_t *entity, const entitydir_t direction);
|
|||||||
void entityRun(entity_t *entity, const entitydir_t direction);
|
void entityRun(entity_t *entity, const entitydir_t direction);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the entity at a specific world position.
|
* Finds the closest other entity whose bounds overlap a probe box
|
||||||
|
* projected out from the given entity's own bounds, along its current
|
||||||
|
* facing direction. Used for interaction targeting - continuous-position
|
||||||
|
* aware, unlike an exact tile match.
|
||||||
*
|
*
|
||||||
* @param map Pointer to the map to check.
|
* @param entity Pointer to the entity to probe from.
|
||||||
* @param pos The world position to check.
|
* @param range Distance, in grid units, to project the probe box.
|
||||||
* @return Pointer to the entity at the position, or NULL if none.
|
* @return Pointer to the closest overlapping entity, or NULL if none.
|
||||||
*/
|
*/
|
||||||
entity_t *entityGetAt(const worldpos_t pos);
|
entity_t *entityGetFacing(entity_t *entity, const float_t range);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the entity with the given global ID, if one is currently loaded.
|
* Gets the entity with the given global ID, if one is currently loaded.
|
||||||
@@ -142,10 +179,7 @@ uint8_t entityGetAvailable();
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Assigns an entity to a chunk, removing it from its current chunk first.
|
* Assigns an entity to a chunk, removing it from its current chunk first.
|
||||||
* Pass 0xFF as chunkIndex to detach the entity from any chunk. If the
|
* Pass 0xFF as chunkIndex to detach the entity from any chunk.
|
||||||
* target chunk has no free entity slots, the entity is left detached
|
|
||||||
* (chunkIndex 0xFF) rather than assigned to a chunk that isn't actually
|
|
||||||
* tracking it - entityUpdateChunk will keep retrying on subsequent moves.
|
|
||||||
*
|
*
|
||||||
* @param entity Pointer to the entity.
|
* @param entity Pointer to the entity.
|
||||||
* @param chunkIndex Index of the chunk to assign to, or 0xFF for none.
|
* @param chunkIndex Index of the chunk to assign to, or 0xFF for none.
|
||||||
@@ -167,4 +201,13 @@ void entityUpdateChunk(entity_t *entity);
|
|||||||
* @param entity Pointer to the entity to move.
|
* @param entity Pointer to the entity to move.
|
||||||
* @param pos The world position to place the entity at.
|
* @param pos The world position to place the entity at.
|
||||||
*/
|
*/
|
||||||
void entityPositionSet(entity_t *entity, const worldpos_t pos);
|
void entityPositionSet(entity_t *entity, const worldpos_t pos);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derives position and renderPosition from the entity's physics body, and
|
||||||
|
* refreshes chunk membership and map area triggers to match. Called once
|
||||||
|
* per entity per frame, after the physics step.
|
||||||
|
*
|
||||||
|
* @param entity Pointer to the entity to sync.
|
||||||
|
*/
|
||||||
|
void entitySyncFromPhysics(entity_t *entity);
|
||||||
@@ -48,4 +48,27 @@ void entityDirGetRelative(
|
|||||||
*outY = 0;
|
*outY = 0;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityDirToVec2(const entitydir_t dir, vec2 out) {
|
||||||
|
assertValidEntityDir(dir, "Invalid direction provided");
|
||||||
|
assertNotNull(out, "Output vector cannot be NULL");
|
||||||
|
|
||||||
|
worldunits_t relX, relY;
|
||||||
|
entityDirGetRelative(dir, &relX, &relY);
|
||||||
|
out[0] = (float_t)relX;
|
||||||
|
out[1] = (float_t)relY;
|
||||||
|
}
|
||||||
|
|
||||||
|
entitydir_t entityDirFromVec2(const vec2 direction) {
|
||||||
|
assertNotNull(direction, "Direction vector cannot be NULL");
|
||||||
|
assertTrue(
|
||||||
|
direction[0] != 0.0f || direction[1] != 0.0f,
|
||||||
|
"Direction vector cannot be zero"
|
||||||
|
);
|
||||||
|
|
||||||
|
if(fabsf(direction[0]) > fabsf(direction[1])) {
|
||||||
|
return direction[0] > 0.0f ? ENTITY_DIR_EAST : ENTITY_DIR_WEST;
|
||||||
|
}
|
||||||
|
return direction[1] > 0.0f ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user