Chunks, models, and more

This commit is contained in:
YourWishes committed 2026-07-01 08:35:01 -05:00
1 parent a34831aa02
commit a9ab8dc1d8
94 files changed
+911 -100

No files matched your search

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_1_1.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_1_2.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_1_3.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_1_4.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_1_5.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_1_6.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_1_7.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_1_8.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_2_1.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_2_2.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_2_3.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_2_4.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_2_5.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_2_6.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_2_7.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_2_8.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_3_1.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_3_2.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_3_3.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_3_4.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_3_5.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_3_6.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_3_7.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_3_8.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_4_1.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_4_2.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_4_3.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_4_4.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_4_5.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_4_6.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_4_7.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_4_8.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_5_1.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_5_2.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_5_3.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_5_4.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_5_5.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_5_6.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_5_7.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_5_8.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_6_1.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_6_2.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_6_3.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_6_4.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_6_5.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_6_6.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_6_7.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_6_8.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_7_1.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_7_2.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_7_3.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_7_4.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_7_5.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_7_6.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_7_7.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_7_8.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_8_1.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_8_2.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_8_3.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_8_4.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_8_5.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_8_6.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_8_7.dmf",
"color": [255, 0, 255, 255]
}
+4
View File
@@ -0,0 +1,4 @@
{
"mesh": "meshes/buildings/house_8_8.dmf",
"color": [255, 0, 255, 255]
}
+10
View File
@@ -0,0 +1,10 @@
{
"mesh": "meshes/chunks/chunk_-1_0_0_0.dmf",
"color": [
255,
255,
255,
255
],
"texture": "tiles.png"
}
+10
View File
@@ -0,0 +1,10 @@
{
"mesh": "meshes/chunks/chunk_0_0_0_0.dmf",
"color": [
255,
255,
255,
255
],
"texture": "tiles.png"
}
+10
View File
@@ -0,0 +1,10 @@
{
"mesh": "meshes/chunks/chunk_0_1_0_0.dmf",
"color": [
255,
255,
255,
255
],
"texture": "tiles.png"
}
+10
View File
@@ -0,0 +1,10 @@
{
"mesh": "meshes/chunks/chunk_1_0_0_0.dmf",
"color": [
255,
255,
255,
255
],
"texture": "tiles.png"
}
+10
View File
@@ -0,0 +1,10 @@
{
"mesh": "meshes/chunks/chunk_1_1_0_0.dmf",
"color": [
255,
255,
255,
255
],
"texture": "tiles.png"
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 110 B

+1
View File
@@ -0,0 +1 @@
<p>Hello Homepage</p>
+11
View File
@@ -0,0 +1,11 @@
<!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>
</head>
<body>
<div id="editor"></div>
</body>
</html>
+224
View File
@@ -0,0 +1,224 @@
"""
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
POST /api/write?path=<file> write file (JSON body: {"content":"..."})
POST /api/mkdir?path=<dir> create directory
"""
import os
import json
import mimetypes
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs, unquote
from pathlib import Path
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)
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)
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))})
# ------------------------------------------------------------------
# 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")
content = shell.replace(
'<div id="editor"></div>',
f'<div id="editor">{fragment}</div>',
1,
).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.")
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")"
python3 server/server.py
+19 -9
View File
@@ -411,16 +411,26 @@ errorret_t assetDispose(void) {
assertIsMainThread("Must be called from the main thread.");
threadStop(&ASSET.loadThread);
// Dispose every non-null entry so type-specific dispose callbacks
// (e.g. assetScriptDispose freeing jerry values) run before the
// scripting engine is torn down.
assetentry_t *entry = ASSET.entries;
// Drain-dispose: repeatedly find and dispose zero-ref LOADED entries
// until none remain. This handles dependency chains where an entry
// (e.g. a model) holds refs on child entries (mesh, texture): dispose
// parents first so child ref counts drop to zero, then pick up the
// children on the next pass. Without this, a forward-only scan fails
// when a shared child entry appears before a parent that still holds a
// ref to it.
bool_t any;
do {
if(entry->type != ASSET_LOADER_TYPE_NULL) {
errorChain(assetEntryDispose(entry));
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
any = false;
assetentry_t *e = ASSET.entries;
do {
if(e->type == ASSET_LOADER_TYPE_NULL) { e++; continue; }
if(e->state != ASSET_ENTRY_STATE_LOADED) { e++; continue; }
if(e->refs.count > 0) { e++; continue; }
errorChain(assetEntryDispose(e));
any = true;
e++;
} while(e < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
} while(any);
// Cleanup zip file.
if(ASSET.zip != NULL) {
+6
View File
@@ -16,6 +16,12 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
.dispose = assetMeshDispose
},
[ASSET_LOADER_TYPE_MODEL] = {
.loadSync = assetModelLoaderSync,
.loadAsync = assetModelLoaderAsync,
.dispose = assetModelDispose
},
[ASSET_LOADER_TYPE_TEXTURE] = {
.loadSync = assetTextureLoaderSync,
.loadAsync = assetTextureLoaderAsync,
+4
View File
@@ -7,6 +7,7 @@
#pragma once
#include "asset/loader/dmf/assetmeshloader.h"
#include "asset/loader/dmf/assetmodelloader.h"
#include "asset/loader/display/assettextureloader.h"
#include "asset/loader/display/assettilesetloader.h"
#include "asset/loader/locale/assetlocaleloader.h"
@@ -17,6 +18,7 @@ typedef enum {
ASSET_LOADER_TYPE_NULL,
ASSET_LOADER_TYPE_MESH,
ASSET_LOADER_TYPE_MODEL,
ASSET_LOADER_TYPE_TEXTURE,
ASSET_LOADER_TYPE_TILESET,
ASSET_LOADER_TYPE_LOCALE,
@@ -28,6 +30,7 @@ typedef enum {
typedef union {
assetmeshloaderloading_t mesh;
assetmodelloaderloading_t model;
assettextureloaderloading_t texture;
assettilesetloaderloading_t tileset;
assetlocaleloaderloading_t locale;
@@ -37,6 +40,7 @@ typedef union {
typedef union {
assetmeshoutput_t mesh;
assetmodeloutput_t model;
assettextureoutput_t texture;
assettilesetoutput_t tileset;
assetlocaleoutput_t locale;
@@ -100,8 +100,8 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
) {
nameLen++;
}
memoryCopy(out->meshNames[m], data + offset, nameLen);
out->meshNames[m][nameLen] = '\0';
memoryCopy(out->modelNames[m], data + offset, nameLen);
out->modelNames[m][nameLen] = '\0';
offset += nameLen + 1;
memoryCopy(
@@ -34,7 +34,7 @@ typedef struct {
typedef struct {
tile_t tiles[CHUNK_TILE_COUNT];
uint8_t meshCount;
char_t meshNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
} assetchunkoutput_t;
@@ -51,7 +51,7 @@ errorret_t assetChunkLoaderAsync(assetloading_t *loading);
/**
* Synchronous loader for chunk assets. Validates the DCF binary previously
* read by the async phase and populates the output assetchunkoutput_t with
* tile data and DMF mesh names.
* tile data and model paths.
*
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
+1
View File
@@ -6,4 +6,5 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
assetmeshloader.c
assetmodelloader.c
)
@@ -0,0 +1,171 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetmodelloader.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#include "asset/asset.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
#include "asset/loader/json/assetjsonloader.h"
#include "display/texture/texture.h"
#include "yyjson.h"
errorret_t assetModelLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Should be called from an async thread.");
errorOk();
}
errorret_t assetModelLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_MODEL, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
switch(loading->loading.model.state) {
case ASSET_MODEL_LOADING_STATE_INITIAL:
loading->loading.model.state = ASSET_MODEL_LOADING_STATE_LOCK_ASSETS;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
break;
case ASSET_MODEL_LOADING_STATE_LOCK_ASSETS: {
// Lock the model's JSON descriptor as a JSON sub-asset. The entry
// key is prefixed with "json:" to avoid a type-collision with the
// model entry itself (both share the same filename). The JSON loader
// reads the real file path supplied in the input.
char_t jsonKey[ASSET_FILE_NAME_MAX];
stringFormat(
jsonKey, sizeof(jsonKey), "json:%s", loading->entry->name
);
assetloaderinput_t jsonInput;
memoryZero(&jsonInput, sizeof(jsonInput));
stringCopy(
jsonInput.json.path, loading->entry->name, ASSET_FILE_NAME_MAX
);
assetentry_t *jsonEntry = assetLock(
jsonKey, ASSET_LOADER_TYPE_JSON, &jsonInput
);
errorret_t ret = assetRequireLoaded(jsonEntry);
if(errorIsNotOk(ret)) {
assetUnlockEntry(jsonEntry);
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
errorChain(ret);
}
yyjson_val *root = yyjson_doc_get_root(jsonEntry->data.json);
// Parse required mesh path.
yyjson_val *meshVal = yyjson_obj_get(root, "mesh");
if(!meshVal || !yyjson_is_str(meshVal)) {
assetUnlockEntry(jsonEntry);
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
errorThrow("Model JSON missing 'mesh' string");
}
const char *meshStr = yyjson_get_str(meshVal);
size_t meshLen = yyjson_get_len(meshVal);
if(meshLen >= ASSET_FILE_NAME_MAX) {
assetUnlockEntry(jsonEntry);
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
errorThrow("Model mesh path exceeds max length");
}
char_t meshName[ASSET_FILE_NAME_MAX];
memoryCopy(meshName, meshStr, meshLen + 1);
// Parse optional texture path (absent = color only, no texture).
char_t textureName[ASSET_FILE_NAME_MAX];
textureName[0] = '\0';
yyjson_val *texVal = yyjson_obj_get(root, "texture");
if(texVal && yyjson_is_str(texVal)) {
const char *texStr = yyjson_get_str(texVal);
size_t texLen = yyjson_get_len(texVal);
if(texLen >= ASSET_FILE_NAME_MAX) {
assetUnlockEntry(jsonEntry);
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
errorThrow("Model texture path exceeds max length");
}
memoryCopy(textureName, texStr, texLen + 1);
}
// Parse optional color (RGBA array 0-255, default white).
color_t color = COLOR_WHITE;
yyjson_val *colorVal = yyjson_obj_get(root, "color");
if(colorVal && yyjson_is_arr(colorVal)) {
uint8_t ch[4] = {255, 255, 255, 255};
size_t colorIdx, colorLen;
yyjson_val *colorElem;
yyjson_arr_foreach(colorVal, colorIdx, colorLen, colorElem) {
if(colorIdx >= 4) break;
if(yyjson_is_int(colorElem)) {
ch[colorIdx] = (uint8_t)yyjson_get_int(colorElem);
}
}
color.r = ch[0];
color.g = ch[1];
color.b = ch[2];
color.a = ch[3];
}
// Release JSON entry; all needed fields are now in local variables.
assetUnlockEntry(jsonEntry);
// Lock and load the mesh sub-asset.
assetentry_t *meshEntry = assetLock(meshName, ASSET_LOADER_TYPE_MESH, NULL);
ret = assetRequireLoaded(meshEntry);
if(errorIsNotOk(ret)) {
assetUnlockEntry(meshEntry);
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
errorChain(ret);
}
// Lock and load the optional texture sub-asset.
assetentry_t *texEntry = NULL;
if(textureName[0] != '\0') {
assetloaderinput_t texInput = { .texture = TEXTURE_FORMAT_RGBA };
texEntry = assetLock(textureName, ASSET_LOADER_TYPE_TEXTURE, &texInput);
ret = assetRequireLoaded(texEntry);
if(errorIsNotOk(ret)) {
assetUnlockEntry(texEntry);
assetUnlockEntry(meshEntry);
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
errorChain(ret);
}
}
assetmodeloutput_t *out = &loading->entry->data.model;
out->meshEntry = meshEntry;
out->texEntry = texEntry;
out->color = color;
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
break;
}
default:
errorOk();
}
errorOk();
}
errorret_t assetModelDispose(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_MODEL, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
assetmodeloutput_t *out = &entry->data.model;
if(out->meshEntry != NULL) {
assetUnlockEntry(out->meshEntry);
out->meshEntry = NULL;
}
if(out->texEntry != NULL) {
assetUnlockEntry(out->texEntry);
out->texEntry = NULL;
}
errorOk();
}
@@ -0,0 +1,58 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "display/color.h"
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef enum {
ASSET_MODEL_LOADING_STATE_INITIAL,
ASSET_MODEL_LOADING_STATE_LOCK_ASSETS
} assetmodelloadingstate_t;
typedef struct {
assetmodelloadingstate_t state;
} assetmodelloaderloading_t;
typedef struct {
assetentry_t *meshEntry;
assetentry_t *texEntry;
color_t color;
} assetmodeloutput_t;
/**
* Async loader stub for model assets. Model loading is performed entirely
* on the main thread; this callback is never reached.
*
* @param loading Loading information for the asset being loaded.
* @returns Always success.
*/
errorret_t assetModelLoaderAsync(assetloading_t *loading);
/**
* Synchronous loader for model assets. Locks the JSON descriptor file as
* a sub-asset, waits for it to load, parses the mesh path, optional
* texture path, and color tint, then releases the JSON entry. Locks and
* waits for mesh and optional texture sub-assets sequentially to stay
* within ASSET_LOADING_COUNT_MAX slot limits.
*
* @param loading Loading information for the asset being loaded.
* @returns Error code indicating success or failure.
*/
errorret_t assetModelLoaderSync(assetloading_t *loading);
/**
* Disposer for model assets. Releases the locks on the mesh and texture
* sub-asset entries.
*
* @param entry Asset entry containing the model to dispose.
* @returns Error code indicating success or failure.
*/
errorret_t assetModelDispose(assetentry_t *entry);
+5 -3
View File
@@ -22,9 +22,11 @@ errorret_t assetJsonLoaderAsync(assetloading_t *loading) {
assertNull(loading->loading.json.buffer, "Buffer already defined?");
assetfile_t *file = &loading->loading.json.file;
assetLoaderErrorChain(loading,
assetFileInit(file, loading->entry->name, NULL, NULL)
);
const char_t *filePath = (
loading->entry->input != NULL &&
loading->entry->input->json.path[0] != '\0'
) ? loading->entry->input->json.path : loading->entry->name;
assetLoaderErrorChain(loading, assetFileInit(file, filePath, NULL, NULL));
if(file->size > ASSET_JSON_FILE_SIZE_MAX) {
assetLoaderErrorThrow(loading, "JSON exceeds maximum allowed size");
+10 -1
View File
@@ -14,7 +14,16 @@
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef struct { void *nothing; } assetjsonloaderinput_t;
/**
* Optional file path override. When path[0] is non-zero the JSON loader
* reads this path from the archive instead of loading->entry->name. Use
* this to decouple the asset pool key from the actual file location (e.g.
* when a parent loader locks a JSON entry under a synthetic key to avoid
* a type-collision with an entry of the same name but different type).
*/
typedef struct {
char_t path[ASSET_FILE_NAME_MAX];
} assetjsonloaderinput_t;
typedef enum {
ASSET_JSON_LOADING_STATE_INITIAL,
+1 -1
View File
@@ -17,7 +17,7 @@ console_t CONSOLE;
void consoleInit(void) {
memoryZero(&CONSOLE, sizeof(console_t));
CONSOLE.visible = false;
CONSOLE.visible = true;
#ifdef DUSK_CONSOLE_POSIX
threadMutexInit(&CONSOLE.printMutex);
+2 -2
View File
@@ -22,10 +22,10 @@ typedef struct chunk_s {
assetentry_t *dcfEntry;
uint8_t meshCount;
char_t meshNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
mat4 meshModels[CHUNK_MESH_COUNT_MAX];
assetentry_t *meshEntries[CHUNK_MESH_COUNT_MAX];
assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX];
uint8_t entities[CHUNK_ENTITY_COUNT_MAX];
} chunk_t;
+29 -34
View File
@@ -148,9 +148,9 @@ void mapChunkUnload(chunk_t *chunk) {
}
for(uint8_t m = 0; m < chunk->meshCount; m++) {
if(chunk->meshEntries[m] == NULL) continue;
assetUnlockEntry(chunk->meshEntries[m]);
chunk->meshEntries[m] = NULL;
if(chunk->modelEntries[m] == NULL) continue;
assetUnlockEntry(chunk->modelEntries[m]);
chunk->modelEntries[m] = NULL;
}
chunk->meshCount = 0;
}
@@ -183,6 +183,7 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
}
errorret_t mapWaitForChunks() {
// Phase 1: Wait for all DCF entries to finish loading.
bool_t anyPending;
do {
anyPending = false;
@@ -190,10 +191,9 @@ errorret_t mapWaitForChunks() {
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
chunk_t *chunk = &MAP.chunks[i];
if(chunk->dcfEntry == NULL) continue;
if(chunk->dcfEntry != NULL) {
assetentrystate_t state = chunk->dcfEntry->state;
if(state == ASSET_ENTRY_STATE_LOADED) {
uint8_t meshCount = chunk->dcfEntry->data.chunk.meshCount;
memoryCopy(
@@ -207,8 +207,8 @@ errorret_t mapWaitForChunks() {
for(uint8_t m = 0; m < meshCount; m++) {
stringCopy(
chunk->meshNames[m],
chunk->dcfEntry->data.chunk.meshNames[m],
chunk->modelNames[m],
chunk->dcfEntry->data.chunk.modelNames[m],
CHUNK_MESH_NAME_MAX
);
glm_vec3_copy(
@@ -221,45 +221,40 @@ errorret_t mapWaitForChunks() {
}
assetUnlockEntry(chunk->dcfEntry);
chunk->dcfEntry = NULL;
for(uint8_t m = 0; m < meshCount; m++) {
chunk->meshEntries[m] = assetLock(
chunk->meshNames[m], ASSET_LOADER_TYPE_MESH, NULL
);
if(chunk->meshEntries[m] == NULL) {
for(uint8_t j = 0; j < m; j++) {
assetUnlockEntry(chunk->meshEntries[j]);
chunk->meshEntries[j] = NULL;
}
errorThrow("Failed to lock mesh: %s", chunk->meshNames[m]);
}
}
chunk->meshCount = meshCount;
// Fall through to the mesh-state check below so cached meshes
// don't force an unnecessary extra sleep iteration.
} else if(state == ASSET_ENTRY_STATE_ERROR) {
assetUnlockEntry(chunk->dcfEntry);
chunk->dcfEntry = NULL;
memorySet(chunk->tiles, TILE_SHAPE_GROUND, sizeof(chunk->tiles));
continue;
} else {
anyPending = true;
continue;
}
}
for(uint8_t m = 0; m < chunk->meshCount; m++) {
if(chunk->meshEntries[m] == NULL) continue;
if(chunk->meshEntries[m]->state != ASSET_ENTRY_STATE_LOADED) {
anyPending = true;
break;
}
}
}
if(anyPending) usleep(1000);
} while(anyPending);
// Phase 2: Load model entries one at a time.
// Models are locked and waited on sequentially so that only one model
// entry occupies a loading slot at a time. This leaves the remaining
// slots free for the model's mesh and texture sub-assets, preventing
// the slot-exhaustion deadlock that occurs when multiple models are
// assigned slots simultaneously and each blocks waiting for sub-assets
// that have nowhere to run.
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
chunk_t *chunk = &MAP.chunks[i];
for(uint8_t m = 0; m < chunk->meshCount; m++) {
if(chunk->modelEntries[m] != NULL) continue;
chunk->modelEntries[m] = assetLock(
chunk->modelNames[m], ASSET_LOADER_TYPE_MODEL, NULL
);
if(chunk->modelEntries[m] == NULL) {
errorThrow("Failed to lock model: %s", chunk->modelNames[m]);
}
errorChain(assetRequireLoaded(chunk->modelEntries[m]));
}
}
errorOk();
}
+16 -32
View File
@@ -24,27 +24,8 @@
#include "asset/loader/assetloader.h"
#include "asset/loader/assetentry.h"
#define TEXTURE_CHUNK_SIZE 16
static texture_t TEXTURE_CHUNK;
static color_t TEXTURE_CHUNK_PIXELS[TEXTURE_CHUNK_SIZE * TEXTURE_CHUNK_SIZE];
errorret_t sceneOverworldInit(scenedata_t *sceneData) {
assertNotNull(sceneData, "Scene data cannot be null");
for(uint32_t i = 0; i < TEXTURE_CHUNK_SIZE * TEXTURE_CHUNK_SIZE; i++) {
uint8_t r = (uint8_t)((i & 7) * 36);
uint8_t g = (uint8_t)(((i >> 3) & 7) * 36);
uint8_t b = (uint8_t)((i >> 6) * 85);
TEXTURE_CHUNK_PIXELS[i] = color4b(r, g, b, 255);
}
errorChain(textureInit(
&TEXTURE_CHUNK,
TEXTURE_CHUNK_SIZE, TEXTURE_CHUNK_SIZE,
TEXTURE_FORMAT_RGBA,
(texturedata_t){ .rgbaColors = TEXTURE_CHUNK_PIXELS }
));
errorOk();
}
@@ -138,25 +119,31 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) {
}
errorret_t sceneOverworldDrawChunks() {
shadermaterial_t chunkMaterial = {
.unlit = {
.color = COLOR_WHITE,
.texture = &TEXTURE_CHUNK
}
};
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
chunk_t *chunk = &MAP.chunks[i];
if(chunk->meshCount == 0) continue;
for(uint8_t m = 0; m < chunk->meshCount; m++) {
if(chunk->meshEntries[m] == NULL) continue;
if(chunk->modelEntries[m] == NULL) continue;
if(chunk->modelEntries[m]->state != ASSET_ENTRY_STATE_LOADED) continue;
assetmodeloutput_t *model = &chunk->modelEntries[m]->data.model;
if(model->meshEntry == NULL) continue;
texture_t *tex = model->texEntry != NULL
? &model->texEntry->data.texture
: NULL;
shadermaterial_t mat = {
.unlit = { .color = model->color, .texture = tex }
};
errorChain(shaderSetMatrix(
&SHADER_UNLIT, SHADER_UNLIT_MODEL, chunk->meshModels[m]
));
errorChain(shaderSetMaterial(&SHADER_UNLIT, &chunkMaterial));
errorChain(meshDraw(&chunk->meshEntries[m]->data.mesh.mesh, 0, -1));
errorChain(shaderSetMaterial(&SHADER_UNLIT, &mat));
errorChain(meshDraw(
&model->meshEntry->data.mesh.mesh, 0, -1
));
}
}
@@ -171,8 +158,5 @@ errorret_t sceneOverworldDrawChunks() {
errorret_t sceneOverworldDispose(scenedata_t *sceneData) {
assertNotNull(sceneData, "Scene data cannot be null");
errorChain(textureDispose(&TEXTURE_CHUNK));
errorOk();
}
+39 -14
View File
@@ -26,8 +26,8 @@ JSON input (assetsraw/chunks/chunk_X_Y_Z.json):
Version 3 DCF format (after 8-byte header + tiles):
uint8_t meshCount
for each mesh:
null-terminated string (relative asset path to .dmf)
for each model:
null-terminated string (relative asset path to .json model)
float32[3] (x, y, z offset)
DMF format:
@@ -89,11 +89,11 @@ def tile_index(x, y, z):
return x + y * CHUNK_WIDTH + z * CHUNK_WIDTH * CHUNK_HEIGHT
def find_mesh(filename):
"""Search ASSETS_DIR/meshes/ recursively for filename.
def find_model(filename):
"""Search ASSETS_DIR/models/ recursively for filename.
Returns the path relative to ASSETS_DIR (forward slashes), or None."""
meshes_root = os.path.join(ASSETS_DIR, 'meshes')
for dirpath, _, filenames in os.walk(meshes_root):
models_root = os.path.join(ASSETS_DIR, 'models')
for dirpath, _, filenames in os.walk(models_root):
if filename in filenames:
rel = os.path.relpath(
os.path.join(dirpath, filename), ASSETS_DIR
@@ -102,6 +102,17 @@ def find_mesh(filename):
return None
def write_model_json(path, mesh_rel, texture_rel, color):
"""Write a model JSON descriptor file."""
obj = {'mesh': mesh_rel, 'color': color}
if texture_rel:
obj['texture'] = texture_rel
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'w') as f:
json.dump(obj, f, indent=2)
print(f' Wrote model JSON {path}')
def derive_dcf_path(json_path):
"""assetsraw/chunks/chunk_X_Y_Z.json -> assets/chunks/X_Y_Z.dcf"""
base = os.path.splitext(os.path.basename(json_path))[0]
@@ -228,31 +239,45 @@ def from_json(json_path, dcf_path):
)
terrain_verts = build_terrain_verts(tiles)
mesh_names = []
model_names = []
mesh_offsets = []
# Terrain mesh (only written if non-empty)
dcf_base = os.path.splitext(os.path.basename(dcf_path))[0]
# Terrain mesh + model (only written if non-empty)
terrain_dir = os.path.join(ASSETS_DIR, 'meshes', 'chunks')
models_dir = os.path.join(ASSETS_DIR, 'models', 'chunks')
os.makedirs(terrain_dir, exist_ok=True)
os.makedirs(models_dir, exist_ok=True)
if terrain_verts:
dmf_name = f'chunk_{dcf_base}_0.dmf'
write_dmf(os.path.join(terrain_dir, dmf_name), terrain_verts)
mesh_names.append(f'meshes/chunks/{dmf_name}')
mesh_rel = f'meshes/chunks/{dmf_name}'
json_name = f'chunk_{dcf_base}_0.json'
write_model_json(
os.path.join(models_dir, json_name),
mesh_rel,
'tiles.png',
[255, 255, 255, 255]
)
model_names.append(f'models/chunks/{json_name}')
mesh_offsets.append((0.0, 0.0, 0.0))
# External mesh references
# External mesh references -> look up matching model JSON
for mesh in data.get('meshes', []):
filename = mesh['file']
pos = mesh.get('pos', [0, 0, 0])
rel = find_mesh(filename)
model_filename = os.path.splitext(filename)[0] + '.json'
rel = find_model(model_filename)
if rel is None:
raise ValueError(f"Mesh not found under assets/meshes/: {filename}")
mesh_names.append(rel)
raise ValueError(
f"Model not found under assets/models/: {model_filename}"
)
model_names.append(rel)
mesh_offsets.append((float(pos[0]), float(pos[1]), float(pos[2])))
print(f' Resolved {filename} -> {rel}')
write_v3(dcf_path, bytes(tiles), mesh_names, mesh_offsets)
write_v3(dcf_path, bytes(tiles), model_names, mesh_offsets)
def process_json(json_path):