Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fb1c9fb42 | ||
|
|
5f08337726 | ||
|
|
0bc80d5df3 | ||
|
|
2432443ea6 | ||
|
|
fa138971e7 | ||
|
|
d7982599d1 | ||
|
|
f16c80aa0f | ||
|
|
ef5adf8274 | ||
|
|
2ffc65b1b1 | ||
|
|
d4a17bd98d | ||
|
|
dcf5f434c5 | ||
|
|
78cd973a33 |
No files matched your search
@@ -32,7 +32,7 @@ jobs:
|
|||||||
git \
|
git \
|
||||||
libssl-dev
|
libssl-dev
|
||||||
- name: Build Linux
|
- name: Build Linux
|
||||||
run: ./scripts/linux/build-linux.sh
|
run: ./scripts/build-linux.sh
|
||||||
- name: Upload Linux binary
|
- name: Upload Linux binary
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
- name: Setup pspdev
|
- name: Setup pspdev
|
||||||
uses: ./.github/actions/setup-pspdev
|
uses: ./.github/actions/setup-pspdev
|
||||||
- name: Build PSP
|
- name: Build PSP
|
||||||
run: ./scripts/psp/build-psp.sh
|
run: ./scripts/build-psp.sh
|
||||||
- name: Move EBOOT.PBP to Dusk subfolder
|
- name: Move EBOOT.PBP to Dusk subfolder
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ./git-artifcats/Dusk/PSP/GAME/Dusk
|
mkdir -p ./git-artifcats/Dusk/PSP/GAME/Dusk
|
||||||
@@ -103,7 +103,7 @@ jobs:
|
|||||||
libgles2:arm64 \
|
libgles2:arm64 \
|
||||||
libgl1-mesa-dev:arm64
|
libgl1-mesa-dev:arm64
|
||||||
- name: Build Knulli
|
- name: Build Knulli
|
||||||
run: ./scripts/knulli/build-knulli.sh
|
run: ./scripts/build-knulli.sh
|
||||||
- name: Move output to Dusk subfolder
|
- name: Move output to Dusk subfolder
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ./git-artifcats/Dusk
|
mkdir -p ./git-artifcats/Dusk
|
||||||
@@ -134,7 +134,7 @@ jobs:
|
|||||||
gamecube-sdl2 ppc-liblzma ppc-libzip \
|
gamecube-sdl2 ppc-liblzma ppc-libzip \
|
||||||
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
|
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
|
||||||
- name: Build GameCube
|
- name: Build GameCube
|
||||||
run: ./scripts/gamecube/build-gamecube.sh
|
run: ./scripts/build-gamecube.sh
|
||||||
- name: Copy output files
|
- name: Copy output files
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ./git-artifcats/Dusk
|
mkdir -p ./git-artifcats/Dusk
|
||||||
@@ -166,7 +166,7 @@ jobs:
|
|||||||
gamecube-sdl2 ppc-liblzma ppc-libzip \
|
gamecube-sdl2 ppc-liblzma ppc-libzip \
|
||||||
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
|
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
|
||||||
- name: Build GameCube ISO
|
- name: Build GameCube ISO
|
||||||
run: ./scripts/gamecube/build-gamecube-iso.sh
|
run: ./scripts/build-gamecube-iso.sh
|
||||||
- name: Copy output files
|
- name: Copy output files
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ./git-artifcats/Dusk
|
mkdir -p ./git-artifcats/Dusk
|
||||||
@@ -199,7 +199,7 @@ jobs:
|
|||||||
gamecube-sdl2 ppc-liblzma ppc-libzip \
|
gamecube-sdl2 ppc-liblzma ppc-libzip \
|
||||||
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
|
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
|
||||||
- name: Build Wii
|
- name: Build Wii
|
||||||
run: ./scripts/wii/build-wii.sh
|
run: ./scripts/build-wii.sh
|
||||||
- name: Copy output files
|
- name: Copy output files
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ./git-artifcats/Dusk/apps/Dusk
|
mkdir -p ./git-artifcats/Dusk/apps/Dusk
|
||||||
@@ -232,7 +232,7 @@ jobs:
|
|||||||
gamecube-sdl2 ppc-liblzma ppc-libzip \
|
gamecube-sdl2 ppc-liblzma ppc-libzip \
|
||||||
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
|
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
|
||||||
- name: Build Wii ISO
|
- name: Build Wii ISO
|
||||||
run: ./scripts/wii/build-wii-iso.sh
|
run: ./scripts/build-wii-iso.sh
|
||||||
- name: Copy output files
|
- name: Copy output files
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ./git-artifcats/Dusk
|
mkdir -p ./git-artifcats/Dusk
|
||||||
|
|||||||
@@ -32,4 +32,4 @@ jobs:
|
|||||||
git \
|
git \
|
||||||
libssl-dev
|
libssl-dev
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: ./scripts/linux/test-linux.sh
|
run: ./scripts/test-linux.sh
|
||||||
+1
-2
@@ -106,5 +106,4 @@ yarn.lock
|
|||||||
/build*
|
/build*
|
||||||
/assets/test
|
/assets/test
|
||||||
/tools_old
|
/tools_old
|
||||||
/assets/test.png
|
/assets/test.png
|
||||||
/assets/audio/boa.mp3
|
|
||||||
@@ -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`.
|
||||||
+15
-40
@@ -8,22 +8,16 @@ cmake_minimum_required(VERSION 3.13)
|
|||||||
|
|
||||||
set(CMAKE_C_STANDARD 11)
|
set(CMAKE_C_STANDARD 11)
|
||||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||||
set(CMAKE_MODULE_PATH
|
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules)
|
||||||
${CMAKE_MODULE_PATH}
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules
|
|
||||||
)
|
|
||||||
cmake_policy(SET CMP0079 NEW)
|
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")
|
||||||
set(DUSK_GAME_SHORT_DESCRIPTION "Dusk game" CACHE STRING "One-line description")
|
set(DUSK_GAME_SHORT_DESCRIPTION "Dusk game" CACHE STRING "One-line description")
|
||||||
set(DUSK_GAME_LONG_DESCRIPTION
|
set(DUSK_GAME_LONG_DESCRIPTION "No description yet." CACHE STRING "Full description")
|
||||||
"No description yet." CACHE STRING "Full description"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Prep cache
|
# Prep cache
|
||||||
set(DUSK_CACHE_TARGET "dusk-target")
|
set(DUSK_CACHE_TARGET "dusk-target")
|
||||||
@@ -37,19 +31,13 @@ set(DUSK_TEMP_DIR "${DUSK_BUILD_DIR}/temp")
|
|||||||
set(DUSK_TOOLS_DIR "${DUSK_ROOT_DIR}/tools")
|
set(DUSK_TOOLS_DIR "${DUSK_ROOT_DIR}/tools")
|
||||||
set(DUSK_DATA_DIR "${DUSK_ROOT_DIR}/data")
|
set(DUSK_DATA_DIR "${DUSK_ROOT_DIR}/data")
|
||||||
set(DUSK_ASSETS_DIR "${DUSK_ROOT_DIR}/assets")
|
set(DUSK_ASSETS_DIR "${DUSK_ROOT_DIR}/assets")
|
||||||
set(DUSK_BUILT_ASSETS_DIR "${DUSK_BUILD_DIR}/built_assets"
|
set(DUSK_BUILT_ASSETS_DIR "${DUSK_BUILD_DIR}/built_assets" CACHE INTERNAL ${DUSK_CACHE_TARGET})
|
||||||
CACHE INTERNAL ${DUSK_CACHE_TARGET}
|
|
||||||
)
|
|
||||||
set(DUSK_GENERATED_HEADERS_DIR "${DUSK_BUILD_DIR}/generated")
|
set(DUSK_GENERATED_HEADERS_DIR "${DUSK_BUILD_DIR}/generated")
|
||||||
set(DUSK_BUILD_BINARY ${DUSK_BUILD_DIR}/Dusk
|
set(DUSK_BUILD_BINARY ${DUSK_BUILD_DIR}/Dusk CACHE INTERNAL ${DUSK_CACHE_TARGET})
|
||||||
CACHE INTERNAL ${DUSK_CACHE_TARGET}
|
|
||||||
)
|
|
||||||
set(DUSK_ASSETS "" CACHE INTERNAL ${DUSK_CACHE_TARGET})
|
set(DUSK_ASSETS "" CACHE INTERNAL ${DUSK_CACHE_TARGET})
|
||||||
set(DUSK_LIBRARY_TARGET_NAME "DuskCore" CACHE INTERNAL ${DUSK_CACHE_TARGET})
|
set(DUSK_LIBRARY_TARGET_NAME "DuskCore" CACHE INTERNAL ${DUSK_CACHE_TARGET})
|
||||||
set(DUSK_BINARY_TARGET_NAME "Dusk" CACHE INTERNAL ${DUSK_CACHE_TARGET})
|
set(DUSK_BINARY_TARGET_NAME "Dusk" CACHE INTERNAL ${DUSK_CACHE_TARGET})
|
||||||
set(DUSK_ASSETS_ZIP "${DUSK_BUILD_DIR}/dusk.dsk"
|
set(DUSK_ASSETS_ZIP "${DUSK_BUILD_DIR}/dusk.dsk" CACHE INTERNAL ${DUSK_CACHE_TARGET})
|
||||||
CACHE INTERNAL ${DUSK_CACHE_TARGET}
|
|
||||||
)
|
|
||||||
|
|
||||||
if(NOT DEFINED DUSK_TARGET_SYSTEM)
|
if(NOT DEFINED DUSK_TARGET_SYSTEM)
|
||||||
set(DUSK_TARGET_SYSTEM "linux")
|
set(DUSK_TARGET_SYSTEM "linux")
|
||||||
@@ -84,9 +72,7 @@ if(DUSK_BUILD_TESTS)
|
|||||||
${DUSK_LIBRARY_TARGET_NAME}
|
${DUSK_LIBRARY_TARGET_NAME}
|
||||||
)
|
)
|
||||||
else()
|
else()
|
||||||
set(DUSK_LIBRARY_TARGET_NAME "${DUSK_BINARY_TARGET_NAME}"
|
set(DUSK_LIBRARY_TARGET_NAME "${DUSK_BINARY_TARGET_NAME}" CACHE INTERNAL ${DUSK_CACHE_TARGET})
|
||||||
CACHE INTERNAL ${DUSK_CACHE_TARGET}
|
|
||||||
)
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(NOT DEFINED DUSK_VERSION)
|
if(NOT DEFINED DUSK_VERSION)
|
||||||
@@ -104,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)
|
||||||
|
|
||||||
@@ -135,21 +115,16 @@ if(DUSK_BUILD_TESTS)
|
|||||||
add_subdirectory(test)
|
add_subdirectory(test)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Build assets. Runs unconditionally on every build (rather than being
|
# Build assets
|
||||||
# gated behind a custom_command's file-level DEPENDS list) because
|
file(GLOB_RECURSE DUSK_ASSET_FILES CONFIGURE_DEPENDS "${DUSK_ASSETS_DIR}/*")
|
||||||
# CONFIGURE_DEPENDS globs only pick up added/removed files on the next
|
add_custom_command(
|
||||||
# CMake configure, so that approach could miss/lag a rebuild behind an
|
OUTPUT "${DUSK_ASSETS_ZIP}"
|
||||||
# asset edit. tools.asset.pack itself decides whether anything actually
|
|
||||||
# changed (see its manifest check) and no-ops quickly when nothing did,
|
|
||||||
# so this stays cheap on a build where assets are untouched.
|
|
||||||
add_custom_target(DUSK_ASSETS_BUILT ALL
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${DUSK_ASSETS_DIR}"
|
COMMAND ${CMAKE_COMMAND} -E make_directory "${DUSK_ASSETS_DIR}"
|
||||||
COMMAND ${Python3_EXECUTABLE} -m tools.asset.pack
|
COMMAND ${CMAKE_COMMAND} -E rm -f "${DUSK_ASSETS_ZIP}"
|
||||||
--input "${DUSK_ASSETS_DIR}"
|
COMMAND ${CMAKE_COMMAND} -E tar "cf" "${DUSK_ASSETS_ZIP}" --format=zip -- .
|
||||||
--output "${DUSK_ASSETS_ZIP}"
|
WORKING_DIRECTORY "${DUSK_ASSETS_DIR}"
|
||||||
WORKING_DIRECTORY "${DUSK_ROOT_DIR}"
|
DEPENDS ${DUSK_ASSET_FILES}
|
||||||
BYPRODUCTS "${DUSK_ASSETS_ZIP}"
|
|
||||||
COMMENT "Packing assets into dusk.dsk"
|
|
||||||
VERBATIM
|
VERBATIM
|
||||||
)
|
)
|
||||||
|
add_custom_target(DUSK_ASSETS_BUILT DEPENDS "${DUSK_ASSETS_ZIP}")
|
||||||
add_dependencies(${DUSK_LIBRARY_TARGET_NAME} DUSK_ASSETS_BUILT)
|
add_dependencies(${DUSK_LIBRARY_TARGET_NAME} DUSK_ASSETS_BUILT)
|
||||||
@@ -9,18 +9,15 @@ following packages, depending on your system;
|
|||||||
|
|
||||||
Fedora;
|
Fedora;
|
||||||
```
|
```
|
||||||
sudo dnf install git make gcc cmake python python-polib python3-pillow python3-dotenv python3-numpy python-qt5 python3-pyopengl SDL2-devel zlib-devel libzip-devel bzip2-devel openssl-devel lzma-sdk-devel xz xz-devel lua-devel libmad-devel
|
sudo dnf install git make gcc cmake python python-polib python3-pillow python3-dotenv python3-numpy python-qt5 python3-pyopengl SDL2-devel zlib-devel libzip-devel bzip2-devel openssl-devel lzma-sdk-devel xz xz-devel lua-devel
|
||||||
```
|
```
|
||||||
`libmad-devel` (used for MP3 decoding) is GPL-licensed and ships in the
|
|
||||||
[RPM Fusion](https://rpmfusion.org/) free repository rather than Fedora's
|
|
||||||
base repos - make sure RPM Fusion is enabled first.
|
|
||||||
|
|
||||||
Ubuntu;
|
Ubuntu;
|
||||||
```
|
```
|
||||||
sudo apt-get install git build-essential gcc python python-polib python3-pillow python3-dotenv python3-numpy python3-pyqt5 python3-opengl libmad0-dev
|
sudo apt-get install git build-essential gcc python python-polib python3-pillow python3-dotenv python3-numpy python3-pyqt5 python3-opengl
|
||||||
```
|
```
|
||||||
|
|
||||||
Arch Linux;
|
Arch Linux;
|
||||||
```
|
```
|
||||||
sudo pacman -S git base-devel gcc python python-polib python-pillow python-dotenv python-numpy python-pyqt5 python-opengl libmad
|
sudo pacman -S git base-devel gcc python python-polib python-pillow python-dotenv python-numpy python-pyqt5 python-opengl
|
||||||
```
|
```
|
||||||
+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.
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"moves": {
|
|
||||||
"fire": {
|
|
||||||
// Target flags
|
|
||||||
"enemies": true,
|
|
||||||
"allies": true,
|
|
||||||
"multiple": false,
|
|
||||||
"self": true,
|
|
||||||
"default": "enemy",
|
|
||||||
"target": "SINGLE_ENEMY"
|
|
||||||
},
|
|
||||||
|
|
||||||
"slash": {
|
|
||||||
"enemies": true,
|
|
||||||
"allies": true,
|
|
||||||
"self": false,
|
|
||||||
"default": "enemy",
|
|
||||||
"target": "SINGLE_ENEMY"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"type": "MODAL",
|
|
||||||
"title": "Fire",
|
|
||||||
"message": "A burst of flame scorches the target."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "WAIT",
|
|
||||||
"seconds": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "MODAL_CLOSE"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "BATTLE_FIGHTER_DAMAGE",
|
|
||||||
"fighter": "target",
|
|
||||||
"amount": 12
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"type": "MODAL",
|
|
||||||
"title": "Slash",
|
|
||||||
"message": "A quick blade strike cuts across the target."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "WAIT",
|
|
||||||
"seconds": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "MODAL_CLOSE"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "BATTLE_FIGHTER_DAMAGE",
|
|
||||||
"fighter": "target",
|
|
||||||
"amount": 8
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"type": "BATTLE_START",
|
|
||||||
"enemies": [
|
|
||||||
{
|
|
||||||
"attack": 8, "defense": 4, "magic": 0, "speed": 5, "luck": 2,
|
|
||||||
"healthMax": 30, "mpMax": 0,
|
|
||||||
"abilities": ["fire"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"attack": 6, "defense": 3, "magic": 0, "speed": 7, "luck": 2,
|
|
||||||
"healthMax": 20, "mpMax": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
{
|
|
||||||
"items": [
|
|
||||||
// First thing, try and find a save device, used for settings and what not.
|
|
||||||
{
|
|
||||||
"type": "MODAL",
|
|
||||||
"title": "initial.checking_save.title",
|
|
||||||
"message": "initial.checking_save.message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "WAIT",
|
|
||||||
"seconds": 0.2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "SAVE_DEVICE_CHECK",
|
|
||||||
"successMarker": "CONTINUE",
|
|
||||||
"failureMarker": "SAVE_DEVICE_NOT_FOUND"
|
|
||||||
},
|
|
||||||
|
|
||||||
// No save device found
|
|
||||||
{
|
|
||||||
"type": "MARKER",
|
|
||||||
"name": "SAVE_DEVICE_NOT_FOUND"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "MODAL_CLOSE"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "MODAL_OPTIONS_MARKERS",
|
|
||||||
"title": "initial.no_device.title",
|
|
||||||
"message": "initial.no_device.message",
|
|
||||||
"options": [
|
|
||||||
{
|
|
||||||
"text": "initial.no_device.retry",
|
|
||||||
"marker": "SAVE_DEVICE_RETRY"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "initial.no_device.continue",
|
|
||||||
"marker": "CONTINUE"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
// Save device not found, user to retry, just close modal and loop
|
|
||||||
{
|
|
||||||
"type": "MARKER",
|
|
||||||
"name": "SAVE_DEVICE_RETRY"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "MODAL_CLOSE"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "RESTART"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Save device found (or continuing without one)
|
|
||||||
{
|
|
||||||
"type": "MARKER",
|
|
||||||
"name": "CONTINUE"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "MODAL_CLOSE"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "CUTSCENE",
|
|
||||||
"name": "cutscenes/main_menu.jsonc"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
{
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"type": "MARKER",
|
|
||||||
"name": "MAIN_MENU_IDLE"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "UI_SHOW",
|
|
||||||
"name": "main_menu"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Options button goes here - no options screen yet, so this is just
|
|
||||||
// an alias landing right back on the idle loop below.
|
|
||||||
{
|
|
||||||
"type": "MARKER",
|
|
||||||
"name": "OPTIONS"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "IDLE"
|
|
||||||
},
|
|
||||||
|
|
||||||
// New Game button goes here.
|
|
||||||
{
|
|
||||||
"type": "MARKER",
|
|
||||||
"name": "NEW_GAME"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "MODAL",
|
|
||||||
"title": "main_menu.checking_save.title",
|
|
||||||
"message": "main_menu.checking_save.message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "WAIT",
|
|
||||||
"seconds": 0.2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "SAVE_DEVICE_CHECK",
|
|
||||||
"successMarker": "SAVE_DEVICE_FOUND",
|
|
||||||
"failureMarker": "SAVE_DEVICE_NOT_FOUND"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Save device not found
|
|
||||||
{
|
|
||||||
"type": "MARKER",
|
|
||||||
"name": "SAVE_DEVICE_NOT_FOUND"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "MODAL_CLOSE"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "MODAL_OPTIONS_MARKERS",
|
|
||||||
"title": "main_menu.no_device.title",
|
|
||||||
"message": "main_menu.no_device.message",
|
|
||||||
"options": [
|
|
||||||
{
|
|
||||||
"text": "main_menu.no_device.retry",
|
|
||||||
"marker": "NEW_GAME"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "main_menu.no_device.continue",
|
|
||||||
"marker": "SAVE_DEVICE_FOUND"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
// Save data failed to load (e.g. corrupt/unreadable) - only option is
|
|
||||||
// to retry, no "continue without saving" here since we already know a
|
|
||||||
// device is present.
|
|
||||||
{
|
|
||||||
"type": "MARKER",
|
|
||||||
"name": "LOAD_ERROR"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "MODAL_OPTIONS_MARKERS",
|
|
||||||
"title": "main_menu.save_load_error.title",
|
|
||||||
"message": "main_menu.save_load_error.message",
|
|
||||||
"options": [
|
|
||||||
{
|
|
||||||
"text": "main_menu.save_load_error.retry",
|
|
||||||
"marker": "NEW_GAME"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
// Quit Game button goes here. Cancelling jumps back to
|
|
||||||
// MAIN_MENU_IDLE; confirming falls through to QUIT_GAME, which
|
|
||||||
// ends the process directly.
|
|
||||||
{
|
|
||||||
"type": "MARKER",
|
|
||||||
"name": "QUIT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "MODAL_OPTIONS_MARKERS",
|
|
||||||
"title": "main_menu.quit_confirm",
|
|
||||||
"options": [
|
|
||||||
{
|
|
||||||
"text": "ui.confirm.confirm",
|
|
||||||
"marker": "QUIT_CONFIRMED"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "ui.confirm.cancel",
|
|
||||||
"marker": "MAIN_MENU_IDLE"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"type": "MARKER",
|
|
||||||
"name": "QUIT_CONFIRMED"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "QUIT_GAME"
|
|
||||||
},
|
|
||||||
|
|
||||||
// main menu just listens for cutscene ending to show the select save modal
|
|
||||||
// so we have that here.
|
|
||||||
{
|
|
||||||
"type": "MARKER",
|
|
||||||
"name": "SAVE_DEVICE_FOUND"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "MODAL_CLOSE"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "SAVE_LOAD_ALL_SLOTS",
|
|
||||||
"successMarker": "LOADED",
|
|
||||||
"failureMarker": "LOAD_ERROR"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Must be the very last item - falling off the end here is what
|
|
||||||
// completes the cutscene and fires uiMainMenuOpenSelectSave (see
|
|
||||||
// cutsceneSystemSetOnComplete in ui/screen/mainmenu/uimainmenu.c).
|
|
||||||
// Placing this anywhere earlier would fall through into whatever
|
|
||||||
// comes next instead (e.g. the QUIT block, if placed just before it).
|
|
||||||
{
|
|
||||||
"type": "MARKER",
|
|
||||||
"name": "LOADED"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
// Placeholder for the new-game intro flow - just shows a modal for now.
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"type": "MODAL",
|
|
||||||
"title": "New Game",
|
|
||||||
"message": "This is a placeholder for the new game intro.",
|
|
||||||
"blocking": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"items": {
|
|
||||||
"potion": { "type": "MEDICINE" },
|
|
||||||
"potato": { "type": "FOOD" },
|
|
||||||
"apple": { "type": "FOOD" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-224
@@ -5,113 +5,11 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n==2 ? 1 : (n<7 ? 2 : 3));\n"
|
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n==2 ? 1 : (n<7 ? 2 : 3));\n"
|
||||||
|
|
||||||
# Initial Scene
|
|
||||||
msgid "initial.checking_save.title"
|
|
||||||
msgstr "Checking for save data"
|
|
||||||
|
|
||||||
msgid "initial.checking_save.message"
|
|
||||||
msgstr "Please wait..."
|
|
||||||
|
|
||||||
msgid "initial.no_device.title"
|
|
||||||
msgstr "No Save Device Found"
|
|
||||||
|
|
||||||
msgid "initial.no_device.message"
|
|
||||||
msgstr "Could not find a save device, ensure it is connected and try again. You can continue, but progress will not be saved."
|
|
||||||
|
|
||||||
msgid "initial.no_device.retry"
|
|
||||||
msgstr "Try again"
|
|
||||||
|
|
||||||
msgid "initial.no_device.continue"
|
|
||||||
msgstr "Continue without saving"
|
|
||||||
|
|
||||||
|
|
||||||
# Main Menu Scene
|
|
||||||
msgid "main_menu.start_game"
|
|
||||||
msgstr "Start Game"
|
|
||||||
|
|
||||||
msgid "main_menu.options"
|
|
||||||
msgstr "Options"
|
|
||||||
|
|
||||||
msgid "main_menu.quit"
|
|
||||||
msgstr "Quit Game"
|
|
||||||
|
|
||||||
msgid "main_menu.quit_confirm"
|
|
||||||
msgstr "Are you sure you want to quit?"
|
|
||||||
|
|
||||||
msgid "main_menu.checking_save.title"
|
|
||||||
msgstr "Checking for save data"
|
|
||||||
|
|
||||||
msgid "main_menu.checking_save.message"
|
|
||||||
msgstr "Please wait..."
|
|
||||||
|
|
||||||
msgid "main_menu.no_device.title"
|
|
||||||
msgstr "No Save Device Found"
|
|
||||||
|
|
||||||
msgid "main_menu.no_device.message"
|
|
||||||
msgstr "Could not find a save device, ensure it is connected and try again. You can continue, but progress will not be saved."
|
|
||||||
|
|
||||||
msgid "main_menu.no_device.retry"
|
|
||||||
msgstr "Try again"
|
|
||||||
|
|
||||||
msgid "main_menu.no_device.continue"
|
|
||||||
msgstr "Continue without saving"
|
|
||||||
|
|
||||||
msgid "main_menu.save_load_error.title"
|
|
||||||
msgstr "Error"
|
|
||||||
|
|
||||||
msgid "main_menu.save_load_error.message"
|
|
||||||
msgstr "Failed to load save data. Please try again."
|
|
||||||
|
|
||||||
msgid "main_menu.save_load_error.retry"
|
|
||||||
msgstr "Try Again"
|
|
||||||
|
|
||||||
|
|
||||||
# Select Save Screen
|
|
||||||
msgid "ui.select_save.title"
|
|
||||||
msgstr "Select Save"
|
|
||||||
|
|
||||||
msgid "ui.select_save.empty"
|
|
||||||
msgstr "Empty Slot"
|
|
||||||
|
|
||||||
msgid "ui.select_save.corrupt"
|
|
||||||
msgstr "Corrupted Save"
|
|
||||||
|
|
||||||
msgid "ui.select_save.corrupt_warning"
|
|
||||||
msgstr "This save file is corrupted and could not be loaded. Please contact support if this issue persists."
|
|
||||||
|
|
||||||
msgid "ui.select_save.slot_format"
|
|
||||||
msgstr "%s Lv.%d %s"
|
|
||||||
|
|
||||||
msgid "ui.select_save.delete_mode"
|
|
||||||
msgstr "Delete a Save"
|
|
||||||
|
|
||||||
msgid "ui.select_save.delete_confirm"
|
|
||||||
msgstr "Are you sure you want to delete this save?"
|
|
||||||
|
|
||||||
msgid "ui.select_save.name_title"
|
|
||||||
msgstr "Enter game save file name"
|
|
||||||
|
|
||||||
msgid "ui.save_slot.number_format"
|
|
||||||
msgstr "Slot %d"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#: ui/menu.c:10
|
#: ui/menu.c:10
|
||||||
msgid "ui.title"
|
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"
|
||||||
@@ -158,118 +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/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"
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Items
|
|
||||||
#
|
|
||||||
msgid "item.potion.name"
|
msgid "item.potion.name"
|
||||||
msgstr "Potion"
|
msgstr "Potion"
|
||||||
|
|
||||||
@@ -277,13 +63,4 @@ msgid "item.potato.name"
|
|||||||
msgstr "Potato"
|
msgstr "Potato"
|
||||||
|
|
||||||
msgid "item.apple.name"
|
msgid "item.apple.name"
|
||||||
msgstr "Apple"
|
msgstr "Apple"
|
||||||
|
|
||||||
#
|
|
||||||
# Moves
|
|
||||||
#
|
|
||||||
msgid "move.fire.name"
|
|
||||||
msgstr "Fire"
|
|
||||||
|
|
||||||
msgid "move.slash.name"
|
|
||||||
msgstr "Slash"
|
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: ExampleApp 1.0\n"
|
||||||
|
"Language: es\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n==1 ? 0 : 1);\n"
|
||||||
|
|
||||||
|
#: ui/menu.c:10
|
||||||
|
msgid "ui.title"
|
||||||
|
msgstr ""
|
||||||
|
"Bienvenido"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.tabs.general"
|
||||||
|
msgstr "General"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.tabs.input"
|
||||||
|
msgstr "Entrada"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.tabs.display"
|
||||||
|
msgstr "Pantalla"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.tabs.audio"
|
||||||
|
msgstr "Audio"
|
||||||
|
|
||||||
|
msgid "ui.settings.input.deadzone"
|
||||||
|
msgstr "Deadzone"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||||
|
msgid "ui.settings.general.language"
|
||||||
|
msgstr "Idioma"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||||
|
msgid "ui.settings.general.language_detail"
|
||||||
|
msgstr "Se aplica después de reiniciar la aplicación."
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.apply"
|
||||||
|
msgstr "Aplicar"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/uiconfirm.c
|
||||||
|
msgid "ui.confirm.discard_changes"
|
||||||
|
msgstr "¿Descartar los cambios no guardados?"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.characters"
|
||||||
|
msgstr "Personajes"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.items"
|
||||||
|
msgstr "Objetos"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.settings"
|
||||||
|
msgstr "Configuración"
|
||||||
|
|
||||||
|
#: src/dusk/rpg/item/item.json
|
||||||
|
msgid "item.potion.name"
|
||||||
|
msgstr "Poción"
|
||||||
|
|
||||||
|
#: src/dusk/rpg/item/item.json
|
||||||
|
msgid "item.potato.name"
|
||||||
|
msgstr "Papa"
|
||||||
|
|
||||||
|
#: src/dusk/rpg/item/item.json
|
||||||
|
msgid "item.apple.name"
|
||||||
|
msgstr "Manzana"
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: ExampleApp 1.0\n"
|
||||||
|
"Language: ja\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Plural-Forms: nplurals=1; plural=(0);\n"
|
||||||
|
|
||||||
|
#: ui/menu.c:10
|
||||||
|
msgid "ui.title"
|
||||||
|
msgstr ""
|
||||||
|
"歓迎"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.tabs.general"
|
||||||
|
msgstr "一般"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.tabs.input"
|
||||||
|
msgstr "入力"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.tabs.display"
|
||||||
|
msgstr "表示"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.tabs.audio"
|
||||||
|
msgstr "オーディオ"
|
||||||
|
|
||||||
|
msgid "ui.settings.input.deadzone"
|
||||||
|
msgstr "デッドゾーン"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||||
|
msgid "ui.settings.general.language"
|
||||||
|
msgstr "言語"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||||
|
msgid "ui.settings.general.language_detail"
|
||||||
|
msgstr "アプリケーションを再起動すると適用されます。"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.apply"
|
||||||
|
msgstr "適用"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/uiconfirm.c
|
||||||
|
msgid "ui.confirm.discard_changes"
|
||||||
|
msgstr "未保存の変更を破棄しますか?"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.characters"
|
||||||
|
msgstr "キャラクター"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.items"
|
||||||
|
msgstr "アイテム"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.settings"
|
||||||
|
msgstr "設定"
|
||||||
|
|
||||||
|
#: src/dusk/rpg/item/item.json
|
||||||
|
msgid "item.potion.name"
|
||||||
|
msgstr "ポーション"
|
||||||
|
|
||||||
|
#: src/dusk/rpg/item/item.json
|
||||||
|
msgid "item.potato.name"
|
||||||
|
msgstr "ジャガイモ"
|
||||||
|
|
||||||
|
#: src/dusk/rpg/item/item.json
|
||||||
|
msgid "item.apple.name"
|
||||||
|
msgstr "リンゴ"
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"meshes":[{"file":"house_4_4.dmf","pos":[2,1,0]},{"file":"house_6_3.dmf","pos":[1,9,0]},{"file":"house_2_2.dmf","pos":[12,6,0]}],"tiles":[[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0]]}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"tiles":[[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[6,0],[2,0],[2,0],[2,0],[2,0],[2,0],[2,0],[7,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[3,0],[1,1],[1,1],[1,1],[1,1],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[3,0],[1,1],[1,1],[1,1],[1,1],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[3,0],[1,1],[1,1],[1,1],[1,1],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[8,0],[4,0],[4,0],[4,0],[12,0],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[3,0],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[3,0],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[8,0],[4,0],[4,0],[9,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0]],"meshes":[{"file":"house_5_3.dmf","pos":[1,1,0]}],"entities":[{"type":"global","globalId":3,"pos":[8,8,1]},{"type":"global","globalId":4,"pos":[12,2,0]}],"areas":[{"min":[11,3,0],"max":[16,9,10],"callbackId":1,"notify":3,"trigger":6}]}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"meshes":[{"file":"house_8_4.dmf","pos":[1,1,0]},{"file":"house_3_3.dmf","pos":[11,2,0]},{"file":"house_2_2.dmf","pos":[3,11,0]}],"tiles":[[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,null,null,null,[1,0],[1,0],null,null,null,[1,0],[1,0],[1,0],null,null,null,null,null,null,null,null,[1,0],[1,0],null,null,null,[1,0],[1,0],[1,0],null,null,null,null,null,null,null,null,[1,0],[1,0],null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0]]}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"meshes":[{"file":"house_3_2.dmf","pos":[1,1,0]},{"file":"house_2_3.dmf","pos":[7,2,0]},{"file":"house_4_2.dmf","pos":[1,8,0]},{"file":"house_1_1.dmf","pos":[11,10,0]},{"file":"house_5_2.dmf","pos":[13,6,0]}],"tiles":[[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,[1,0],null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0]]}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"meshes":[{"file":"house_5_5.dmf","pos":[3,2,0]},{"file":"house_2_3.dmf","pos":[10,3,0]},{"file":"house_3_1.dmf","pos":[1,12,0]}],"tiles":[[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0]]}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"tiles":[[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,3],[1,3],[1,3],[1,3],[1,3],[1,3],[1,0],[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,3],[1,3],[1,3],[1,3],[1,3],[1,3],[1,0],[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,3],[1,3],[1,3],[1,3],[1,3],[1,3],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[4,1],[4,1],[4,1],[4,1],[4,1],[4,1],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[4,0],[4,0],[4,0],[4,0],[4,0],[4,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0]],"meshes":[]}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"mesh": "meshes/chunks/chunk_-1_0_0_0.dmf",
|
||||||
|
"color": [
|
||||||
|
255,
|
||||||
|
255,
|
||||||
|
255,
|
||||||
|
255
|
||||||
|
],
|
||||||
|
"texture": "tiles.png"
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"mesh": "meshes/chunks/chunk_0_0_0_0.dmf",
|
||||||
|
"color": [
|
||||||
|
255,
|
||||||
|
255,
|
||||||
|
255,
|
||||||
|
255
|
||||||
|
],
|
||||||
|
"texture": "tiles.png"
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"mesh": "meshes/chunks/chunk_0_1_0_0.dmf",
|
||||||
|
"color": [
|
||||||
|
255,
|
||||||
|
255,
|
||||||
|
255,
|
||||||
|
255
|
||||||
|
],
|
||||||
|
"texture": "tiles.png"
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"mesh": "meshes/chunks/chunk_1_0_0_0.dmf",
|
||||||
|
"color": [
|
||||||
|
255,
|
||||||
|
255,
|
||||||
|
255,
|
||||||
|
255
|
||||||
|
],
|
||||||
|
"texture": "tiles.png"
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"mesh": "meshes/chunks/chunk_1_1_0_0.dmf",
|
||||||
|
"color": [
|
||||||
|
255,
|
||||||
|
255,
|
||||||
|
255,
|
||||||
|
255
|
||||||
|
],
|
||||||
|
"texture": "tiles.png"
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"mesh": "meshes/chunks/chunk_2_0_0_0.dmf",
|
||||||
|
"color": [
|
||||||
|
255,
|
||||||
|
255,
|
||||||
|
255,
|
||||||
|
255
|
||||||
|
],
|
||||||
|
"texture": "tiles.png"
|
||||||
|
}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"members": {
|
|
||||||
"hero": {
|
|
||||||
"attack": 10,
|
|
||||||
"defense": 5,
|
|
||||||
"magic": 0,
|
|
||||||
"speed": 10,
|
|
||||||
"luck": 0,
|
|
||||||
"healthMax": 30,
|
|
||||||
"mpMax": 10
|
|
||||||
},
|
|
||||||
"companion": {
|
|
||||||
"attack": 8,
|
|
||||||
"defense": 6,
|
|
||||||
"magic": 4,
|
|
||||||
"speed": 8,
|
|
||||||
"luck": 1,
|
|
||||||
"healthMax": 25,
|
|
||||||
"mpMax": 15
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"flags": {
|
|
||||||
"test": {
|
|
||||||
"initial": true
|
|
||||||
},
|
|
||||||
"test2": {
|
|
||||||
"initial": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1,32 +0,0 @@
|
|||||||
# Copyright (c) 2026 Dominic Masters
|
|
||||||
#
|
|
||||||
# This software is released under the MIT License.
|
|
||||||
# https://opensource.org/licenses/MIT
|
|
||||||
|
|
||||||
# NOTE: libmad itself is GPL-licensed (see https://www.underbit.com/products/mad/),
|
|
||||||
# not MIT like the rest of this project. Any binary linking it must comply
|
|
||||||
# with the GPL - a deliberate tradeoff accepted for MP3 decode correctness
|
|
||||||
# (see audiostreammp3decodersw.c's own comment on why minimp3 wasn't enough).
|
|
||||||
|
|
||||||
find_path(LIBMAD_INCLUDE_DIR NAMES mad.h)
|
|
||||||
mark_as_advanced(LIBMAD_INCLUDE_DIR)
|
|
||||||
|
|
||||||
find_library(LIBMAD_LIBRARY NAMES mad)
|
|
||||||
mark_as_advanced(LIBMAD_LIBRARY)
|
|
||||||
|
|
||||||
include(FindPackageHandleStandardArgs)
|
|
||||||
find_package_handle_standard_args(
|
|
||||||
libmad
|
|
||||||
REQUIRED_VARS
|
|
||||||
LIBMAD_LIBRARY
|
|
||||||
LIBMAD_INCLUDE_DIR
|
|
||||||
)
|
|
||||||
|
|
||||||
if(libmad_FOUND AND NOT TARGET libmad::mad)
|
|
||||||
add_library(libmad::mad UNKNOWN IMPORTED)
|
|
||||||
set_target_properties(libmad::mad
|
|
||||||
PROPERTIES
|
|
||||||
INTERFACE_INCLUDE_DIRECTORIES ${LIBMAD_INCLUDE_DIR}
|
|
||||||
IMPORTED_LOCATION ${LIBMAD_LIBRARY}
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
@@ -61,8 +61,6 @@ target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
|
|||||||
zstd
|
zstd
|
||||||
z
|
z
|
||||||
lzma
|
lzma
|
||||||
asnd
|
|
||||||
mad
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO")
|
if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO")
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
|||||||
)
|
)
|
||||||
|
|
||||||
set(DUSK_BACKTRACE ON CACHE BOOL "Enable backtrace support for assert failures.")
|
set(DUSK_BACKTRACE ON CACHE BOOL "Enable backtrace support for assert failures.")
|
||||||
set(DUSK_TRACK_MEMORY ON CACHE BOOL "Track memoryAllocate() call sites to help find memory leaks.")
|
|
||||||
|
|
||||||
# Define platform-specific macros.
|
# Define platform-specific macros.
|
||||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||||
|
|||||||
+1
-61
@@ -30,7 +30,6 @@ target_link_libraries(${DUSK_BINARY_TARGET_NAME} PUBLIC
|
|||||||
pspgu
|
pspgu
|
||||||
pspaudio
|
pspaudio
|
||||||
pspaudiolib
|
pspaudiolib
|
||||||
pspmp3
|
|
||||||
psputility
|
psputility
|
||||||
pspvfpu
|
pspvfpu
|
||||||
pspvram
|
pspvram
|
||||||
@@ -60,42 +59,12 @@ target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
|
|||||||
DUSK_DISPLAY_OVERSCAN=6
|
DUSK_DISPLAY_OVERSCAN=6
|
||||||
)
|
)
|
||||||
|
|
||||||
# logDebug()/logError() additionally write every call to a file on the
|
|
||||||
# Memory Stick (ms0:/PSP/GAME/Dusk/*.log) - a real, measured source of
|
|
||||||
# per-call hitching (fopen+write+fclose every log line), not just a
|
|
||||||
# theoretical cost. Default off so a normal build doesn't pay it; opt in
|
|
||||||
# with -DDUSK_PSP_LOG_FILE=ON when the log file itself is actually needed
|
|
||||||
# (e.g. no way to see stdout on real hardware).
|
|
||||||
set(DUSK_PSP_LOG_FILE OFF CACHE BOOL "Also write PSP debug/error logs to ms0:/PSP/GAME/Dusk/*.log (slow - Memory Stick I/O per call).")
|
|
||||||
if(DUSK_PSP_LOG_FILE)
|
|
||||||
target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
|
|
||||||
DUSK_PSP_LOG_FILE
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||||
target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
|
target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
|
||||||
DUSK_ASSERTIONS_FAKED
|
DUSK_ASSERTIONS_FAKED
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Generate PARAM.SFO as a normal tracked build output (instead of letting
|
|
||||||
# create_pbp_file() auto-generate + delete it) so it can be reused below by
|
|
||||||
# a properly dependency-tracked EBOOT.PBP repack step.
|
|
||||||
set(DUSK_PSP_PARAM_SFO "${DUSK_BUILD_DIR}/PARAM.SFO")
|
|
||||||
add_custom_command(
|
|
||||||
OUTPUT "${DUSK_PSP_PARAM_SFO}"
|
|
||||||
COMMAND "$ENV{PSPDEV}/bin/mksfoex" "-d" "MEMSIZE=1" "-s" "APP_VER=01.00"
|
|
||||||
"${DUSK_BINARY_TARGET_NAME}" "${DUSK_PSP_PARAM_SFO}"
|
|
||||||
COMMENT "Generating PARAM.SFO for ${DUSK_BINARY_TARGET_NAME}"
|
|
||||||
VERBATIM
|
|
||||||
)
|
|
||||||
add_custom_target(DuskPspParamSfo DEPENDS "${DUSK_PSP_PARAM_SFO}")
|
|
||||||
|
|
||||||
# create_pbp_file()'s own POST_BUILD chain (below) also consumes
|
|
||||||
# DUSK_PSP_PARAM_SFO, so make sure it exists before that chain runs.
|
|
||||||
add_dependencies(${DUSK_BINARY_TARGET_NAME} DuskPspParamSfo)
|
|
||||||
|
|
||||||
# Postbuild, create .pbp file for PSP.
|
# Postbuild, create .pbp file for PSP.
|
||||||
create_pbp_file(
|
create_pbp_file(
|
||||||
TARGET "${DUSK_BINARY_TARGET_NAME}"
|
TARGET "${DUSK_BINARY_TARGET_NAME}"
|
||||||
@@ -105,33 +74,4 @@ create_pbp_file(
|
|||||||
TITLE "${DUSK_BINARY_TARGET_NAME}"
|
TITLE "${DUSK_BINARY_TARGET_NAME}"
|
||||||
PSAR_PATH ${DUSK_ASSETS_ZIP}
|
PSAR_PATH ${DUSK_ASSETS_ZIP}
|
||||||
VERSION 01.00
|
VERSION 01.00
|
||||||
SFO_PATH "${DUSK_PSP_PARAM_SFO}"
|
)
|
||||||
OUTPUT_DIR "${DUSK_BUILD_DIR}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# CreatePBP.cmake's pack-pbp step is a POST_BUILD command tied to the
|
|
||||||
# executable target, so it only reruns when the ELF itself relinks. That
|
|
||||||
# means regenerating dusk.dsk (assets) alone, without touching any C
|
|
||||||
# source, silently leaves EBOOT.PBP embedding a stale asset pak. Repack it
|
|
||||||
# here as a normal file-tracked custom command depending on both the
|
|
||||||
# executable and the asset zip, so EBOOT.PBP always reflects the current
|
|
||||||
# assets even when nothing else about the build changed.
|
|
||||||
set(DUSK_PSP_EBOOT "${DUSK_BUILD_DIR}/EBOOT.PBP")
|
|
||||||
if(BUILD_PRX)
|
|
||||||
set(DUSK_PSP_EXECUTABLE "$<TARGET_FILE:${DUSK_BINARY_TARGET_NAME}>.prx")
|
|
||||||
else()
|
|
||||||
set(DUSK_PSP_EXECUTABLE "$<TARGET_FILE:${DUSK_BINARY_TARGET_NAME}>")
|
|
||||||
endif()
|
|
||||||
add_custom_command(
|
|
||||||
OUTPUT "${DUSK_PSP_EBOOT}"
|
|
||||||
COMMAND "$ENV{PSPDEV}/bin/pack-pbp" "${DUSK_PSP_EBOOT}" "${DUSK_PSP_PARAM_SFO}"
|
|
||||||
"NULL" "NULL" "NULL" "NULL" "NULL"
|
|
||||||
"${DUSK_PSP_EXECUTABLE}" "${DUSK_ASSETS_ZIP}"
|
|
||||||
DEPENDS
|
|
||||||
"$<TARGET_FILE:${DUSK_BINARY_TARGET_NAME}>"
|
|
||||||
"${DUSK_PSP_PARAM_SFO}"
|
|
||||||
"${DUSK_ASSETS_ZIP}"
|
|
||||||
COMMENT "Repacking EBOOT.PBP (tracks executable + asset pak freshness)"
|
|
||||||
VERBATIM
|
|
||||||
)
|
|
||||||
add_custom_target(DuskPspEbootRepack ALL DEPENDS "${DUSK_PSP_EBOOT}")
|
|
||||||
@@ -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 "SD" 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(
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
// Copyright (c) 2026 Dominic Masters
|
||||||
|
//
|
||||||
|
// This software is released under the MIT License.
|
||||||
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// ChunkTerrain - JS port of the terrain-quad generation performed by
|
||||||
|
// tools/asset/chunk/__main__.py (build_terrain_verts / _tile_quad /
|
||||||
|
// _RAMP_CORNERS) so the editor's 3D preview can show the same geometry the
|
||||||
|
// Python compiler will bake into the chunk's terrain mesh, without waiting
|
||||||
|
// for a compile pass. Vertex layout matches DMF: interleaved [u, v, x, y, z].
|
||||||
|
|
||||||
|
const ChunkTerrain = (() => {
|
||||||
|
const CHUNK_WIDTH = 16;
|
||||||
|
const CHUNK_HEIGHT = 16;
|
||||||
|
const CHUNK_DEPTH = 4;
|
||||||
|
const TILE_COUNT = CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH;
|
||||||
|
|
||||||
|
// World-space Z distance of one Z-layer/story, in world-float space.
|
||||||
|
// Must match WORLD_LAYER_HEIGHT in src/dusk/rpg/overworld/worldpos.h.
|
||||||
|
const WORLD_LAYER_HEIGHT = Math.SQRT1_2;
|
||||||
|
|
||||||
|
const TILE_SHAPE_NULL = 0;
|
||||||
|
const TILE_SHAPE_GROUND = 1;
|
||||||
|
const TILE_SHAPE_RAMP_NORTH = 2;
|
||||||
|
const TILE_SHAPE_RAMP_EAST = 3;
|
||||||
|
const TILE_SHAPE_RAMP_SOUTH = 4;
|
||||||
|
const TILE_SHAPE_RAMP_WEST = 5;
|
||||||
|
const TILE_SHAPE_RAMP_NORTHEAST = 6;
|
||||||
|
const TILE_SHAPE_RAMP_NORTHWEST = 7;
|
||||||
|
const TILE_SHAPE_RAMP_SOUTHEAST = 8;
|
||||||
|
const TILE_SHAPE_RAMP_SOUTHWEST = 9;
|
||||||
|
const TILE_SHAPE_RAMP_NORTHEAST_INNER = 10;
|
||||||
|
const TILE_SHAPE_RAMP_NORTHWEST_INNER = 11;
|
||||||
|
const TILE_SHAPE_RAMP_SOUTHEAST_INNER = 12;
|
||||||
|
const TILE_SHAPE_RAMP_SOUTHWEST_INNER = 13;
|
||||||
|
|
||||||
|
// Per-shape corner heights as [sw, se, ne, nw] offsets from tile base Z.
|
||||||
|
// NORTH=+Y, EAST=+X. Cardinal ramps: low face at 0, high face at 1.
|
||||||
|
// Diagonal outer-corner ramps: only the named corner raised, rest at 0.
|
||||||
|
// Diagonal inner-corner ramps: only the corner opposite the named one is
|
||||||
|
// lowered to 0, the rest (including the named corner) stay raised at 1.
|
||||||
|
const RAMP_CORNERS = {
|
||||||
|
[TILE_SHAPE_GROUND]: [0.0, 0.0, 0.0, 0.0],
|
||||||
|
[TILE_SHAPE_RAMP_NORTH]: [0.0, 0.0, 1.0, 1.0],
|
||||||
|
[TILE_SHAPE_RAMP_SOUTH]: [1.0, 1.0, 0.0, 0.0],
|
||||||
|
[TILE_SHAPE_RAMP_EAST]: [0.0, 1.0, 1.0, 0.0],
|
||||||
|
[TILE_SHAPE_RAMP_WEST]: [1.0, 0.0, 0.0, 1.0],
|
||||||
|
[TILE_SHAPE_RAMP_NORTHEAST]: [0.0, 0.0, 1.0, 0.0],
|
||||||
|
[TILE_SHAPE_RAMP_NORTHWEST]: [0.0, 0.0, 0.0, 1.0],
|
||||||
|
[TILE_SHAPE_RAMP_SOUTHEAST]: [0.0, 1.0, 0.0, 0.0],
|
||||||
|
[TILE_SHAPE_RAMP_SOUTHWEST]: [1.0, 0.0, 0.0, 0.0],
|
||||||
|
[TILE_SHAPE_RAMP_NORTHEAST_INNER]: [0.0, 1.0, 1.0, 1.0],
|
||||||
|
[TILE_SHAPE_RAMP_NORTHWEST_INNER]: [1.0, 0.0, 1.0, 1.0],
|
||||||
|
[TILE_SHAPE_RAMP_SOUTHEAST_INNER]: [1.0, 1.0, 1.0, 0.0],
|
||||||
|
[TILE_SHAPE_RAMP_SOUTHWEST_INNER]: [1.0, 1.0, 0.0, 1.0],
|
||||||
|
};
|
||||||
|
|
||||||
|
function tileIndex(x, y, z) {
|
||||||
|
return x + y * CHUNK_WIDTH + z * CHUNK_WIDTH * CHUNK_HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Six vertices (2 CCW triangles) for a quad with per-corner Z heights.
|
||||||
|
// Corner order: SW=(fx,fy), SE=(fx+1,fy), NE=(fx+1,fy+1), NW=(fx,fy+1).
|
||||||
|
function pushTileQuad(out, u0, u1, v0, v1, fx, fy, swZ, seZ, neZ, nwZ) {
|
||||||
|
out.push(
|
||||||
|
u0, v0, fx, fy, swZ,
|
||||||
|
u1, v0, fx + 1, fy, seZ,
|
||||||
|
u1, v1, fx + 1, fy + 1, neZ,
|
||||||
|
u0, v0, fx, fy, swZ,
|
||||||
|
u1, v1, fx + 1, fy + 1, neZ,
|
||||||
|
u0, v1, fx, fy + 1, nwZ,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate terrain quads for every non-null tile in a flat tiles array
|
||||||
|
// (length TILE_COUNT, indexed via tileIndex). Returns a Float32Array of
|
||||||
|
// interleaved [u, v, x, y, z] vertices, matching DMF.VERTEX_FLOATS.
|
||||||
|
function buildTerrainVerts(tiles) {
|
||||||
|
const out = [];
|
||||||
|
for(let z = 0; z < CHUNK_DEPTH; z++) {
|
||||||
|
for(let y = 0; y < CHUNK_HEIGHT; y++) {
|
||||||
|
for(let x = 0; x < CHUNK_WIDTH; x++) {
|
||||||
|
const type = tiles[tileIndex(x, y, z)] || TILE_SHAPE_NULL;
|
||||||
|
const corners = RAMP_CORNERS[type];
|
||||||
|
if(!corners) continue;
|
||||||
|
|
||||||
|
const u0 = x / CHUNK_WIDTH;
|
||||||
|
const u1 = (x + 1) / CHUNK_WIDTH;
|
||||||
|
const v0 = y / CHUNK_HEIGHT;
|
||||||
|
const v1 = (y + 1) / CHUNK_HEIGHT;
|
||||||
|
const [sw, se, ne, nw] = corners;
|
||||||
|
|
||||||
|
pushTileQuad(
|
||||||
|
out, u0, u1, v0, v1, x, y,
|
||||||
|
(z + sw) * WORLD_LAYER_HEIGHT,
|
||||||
|
(z + se) * WORLD_LAYER_HEIGHT,
|
||||||
|
(z + ne) * WORLD_LAYER_HEIGHT,
|
||||||
|
(z + nw) * WORLD_LAYER_HEIGHT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Float32Array(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Four edges (SW-SE, SE-NE, NE-NW, NW-SW) of a tile's quad as line
|
||||||
|
// segments, lifted slightly above the terrain surface to avoid
|
||||||
|
// z-fighting - same corner-height convention as pushTileQuad().
|
||||||
|
function pushTileGridLines(out, fx, fy, swZ, seZ, neZ, nwZ) {
|
||||||
|
const eps = 0.01;
|
||||||
|
const sw = [fx, fy, swZ + eps];
|
||||||
|
const se = [fx + 1, fy, seZ + eps];
|
||||||
|
const ne = [fx + 1, fy + 1, neZ + eps];
|
||||||
|
const nw = [fx, fy + 1, nwZ + eps];
|
||||||
|
const edges = [sw, se, se, ne, ne, nw, nw, sw];
|
||||||
|
for(const [x, y, z] of edges) out.push(0, 0, x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate grid-outline line segments for every non-null tile, following
|
||||||
|
// the same per-corner heights as buildTerrainVerts() so lines hug ramps
|
||||||
|
// instead of cutting through them. Returns a Float32Array of interleaved
|
||||||
|
// [u, v, x, y, z] vertices meant to be drawn with gl.LINES (pairs).
|
||||||
|
function buildGridLines(tiles) {
|
||||||
|
const out = [];
|
||||||
|
for(let z = 0; z < CHUNK_DEPTH; z++) {
|
||||||
|
for(let y = 0; y < CHUNK_HEIGHT; y++) {
|
||||||
|
for(let x = 0; x < CHUNK_WIDTH; x++) {
|
||||||
|
const type = tiles[tileIndex(x, y, z)] || TILE_SHAPE_NULL;
|
||||||
|
const corners = RAMP_CORNERS[type];
|
||||||
|
if(!corners) continue;
|
||||||
|
|
||||||
|
const [sw, se, ne, nw] = corners;
|
||||||
|
pushTileGridLines(
|
||||||
|
out, x, y,
|
||||||
|
(z + sw) * WORLD_LAYER_HEIGHT,
|
||||||
|
(z + se) * WORLD_LAYER_HEIGHT,
|
||||||
|
(z + ne) * WORLD_LAYER_HEIGHT,
|
||||||
|
(z + nw) * WORLD_LAYER_HEIGHT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Float32Array(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.freeze({
|
||||||
|
CHUNK_WIDTH, CHUNK_HEIGHT, CHUNK_DEPTH, TILE_COUNT, WORLD_LAYER_HEIGHT,
|
||||||
|
TILE_SHAPE_NULL, TILE_SHAPE_GROUND,
|
||||||
|
TILE_SHAPE_RAMP_NORTH, TILE_SHAPE_RAMP_EAST,
|
||||||
|
TILE_SHAPE_RAMP_SOUTH, TILE_SHAPE_RAMP_WEST,
|
||||||
|
TILE_SHAPE_RAMP_NORTHEAST, TILE_SHAPE_RAMP_NORTHWEST,
|
||||||
|
TILE_SHAPE_RAMP_SOUTHEAST, TILE_SHAPE_RAMP_SOUTHWEST,
|
||||||
|
TILE_SHAPE_RAMP_NORTHEAST_INNER, TILE_SHAPE_RAMP_NORTHWEST_INNER,
|
||||||
|
TILE_SHAPE_RAMP_SOUTHEAST_INNER, TILE_SHAPE_RAMP_SOUTHWEST_INNER,
|
||||||
|
RAMP_CORNERS, tileIndex, buildTerrainVerts, buildGridLines,
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
// Copyright (c) 2026 Dominic Masters
|
||||||
|
//
|
||||||
|
// This software is released under the MIT License.
|
||||||
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// DMF - Dusk Mesh Format
|
||||||
|
//
|
||||||
|
// Header (12 bytes):
|
||||||
|
// [0-3] "DMF\0" magic
|
||||||
|
// [4-7] uint32 version (little-endian)
|
||||||
|
// [8-11] uint32 vertCount (little-endian)
|
||||||
|
//
|
||||||
|
// Followed by vertCount vertices, each 20 bytes:
|
||||||
|
// [0-7] float32[2] uv (little-endian)
|
||||||
|
// [8-19] float32[3] pos (little-endian)
|
||||||
|
|
||||||
|
const DMF = (() => {
|
||||||
|
const MAGIC = [0x44, 0x4d, 0x46, 0x00]; // "DMF\0"
|
||||||
|
const VERSION = 1;
|
||||||
|
const HEADER_SIZE = 12;
|
||||||
|
const VERTEX_FLOATS = 5; // uv[2] + pos[3]
|
||||||
|
const VERTEX_SIZE = VERTEX_FLOATS * 4;
|
||||||
|
|
||||||
|
// Decode a DMF ArrayBuffer into { version, vertexCount, floats }, where
|
||||||
|
// floats is a Float32Array of tightly interleaved [u, v, x, y, z] per
|
||||||
|
// vertex - ready to hand straight to gl.bufferData().
|
||||||
|
function decode(buffer) {
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
|
||||||
|
if(bytes.length < HEADER_SIZE) throw new Error("File too small to be a valid DMF");
|
||||||
|
if(
|
||||||
|
bytes[0] !== MAGIC[0] || bytes[1] !== MAGIC[1] ||
|
||||||
|
bytes[2] !== MAGIC[2] || bytes[3] !== MAGIC[3]
|
||||||
|
) {
|
||||||
|
throw new Error("Invalid DMF magic bytes - not a DMF file");
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = view.getUint32(4, true);
|
||||||
|
if(version !== VERSION) {
|
||||||
|
throw new Error(`Unsupported DMF version: ${version}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const vertexCount = view.getUint32(8, true);
|
||||||
|
const expected = HEADER_SIZE + vertexCount * VERTEX_SIZE;
|
||||||
|
if(bytes.length < expected) {
|
||||||
|
throw new Error(`DMF vertex data truncated (expected ${expected} bytes, got ${bytes.length})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const floats = new Float32Array(vertexCount * VERTEX_FLOATS);
|
||||||
|
for(let i = 0; i < floats.length; i++) {
|
||||||
|
floats[i] = view.getFloat32(HEADER_SIZE + i * 4, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { version, vertexCount, floats };
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.freeze({
|
||||||
|
decode,
|
||||||
|
VERSION, HEADER_SIZE, VERTEX_FLOATS, VERTEX_SIZE,
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h1>Dusk Map Editor</h1>
|
||||||
|
<p class="text-muted">Browse and edit project assets.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<link rel="stylesheet" href="/map/map.css">
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex flex-wrap align-items-end gap-2 mb-2">
|
||||||
|
<div>
|
||||||
|
<label class="form-label mb-0">X</label>
|
||||||
|
<input type="number" id="coordX" class="form-control form-control-sm" style="width:5rem" value="0">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-label mb-0">Y</label>
|
||||||
|
<input type="number" id="coordY" class="form-control form-control-sm" style="width:5rem" value="0">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-label mb-0">Z</label>
|
||||||
|
<input type="number" id="coordZ" class="form-control form-control-sm" style="width:5rem" value="0">
|
||||||
|
</div>
|
||||||
|
<button id="btnLoad" type="button" class="btn btn-sm btn-secondary">Load</button>
|
||||||
|
|
||||||
|
<div class="dropdown">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-outline-secondary dropdown-toggle"
|
||||||
|
type="button" data-bs-toggle="dropdown"
|
||||||
|
>Browse</button>
|
||||||
|
<ul class="dropdown-menu" id="chunkBrowseList"></ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dpad" title="Navigate to the neighboring chunk">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary dpad-n" id="navN" title="North (+Y)">N</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary dpad-w" id="navW" title="West (-X)">W</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary dpad-e" id="navE" title="East (+X)">E</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary dpad-s" id="navS" title="South (-Y)">S</button>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-column gap-1">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="navUp" title="Up (+Z)">Up</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="navDown" title="Down (-Z)">Down</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="btnSave" type="button" class="btn btn-sm btn-primary ms-auto">
|
||||||
|
Save & Compile
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="statusLine" class="small text-muted mb-2"> </div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-lg-6">
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-2">
|
||||||
|
<label class="form-label mb-0">Z-Level</label>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="zLevelDown">-</button>
|
||||||
|
<input type="range" id="zLevel" class="form-range" style="width:150px" min="0" max="3" value="0">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="zLevelUp">+</button>
|
||||||
|
<span id="zLevelLabel">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-3">
|
||||||
|
<div id="toolPalette" class="tile-palette"></div>
|
||||||
|
<div id="tilePalette" class="tile-palette"></div>
|
||||||
|
<canvas id="gridCanvas" class="border tile-grid" width="512" height="512"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mt-3">Meshes</h5>
|
||||||
|
<ul id="meshList" class="list-group mb-2"></ul>
|
||||||
|
<div class="d-flex gap-2 align-items-end flex-wrap">
|
||||||
|
<div style="min-width:220px">
|
||||||
|
<label class="form-label mb-0">Model</label>
|
||||||
|
<select id="meshPicker" class="form-select form-select-sm"></select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-label mb-0">X</label>
|
||||||
|
<input id="meshX" type="number" class="form-control form-control-sm" style="width:5rem" value="0">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-label mb-0">Y</label>
|
||||||
|
<input id="meshY" type="number" class="form-control form-control-sm" style="width:5rem" value="0">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-label mb-0">Z</label>
|
||||||
|
<input id="meshZ" type="number" class="form-control form-control-sm" style="width:5rem" value="0">
|
||||||
|
</div>
|
||||||
|
<button id="btnAddMesh" type="button" class="btn btn-sm btn-outline-primary">Add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-lg-6">
|
||||||
|
<p class="small text-muted mb-2">Preview (scroll to zoom)</p>
|
||||||
|
<canvas id="previewCanvas" class="border" width="512" height="512"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/common/dmf.js"></script>
|
||||||
|
<script src="/common/chunkterrain.js"></script>
|
||||||
|
<script src="/map/renderer.js"></script>
|
||||||
|
<script src="/map/map.js"></script>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
.tile-palette {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
max-height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-swatch {
|
||||||
|
position: relative;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-swatch.active {
|
||||||
|
border-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-swatch-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dpad {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 2rem);
|
||||||
|
grid-template-rows: repeat(3, 2rem);
|
||||||
|
grid-template-areas:
|
||||||
|
". n ."
|
||||||
|
"w . e"
|
||||||
|
". s .";
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dpad-n { grid-area: n; }
|
||||||
|
.dpad-w { grid-area: w; }
|
||||||
|
.dpad-e { grid-area: e; }
|
||||||
|
.dpad-s { grid-area: s; }
|
||||||
|
.dpad button { padding: 0; }
|
||||||
|
|
||||||
|
.tile-grid {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
cursor: crosshair;
|
||||||
|
touch-action: none;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#previewCanvas {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
@@ -0,0 +1,918 @@
|
|||||||
|
// Copyright (c) 2026 Dominic Masters
|
||||||
|
//
|
||||||
|
// This software is released under the MIT License.
|
||||||
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
const SHAPES = [
|
||||||
|
{ type: ChunkTerrain.TILE_SHAPE_NULL, name: "Erase", color: "#20232a" },
|
||||||
|
{ type: ChunkTerrain.TILE_SHAPE_GROUND, name: "Ground", color: "#4caf50" },
|
||||||
|
{ type: ChunkTerrain.TILE_SHAPE_RAMP_NORTH, name: "Ramp North", color: "#2196f3" },
|
||||||
|
{ type: ChunkTerrain.TILE_SHAPE_RAMP_EAST, name: "Ramp East", color: "#03a9f4" },
|
||||||
|
{ type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTH, name: "Ramp South", color: "#00bcd4" },
|
||||||
|
{ type: ChunkTerrain.TILE_SHAPE_RAMP_WEST, name: "Ramp West", color: "#009688" },
|
||||||
|
{ type: ChunkTerrain.TILE_SHAPE_RAMP_NORTHEAST, name: "Ramp NE", color: "#ff9800" },
|
||||||
|
{ type: ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST, name: "Ramp NW", color: "#ff5722" },
|
||||||
|
{ type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST, name: "Ramp SE", color: "#9c27b0" },
|
||||||
|
{ type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST, name: "Ramp SW", color: "#e91e63" },
|
||||||
|
{ type: ChunkTerrain.TILE_SHAPE_RAMP_NORTHEAST_INNER, name: "Ramp NE Inner", color: "#795548" },
|
||||||
|
{ type: ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST_INNER, name: "Ramp NW Inner", color: "#607d8b" },
|
||||||
|
{ type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST_INNER, name: "Ramp SE Inner", color: "#8bc34a" },
|
||||||
|
{ type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST_INNER, name: "Ramp SW Inner", color: "#ffc107" },
|
||||||
|
];
|
||||||
|
const SHAPE_COLORS = Object.fromEntries(SHAPES.map((s) => [s.type, s.color]));
|
||||||
|
|
||||||
|
// Arrow direction per ramp, in canvas/screen space: north is up (dy < 0),
|
||||||
|
// east is right (dx > 0) - matches renderGrid()'s north-is-up convention.
|
||||||
|
const SHAPE_DIRECTIONS = {
|
||||||
|
[ChunkTerrain.TILE_SHAPE_RAMP_NORTH]: { dx: 0, dy: -1 },
|
||||||
|
[ChunkTerrain.TILE_SHAPE_RAMP_SOUTH]: { dx: 0, dy: 1 },
|
||||||
|
[ChunkTerrain.TILE_SHAPE_RAMP_EAST]: { dx: 1, dy: 0 },
|
||||||
|
[ChunkTerrain.TILE_SHAPE_RAMP_WEST]: { dx: -1, dy: 0 },
|
||||||
|
[ChunkTerrain.TILE_SHAPE_RAMP_NORTHEAST]: { dx: 1, dy: -1 },
|
||||||
|
[ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST]: { dx: -1, dy: -1 },
|
||||||
|
[ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST]: { dx: 1, dy: 1 },
|
||||||
|
[ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST]: { dx: -1, dy: 1 },
|
||||||
|
[ChunkTerrain.TILE_SHAPE_RAMP_NORTHEAST_INNER]: { dx: 1, dy: -1 },
|
||||||
|
[ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST_INNER]: { dx: -1, dy: -1 },
|
||||||
|
[ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST_INNER]: { dx: 1, dy: 1 },
|
||||||
|
[ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST_INNER]: { dx: -1, dy: 1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const INNER_CORNER_SHAPES = new Set([
|
||||||
|
ChunkTerrain.TILE_SHAPE_RAMP_NORTHEAST_INNER,
|
||||||
|
ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST_INNER,
|
||||||
|
ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST_INNER,
|
||||||
|
ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST_INNER,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Draws a single-headed arrow from (tailX, tailY) to (tipX, tipY).
|
||||||
|
function drawArrowSegment(ctx, tailX, tailY, tipX, tipY, size) {
|
||||||
|
ctx.strokeStyle = "#ffffff";
|
||||||
|
ctx.fillStyle = "#ffffff";
|
||||||
|
ctx.lineWidth = Math.max(1, size * 0.08);
|
||||||
|
ctx.lineCap = "round";
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(tailX, tailY);
|
||||||
|
ctx.lineTo(tipX, tipY);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
const headLen = size * 0.18;
|
||||||
|
const angle = Math.atan2(tipY - tailY, tipX - tailX);
|
||||||
|
const leftAngle = angle + Math.PI * 0.8;
|
||||||
|
const rightAngle = angle - Math.PI * 0.8;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(tipX, tipY);
|
||||||
|
ctx.lineTo(tipX + Math.cos(leftAngle) * headLen, tipY + Math.sin(leftAngle) * headLen);
|
||||||
|
ctx.lineTo(tipX + Math.cos(rightAngle) * headLen, tipY + Math.sin(rightAngle) * headLen);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draws an arrow centered at (cx, cy) pointing toward the ramp's high
|
||||||
|
// side, so ramp direction is visible at a glance on both the palette
|
||||||
|
// swatches and the tile grid. Used for cardinal ramps and outer-corner
|
||||||
|
// ramps (a single raised corner).
|
||||||
|
function drawArrow(ctx, cx, cy, size, dx, dy) {
|
||||||
|
const mag = Math.hypot(dx, dy) || 1;
|
||||||
|
const ux = dx / mag, uy = dy / mag;
|
||||||
|
const len = size * 0.32;
|
||||||
|
drawArrowSegment(ctx, cx - ux * len, cy - uy * len, cx + ux * len, cy + uy * len, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draws a plain right-angle bracket - two line segments meeting at 90
|
||||||
|
// degrees at the tile's corner, one running along each of the two edges
|
||||||
|
// adjacent to it - so inner-corner ramps (three corners raised, one
|
||||||
|
// dropped) read as "the adjacent sides meeting in a 90-degree corner",
|
||||||
|
// distinct from the single diagonal arrow used for outer-corner ramps.
|
||||||
|
function drawCornerBracket(ctx, cx, cy, size, dx, dy) {
|
||||||
|
const cornerDist = size * 0.42;
|
||||||
|
const armLen = size * 0.34;
|
||||||
|
const cornerX = cx + dx * cornerDist;
|
||||||
|
const cornerY = cy + dy * cornerDist;
|
||||||
|
|
||||||
|
ctx.strokeStyle = "#ffffff";
|
||||||
|
ctx.lineWidth = Math.max(1, size * 0.08);
|
||||||
|
ctx.lineCap = "round";
|
||||||
|
ctx.lineJoin = "round";
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(cornerX - dx * armLen, cornerY);
|
||||||
|
ctx.lineTo(cornerX, cornerY);
|
||||||
|
ctx.lineTo(cornerX, cornerY - dy * armLen);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draws the direction icon for a ramp tile `type` in a `size`x`size` area
|
||||||
|
// centered at (cx, cy): a diagonal arrow for cardinal/outer-corner ramps,
|
||||||
|
// or a right-angle bracket for inner-corner ramps. No-op for shapes with
|
||||||
|
// no direction (ground, erase).
|
||||||
|
function drawShapeIcon(ctx, cx, cy, size, type) {
|
||||||
|
const dir = SHAPE_DIRECTIONS[type];
|
||||||
|
if(!dir) return;
|
||||||
|
if(INNER_CORNER_SHAPES.has(type)) {
|
||||||
|
drawCornerBracket(ctx, cx, cy, size, dir.dx, dir.dy);
|
||||||
|
} else {
|
||||||
|
drawArrow(ctx, cx, cy, size, dir.dx, dir.dy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draws a small pencil icon centered in a `size`x`size` canvas.
|
||||||
|
function drawPencilIcon(ctx, size) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(size / 2, size / 2);
|
||||||
|
ctx.rotate(-Math.PI / 4);
|
||||||
|
|
||||||
|
const shaftW = size * 0.18;
|
||||||
|
const shaftH = size * 0.62;
|
||||||
|
|
||||||
|
ctx.fillStyle = "#e0e0e0";
|
||||||
|
ctx.fillRect(-shaftW / 2, shaftH * 0.28, shaftW, shaftH * 0.22);
|
||||||
|
|
||||||
|
ctx.fillStyle = "#f0c419";
|
||||||
|
ctx.fillRect(-shaftW / 2, -shaftH * 0.5, shaftW, shaftH * 0.78);
|
||||||
|
|
||||||
|
ctx.fillStyle = "#8d6e63";
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(-shaftW / 2, -shaftH * 0.5);
|
||||||
|
ctx.lineTo(shaftW / 2, -shaftH * 0.5);
|
||||||
|
ctx.lineTo(0, -shaftH * 0.5 - shaftW);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draws a small paint bucket icon centered in a `size`x`size` canvas.
|
||||||
|
function drawBucketIcon(ctx, size) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(size / 2, size / 2);
|
||||||
|
|
||||||
|
const w = size * 0.5, h = size * 0.34;
|
||||||
|
|
||||||
|
ctx.strokeStyle = "#e0e0e0";
|
||||||
|
ctx.lineWidth = Math.max(1, size * 0.06);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(0, -h * 0.4, w * 0.4, Math.PI, 0);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.fillStyle = "#03a9f4";
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(-w / 2, -h * 0.2);
|
||||||
|
ctx.lineTo(w / 2, -h * 0.2);
|
||||||
|
ctx.lineTo(w * 0.32, h * 0.6);
|
||||||
|
ctx.lineTo(-w * 0.32, h * 0.6);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(-w * 0.55, h * 0.75, size * 0.06, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOOLS = [
|
||||||
|
{ id: "pencil", name: "Pencil", draw: drawPencilIcon },
|
||||||
|
{ id: "bucket", name: "Paint Bucket", draw: drawBucketIcon },
|
||||||
|
];
|
||||||
|
|
||||||
|
let coord = { x: 0, y: 0, z: 0 };
|
||||||
|
let tiles = new Int32Array(ChunkTerrain.TILE_COUNT);
|
||||||
|
let meshes = [];
|
||||||
|
let chunkExists = false;
|
||||||
|
let currentZLevel = 0;
|
||||||
|
let selectedShape = ChunkTerrain.TILE_SHAPE_GROUND;
|
||||||
|
let zoomWorldH = 20;
|
||||||
|
let dirty = false;
|
||||||
|
let hoverTile = null;
|
||||||
|
let activeTool = "pencil";
|
||||||
|
|
||||||
|
let mapRenderer = null;
|
||||||
|
let terrainMesh = null;
|
||||||
|
let gridLinesMesh = null;
|
||||||
|
let terrainTexturePromise = null;
|
||||||
|
let modelIndex = null;
|
||||||
|
let neighborChunks = [];
|
||||||
|
const modelCache = new Map();
|
||||||
|
|
||||||
|
const UNDO_LIMIT = 100;
|
||||||
|
let undoStack = [];
|
||||||
|
|
||||||
|
function markDirty() {
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapshotState() {
|
||||||
|
return {
|
||||||
|
tiles: tiles.slice(),
|
||||||
|
meshes: meshes.map((m) => ({ file: m.file, pos: m.pos.slice() })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushUndo() {
|
||||||
|
undoStack.push(snapshotState());
|
||||||
|
if(undoStack.length > UNDO_LIMIT) undoStack.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
function undo() {
|
||||||
|
if(!undoStack.length) return;
|
||||||
|
const snap = undoStack.pop();
|
||||||
|
tiles = snap.tiles;
|
||||||
|
meshes = snap.meshes;
|
||||||
|
dirty = true;
|
||||||
|
renderGrid();
|
||||||
|
renderMeshList();
|
||||||
|
renderPreview();
|
||||||
|
updateStatus("Undid last change.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if it's OK to proceed (no unsaved changes, or the user
|
||||||
|
// confirmed discarding them).
|
||||||
|
function confirmDiscardIfDirty() {
|
||||||
|
if(!dirty) return true;
|
||||||
|
return window.confirm("You have unsaved changes to this chunk. Discard them?");
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLine() {
|
||||||
|
return document.getElementById("statusLine");
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStatus(msg, isError) {
|
||||||
|
const el = statusLine();
|
||||||
|
el.textContent = msg;
|
||||||
|
el.classList.toggle("text-danger", !!isError);
|
||||||
|
el.classList.toggle("text-muted", !isError);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiReadJson(path) {
|
||||||
|
const res = await fetch(`/api/read?path=${encodeURIComponent(path)}`);
|
||||||
|
const data = await res.json();
|
||||||
|
return { ok: res.ok, data };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiWrite(path, content) {
|
||||||
|
return fetch(`/api/write?path=${encodeURIComponent(path)}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ content }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiDelete(path) {
|
||||||
|
return fetch(`/api/delete?path=${encodeURIComponent(path)}`, { method: "POST" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiCompileChunk(x, y, z) {
|
||||||
|
const res = await fetch(`/api/compile-chunk?x=${x}&y=${y}&z=${z}`, { method: "POST" });
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiLs(path) {
|
||||||
|
const res = await fetch(`/api/ls?path=${encodeURIComponent(path)}`);
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiFind(path, ext) {
|
||||||
|
const res = await fetch(`/api/find?path=${encodeURIComponent(path)}&ext=${encodeURIComponent(ext)}`);
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
function base64ToBuffer(base64) {
|
||||||
|
const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
|
||||||
|
return bytes.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadImageTexture(assetPath) {
|
||||||
|
const { ok, data } = await apiReadJson(assetPath);
|
||||||
|
if(!ok || data.error) return null;
|
||||||
|
const blob = new Blob([base64ToBuffer(data.content)], { type: "image/png" });
|
||||||
|
const bitmap = await createImageBitmap(blob);
|
||||||
|
return mapRenderer.createTextureFromImage(bitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readDmfVertices(assetPath) {
|
||||||
|
const { ok, data } = await apiReadJson(assetPath);
|
||||||
|
if(!ok || data.error) throw new Error(`Failed to read ${assetPath}`);
|
||||||
|
return DMF.decode(base64ToBuffer(data.content)).floats;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureModelIndex() {
|
||||||
|
if(modelIndex) return modelIndex;
|
||||||
|
const res = await apiFind("assets/models", ".json");
|
||||||
|
modelIndex = new Map();
|
||||||
|
for(const file of res.files || []) {
|
||||||
|
const base = file.split("/").pop().replace(/\.json$/, "");
|
||||||
|
modelIndex.set(base, file);
|
||||||
|
}
|
||||||
|
return modelIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scales a mesh's stored [x, y, z] offset for preview, matching the
|
||||||
|
// Z scaling mapChunkLoaded() applies at runtime (chunk.meshOffsets[m][2]
|
||||||
|
// * WORLD_LAYER_HEIGHT) - x/y stay 1:1 since only Z is height-scaled.
|
||||||
|
function scaledMeshOffset(pos) {
|
||||||
|
return [pos[0], pos[1], pos[2] * ChunkTerrain.WORLD_LAYER_HEIGHT];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolves a chunk mesh entry's bare filename (e.g. "house_5_3.dmf") to
|
||||||
|
// its model JSON by basename, mirroring find_model() in
|
||||||
|
// tools/asset/chunk/__main__.py - the chunk JSON only ever stores a bare
|
||||||
|
// filename, not a path.
|
||||||
|
async function resolveModelForMeshFile(file) {
|
||||||
|
await ensureModelIndex();
|
||||||
|
const base = file.replace(/\.[^.]+$/, "");
|
||||||
|
const rel = modelIndex.get(base);
|
||||||
|
if(!rel) return null;
|
||||||
|
|
||||||
|
const modelPath = `assets/models/${rel}`;
|
||||||
|
if(modelCache.has(modelPath)) return modelCache.get(modelPath);
|
||||||
|
|
||||||
|
const promise = (async () => {
|
||||||
|
const { ok, data } = await apiReadJson(modelPath);
|
||||||
|
if(!ok || data.error) throw new Error(`Failed to read model ${modelPath}`);
|
||||||
|
const modelDef = JSON.parse(data.content);
|
||||||
|
const meshFloats = await readDmfVertices(`assets/${modelDef.mesh}`);
|
||||||
|
const mesh = mapRenderer.createMesh(meshFloats);
|
||||||
|
const texture = modelDef.texture ? await loadImageTexture(`assets/${modelDef.texture}`) : null;
|
||||||
|
const colorBytes = modelDef.color || [255, 255, 255, 255];
|
||||||
|
const color = colorBytes.map((c) => c / 255);
|
||||||
|
return { mesh, texture, color };
|
||||||
|
})();
|
||||||
|
modelCache.set(modelPath, promise);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function chunkPaths(x, y, z) {
|
||||||
|
return {
|
||||||
|
raw: `assetsraw/chunks/chunk_${x}_${y}_${z}.json`,
|
||||||
|
dcf: `assets/chunks/${x}_${y}_${z}.dcf`,
|
||||||
|
terrainDmf: `assets/meshes/chunks/chunk_${x}_${y}_${z}_0.dmf`,
|
||||||
|
terrainModel: `assets/models/chunks/chunk_${x}_${y}_${z}_0.json`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads the 8 same-Z neighbors around `coord` (read-only, for context in
|
||||||
|
// the 3D preview) and pre-builds their render data so renderPreview()
|
||||||
|
// doesn't have to rebuild GL buffers on every paint stroke.
|
||||||
|
async function loadNeighborChunks() {
|
||||||
|
const offsets = [];
|
||||||
|
for(let dy = -1; dy <= 1; dy++) {
|
||||||
|
for(let dx = -1; dx <= 1; dx++) {
|
||||||
|
if(dx || dy) offsets.push({ dx, dy });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await Promise.all(offsets.map(async ({ dx, dy }) => {
|
||||||
|
const nx = coord.x + dx, ny = coord.y + dy, nz = coord.z;
|
||||||
|
const { ok, data } = await apiReadJson(chunkPaths(nx, ny, nz).raw);
|
||||||
|
if(!ok || data.error) return null;
|
||||||
|
|
||||||
|
const json = JSON.parse(data.content);
|
||||||
|
const neighborTiles = new Int32Array(ChunkTerrain.TILE_COUNT);
|
||||||
|
for(const t of json.tiles || []) {
|
||||||
|
const [tx, ty, tz] = t.pos;
|
||||||
|
neighborTiles[ChunkTerrain.tileIndex(tx, ty, tz)] = t.type;
|
||||||
|
}
|
||||||
|
const neighborMeshes = (json.meshes || []).map((m) => ({ file: m.file, pos: m.pos || [0, 0, 0] }));
|
||||||
|
|
||||||
|
const floats = ChunkTerrain.buildTerrainVerts(neighborTiles);
|
||||||
|
const terrain = floats.length ? { mesh: mapRenderer.createMesh(floats) } : null;
|
||||||
|
|
||||||
|
const models = [];
|
||||||
|
for(const m of neighborMeshes) {
|
||||||
|
try {
|
||||||
|
const resolved = await resolveModelForMeshFile(m.file);
|
||||||
|
if(resolved) models.push({ ...resolved, offset: scaledMeshOffset(m.pos) });
|
||||||
|
} catch(e) {
|
||||||
|
console.warn("Failed to load neighbor mesh preview", m.file, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
offset: [dx * ChunkTerrain.CHUNK_WIDTH, dy * ChunkTerrain.CHUNK_HEIGHT, 0],
|
||||||
|
terrain,
|
||||||
|
models,
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
neighborChunks = results.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadChunk(x, y, z) {
|
||||||
|
coord = { x, y, z };
|
||||||
|
tiles = new Int32Array(ChunkTerrain.TILE_COUNT);
|
||||||
|
meshes = [];
|
||||||
|
chunkExists = false;
|
||||||
|
|
||||||
|
const { ok, data } = await apiReadJson(chunkPaths(x, y, z).raw);
|
||||||
|
if(ok && !data.error) {
|
||||||
|
chunkExists = true;
|
||||||
|
const json = JSON.parse(data.content);
|
||||||
|
for(const t of json.tiles || []) {
|
||||||
|
const [tx, ty, tz] = t.pos;
|
||||||
|
tiles[ChunkTerrain.tileIndex(tx, ty, tz)] = t.type;
|
||||||
|
}
|
||||||
|
meshes = (json.meshes || []).map((m) => ({ file: m.file, pos: m.pos || [0, 0, 0] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadNeighborChunks();
|
||||||
|
dirty = false;
|
||||||
|
undoStack = [];
|
||||||
|
|
||||||
|
renderGrid();
|
||||||
|
renderMeshList();
|
||||||
|
renderPreview();
|
||||||
|
updateStatus(chunkExists
|
||||||
|
? `Loaded chunk_${x}_${y}_${z}.json`
|
||||||
|
: `New (empty) chunk at ${x}, ${y}, ${z} - paint a tile and save to create it.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveChunk() {
|
||||||
|
const tilesArr = [];
|
||||||
|
for(let z = 0; z < ChunkTerrain.CHUNK_DEPTH; z++) {
|
||||||
|
for(let y = 0; y < ChunkTerrain.CHUNK_HEIGHT; y++) {
|
||||||
|
for(let x = 0; x < ChunkTerrain.CHUNK_WIDTH; x++) {
|
||||||
|
const type = tiles[ChunkTerrain.tileIndex(x, y, z)];
|
||||||
|
if(type && type !== ChunkTerrain.TILE_SHAPE_NULL) {
|
||||||
|
tilesArr.push({ pos: [x, y, z], type, tile: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const paths = chunkPaths(coord.x, coord.y, coord.z);
|
||||||
|
|
||||||
|
if(tilesArr.length === 0 && meshes.length === 0) {
|
||||||
|
await Promise.all(Object.values(paths).map((p) => apiDelete(p)));
|
||||||
|
chunkExists = false;
|
||||||
|
dirty = false;
|
||||||
|
updateStatus(`Chunk ${coord.x}, ${coord.y}, ${coord.z} is empty - deleted.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = { tiles: tilesArr, meshes: meshes.map((m) => ({ file: m.file, pos: m.pos })) };
|
||||||
|
await apiWrite(paths.raw, JSON.stringify(json, null, 2));
|
||||||
|
chunkExists = true;
|
||||||
|
dirty = false;
|
||||||
|
updateStatus("Saved. Compiling...");
|
||||||
|
|
||||||
|
const result = await apiCompileChunk(coord.x, coord.y, coord.z);
|
||||||
|
if(result.ok) {
|
||||||
|
updateStatus(`Compiled OK.\n${result.stdout}`);
|
||||||
|
} else {
|
||||||
|
updateStatus(`Compile FAILED (code ${result.returncode}):\n${result.stderr}`, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPalette() {
|
||||||
|
const el = document.getElementById("tilePalette");
|
||||||
|
el.innerHTML = "";
|
||||||
|
for(const s of SHAPES) {
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.type = "button";
|
||||||
|
btn.className = "tile-swatch" + (s.type === selectedShape ? " active" : "");
|
||||||
|
btn.style.background = s.color;
|
||||||
|
btn.title = s.name;
|
||||||
|
btn.addEventListener("click", () => {
|
||||||
|
selectedShape = s.type;
|
||||||
|
buildPalette();
|
||||||
|
});
|
||||||
|
|
||||||
|
if(SHAPE_DIRECTIONS[s.type]) {
|
||||||
|
const icon = document.createElement("canvas");
|
||||||
|
icon.width = 32;
|
||||||
|
icon.height = 32;
|
||||||
|
icon.className = "tile-swatch-icon";
|
||||||
|
drawShapeIcon(icon.getContext("2d"), 16, 16, 32, s.type);
|
||||||
|
btn.appendChild(icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
el.appendChild(btn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildToolPalette() {
|
||||||
|
const el = document.getElementById("toolPalette");
|
||||||
|
el.innerHTML = "";
|
||||||
|
for(const tool of TOOLS) {
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.type = "button";
|
||||||
|
btn.className = "tile-swatch" + (tool.id === activeTool ? " active" : "");
|
||||||
|
btn.style.background = "#2a2d33";
|
||||||
|
btn.title = tool.name;
|
||||||
|
btn.addEventListener("click", () => {
|
||||||
|
activeTool = tool.id;
|
||||||
|
buildToolPalette();
|
||||||
|
});
|
||||||
|
|
||||||
|
const icon = document.createElement("canvas");
|
||||||
|
icon.width = 32;
|
||||||
|
icon.height = 32;
|
||||||
|
icon.className = "tile-swatch-icon";
|
||||||
|
tool.draw(icon.getContext("2d"), 32);
|
||||||
|
btn.appendChild(icon);
|
||||||
|
|
||||||
|
el.appendChild(btn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function canvasToTile(e, canvas) {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const cell = canvas.width / ChunkTerrain.CHUNK_WIDTH;
|
||||||
|
const px = (e.clientX - rect.left) * (canvas.width / rect.width);
|
||||||
|
const py = (e.clientY - rect.top) * (canvas.height / rect.height);
|
||||||
|
const x = Math.floor(px / cell);
|
||||||
|
const yFromTop = Math.floor(py / cell);
|
||||||
|
const y = ChunkTerrain.CHUNK_HEIGHT - 1 - yFromTop;
|
||||||
|
if(x < 0 || x >= ChunkTerrain.CHUNK_WIDTH || y < 0 || y >= ChunkTerrain.CHUNK_HEIGHT) return null;
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGrid() {
|
||||||
|
const canvas = document.getElementById("gridCanvas");
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
const cell = canvas.width / ChunkTerrain.CHUNK_WIDTH;
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
for(let y = 0; y < ChunkTerrain.CHUNK_HEIGHT; y++) {
|
||||||
|
for(let x = 0; x < ChunkTerrain.CHUNK_WIDTH; x++) {
|
||||||
|
const type = tiles[ChunkTerrain.tileIndex(x, y, currentZLevel)];
|
||||||
|
const sy = (ChunkTerrain.CHUNK_HEIGHT - 1 - y) * cell;
|
||||||
|
ctx.fillStyle = SHAPE_COLORS[type] || "#15171b";
|
||||||
|
ctx.fillRect(x * cell, sy, cell, cell);
|
||||||
|
ctx.strokeStyle = "rgba(255, 255, 255, 0.25)";
|
||||||
|
ctx.strokeRect(x * cell, sy, cell, cell);
|
||||||
|
|
||||||
|
drawShapeIcon(ctx, x * cell + cell / 2, sy + cell / 2, cell, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fillStyle = "#ffffff";
|
||||||
|
for(const m of meshes) {
|
||||||
|
const [mx, my, mz] = m.pos;
|
||||||
|
if(Math.round(mz) !== currentZLevel) continue;
|
||||||
|
const sy = (ChunkTerrain.CHUNK_HEIGHT - 1 - my) * cell;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(mx * cell + cell / 2, sy + cell / 2, cell * 0.3, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(hoverTile) {
|
||||||
|
const sy = (ChunkTerrain.CHUNK_HEIGHT - 1 - hoverTile.y) * cell;
|
||||||
|
ctx.strokeStyle = "#ffeb3b";
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.strokeRect(hoverTile.x * cell + 1, sy + 1, cell - 2, cell - 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hitTestMesh(e, canvas) {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const cell = canvas.width / ChunkTerrain.CHUNK_WIDTH;
|
||||||
|
const px = (e.clientX - rect.left) * (canvas.width / rect.width);
|
||||||
|
const py = (e.clientY - rect.top) * (canvas.height / rect.height);
|
||||||
|
|
||||||
|
let closest = -1;
|
||||||
|
let closestDist = cell * 0.5;
|
||||||
|
meshes.forEach((m, i) => {
|
||||||
|
const [mx, my, mz] = m.pos;
|
||||||
|
if(Math.round(mz) !== currentZLevel) return;
|
||||||
|
const cx = mx * cell + cell / 2;
|
||||||
|
const cy = (ChunkTerrain.CHUNK_HEIGHT - 1 - my) * cell + cell / 2;
|
||||||
|
const dist = Math.hypot(px - cx, py - cy);
|
||||||
|
if(dist < closestDist) { closestDist = dist; closest = i; }
|
||||||
|
});
|
||||||
|
return closest;
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveMeshTo(e, index) {
|
||||||
|
const canvas = document.getElementById("gridCanvas");
|
||||||
|
const t = canvasToTile(e, canvas);
|
||||||
|
if(!t) return;
|
||||||
|
meshes[index].pos[0] = t.x;
|
||||||
|
meshes[index].pos[1] = t.y;
|
||||||
|
markDirty();
|
||||||
|
renderGrid();
|
||||||
|
renderMeshList();
|
||||||
|
renderPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintAt(e) {
|
||||||
|
const canvas = document.getElementById("gridCanvas");
|
||||||
|
const t = canvasToTile(e, canvas);
|
||||||
|
if(!t) return;
|
||||||
|
tiles[ChunkTerrain.tileIndex(t.x, t.y, currentZLevel)] = selectedShape;
|
||||||
|
markDirty();
|
||||||
|
renderGrid();
|
||||||
|
renderPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4-connected flood fill of same-type tiles at the current Z-level,
|
||||||
|
// starting from (startX, startY), replacing them with selectedShape.
|
||||||
|
function floodFill(startX, startY) {
|
||||||
|
const targetType = tiles[ChunkTerrain.tileIndex(startX, startY, currentZLevel)];
|
||||||
|
if(targetType === selectedShape) return;
|
||||||
|
|
||||||
|
const stack = [[startX, startY]];
|
||||||
|
const visited = new Set();
|
||||||
|
while(stack.length) {
|
||||||
|
const [x, y] = stack.pop();
|
||||||
|
const key = `${x},${y}`;
|
||||||
|
if(visited.has(key)) continue;
|
||||||
|
if(x < 0 || x >= ChunkTerrain.CHUNK_WIDTH || y < 0 || y >= ChunkTerrain.CHUNK_HEIGHT) continue;
|
||||||
|
visited.add(key);
|
||||||
|
|
||||||
|
const idx = ChunkTerrain.tileIndex(x, y, currentZLevel);
|
||||||
|
if(tiles[idx] !== targetType) continue;
|
||||||
|
tiles[idx] = selectedShape;
|
||||||
|
stack.push([x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillAt(e) {
|
||||||
|
const canvas = document.getElementById("gridCanvas");
|
||||||
|
const t = canvasToTile(e, canvas);
|
||||||
|
if(!t) return;
|
||||||
|
pushUndo();
|
||||||
|
floodFill(t.x, t.y);
|
||||||
|
markDirty();
|
||||||
|
renderGrid();
|
||||||
|
renderPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function rebuildTerrainMesh() {
|
||||||
|
const floats = ChunkTerrain.buildTerrainVerts(tiles);
|
||||||
|
terrainMesh = floats.length ? mapRenderer.createMesh(floats) : null;
|
||||||
|
|
||||||
|
const lineFloats = ChunkTerrain.buildGridLines(tiles);
|
||||||
|
gridLinesMesh = lineFloats.length ? mapRenderer.createMesh(lineFloats) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keeps the preview canvas's backing-store resolution matched to its
|
||||||
|
// actual displayed CSS size (times devicePixelRatio) - the canvas
|
||||||
|
// otherwise stays at its fixed width/height attributes (512x512) while
|
||||||
|
// CSS stretches it to fill the column, which upscales and blurs the
|
||||||
|
// WebGL output on wider or high-DPI screens.
|
||||||
|
function resizePreviewCanvas() {
|
||||||
|
const canvas = document.getElementById("previewCanvas");
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
const displayWidth = Math.max(1, Math.round(canvas.clientWidth * dpr));
|
||||||
|
const displayHeight = Math.max(1, Math.round(canvas.clientHeight * dpr));
|
||||||
|
if(canvas.width !== displayWidth || canvas.height !== displayHeight) {
|
||||||
|
canvas.width = displayWidth;
|
||||||
|
canvas.height = displayHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderPreview() {
|
||||||
|
if(!mapRenderer) return;
|
||||||
|
resizePreviewCanvas();
|
||||||
|
rebuildTerrainMesh();
|
||||||
|
|
||||||
|
const models = [];
|
||||||
|
for(const m of meshes) {
|
||||||
|
try {
|
||||||
|
const resolved = await resolveModelForMeshFile(m.file);
|
||||||
|
if(resolved) models.push({ ...resolved, offset: scaledMeshOffset(m.pos) });
|
||||||
|
} catch(e) {
|
||||||
|
console.warn("Failed to load mesh preview", m.file, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let terrainTexture = null;
|
||||||
|
if(terrainMesh || neighborChunks.some((n) => n.terrain)) {
|
||||||
|
if(!terrainTexturePromise) terrainTexturePromise = loadImageTexture("assets/tiles.png");
|
||||||
|
terrainTexture = await terrainTexturePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
const neighbors = neighborChunks.map((n) => ({
|
||||||
|
offset: n.offset,
|
||||||
|
terrain: n.terrain ? { mesh: n.terrain.mesh, texture: terrainTexture } : null,
|
||||||
|
models: n.models,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const zLevelHeight = currentZLevel * ChunkTerrain.WORLD_LAYER_HEIGHT;
|
||||||
|
mapRenderer.render({
|
||||||
|
target: [ChunkTerrain.CHUNK_WIDTH / 2, ChunkTerrain.CHUNK_HEIGHT / 2, zLevelHeight],
|
||||||
|
worldH: zoomWorldH,
|
||||||
|
terrain: terrainMesh ? { mesh: terrainMesh, texture: terrainTexture } : null,
|
||||||
|
gridLines: gridLinesMesh ? { mesh: gridLinesMesh } : null,
|
||||||
|
models,
|
||||||
|
neighbors,
|
||||||
|
highlight: hoverTile ? { x: hoverTile.x, y: hoverTile.y, z: zLevelHeight } : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMeshList() {
|
||||||
|
const ul = document.getElementById("meshList");
|
||||||
|
ul.innerHTML = "";
|
||||||
|
meshes.forEach((m, i) => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.className = "list-group-item d-flex align-items-center gap-2 flex-wrap";
|
||||||
|
|
||||||
|
const label = document.createElement("span");
|
||||||
|
label.className = "me-auto";
|
||||||
|
label.textContent = m.file;
|
||||||
|
li.appendChild(label);
|
||||||
|
|
||||||
|
["X", "Y", "Z"].forEach((axis, axisIndex) => {
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "number";
|
||||||
|
input.className = "form-control form-control-sm";
|
||||||
|
input.style.width = "5rem";
|
||||||
|
input.title = axis;
|
||||||
|
input.value = m.pos[axisIndex];
|
||||||
|
input.addEventListener("change", () => {
|
||||||
|
pushUndo();
|
||||||
|
m.pos[axisIndex] = parseFloat(input.value) || 0;
|
||||||
|
markDirty();
|
||||||
|
renderGrid();
|
||||||
|
renderPreview();
|
||||||
|
});
|
||||||
|
li.appendChild(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.type = "button";
|
||||||
|
btn.className = "btn btn-sm btn-outline-danger";
|
||||||
|
btn.textContent = "Remove";
|
||||||
|
btn.addEventListener("click", () => {
|
||||||
|
pushUndo();
|
||||||
|
meshes.splice(i, 1);
|
||||||
|
markDirty();
|
||||||
|
renderMeshList();
|
||||||
|
renderGrid();
|
||||||
|
renderPreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
li.appendChild(btn);
|
||||||
|
ul.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildMeshPicker() {
|
||||||
|
await ensureModelIndex();
|
||||||
|
const select = document.getElementById("meshPicker");
|
||||||
|
select.innerHTML = "";
|
||||||
|
for(const [base, rel] of [...modelIndex.entries()].sort()) {
|
||||||
|
const opt = document.createElement("option");
|
||||||
|
opt.value = base;
|
||||||
|
opt.textContent = rel;
|
||||||
|
select.appendChild(opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildChunkBrowseList() {
|
||||||
|
const list = document.getElementById("chunkBrowseList");
|
||||||
|
list.innerHTML = "";
|
||||||
|
const res = await apiLs("assetsraw/chunks");
|
||||||
|
for(const entry of res.entries || []) {
|
||||||
|
const m = entry.name.match(/^chunk_(-?\d+)_(-?\d+)_(-?\d+)\.json$/);
|
||||||
|
if(!m) continue;
|
||||||
|
const [, x, y, z] = m;
|
||||||
|
|
||||||
|
const li = document.createElement("li");
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.className = "dropdown-item";
|
||||||
|
a.href = "#";
|
||||||
|
a.textContent = `${x}, ${y}, ${z}`;
|
||||||
|
a.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
goToChunk(parseInt(x, 10), parseInt(y, 10), parseInt(z, 10));
|
||||||
|
});
|
||||||
|
|
||||||
|
li.appendChild(a);
|
||||||
|
list.appendChild(li);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCoordInputs(x, y, z) {
|
||||||
|
document.getElementById("coordX").value = x;
|
||||||
|
document.getElementById("coordY").value = y;
|
||||||
|
document.getElementById("coordZ").value = z;
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToChunk(x, y, z) {
|
||||||
|
if(!confirmDiscardIfDirty()) return;
|
||||||
|
setCoordInputs(x, y, z);
|
||||||
|
loadChunk(x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindEvents() {
|
||||||
|
const gridCanvas = document.getElementById("gridCanvas");
|
||||||
|
let painting = false;
|
||||||
|
let draggingMeshIndex = -1;
|
||||||
|
|
||||||
|
gridCanvas.addEventListener("mousedown", (e) => {
|
||||||
|
if(activeTool === "bucket") {
|
||||||
|
fillAt(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hit = hitTestMesh(e, gridCanvas);
|
||||||
|
pushUndo();
|
||||||
|
if(hit !== -1) {
|
||||||
|
draggingMeshIndex = hit;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
painting = true;
|
||||||
|
paintAt(e);
|
||||||
|
});
|
||||||
|
window.addEventListener("mouseup", () => { painting = false; draggingMeshIndex = -1; });
|
||||||
|
gridCanvas.addEventListener("mousemove", (e) => {
|
||||||
|
hoverTile = canvasToTile(e, gridCanvas);
|
||||||
|
if(draggingMeshIndex !== -1) { moveMeshTo(e, draggingMeshIndex); return; }
|
||||||
|
if(painting) { paintAt(e); return; }
|
||||||
|
renderGrid();
|
||||||
|
renderPreview();
|
||||||
|
});
|
||||||
|
gridCanvas.addEventListener("mouseleave", () => {
|
||||||
|
hoverTile = null;
|
||||||
|
renderGrid();
|
||||||
|
renderPreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
function setZLevel(z) {
|
||||||
|
currentZLevel = Math.max(0, Math.min(ChunkTerrain.CHUNK_DEPTH - 1, z));
|
||||||
|
document.getElementById("zLevel").value = currentZLevel;
|
||||||
|
document.getElementById("zLevelLabel").textContent = currentZLevel;
|
||||||
|
renderGrid();
|
||||||
|
renderPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("zLevel").addEventListener("input", (e) => {
|
||||||
|
setZLevel(parseInt(e.target.value, 10));
|
||||||
|
});
|
||||||
|
document.getElementById("zLevelDown").addEventListener("click", () => setZLevel(currentZLevel - 1));
|
||||||
|
document.getElementById("zLevelUp").addEventListener("click", () => setZLevel(currentZLevel + 1));
|
||||||
|
|
||||||
|
document.getElementById("btnLoad").addEventListener("click", () => {
|
||||||
|
const x = parseInt(document.getElementById("coordX").value, 10) || 0;
|
||||||
|
const y = parseInt(document.getElementById("coordY").value, 10) || 0;
|
||||||
|
const z = parseInt(document.getElementById("coordZ").value, 10) || 0;
|
||||||
|
goToChunk(x, y, z);
|
||||||
|
});
|
||||||
|
|
||||||
|
// NORTH=+Y, EAST=+X (matches tile-shape convention used by the tile
|
||||||
|
// grid and tools/asset/chunk/__main__.py).
|
||||||
|
document.getElementById("navN").addEventListener("click", () => goToChunk(coord.x, coord.y + 1, coord.z));
|
||||||
|
document.getElementById("navS").addEventListener("click", () => goToChunk(coord.x, coord.y - 1, coord.z));
|
||||||
|
document.getElementById("navE").addEventListener("click", () => goToChunk(coord.x + 1, coord.y, coord.z));
|
||||||
|
document.getElementById("navW").addEventListener("click", () => goToChunk(coord.x - 1, coord.y, coord.z));
|
||||||
|
document.getElementById("navUp").addEventListener("click", () => goToChunk(coord.x, coord.y, coord.z + 1));
|
||||||
|
document.getElementById("navDown").addEventListener("click", () => goToChunk(coord.x, coord.y, coord.z - 1));
|
||||||
|
|
||||||
|
function runSave() {
|
||||||
|
saveChunk().catch((e) => updateStatus(`Save failed: ${e.message}`, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("btnSave").addEventListener("click", runSave);
|
||||||
|
|
||||||
|
document.getElementById("btnAddMesh").addEventListener("click", () => {
|
||||||
|
const base = document.getElementById("meshPicker").value;
|
||||||
|
if(!base) return;
|
||||||
|
const pos = [
|
||||||
|
parseFloat(document.getElementById("meshX").value) || 0,
|
||||||
|
parseFloat(document.getElementById("meshY").value) || 0,
|
||||||
|
parseFloat(document.getElementById("meshZ").value) || 0,
|
||||||
|
];
|
||||||
|
pushUndo();
|
||||||
|
meshes.push({ file: `${base}.dmf`, pos });
|
||||||
|
markDirty();
|
||||||
|
renderMeshList();
|
||||||
|
renderGrid();
|
||||||
|
renderPreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("previewCanvas").addEventListener("wheel", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
zoomWorldH = Math.min(60, Math.max(4, zoomWorldH + Math.sign(e.deltaY)));
|
||||||
|
renderPreview();
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
window.addEventListener("resize", () => renderPreview());
|
||||||
|
|
||||||
|
window.addEventListener("keydown", (e) => {
|
||||||
|
if(!(e.ctrlKey || e.metaKey)) return;
|
||||||
|
const key = e.key.toLowerCase();
|
||||||
|
if(key === "z") {
|
||||||
|
e.preventDefault();
|
||||||
|
undo();
|
||||||
|
} else if(key === "s") {
|
||||||
|
e.preventDefault();
|
||||||
|
runSave();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
buildPalette();
|
||||||
|
buildToolPalette();
|
||||||
|
mapRenderer = MapRenderer.create(document.getElementById("previewCanvas"));
|
||||||
|
bindEvents();
|
||||||
|
await Promise.all([buildChunkBrowseList(), buildMeshPicker()]);
|
||||||
|
await loadChunk(coord.x, coord.y, coord.z);
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
// Copyright (c) 2026 Dominic Masters
|
||||||
|
//
|
||||||
|
// This software is released under the MIT License.
|
||||||
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// MapRenderer - a minimal, dependency-free WebGL renderer for the chunk
|
||||||
|
// preview pane. The camera formula is ported 1:1 from
|
||||||
|
// src/dusk/scene/overworld/sceneoverworld.c (lines 62-81) so the preview
|
||||||
|
// approximates the game's own fixed-angle 3rd-person camera: the eye sits
|
||||||
|
// worldH units "behind" the target along Y and a proportional distance
|
||||||
|
// "above" along Z, always looking at the target, up = (0, 1, 0).
|
||||||
|
//
|
||||||
|
// The material model mirrors SHADER_UNLIT: a texture sampler multiplied by
|
||||||
|
// a flat RGBA tint, with a 1x1 white fallback texture standing in for
|
||||||
|
// untextured models.
|
||||||
|
|
||||||
|
const MapRenderer = (() => {
|
||||||
|
const VERT_SRC = `
|
||||||
|
attribute vec2 aUV;
|
||||||
|
attribute vec3 aPos;
|
||||||
|
uniform mat4 uModel;
|
||||||
|
uniform mat4 uView;
|
||||||
|
uniform mat4 uProjection;
|
||||||
|
varying vec2 vUV;
|
||||||
|
void main() {
|
||||||
|
vUV = aUV;
|
||||||
|
gl_Position = uProjection * uView * uModel * vec4(aPos, 1.0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const FRAG_SRC = `
|
||||||
|
precision mediump float;
|
||||||
|
varying vec2 vUV;
|
||||||
|
uniform sampler2D uTexture;
|
||||||
|
uniform vec4 uColor;
|
||||||
|
void main() {
|
||||||
|
gl_FragColor = texture2D(uTexture, vUV) * uColor;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function mat4Identity(out) {
|
||||||
|
out.fill(0);
|
||||||
|
out[0] = out[5] = out[10] = out[15] = 1;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mat4Translation(out, v) {
|
||||||
|
mat4Identity(out);
|
||||||
|
out[12] = v[0];
|
||||||
|
out[13] = v[1];
|
||||||
|
out[14] = v[2];
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mat4Perspective(out, fovy, aspect, near, far) {
|
||||||
|
const f = 1.0 / Math.tan(fovy / 2);
|
||||||
|
out.fill(0);
|
||||||
|
out[0] = f / aspect;
|
||||||
|
out[5] = f;
|
||||||
|
out[10] = (far + near) / (near - far);
|
||||||
|
out[11] = -1;
|
||||||
|
out[14] = (2 * far * near) / (near - far);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function vec3Sub(a, b) {
|
||||||
|
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
||||||
|
}
|
||||||
|
|
||||||
|
function vec3Cross(a, b) {
|
||||||
|
return [
|
||||||
|
a[1] * b[2] - a[2] * b[1],
|
||||||
|
a[2] * b[0] - a[0] * b[2],
|
||||||
|
a[0] * b[1] - a[1] * b[0],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function vec3Normalize(a) {
|
||||||
|
const len = Math.hypot(a[0], a[1], a[2]) || 1;
|
||||||
|
return [a[0] / len, a[1] / len, a[2] / len];
|
||||||
|
}
|
||||||
|
|
||||||
|
function mat4LookAt(out, eye, center, up) {
|
||||||
|
const zAxis = vec3Normalize(vec3Sub(eye, center));
|
||||||
|
const xAxis = vec3Normalize(vec3Cross(up, zAxis));
|
||||||
|
const yAxis = vec3Cross(zAxis, xAxis);
|
||||||
|
|
||||||
|
out[0] = xAxis[0]; out[1] = yAxis[0]; out[2] = zAxis[0]; out[3] = 0;
|
||||||
|
out[4] = xAxis[1]; out[5] = yAxis[1]; out[6] = zAxis[1]; out[7] = 0;
|
||||||
|
out[8] = xAxis[2]; out[9] = yAxis[2]; out[10] = zAxis[2]; out[11] = 0;
|
||||||
|
out[12] = -(xAxis[0] * eye[0] + xAxis[1] * eye[1] + xAxis[2] * eye[2]);
|
||||||
|
out[13] = -(yAxis[0] * eye[0] + yAxis[1] * eye[1] + yAxis[2] * eye[2]);
|
||||||
|
out[14] = -(zAxis[0] * eye[0] + zAxis[1] * eye[1] + zAxis[2] * eye[2]);
|
||||||
|
out[15] = 1;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ported from sceneoverworld.c:62-81. Returns { eye, view }.
|
||||||
|
function computeCamera(target, worldH) {
|
||||||
|
const fov = Math.PI / 4; // glm_rad(45.0f)
|
||||||
|
const zDist = (worldH * 0.5) / Math.tan(fov * 0.5);
|
||||||
|
const offset = -worldH;
|
||||||
|
|
||||||
|
const eye = [target[0], target[1] + offset, target[2] + zDist];
|
||||||
|
const view = mat4LookAt(new Float32Array(16), eye, target, [0, 1, 0]);
|
||||||
|
return { eye, view, fov };
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileShader(gl, type, src) {
|
||||||
|
const shader = gl.createShader(type);
|
||||||
|
gl.shaderSource(shader, src);
|
||||||
|
gl.compileShader(shader);
|
||||||
|
if(!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||||
|
const info = gl.getShaderInfoLog(shader);
|
||||||
|
gl.deleteShader(shader);
|
||||||
|
throw new Error(`Shader compile failed: ${info}`);
|
||||||
|
}
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
function create(canvas) {
|
||||||
|
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
||||||
|
if(!gl) throw new Error("WebGL is not available in this browser");
|
||||||
|
|
||||||
|
const program = gl.createProgram();
|
||||||
|
gl.attachShader(program, compileShader(gl, gl.VERTEX_SHADER, VERT_SRC));
|
||||||
|
gl.attachShader(program, compileShader(gl, gl.FRAGMENT_SHADER, FRAG_SRC));
|
||||||
|
gl.linkProgram(program);
|
||||||
|
if(!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
||||||
|
throw new Error(`Program link failed: ${gl.getProgramInfoLog(program)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const attribs = {
|
||||||
|
aUV: gl.getAttribLocation(program, "aUV"),
|
||||||
|
aPos: gl.getAttribLocation(program, "aPos"),
|
||||||
|
};
|
||||||
|
const uniforms = {
|
||||||
|
uModel: gl.getUniformLocation(program, "uModel"),
|
||||||
|
uView: gl.getUniformLocation(program, "uView"),
|
||||||
|
uProjection: gl.getUniformLocation(program, "uProjection"),
|
||||||
|
uTexture: gl.getUniformLocation(program, "uTexture"),
|
||||||
|
uColor: gl.getUniformLocation(program, "uColor"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const whiteTexture = gl.createTexture();
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, whiteTexture);
|
||||||
|
gl.texImage2D(
|
||||||
|
gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
|
||||||
|
new Uint8Array([255, 255, 255, 255]),
|
||||||
|
);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||||
|
|
||||||
|
gl.enable(gl.DEPTH_TEST);
|
||||||
|
gl.enable(gl.BLEND);
|
||||||
|
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||||
|
// Terrain/mesh winding isn't guaranteed consistent relative to this
|
||||||
|
// preview's fixed camera angle, so backface culling stays off - this
|
||||||
|
// is an authoring preview, not a performance-sensitive renderer, and
|
||||||
|
// an invisible mesh is a worse failure mode than a visible backface.
|
||||||
|
|
||||||
|
// Uploads interleaved [u, v, x, y, z] float data (DMF/terrain layout)
|
||||||
|
// into a GL buffer. Returns { buffer, vertexCount }.
|
||||||
|
function createMesh(floats) {
|
||||||
|
const buffer = gl.createBuffer();
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, floats, gl.STATIC_DRAW);
|
||||||
|
return { buffer, vertexCount: floats.length / 5 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTextureFromImage(image) {
|
||||||
|
const texture = gl.createTexture();
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||||
|
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
|
||||||
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
|
||||||
|
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawMesh(mesh, model, texture, color, depthWrite, mode) {
|
||||||
|
gl.depthMask(depthWrite !== false);
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, mesh.buffer);
|
||||||
|
gl.enableVertexAttribArray(attribs.aUV);
|
||||||
|
gl.vertexAttribPointer(attribs.aUV, 2, gl.FLOAT, false, 20, 0);
|
||||||
|
gl.enableVertexAttribArray(attribs.aPos);
|
||||||
|
gl.vertexAttribPointer(attribs.aPos, 3, gl.FLOAT, false, 20, 8);
|
||||||
|
|
||||||
|
gl.uniformMatrix4fv(uniforms.uModel, false, model);
|
||||||
|
gl.uniform4f(uniforms.uColor, color[0], color[1], color[2], color[3]);
|
||||||
|
|
||||||
|
gl.activeTexture(gl.TEXTURE0);
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, texture || whiteTexture);
|
||||||
|
gl.uniform1i(uniforms.uTexture, 0);
|
||||||
|
|
||||||
|
gl.drawArrays(mode || gl.TRIANGLES, 0, mesh.vertexCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builds a 4-vertex outline of the tile at (x, y, z), lifted slightly
|
||||||
|
// to avoid z-fighting with the terrain surface beneath it.
|
||||||
|
function buildHighlightMesh(x, y, z) {
|
||||||
|
const eps = 0.02;
|
||||||
|
return createMesh(new Float32Array([
|
||||||
|
0, 0, x, y, z + eps,
|
||||||
|
0, 0, x + 1, y, z + eps,
|
||||||
|
0, 0, x + 1, y + 1, z + eps,
|
||||||
|
0, 0, x, y + 1, z + eps,
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neighboring chunks are drawn dimmer and semi-transparent, so they
|
||||||
|
// read as context rather than part of the chunk being edited.
|
||||||
|
const NEIGHBOR_ALPHA = 0.35;
|
||||||
|
const NEIGHBOR_DARKEN = 0.5;
|
||||||
|
|
||||||
|
// scene = {
|
||||||
|
// target: [x, y, z], worldH: number,
|
||||||
|
// terrain: { mesh, texture } | null,
|
||||||
|
// gridLines: { mesh } | null,
|
||||||
|
// models: [{ mesh, texture, color: [r,g,b,a in 0..1], offset: [x,y,z] }],
|
||||||
|
// neighbors: [{
|
||||||
|
// offset: [x,y,z],
|
||||||
|
// terrain: { mesh, texture } | null,
|
||||||
|
// models: [{ mesh, texture, color, offset }],
|
||||||
|
// }],
|
||||||
|
// highlight: { x, y, z } | null,
|
||||||
|
// }
|
||||||
|
function render(scene) {
|
||||||
|
const width = canvas.width, height = canvas.height;
|
||||||
|
gl.viewport(0, 0, width, height);
|
||||||
|
gl.clearColor(0.16, 0.18, 0.22, 1);
|
||||||
|
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
||||||
|
gl.useProgram(program);
|
||||||
|
gl.depthMask(true);
|
||||||
|
|
||||||
|
const { view, fov } = computeCamera(scene.target, scene.worldH);
|
||||||
|
const projection = mat4Perspective(new Float32Array(16), fov, width / height, 0.1, 1000);
|
||||||
|
gl.uniformMatrix4fv(uniforms.uProjection, false, projection);
|
||||||
|
gl.uniformMatrix4fv(uniforms.uView, false, view);
|
||||||
|
|
||||||
|
const identity = mat4Identity(new Float32Array(16));
|
||||||
|
|
||||||
|
if(scene.terrain) {
|
||||||
|
drawMesh(scene.terrain.mesh, identity, scene.terrain.texture, [1, 1, 1, 1], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(scene.gridLines) {
|
||||||
|
drawMesh(scene.gridLines.mesh, identity, null, [0, 0, 0, 0.35], false, gl.LINES);
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const model of scene.models) {
|
||||||
|
const modelMatrix = mat4Translation(new Float32Array(16), model.offset);
|
||||||
|
drawMesh(model.mesh, modelMatrix, model.texture, model.color, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const neighbor of scene.neighbors || []) {
|
||||||
|
if(neighbor.terrain) {
|
||||||
|
const modelMatrix = mat4Translation(new Float32Array(16), neighbor.offset);
|
||||||
|
drawMesh(
|
||||||
|
neighbor.terrain.mesh, modelMatrix, neighbor.terrain.texture,
|
||||||
|
[NEIGHBOR_DARKEN, NEIGHBOR_DARKEN, NEIGHBOR_DARKEN, NEIGHBOR_ALPHA], false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const model of neighbor.models) {
|
||||||
|
const worldOffset = [
|
||||||
|
neighbor.offset[0] + model.offset[0],
|
||||||
|
neighbor.offset[1] + model.offset[1],
|
||||||
|
neighbor.offset[2] + model.offset[2],
|
||||||
|
];
|
||||||
|
const modelMatrix = mat4Translation(new Float32Array(16), worldOffset);
|
||||||
|
const color = [
|
||||||
|
model.color[0] * NEIGHBOR_DARKEN,
|
||||||
|
model.color[1] * NEIGHBOR_DARKEN,
|
||||||
|
model.color[2] * NEIGHBOR_DARKEN,
|
||||||
|
model.color[3] * NEIGHBOR_ALPHA,
|
||||||
|
];
|
||||||
|
drawMesh(model.mesh, modelMatrix, model.texture, color, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(scene.highlight) {
|
||||||
|
const { x, y, z } = scene.highlight;
|
||||||
|
const highlightMesh = buildHighlightMesh(x, y, z);
|
||||||
|
drawMesh(highlightMesh, identity, null, [1, 0.92, 0.23, 1], false, gl.LINE_LOOP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { gl, createMesh, createTextureFromImage, render };
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.freeze({ create, computeCamera });
|
||||||
|
})();
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Dusk Map Editor</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand navbar-dark bg-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="/">Dusk Map Editor</a>
|
||||||
|
<ul class="navbar-nav flex-row">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/map">Map Editor</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div id="editor" class="container-fluid py-3"></div>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
"""
|
||||||
|
Map Editor HTTP Server
|
||||||
|
|
||||||
|
Serves editor/client/public/ as static files with smart routing:
|
||||||
|
/ -> index.html
|
||||||
|
/page -> page.html or page/index.html
|
||||||
|
/index -> index.html
|
||||||
|
|
||||||
|
API endpoints (all paths relative to project root):
|
||||||
|
GET /api/ls?path=<dir> directory listing
|
||||||
|
GET /api/read?path=<file> file contents
|
||||||
|
GET /api/find?path=<dir>&ext=<.json> recursive file listing by extension
|
||||||
|
POST /api/write?path=<file> write file (JSON body: {"content":"..."})
|
||||||
|
POST /api/mkdir?path=<dir> create directory
|
||||||
|
POST /api/delete?path=<file> delete a file
|
||||||
|
POST /api/compile-chunk?x=&y=&z= recompile a chunk JSON to .dcf
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import mimetypes
|
||||||
|
import subprocess
|
||||||
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||||
|
from urllib.parse import urlparse, parse_qs, unquote
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
CHUNK_COORD_RE = re.compile(r"^-?\d+$")
|
||||||
|
|
||||||
|
PORT = 3000
|
||||||
|
SCRIPT_DIR = Path(__file__).parent.resolve()
|
||||||
|
PUBLIC_DIR = SCRIPT_DIR.parent / "client" / "public"
|
||||||
|
PROJECT_ROOT = SCRIPT_DIR.parent.parent.resolve()
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(BaseHTTPRequestHandler):
|
||||||
|
def do_GET(self):
|
||||||
|
parsed = urlparse(self.path)
|
||||||
|
path = unquote(parsed.path)
|
||||||
|
if path.startswith("/api/"):
|
||||||
|
params = parse_qs(parsed.query)
|
||||||
|
if path == "/api/ls":
|
||||||
|
self.api_ls(params)
|
||||||
|
elif path == "/api/read":
|
||||||
|
self.api_read(params)
|
||||||
|
elif path == "/api/find":
|
||||||
|
self.api_find(params)
|
||||||
|
else:
|
||||||
|
self.send_error(404)
|
||||||
|
else:
|
||||||
|
self.serve_static(path)
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
parsed = urlparse(self.path)
|
||||||
|
params = parse_qs(parsed.query)
|
||||||
|
if parsed.path == "/api/write":
|
||||||
|
self.api_write(params)
|
||||||
|
elif parsed.path == "/api/mkdir":
|
||||||
|
self.api_mkdir(params)
|
||||||
|
elif parsed.path == "/api/delete":
|
||||||
|
self.api_delete(params)
|
||||||
|
elif parsed.path == "/api/compile-chunk":
|
||||||
|
self.api_compile_chunk(params)
|
||||||
|
else:
|
||||||
|
self.send_error(404)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def resolve_project_path(self, rel):
|
||||||
|
rel = rel.lstrip("/")
|
||||||
|
target = (PROJECT_ROOT / rel).resolve()
|
||||||
|
if not str(target).startswith(str(PROJECT_ROOT)):
|
||||||
|
return None
|
||||||
|
return target
|
||||||
|
|
||||||
|
def send_json(self, data, code=200):
|
||||||
|
body = json.dumps(data, indent=2).encode("utf-8")
|
||||||
|
self.send_response(code)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.send_header("Content-Length", len(body))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
def read_body(self):
|
||||||
|
length = int(self.headers.get("Content-Length", 0))
|
||||||
|
return self.rfile.read(length)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# API handlers
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def api_ls(self, params):
|
||||||
|
rel = params.get("path", [""])[0]
|
||||||
|
target = self.resolve_project_path(rel)
|
||||||
|
if target is None:
|
||||||
|
self.send_json({"error": "invalid path"}, 400)
|
||||||
|
return
|
||||||
|
if not target.exists():
|
||||||
|
self.send_json({"error": "path not found"}, 404)
|
||||||
|
return
|
||||||
|
if not target.is_dir():
|
||||||
|
self.send_json({"error": "not a directory"}, 400)
|
||||||
|
return
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
for entry in sorted(target.iterdir(), key=lambda e: (e.is_file(), e.name)):
|
||||||
|
entries.append({
|
||||||
|
"name": entry.name,
|
||||||
|
"type": "dir" if entry.is_dir() else "file",
|
||||||
|
"size": entry.stat().st_size if entry.is_file() else None,
|
||||||
|
})
|
||||||
|
self.send_json({
|
||||||
|
"path": str(target.relative_to(PROJECT_ROOT)),
|
||||||
|
"entries": entries,
|
||||||
|
})
|
||||||
|
|
||||||
|
def api_read(self, params):
|
||||||
|
rel = params.get("path", [""])[0]
|
||||||
|
target = self.resolve_project_path(rel)
|
||||||
|
if target is None:
|
||||||
|
self.send_json({"error": "invalid path"}, 400)
|
||||||
|
return
|
||||||
|
if not target.exists():
|
||||||
|
self.send_json({"error": "file not found"}, 404)
|
||||||
|
return
|
||||||
|
if not target.is_file():
|
||||||
|
self.send_json({"error": "not a file"}, 400)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
content = target.read_text(encoding="utf-8")
|
||||||
|
self.send_json({
|
||||||
|
"path": str(target.relative_to(PROJECT_ROOT)),
|
||||||
|
"content": content,
|
||||||
|
})
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
import base64
|
||||||
|
content = base64.b64encode(target.read_bytes()).decode("ascii")
|
||||||
|
self.send_json({
|
||||||
|
"path": str(target.relative_to(PROJECT_ROOT)),
|
||||||
|
"content": content,
|
||||||
|
"encoding": "base64",
|
||||||
|
})
|
||||||
|
|
||||||
|
def api_write(self, params):
|
||||||
|
rel = params.get("path", [""])[0]
|
||||||
|
target = self.resolve_project_path(rel)
|
||||||
|
if target is None:
|
||||||
|
self.send_json({"error": "invalid path"}, 400)
|
||||||
|
return
|
||||||
|
|
||||||
|
body = self.read_body()
|
||||||
|
try:
|
||||||
|
data = json.loads(body)
|
||||||
|
content = data.get("content", "")
|
||||||
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||||
|
content = body.decode("utf-8")
|
||||||
|
|
||||||
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
target.write_text(content, encoding="utf-8")
|
||||||
|
self.send_json({"ok": True, "path": str(target.relative_to(PROJECT_ROOT))})
|
||||||
|
|
||||||
|
def api_mkdir(self, params):
|
||||||
|
rel = params.get("path", [""])[0]
|
||||||
|
target = self.resolve_project_path(rel)
|
||||||
|
if target is None:
|
||||||
|
self.send_json({"error": "invalid path"}, 400)
|
||||||
|
return
|
||||||
|
target.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.send_json({"ok": True, "path": str(target.relative_to(PROJECT_ROOT))})
|
||||||
|
|
||||||
|
def api_delete(self, params):
|
||||||
|
rel = params.get("path", [""])[0]
|
||||||
|
target = self.resolve_project_path(rel)
|
||||||
|
if target is None:
|
||||||
|
self.send_json({"error": "invalid path"}, 400)
|
||||||
|
return
|
||||||
|
if target.is_file():
|
||||||
|
target.unlink()
|
||||||
|
self.send_json({"ok": True, "path": str(target.relative_to(PROJECT_ROOT))})
|
||||||
|
|
||||||
|
def api_find(self, params):
|
||||||
|
rel = params.get("path", [""])[0]
|
||||||
|
ext = params.get("ext", [""])[0]
|
||||||
|
target = self.resolve_project_path(rel)
|
||||||
|
if target is None:
|
||||||
|
self.send_json({"error": "invalid path"}, 400)
|
||||||
|
return
|
||||||
|
if not target.is_dir():
|
||||||
|
self.send_json({"error": "not a directory"}, 400)
|
||||||
|
return
|
||||||
|
|
||||||
|
pattern = f"*{ext}" if ext else "*"
|
||||||
|
paths = sorted(
|
||||||
|
str(p.relative_to(target)).replace(os.sep, "/")
|
||||||
|
for p in target.rglob(pattern)
|
||||||
|
if p.is_file()
|
||||||
|
)
|
||||||
|
self.send_json({"path": str(target.relative_to(PROJECT_ROOT)), "files": paths})
|
||||||
|
|
||||||
|
def api_compile_chunk(self, params):
|
||||||
|
x = params.get("x", [""])[0]
|
||||||
|
y = params.get("y", [""])[0]
|
||||||
|
z = params.get("z", [""])[0]
|
||||||
|
if not (CHUNK_COORD_RE.match(x) and CHUNK_COORD_RE.match(y) and CHUNK_COORD_RE.match(z)):
|
||||||
|
self.send_json({"error": "invalid chunk coordinates"}, 400)
|
||||||
|
return
|
||||||
|
|
||||||
|
json_path = PROJECT_ROOT / "assetsraw" / "chunks" / f"chunk_{x}_{y}_{z}.json"
|
||||||
|
if not json_path.is_file():
|
||||||
|
self.send_json({"error": "chunk json not found"}, 404)
|
||||||
|
return
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, "-m", "tools.asset.chunk", str(json_path)],
|
||||||
|
cwd=str(PROJECT_ROOT),
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
self.send_json({
|
||||||
|
"ok": result.returncode == 0,
|
||||||
|
"returncode": result.returncode,
|
||||||
|
"stdout": result.stdout,
|
||||||
|
"stderr": result.stderr,
|
||||||
|
})
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Static file serving
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def serve_static(self, path):
|
||||||
|
clean = path.lstrip("/")
|
||||||
|
candidates = []
|
||||||
|
|
||||||
|
if clean == "" or clean == "index" or clean == "index.html":
|
||||||
|
candidates = ["index.html"]
|
||||||
|
else:
|
||||||
|
candidates.append(clean)
|
||||||
|
if "." not in Path(clean).name:
|
||||||
|
candidates.append(clean + ".html")
|
||||||
|
candidates.append(clean + "/index.html")
|
||||||
|
|
||||||
|
for candidate in candidates:
|
||||||
|
file_path = PUBLIC_DIR / candidate
|
||||||
|
if file_path.is_file():
|
||||||
|
self.serve_file(file_path)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.send_error(404)
|
||||||
|
|
||||||
|
def serve_file(self, file_path):
|
||||||
|
mime, _ = mimetypes.guess_type(str(file_path))
|
||||||
|
mime = mime or "application/octet-stream"
|
||||||
|
|
||||||
|
template = PUBLIC_DIR / "template.html"
|
||||||
|
if (
|
||||||
|
mime == "text/html"
|
||||||
|
and file_path.name != "template.html"
|
||||||
|
and template.is_file()
|
||||||
|
):
|
||||||
|
fragment = file_path.read_text(encoding="utf-8")
|
||||||
|
shell = template.read_text(encoding="utf-8")
|
||||||
|
marker = shell.find('id="editor"')
|
||||||
|
if marker == -1:
|
||||||
|
content = shell.encode("utf-8")
|
||||||
|
else:
|
||||||
|
tag_end = shell.index(">", marker) + 1
|
||||||
|
close_idx = shell.index("</div>", tag_end)
|
||||||
|
content = (shell[:tag_end] + fragment + shell[close_idx:]).encode("utf-8")
|
||||||
|
else:
|
||||||
|
content = file_path.read_bytes()
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", mime)
|
||||||
|
self.send_header("Content-Length", len(content))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(content)
|
||||||
|
|
||||||
|
def log_message(self, fmt, *args):
|
||||||
|
print(f" {self.address_string()} {fmt % args}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(f"Map Editor")
|
||||||
|
print(f" http://localhost:{PORT}")
|
||||||
|
print(f" public : {PUBLIC_DIR}")
|
||||||
|
print(f" project: {PROJECT_ROOT}")
|
||||||
|
server = HTTPServer(("", PORT), Handler)
|
||||||
|
try:
|
||||||
|
server.serve_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nStopped.")
|
||||||
Executable
+4
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
python3 server/server.py
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
|
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
|
||||||
docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/gamecube/build-gamecube.sh"
|
docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-gamecube.sh"
|
||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
|
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
|
||||||
docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/gamecube/build-gamecube-iso.sh"
|
docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-gamecube-iso.sh"
|
||||||
File renamed without changes.
File renamed without changes.
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
docker build -t dusk-knulli -f docker/knulli/Dockerfile .
|
docker build -t dusk-knulli -f docker/knulli/Dockerfile .
|
||||||
docker run --rm -v "$(pwd):/workdir" dusk-knulli /bin/bash -c "./scripts/knulli/build-knulli.sh"
|
docker run --rm -v "$(pwd):/workdir" dusk-knulli /bin/bash -c "./scripts/build-knulli.sh"
|
||||||
File renamed without changes.
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
docker build -t dusk-linux -f docker/linux/Dockerfile .
|
docker build -t dusk-linux -f docker/linux/Dockerfile .
|
||||||
docker run --rm -v "$(pwd):/workdir" dusk-linux /bin/bash -c "./scripts/linux/build-linux.sh"
|
docker run --rm -v "$(pwd):/workdir" dusk-linux /bin/bash -c "./scripts/build-linux.sh"
|
||||||
File renamed without changes.
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
docker build -t dusk-psp -f docker/psp/Dockerfile .
|
docker build -t dusk-psp -f docker/psp/Dockerfile .
|
||||||
docker run --rm -v "$(pwd):/workdir" dusk-psp /bin/bash -c "./scripts/psp/build-psp.sh"
|
docker run --rm -v "$(pwd):/workdir" dusk-psp /bin/bash -c "./scripts/build-psp.sh"
|
||||||
File renamed without changes.
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
docker build -t dusk-vita -f docker/vita/Dockerfile .
|
docker build -t dusk-vita -f docker/vita/Dockerfile .
|
||||||
docker run --rm -v "$(pwd):/workdir" dusk-vita /bin/bash -c "./scripts/vita/build-vita.sh"
|
docker run --rm -v "$(pwd):/workdir" dusk-vita /bin/bash -c "./scripts/build-vita.sh"
|
||||||
File renamed without changes.
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
|
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
|
||||||
docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/wii/build-wii.sh"
|
docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-wii.sh"
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
|
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
|
||||||
docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/wii/build-wii-iso.sh"
|
docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-wii-iso.sh"
|
||||||
File renamed without changes.
File renamed without changes.
@@ -4,4 +4,4 @@ docker run \
|
|||||||
--rm \
|
--rm \
|
||||||
-v "${GITHUB_WORKSPACE}:/workdir" \
|
-v "${GITHUB_WORKSPACE}:/workdir" \
|
||||||
dusk-linux \
|
dusk-linux \
|
||||||
/bin/bash -c "./scripts/linux/test-linux.sh"
|
/bin/bash -c "./scripts/test-linux.sh"
|
||||||
File renamed without changes.
+2
-16
@@ -5,31 +5,17 @@
|
|||||||
|
|
||||||
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)
|
||||||
add_subdirectory(duskgl)
|
add_subdirectory(duskgl)
|
||||||
add_subdirectory(duskmad)
|
|
||||||
|
|
||||||
elseif(DUSK_TARGET_SYSTEM STREQUAL "psp")
|
elseif(DUSK_TARGET_SYSTEM STREQUAL "psp")
|
||||||
add_subdirectory(duskpsp)
|
add_subdirectory(duskpsp)
|
||||||
add_subdirectory(dusksdl2)
|
add_subdirectory(dusksdl2)
|
||||||
add_subdirectory(duskgl)
|
add_subdirectory(duskgl)
|
||||||
|
|
||||||
elseif(DUSK_TARGET_SYSTEM STREQUAL "vita")
|
elseif(DUSK_TARGET_SYSTEM STREQUAL "wii" OR DUSK_TARGET_SYSTEM STREQUAL "gamecube")
|
||||||
add_subdirectory(duskvita)
|
|
||||||
add_subdirectory(dusksdl2)
|
|
||||||
add_subdirectory(duskgl)
|
|
||||||
|
|
||||||
elseif(
|
|
||||||
DUSK_TARGET_SYSTEM STREQUAL "wii" OR
|
|
||||||
DUSK_TARGET_SYSTEM STREQUAL "gamecube"
|
|
||||||
)
|
|
||||||
add_subdirectory(duskdolphin)
|
add_subdirectory(duskdolphin)
|
||||||
add_subdirectory(duskmad)
|
|
||||||
|
|
||||||
endif()
|
endif()
|
||||||
+6
-21
@@ -14,21 +14,12 @@ if(NOT libzip_FOUND)
|
|||||||
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC zip)
|
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC zip)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# assetdsk.c calls crc32() directly (to verify dusk.dsk archive checksums).
|
|
||||||
# find_package(libzip) above already resolves ZLIB as a side effect, so
|
|
||||||
# ZLIB_FOUND may already be true without ZLIB::ZLIB having been linked to
|
|
||||||
# our target - link it unconditionally rather than guarding on ZLIB_FOUND.
|
|
||||||
find_package(ZLIB REQUIRED)
|
|
||||||
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC ZLIB::ZLIB)
|
|
||||||
|
|
||||||
if(NOT stb_image_FOUND)
|
if(NOT stb_image_FOUND)
|
||||||
find_package(stb REQUIRED)
|
find_package(stb REQUIRED)
|
||||||
if(STB_IMAGE_FOUND)
|
if(STB_IMAGE_FOUND)
|
||||||
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC stb_image)
|
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC stb_image)
|
||||||
else()
|
else()
|
||||||
message(FATAL_ERROR
|
message(FATAL_ERROR "stb_image not found. Please ensure stb is correctly fetched.")
|
||||||
"stb_image not found. Please ensure stb is correctly fetched."
|
|
||||||
)
|
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -37,9 +28,7 @@ if(NOT yyjson_FOUND)
|
|||||||
if(yyjson_FOUND)
|
if(yyjson_FOUND)
|
||||||
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC yyjson::yyjson)
|
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC yyjson::yyjson)
|
||||||
else()
|
else()
|
||||||
message(FATAL_ERROR
|
message(FATAL_ERROR "yyjson not found. Please ensure yyjson is correctly fetched.")
|
||||||
"yyjson not found. Please ensure yyjson is correctly fetched."
|
|
||||||
)
|
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -50,12 +39,6 @@ if(DUSK_BACKTRACE)
|
|||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(DUSK_TRACK_MEMORY)
|
|
||||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
|
||||||
DUSK_TRACK_MEMORY
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# Includes
|
# Includes
|
||||||
target_include_directories(${DUSK_LIBRARY_TARGET_NAME}
|
target_include_directories(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
@@ -70,9 +53,9 @@ target_sources(${DUSK_BINARY_TARGET_NAME}
|
|||||||
|
|
||||||
# Subdirs
|
# Subdirs
|
||||||
add_subdirectory(animation)
|
add_subdirectory(animation)
|
||||||
|
add_subdirectory(event)
|
||||||
add_subdirectory(assert)
|
add_subdirectory(assert)
|
||||||
add_subdirectory(asset)
|
add_subdirectory(asset)
|
||||||
add_subdirectory(audio)
|
|
||||||
add_subdirectory(console)
|
add_subdirectory(console)
|
||||||
add_subdirectory(display)
|
add_subdirectory(display)
|
||||||
add_subdirectory(log)
|
add_subdirectory(log)
|
||||||
@@ -80,10 +63,12 @@ add_subdirectory(engine)
|
|||||||
add_subdirectory(error)
|
add_subdirectory(error)
|
||||||
add_subdirectory(input)
|
add_subdirectory(input)
|
||||||
add_subdirectory(locale)
|
add_subdirectory(locale)
|
||||||
add_subdirectory(perf)
|
add_subdirectory(rpg)
|
||||||
add_subdirectory(scene)
|
add_subdirectory(scene)
|
||||||
add_subdirectory(system)
|
add_subdirectory(system)
|
||||||
add_subdirectory(time)
|
add_subdirectory(time)
|
||||||
|
add_subdirectory(ui)
|
||||||
|
add_subdirectory(network)
|
||||||
add_subdirectory(save)
|
add_subdirectory(save)
|
||||||
add_subdirectory(util)
|
add_subdirectory(util)
|
||||||
add_subdirectory(thread)
|
add_subdirectory(thread)
|
||||||
@@ -6,4 +6,5 @@
|
|||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
easing.c
|
easing.c
|
||||||
|
animation.c
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// Copyright (c) 2026 Dominic Masters
|
||||||
|
//
|
||||||
|
// This software is released under the MIT License.
|
||||||
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
#include "animation.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/math.h"
|
||||||
|
|
||||||
|
void animationInit(
|
||||||
|
animation_t *anim,
|
||||||
|
keyframe_t *keyframes,
|
||||||
|
uint16_t keyframeCount
|
||||||
|
) {
|
||||||
|
assertNotNull(anim, "Animation pointer cannot be null.");
|
||||||
|
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
|
||||||
|
assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
|
||||||
|
|
||||||
|
anim->keyframes = keyframes;
|
||||||
|
anim->keyframeCount = keyframeCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
float_t animationGetValue(animation_t *anim, const float_t time) {
|
||||||
|
assertNotNull(anim, "Animation pointer cannot be null.");
|
||||||
|
assertNotNull(anim->keyframes, "Keyframes pointer cannot be null.");
|
||||||
|
assertTrue(anim->keyframeCount > 0, "Keyframe count invalid.");
|
||||||
|
assertTrue(time >= 0, "Time must be non-negative.");
|
||||||
|
|
||||||
|
keyframe_t *start;
|
||||||
|
keyframe_t *end;
|
||||||
|
keyframe_t *last = anim->keyframes + anim->keyframeCount - 1;
|
||||||
|
keyframe_t *current = anim->keyframes;
|
||||||
|
start = current;
|
||||||
|
|
||||||
|
do {
|
||||||
|
if(current->time > time) {
|
||||||
|
end = current;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
start = current;
|
||||||
|
current++;
|
||||||
|
|
||||||
|
if(current > last) {
|
||||||
|
end = start;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while(true);
|
||||||
|
|
||||||
|
float_t t = (time - start->time) / (end->time - start->time);
|
||||||
|
return mathLerp(start->value, end->value, easingApply(start->easing, t));
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (c) 2026 Dominic Masters
|
||||||
|
//
|
||||||
|
// This software is released under the MIT License.
|
||||||
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "keyframe.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
keyframe_t *keyframes;
|
||||||
|
uint16_t keyframeCount;
|
||||||
|
} animation_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes an animation.
|
||||||
|
*
|
||||||
|
* @param anim The animation to initialize.
|
||||||
|
* @param keyframes The keyframes to use for the animation.
|
||||||
|
* @param keyframeCount The number of keyframes in the animation.
|
||||||
|
*/
|
||||||
|
void animationInit(
|
||||||
|
animation_t *anim,
|
||||||
|
keyframe_t *keyframes,
|
||||||
|
uint16_t keyframeCount
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the value of the animation at a given time.
|
||||||
|
*
|
||||||
|
* @param anim The animation to get the value from.
|
||||||
|
* @param time The time at which to get the value, in seconds.
|
||||||
|
* @return The value of the animation at the given time.
|
||||||
|
*/
|
||||||
|
float_t animationGetValue(animation_t *anim, const float_t time);
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Copyright (c) 2026 Dominic Masters
|
||||||
|
//
|
||||||
|
// This software is released under the MIT License.
|
||||||
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "easing.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
float_t time;
|
||||||
|
float_t value;
|
||||||
|
easingtype_t easing;
|
||||||
|
} keyframe_t;
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
asset.c
|
asset.c
|
||||||
assetdsk.c
|
assetbatch.c
|
||||||
assetfile.c
|
assetfile.c
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+6
-23
@@ -19,8 +19,6 @@ asset_t ASSET;
|
|||||||
errorret_t assetInit(void) {
|
errorret_t assetInit(void) {
|
||||||
memoryZero(&ASSET, sizeof(asset_t));
|
memoryZero(&ASSET, sizeof(asset_t));
|
||||||
|
|
||||||
threadMutexInit(&ASSET.zipLock);
|
|
||||||
|
|
||||||
for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
|
for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
|
||||||
threadMutexInit(&ASSET.loading[i].mutex);
|
threadMutexInit(&ASSET.loading[i].mutex);
|
||||||
}
|
}
|
||||||
@@ -37,10 +35,9 @@ errorret_t assetInit(void) {
|
|||||||
bool_t assetFileExists(const char_t *filename) {
|
bool_t assetFileExists(const char_t *filename) {
|
||||||
assertStrLenMax(filename, ASSET_FILE_NAME_MAX, "Filename too long.");
|
assertStrLenMax(filename, ASSET_FILE_NAME_MAX, "Filename too long.");
|
||||||
|
|
||||||
threadMutexLock(&ASSET.zipLock);
|
zip_int64_t idx = zip_name_locate(ASSET.zip, filename, 0);
|
||||||
bool_t found = zip_name_locate(ASSET.zip, filename, 0) >= 0;
|
if(idx < 0) return false;
|
||||||
threadMutexUnlock(&ASSET.zipLock);
|
return true;
|
||||||
return found;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assetentry_t * assetGetEntry(
|
assetentry_t * assetGetEntry(
|
||||||
@@ -120,15 +117,6 @@ errorret_t assetRequireLoaded(assetentry_t *entry) {
|
|||||||
assetEntryLock(entry);
|
assetEntryLock(entry);
|
||||||
|
|
||||||
while(entry->state != ASSET_ENTRY_STATE_LOADED) {
|
while(entry->state != ASSET_ENTRY_STATE_LOADED) {
|
||||||
// A failed load transitions to ERROR, not LOADED - without this check
|
|
||||||
// this loop spins forever on any load failure (assetUpdate() itself
|
|
||||||
// still returns OK, since a single asset failing isn't meant to halt
|
|
||||||
// the whole update loop - see its ASSET_ENTRY_STATE_ERROR handling).
|
|
||||||
if(entry->state == ASSET_ENTRY_STATE_ERROR) {
|
|
||||||
assetEntryUnlock(entry);
|
|
||||||
errorThrow("Failed to load asset: %s", entry->name);
|
|
||||||
}
|
|
||||||
|
|
||||||
usleep(1000);
|
usleep(1000);
|
||||||
errorret_t ret = assetUpdate();
|
errorret_t ret = assetUpdate();
|
||||||
if(errorIsNotOk(ret)) {
|
if(errorIsNotOk(ret)) {
|
||||||
@@ -334,9 +322,7 @@ errorret_t assetUpdate(void) {
|
|||||||
} else if(loading->entry->state == ASSET_ENTRY_STATE_LOADED) {
|
} else if(loading->entry->state == ASSET_ENTRY_STATE_LOADED) {
|
||||||
assetentry_t *loadedEntry = loading->entry;
|
assetentry_t *loadedEntry = loading->entry;
|
||||||
loading->entry = NULL;
|
loading->entry = NULL;
|
||||||
if(loadedEntry->onLoaded) {
|
eventInvoke(&loadedEntry->onLoaded, loadedEntry);
|
||||||
loadedEntry->onLoaded(loadedEntry, loadedEntry->onLoadedUser);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loading++;
|
loading++;
|
||||||
@@ -360,10 +346,8 @@ errorret_t assetUpdate(void) {
|
|||||||
assetentry_t *errEntry = loading->entry;
|
assetentry_t *errEntry = loading->entry;
|
||||||
loading->entry = NULL;
|
loading->entry = NULL;
|
||||||
threadMutexUnlock(&loading->mutex);
|
threadMutexUnlock(&loading->mutex);
|
||||||
if(errEntry->onError) {
|
eventInvoke(&errEntry->onError, errEntry);
|
||||||
errEntry->onError(errEntry, errEntry->onErrorUser);
|
errorThrow("Failed to load asset asynchronously.");
|
||||||
}
|
|
||||||
loading++;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,6 +434,5 @@ errorret_t assetDispose(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
errorChain(assetDisposePlatform());
|
errorChain(assetDisposePlatform());
|
||||||
threadMutexDispose(&ASSET.zipLock);
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -26,40 +26,10 @@
|
|||||||
#define ASSET_LOADING_COUNT_MAX 10
|
#define ASSET_LOADING_COUNT_MAX 10
|
||||||
#define ASSET_ENTRY_COUNT_MAX 64
|
#define ASSET_ENTRY_COUNT_MAX 64
|
||||||
|
|
||||||
#define ASSET_BASE_DIRECTORY_MAX 256
|
|
||||||
|
|
||||||
typedef struct asset_s {
|
typedef struct asset_s {
|
||||||
// The dusk.dsk archive - a single uncompressed (ZIP_STORED) zip, so
|
|
||||||
// every entry gets the reliable repeated seeking/re-opening that
|
|
||||||
// libzip only supports reliably for uncompressed entries (locale
|
|
||||||
// strings, MP3 loop restarts, etc.).
|
|
||||||
zip_t *zip;
|
zip_t *zip;
|
||||||
|
|
||||||
// Directory dusk.dsk was actually found/opened in (on PSP, the directory
|
|
||||||
// EBOOT.PBP lives in, since dusk.dsk is embedded inside it rather than a
|
|
||||||
// loose file) - populated by each platform's own assetInitXxx() once it
|
|
||||||
// locates the file. Save devices that persist to a real filesystem path
|
|
||||||
// (Linux, PSP, Wii SD) use this as their base directory instead of a
|
|
||||||
// separate hardcoded location, so saves live next to wherever the game
|
|
||||||
// itself was actually launched from rather than some unrelated fixed
|
|
||||||
// path. Not meaningful for devices with no filesystem-path concept at
|
|
||||||
// all (GameCube/Wii memory card, Wii NAND's ISFS namespace).
|
|
||||||
char_t baseDirectory[ASSET_BASE_DIRECTORY_MAX];
|
|
||||||
|
|
||||||
assetplatform_t platform;
|
assetplatform_t platform;
|
||||||
|
|
||||||
// Guards every libzip call against ASSET.zip (zip_fopen,
|
|
||||||
// zip_fread, zip_fclose, zip_fseek, zip_stat, zip_name_locate - see
|
|
||||||
// assetfile.c/assetFileExists). libzip is documented as not thread-safe,
|
|
||||||
// and this asset system genuinely calls it from multiple real threads at
|
|
||||||
// once (main thread, the background load thread below, and - on
|
|
||||||
// platforms that stream PCM from an asset on their own thread, like PSP
|
|
||||||
// - an audio feeder thread too). Confirmed necessary on real PSP
|
|
||||||
// hardware: without this lock, concurrent zip_fopen/zip_fread calls from
|
|
||||||
// different threads corrupted reads (EINVAL, then zlib data errors) once
|
|
||||||
// dusk.dsk stopped being read entirely into memory up front.
|
|
||||||
threadmutex_t zipLock;
|
|
||||||
|
|
||||||
// Background loading thread.
|
// Background loading thread.
|
||||||
thread_t loadThread;
|
thread_t loadThread;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "assetbatch.h"
|
||||||
|
#include "asset.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
void assetBatchInit(
|
||||||
|
assetbatch_t *batch,
|
||||||
|
const uint16_t count,
|
||||||
|
const assetbatchdesc_t *descs
|
||||||
|
) {
|
||||||
|
assertNotNull(batch, "Batch cannot be NULL.");
|
||||||
|
assertNotNull(descs, "Descs cannot be NULL.");
|
||||||
|
assertTrue(count > 0, "Count must be greater than 0.");
|
||||||
|
assertTrue(
|
||||||
|
count <= ASSET_BATCH_COUNT_MAX, "Count exceeds ASSET_BATCH_COUNT_MAX."
|
||||||
|
);
|
||||||
|
|
||||||
|
memoryZero(batch, sizeof(assetbatch_t));
|
||||||
|
batch->count = count;
|
||||||
|
|
||||||
|
eventInit(
|
||||||
|
&batch->onLoaded,
|
||||||
|
batch->onLoadedCallbacks, batch->onLoadedUsers, ASSET_BATCH_EVENT_MAX
|
||||||
|
);
|
||||||
|
eventInit(
|
||||||
|
&batch->onEntryLoaded,
|
||||||
|
batch->onEntryLoadedCallbacks,
|
||||||
|
batch->onEntryLoadedUsers,
|
||||||
|
ASSET_BATCH_EVENT_MAX
|
||||||
|
);
|
||||||
|
eventInit(
|
||||||
|
&batch->onError,
|
||||||
|
batch->onErrorCallbacks, batch->onErrorUsers, ASSET_BATCH_EVENT_MAX
|
||||||
|
);
|
||||||
|
eventInit(
|
||||||
|
&batch->onEntryError,
|
||||||
|
batch->onEntryErrorCallbacks,
|
||||||
|
batch->onEntryErrorUsers,
|
||||||
|
ASSET_BATCH_EVENT_MAX
|
||||||
|
);
|
||||||
|
|
||||||
|
for(uint16_t i = 0; i < count; i++) {
|
||||||
|
batch->inputs[i] = descs[i].input;
|
||||||
|
batch->entries[i] = assetLock(
|
||||||
|
descs[i].path, descs[i].type, &batch->inputs[i]
|
||||||
|
);
|
||||||
|
|
||||||
|
if(batch->entries[i]->state == ASSET_ENTRY_STATE_LOADED) {
|
||||||
|
// Already loaded (cached) - count it now, no subscription needed.
|
||||||
|
batch->loadedCount++;
|
||||||
|
} else if(batch->entries[i]->state == ASSET_ENTRY_STATE_ERROR) {
|
||||||
|
batch->errorCount++;
|
||||||
|
} else {
|
||||||
|
eventSubscribe(
|
||||||
|
&batch->entries[i]->onLoaded, assetBatchEntryOnLoadedCb, batch
|
||||||
|
);
|
||||||
|
eventSubscribe(
|
||||||
|
&batch->entries[i]->onError, assetBatchEntryOnErrorCb, batch
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void assetBatchLock(assetbatch_t *batch) {
|
||||||
|
assertNotNull(batch, "Batch cannot be NULL.");
|
||||||
|
for(uint16_t i = 0; i < batch->count; i++) {
|
||||||
|
assetEntryLock(batch->entries[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void assetBatchUnlock(assetbatch_t *batch) {
|
||||||
|
assertNotNull(batch, "Batch cannot be NULL.");
|
||||||
|
for(uint16_t i = 0; i < batch->count; i++) {
|
||||||
|
assetEntryUnlock(batch->entries[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t assetBatchIsLoaded(const assetbatch_t *batch) {
|
||||||
|
assertNotNull(batch, "Batch cannot be NULL.");
|
||||||
|
for(uint16_t i = 0; i < batch->count; i++) {
|
||||||
|
if(batch->entries[i]->state != ASSET_ENTRY_STATE_LOADED) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t assetBatchHasError(const assetbatch_t *batch) {
|
||||||
|
assertNotNull(batch, "Batch cannot be NULL.");
|
||||||
|
for(uint16_t i = 0; i < batch->count; i++) {
|
||||||
|
if(batch->entries[i]->state == ASSET_ENTRY_STATE_ERROR) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t assetBatchRequireLoaded(assetbatch_t *batch) {
|
||||||
|
assertNotNull(batch, "Batch cannot be NULL.");
|
||||||
|
|
||||||
|
bool_t allDone;
|
||||||
|
do {
|
||||||
|
allDone = true;
|
||||||
|
for(uint16_t i = 0; i < batch->count; i++) {
|
||||||
|
const assetentrystate_t state = batch->entries[i]->state;
|
||||||
|
if(state == ASSET_ENTRY_STATE_ERROR) {
|
||||||
|
errorThrow("Asset '%s' failed to load.", batch->entries[i]->name);
|
||||||
|
}
|
||||||
|
if(state != ASSET_ENTRY_STATE_LOADED) {
|
||||||
|
allDone = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!allDone) {
|
||||||
|
usleep(1000);
|
||||||
|
errorChain(assetUpdate());
|
||||||
|
}
|
||||||
|
} while(!allDone);
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
void assetBatchDispose(assetbatch_t *batch) {
|
||||||
|
assertNotNull(batch, "Batch cannot be NULL.");
|
||||||
|
for(uint16_t i = 0; i < batch->count; i++) {
|
||||||
|
if(batch->entries[i]) {
|
||||||
|
// Unsubscribe while we still hold a lock so the entry is live.
|
||||||
|
eventUnsubscribe(&batch->entries[i]->onLoaded, assetBatchEntryOnLoadedCb);
|
||||||
|
eventUnsubscribe(&batch->entries[i]->onError, assetBatchEntryOnErrorCb);
|
||||||
|
assetUnlockEntry(batch->entries[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
memoryZero(batch, sizeof(assetbatch_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
void assetBatchEntryOnLoadedCb(void *params, void *user) {
|
||||||
|
assetentry_t *entry = (assetentry_t *)params;
|
||||||
|
assetbatch_t *batch = (assetbatch_t *)user;
|
||||||
|
|
||||||
|
batch->loadedCount++;
|
||||||
|
eventInvoke(&batch->onEntryLoaded, entry);
|
||||||
|
|
||||||
|
if((uint16_t)(batch->loadedCount + batch->errorCount) >= batch->count) {
|
||||||
|
if(batch->errorCount == 0) {
|
||||||
|
eventInvoke(&batch->onLoaded, batch);
|
||||||
|
} else {
|
||||||
|
eventInvoke(&batch->onError, batch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void assetBatchEntryOnErrorCb(void *params, void *user) {
|
||||||
|
assetentry_t *entry = (assetentry_t *)params;
|
||||||
|
assetbatch_t *batch = (assetbatch_t *)user;
|
||||||
|
|
||||||
|
batch->errorCount++;
|
||||||
|
eventInvoke(&batch->onEntryError, entry);
|
||||||
|
|
||||||
|
if((uint16_t)(batch->loadedCount + batch->errorCount) >= batch->count) {
|
||||||
|
eventInvoke(&batch->onError, batch);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "asset/loader/assetentry.h"
|
||||||
|
#include "asset/loader/assetloader.h"
|
||||||
|
#include "event/event.h"
|
||||||
|
|
||||||
|
#define ASSET_BATCH_COUNT_MAX 64
|
||||||
|
#define ASSET_BATCH_EVENT_MAX 4
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char_t *path;
|
||||||
|
assetloadertype_t type;
|
||||||
|
assetloaderinput_t input;
|
||||||
|
} assetbatchdesc_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
assetentry_t *entries[ASSET_BATCH_COUNT_MAX];
|
||||||
|
assetloaderinput_t inputs[ASSET_BATCH_COUNT_MAX];
|
||||||
|
uint16_t count;
|
||||||
|
uint16_t loadedCount;
|
||||||
|
uint16_t errorCount;
|
||||||
|
|
||||||
|
/** Fires once when every entry loaded. params = assetbatch_t * */
|
||||||
|
event_t onLoaded;
|
||||||
|
eventcallback_t onLoadedCallbacks[ASSET_BATCH_EVENT_MAX];
|
||||||
|
void *onLoadedUsers[ASSET_BATCH_EVENT_MAX];
|
||||||
|
|
||||||
|
/** Fires each time a single entry loads. params = assetentry_t * */
|
||||||
|
event_t onEntryLoaded;
|
||||||
|
eventcallback_t onEntryLoadedCallbacks[ASSET_BATCH_EVENT_MAX];
|
||||||
|
void *onEntryLoadedUsers[ASSET_BATCH_EVENT_MAX];
|
||||||
|
|
||||||
|
/** Fires when all entries finish (any with errors). params: assetbatch_t * */
|
||||||
|
event_t onError;
|
||||||
|
eventcallback_t onErrorCallbacks[ASSET_BATCH_EVENT_MAX];
|
||||||
|
void *onErrorUsers[ASSET_BATCH_EVENT_MAX];
|
||||||
|
|
||||||
|
/** Fires each time a single entry errors. params = assetentry_t * */
|
||||||
|
event_t onEntryError;
|
||||||
|
eventcallback_t onEntryErrorCallbacks[ASSET_BATCH_EVENT_MAX];
|
||||||
|
void *onEntryErrorUsers[ASSET_BATCH_EVENT_MAX];
|
||||||
|
} assetbatch_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialises the batch from an array of descriptors. Each entry is locked
|
||||||
|
* and queued for loading immediately.
|
||||||
|
*
|
||||||
|
* @param batch Batch to initialise.
|
||||||
|
* @param descs Array of entry descriptors (need not outlive this call).
|
||||||
|
* @param count Number of descriptors (must be <= ASSET_BATCH_COUNT_MAX).
|
||||||
|
*/
|
||||||
|
void assetBatchInit(
|
||||||
|
assetbatch_t *batch,
|
||||||
|
uint16_t count,
|
||||||
|
const assetbatchdesc_t *descs
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acquires one additional lock on every entry in the batch.
|
||||||
|
*
|
||||||
|
* @param batch Batch to lock.
|
||||||
|
*/
|
||||||
|
void assetBatchLock(assetbatch_t *batch);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Releases one lock from every entry in the batch. When an entry's lock
|
||||||
|
* count reaches zero it will be reaped on the next assetUpdate.
|
||||||
|
*
|
||||||
|
* @param batch Batch to unlock.
|
||||||
|
*/
|
||||||
|
void assetBatchUnlock(assetbatch_t *batch);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if every entry in the batch has finished loading.
|
||||||
|
*
|
||||||
|
* @param batch Batch to query.
|
||||||
|
*/
|
||||||
|
bool_t assetBatchIsLoaded(const assetbatch_t *batch);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if any entry in the batch is in an error state.
|
||||||
|
*
|
||||||
|
* @param batch Batch to query.
|
||||||
|
*/
|
||||||
|
bool_t assetBatchHasError(const assetbatch_t *batch);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blocks until every entry is loaded. Returns an error if any entry fails.
|
||||||
|
*
|
||||||
|
* @param batch Batch to wait on.
|
||||||
|
*/
|
||||||
|
errorret_t assetBatchRequireLoaded(assetbatch_t *batch);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Releases the batch's lock on every entry and clears the batch. After this
|
||||||
|
* call the batch struct may be reused with assetBatchInit.
|
||||||
|
*
|
||||||
|
* @param batch Batch to dispose.
|
||||||
|
*/
|
||||||
|
void assetBatchDispose(assetbatch_t *batch);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event trampoline invoked when a batch entry finishes loading.
|
||||||
|
* Increments the loaded counter and fires batch-level events.
|
||||||
|
*
|
||||||
|
* @param params The loaded assetentry_t pointer.
|
||||||
|
* @param user The owning assetbatch_t pointer.
|
||||||
|
*/
|
||||||
|
void assetBatchEntryOnLoadedCb(void *params, void *user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event trampoline invoked when a batch entry fails to load.
|
||||||
|
* Increments the error counter and fires batch-level events.
|
||||||
|
*
|
||||||
|
* @param params The errored assetentry_t pointer.
|
||||||
|
* @param user The owning assetbatch_t pointer.
|
||||||
|
*/
|
||||||
|
void assetBatchEntryOnErrorCb(void *params, void *user);
|
||||||
Loaded 100 of 576 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user