Chunks, models, and more
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_1_1.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_1_2.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_1_3.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_1_4.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_1_5.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_1_6.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_1_7.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_1_8.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_2_1.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_2_2.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_2_3.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_2_4.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_2_5.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_2_6.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_2_7.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_2_8.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_3_1.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_3_2.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_3_3.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_3_4.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_3_5.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_3_6.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_3_7.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_3_8.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_4_1.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_4_2.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_4_3.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_4_4.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_4_5.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_4_6.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_4_7.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_4_8.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_5_1.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_5_2.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_5_3.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_5_4.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_5_5.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_5_6.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_5_7.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_5_8.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_6_1.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_6_2.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_6_3.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_6_4.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_6_5.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_6_6.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_6_7.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_6_8.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_7_1.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_7_2.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_7_3.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_7_4.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_7_5.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_7_6.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_7_7.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_7_8.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_8_1.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_8_2.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_8_3.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_8_4.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_8_5.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_8_6.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_8_7.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_8_8.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 110 B |
@@ -0,0 +1 @@
|
||||
<p>Hello Homepage</p>
|
||||
@@ -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>
|
||||
@@ -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.")
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
python3 server/server.py
|
||||
+19
-9
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,76 +191,70 @@ 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;
|
||||
assetentrystate_t state = chunk->dcfEntry->state;
|
||||
if(state == ASSET_ENTRY_STATE_LOADED) {
|
||||
uint8_t meshCount = chunk->dcfEntry->data.chunk.meshCount;
|
||||
memoryCopy(
|
||||
chunk->tiles,
|
||||
chunk->dcfEntry->data.chunk.tiles,
|
||||
sizeof(chunk->tiles)
|
||||
);
|
||||
worldpos_t wp;
|
||||
chunkPosToWorldPos(&chunk->position, &wp);
|
||||
vec3 wpf = { (float_t)wp.x, (float_t)wp.y, (float_t)wp.z };
|
||||
|
||||
if(state == ASSET_ENTRY_STATE_LOADED) {
|
||||
uint8_t meshCount = chunk->dcfEntry->data.chunk.meshCount;
|
||||
memoryCopy(
|
||||
chunk->tiles,
|
||||
chunk->dcfEntry->data.chunk.tiles,
|
||||
sizeof(chunk->tiles)
|
||||
for(uint8_t m = 0; m < meshCount; m++) {
|
||||
stringCopy(
|
||||
chunk->modelNames[m],
|
||||
chunk->dcfEntry->data.chunk.modelNames[m],
|
||||
CHUNK_MESH_NAME_MAX
|
||||
);
|
||||
worldpos_t wp;
|
||||
chunkPosToWorldPos(&chunk->position, &wp);
|
||||
vec3 wpf = { (float_t)wp.x, (float_t)wp.y, (float_t)wp.z };
|
||||
|
||||
for(uint8_t m = 0; m < meshCount; m++) {
|
||||
stringCopy(
|
||||
chunk->meshNames[m],
|
||||
chunk->dcfEntry->data.chunk.meshNames[m],
|
||||
CHUNK_MESH_NAME_MAX
|
||||
);
|
||||
glm_vec3_copy(
|
||||
chunk->dcfEntry->data.chunk.meshOffsets[m],
|
||||
chunk->meshOffsets[m]
|
||||
);
|
||||
vec3 pos;
|
||||
glm_vec3_add(wpf, chunk->meshOffsets[m], pos);
|
||||
glm_translate_make(chunk->meshModels[m], pos);
|
||||
}
|
||||
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;
|
||||
glm_vec3_copy(
|
||||
chunk->dcfEntry->data.chunk.meshOffsets[m],
|
||||
chunk->meshOffsets[m]
|
||||
);
|
||||
vec3 pos;
|
||||
glm_vec3_add(wpf, chunk->meshOffsets[m], pos);
|
||||
glm_translate_make(chunk->meshModels[m], pos);
|
||||
}
|
||||
assetUnlockEntry(chunk->dcfEntry);
|
||||
chunk->dcfEntry = NULL;
|
||||
chunk->meshCount = meshCount;
|
||||
} else if(state == ASSET_ENTRY_STATE_ERROR) {
|
||||
assetUnlockEntry(chunk->dcfEntry);
|
||||
chunk->dcfEntry = NULL;
|
||||
memorySet(chunk->tiles, TILE_SHAPE_GROUND, sizeof(chunk->tiles));
|
||||
} else {
|
||||
anyPending = true;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user