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.");
|
assertIsMainThread("Must be called from the main thread.");
|
||||||
threadStop(&ASSET.loadThread);
|
threadStop(&ASSET.loadThread);
|
||||||
|
|
||||||
// Dispose every non-null entry so type-specific dispose callbacks
|
// Drain-dispose: repeatedly find and dispose zero-ref LOADED entries
|
||||||
// (e.g. assetScriptDispose freeing jerry values) run before the
|
// until none remain. This handles dependency chains where an entry
|
||||||
// scripting engine is torn down.
|
// (e.g. a model) holds refs on child entries (mesh, texture): dispose
|
||||||
assetentry_t *entry = ASSET.entries;
|
// 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 {
|
do {
|
||||||
if(entry->type != ASSET_LOADER_TYPE_NULL) {
|
any = false;
|
||||||
errorChain(assetEntryDispose(entry));
|
assetentry_t *e = ASSET.entries;
|
||||||
}
|
do {
|
||||||
entry++;
|
if(e->type == ASSET_LOADER_TYPE_NULL) { e++; continue; }
|
||||||
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
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.
|
// Cleanup zip file.
|
||||||
if(ASSET.zip != NULL) {
|
if(ASSET.zip != NULL) {
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
|
|||||||
.dispose = assetMeshDispose
|
.dispose = assetMeshDispose
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[ASSET_LOADER_TYPE_MODEL] = {
|
||||||
|
.loadSync = assetModelLoaderSync,
|
||||||
|
.loadAsync = assetModelLoaderAsync,
|
||||||
|
.dispose = assetModelDispose
|
||||||
|
},
|
||||||
|
|
||||||
[ASSET_LOADER_TYPE_TEXTURE] = {
|
[ASSET_LOADER_TYPE_TEXTURE] = {
|
||||||
.loadSync = assetTextureLoaderSync,
|
.loadSync = assetTextureLoaderSync,
|
||||||
.loadAsync = assetTextureLoaderAsync,
|
.loadAsync = assetTextureLoaderAsync,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "asset/loader/dmf/assetmeshloader.h"
|
#include "asset/loader/dmf/assetmeshloader.h"
|
||||||
|
#include "asset/loader/dmf/assetmodelloader.h"
|
||||||
#include "asset/loader/display/assettextureloader.h"
|
#include "asset/loader/display/assettextureloader.h"
|
||||||
#include "asset/loader/display/assettilesetloader.h"
|
#include "asset/loader/display/assettilesetloader.h"
|
||||||
#include "asset/loader/locale/assetlocaleloader.h"
|
#include "asset/loader/locale/assetlocaleloader.h"
|
||||||
@@ -17,6 +18,7 @@ typedef enum {
|
|||||||
ASSET_LOADER_TYPE_NULL,
|
ASSET_LOADER_TYPE_NULL,
|
||||||
|
|
||||||
ASSET_LOADER_TYPE_MESH,
|
ASSET_LOADER_TYPE_MESH,
|
||||||
|
ASSET_LOADER_TYPE_MODEL,
|
||||||
ASSET_LOADER_TYPE_TEXTURE,
|
ASSET_LOADER_TYPE_TEXTURE,
|
||||||
ASSET_LOADER_TYPE_TILESET,
|
ASSET_LOADER_TYPE_TILESET,
|
||||||
ASSET_LOADER_TYPE_LOCALE,
|
ASSET_LOADER_TYPE_LOCALE,
|
||||||
@@ -28,6 +30,7 @@ typedef enum {
|
|||||||
|
|
||||||
typedef union {
|
typedef union {
|
||||||
assetmeshloaderloading_t mesh;
|
assetmeshloaderloading_t mesh;
|
||||||
|
assetmodelloaderloading_t model;
|
||||||
assettextureloaderloading_t texture;
|
assettextureloaderloading_t texture;
|
||||||
assettilesetloaderloading_t tileset;
|
assettilesetloaderloading_t tileset;
|
||||||
assetlocaleloaderloading_t locale;
|
assetlocaleloaderloading_t locale;
|
||||||
@@ -37,6 +40,7 @@ typedef union {
|
|||||||
|
|
||||||
typedef union {
|
typedef union {
|
||||||
assetmeshoutput_t mesh;
|
assetmeshoutput_t mesh;
|
||||||
|
assetmodeloutput_t model;
|
||||||
assettextureoutput_t texture;
|
assettextureoutput_t texture;
|
||||||
assettilesetoutput_t tileset;
|
assettilesetoutput_t tileset;
|
||||||
assetlocaleoutput_t locale;
|
assetlocaleoutput_t locale;
|
||||||
|
|||||||
@@ -100,8 +100,8 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
|||||||
) {
|
) {
|
||||||
nameLen++;
|
nameLen++;
|
||||||
}
|
}
|
||||||
memoryCopy(out->meshNames[m], data + offset, nameLen);
|
memoryCopy(out->modelNames[m], data + offset, nameLen);
|
||||||
out->meshNames[m][nameLen] = '\0';
|
out->modelNames[m][nameLen] = '\0';
|
||||||
offset += nameLen + 1;
|
offset += nameLen + 1;
|
||||||
|
|
||||||
memoryCopy(
|
memoryCopy(
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ typedef struct {
|
|||||||
typedef struct {
|
typedef struct {
|
||||||
tile_t tiles[CHUNK_TILE_COUNT];
|
tile_t tiles[CHUNK_TILE_COUNT];
|
||||||
uint8_t meshCount;
|
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];
|
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
|
||||||
} assetchunkoutput_t;
|
} assetchunkoutput_t;
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ errorret_t assetChunkLoaderAsync(assetloading_t *loading);
|
|||||||
/**
|
/**
|
||||||
* Synchronous loader for chunk assets. Validates the DCF binary previously
|
* Synchronous loader for chunk assets. Validates the DCF binary previously
|
||||||
* read by the async phase and populates the output assetchunkoutput_t with
|
* 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.
|
* @param loading Loading information for the asset being loaded.
|
||||||
* @return Error code indicating success or failure of the load operation.
|
* @return Error code indicating success or failure of the load operation.
|
||||||
|
|||||||
@@ -6,4 +6,5 @@
|
|||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
assetmeshloader.c
|
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?");
|
assertNull(loading->loading.json.buffer, "Buffer already defined?");
|
||||||
|
|
||||||
assetfile_t *file = &loading->loading.json.file;
|
assetfile_t *file = &loading->loading.json.file;
|
||||||
assetLoaderErrorChain(loading,
|
const char_t *filePath = (
|
||||||
assetFileInit(file, loading->entry->name, NULL, NULL)
|
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) {
|
if(file->size > ASSET_JSON_FILE_SIZE_MAX) {
|
||||||
assetLoaderErrorThrow(loading, "JSON exceeds maximum allowed size");
|
assetLoaderErrorThrow(loading, "JSON exceeds maximum allowed size");
|
||||||
|
|||||||
@@ -14,7 +14,16 @@
|
|||||||
typedef struct assetloading_s assetloading_t;
|
typedef struct assetloading_s assetloading_t;
|
||||||
typedef struct assetentry_s assetentry_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 {
|
typedef enum {
|
||||||
ASSET_JSON_LOADING_STATE_INITIAL,
|
ASSET_JSON_LOADING_STATE_INITIAL,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ console_t CONSOLE;
|
|||||||
|
|
||||||
void consoleInit(void) {
|
void consoleInit(void) {
|
||||||
memoryZero(&CONSOLE, sizeof(console_t));
|
memoryZero(&CONSOLE, sizeof(console_t));
|
||||||
CONSOLE.visible = false;
|
CONSOLE.visible = true;
|
||||||
|
|
||||||
#ifdef DUSK_CONSOLE_POSIX
|
#ifdef DUSK_CONSOLE_POSIX
|
||||||
threadMutexInit(&CONSOLE.printMutex);
|
threadMutexInit(&CONSOLE.printMutex);
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ typedef struct chunk_s {
|
|||||||
assetentry_t *dcfEntry;
|
assetentry_t *dcfEntry;
|
||||||
|
|
||||||
uint8_t meshCount;
|
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];
|
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
|
||||||
mat4 meshModels[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];
|
uint8_t entities[CHUNK_ENTITY_COUNT_MAX];
|
||||||
} chunk_t;
|
} chunk_t;
|
||||||
|
|||||||
@@ -148,9 +148,9 @@ void mapChunkUnload(chunk_t *chunk) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for(uint8_t m = 0; m < chunk->meshCount; m++) {
|
for(uint8_t m = 0; m < chunk->meshCount; m++) {
|
||||||
if(chunk->meshEntries[m] == NULL) continue;
|
if(chunk->modelEntries[m] == NULL) continue;
|
||||||
assetUnlockEntry(chunk->meshEntries[m]);
|
assetUnlockEntry(chunk->modelEntries[m]);
|
||||||
chunk->meshEntries[m] = NULL;
|
chunk->modelEntries[m] = NULL;
|
||||||
}
|
}
|
||||||
chunk->meshCount = 0;
|
chunk->meshCount = 0;
|
||||||
}
|
}
|
||||||
@@ -183,6 +183,7 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
errorret_t mapWaitForChunks() {
|
errorret_t mapWaitForChunks() {
|
||||||
|
// Phase 1: Wait for all DCF entries to finish loading.
|
||||||
bool_t anyPending;
|
bool_t anyPending;
|
||||||
do {
|
do {
|
||||||
anyPending = false;
|
anyPending = false;
|
||||||
@@ -190,10 +191,9 @@ errorret_t mapWaitForChunks() {
|
|||||||
|
|
||||||
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||||
chunk_t *chunk = &MAP.chunks[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) {
|
if(state == ASSET_ENTRY_STATE_LOADED) {
|
||||||
uint8_t meshCount = chunk->dcfEntry->data.chunk.meshCount;
|
uint8_t meshCount = chunk->dcfEntry->data.chunk.meshCount;
|
||||||
memoryCopy(
|
memoryCopy(
|
||||||
@@ -207,8 +207,8 @@ errorret_t mapWaitForChunks() {
|
|||||||
|
|
||||||
for(uint8_t m = 0; m < meshCount; m++) {
|
for(uint8_t m = 0; m < meshCount; m++) {
|
||||||
stringCopy(
|
stringCopy(
|
||||||
chunk->meshNames[m],
|
chunk->modelNames[m],
|
||||||
chunk->dcfEntry->data.chunk.meshNames[m],
|
chunk->dcfEntry->data.chunk.modelNames[m],
|
||||||
CHUNK_MESH_NAME_MAX
|
CHUNK_MESH_NAME_MAX
|
||||||
);
|
);
|
||||||
glm_vec3_copy(
|
glm_vec3_copy(
|
||||||
@@ -221,45 +221,40 @@ errorret_t mapWaitForChunks() {
|
|||||||
}
|
}
|
||||||
assetUnlockEntry(chunk->dcfEntry);
|
assetUnlockEntry(chunk->dcfEntry);
|
||||||
chunk->dcfEntry = NULL;
|
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;
|
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) {
|
} else if(state == ASSET_ENTRY_STATE_ERROR) {
|
||||||
assetUnlockEntry(chunk->dcfEntry);
|
assetUnlockEntry(chunk->dcfEntry);
|
||||||
chunk->dcfEntry = NULL;
|
chunk->dcfEntry = NULL;
|
||||||
memorySet(chunk->tiles, TILE_SHAPE_GROUND, sizeof(chunk->tiles));
|
memorySet(chunk->tiles, TILE_SHAPE_GROUND, sizeof(chunk->tiles));
|
||||||
continue;
|
|
||||||
} else {
|
} else {
|
||||||
anyPending = true;
|
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);
|
if(anyPending) usleep(1000);
|
||||||
} while(anyPending);
|
} 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();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,27 +24,8 @@
|
|||||||
#include "asset/loader/assetloader.h"
|
#include "asset/loader/assetloader.h"
|
||||||
#include "asset/loader/assetentry.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) {
|
errorret_t sceneOverworldInit(scenedata_t *sceneData) {
|
||||||
assertNotNull(sceneData, "Scene data cannot be null");
|
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();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,25 +119,31 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
errorret_t sceneOverworldDrawChunks() {
|
errorret_t sceneOverworldDrawChunks() {
|
||||||
shadermaterial_t chunkMaterial = {
|
|
||||||
.unlit = {
|
|
||||||
.color = COLOR_WHITE,
|
|
||||||
.texture = &TEXTURE_CHUNK
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||||
chunk_t *chunk = &MAP.chunks[i];
|
chunk_t *chunk = &MAP.chunks[i];
|
||||||
if(chunk->meshCount == 0) continue;
|
if(chunk->meshCount == 0) continue;
|
||||||
|
|
||||||
for(uint8_t m = 0; m < chunk->meshCount; m++) {
|
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(
|
errorChain(shaderSetMatrix(
|
||||||
&SHADER_UNLIT, SHADER_UNLIT_MODEL, chunk->meshModels[m]
|
&SHADER_UNLIT, SHADER_UNLIT_MODEL, chunk->meshModels[m]
|
||||||
));
|
));
|
||||||
errorChain(shaderSetMaterial(&SHADER_UNLIT, &chunkMaterial));
|
errorChain(shaderSetMaterial(&SHADER_UNLIT, &mat));
|
||||||
errorChain(meshDraw(&chunk->meshEntries[m]->data.mesh.mesh, 0, -1));
|
errorChain(meshDraw(
|
||||||
|
&model->meshEntry->data.mesh.mesh, 0, -1
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,8 +158,5 @@ errorret_t sceneOverworldDrawChunks() {
|
|||||||
|
|
||||||
errorret_t sceneOverworldDispose(scenedata_t *sceneData) {
|
errorret_t sceneOverworldDispose(scenedata_t *sceneData) {
|
||||||
assertNotNull(sceneData, "Scene data cannot be null");
|
assertNotNull(sceneData, "Scene data cannot be null");
|
||||||
|
|
||||||
errorChain(textureDispose(&TEXTURE_CHUNK));
|
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ JSON input (assetsraw/chunks/chunk_X_Y_Z.json):
|
|||||||
|
|
||||||
Version 3 DCF format (after 8-byte header + tiles):
|
Version 3 DCF format (after 8-byte header + tiles):
|
||||||
uint8_t meshCount
|
uint8_t meshCount
|
||||||
for each mesh:
|
for each model:
|
||||||
null-terminated string (relative asset path to .dmf)
|
null-terminated string (relative asset path to .json model)
|
||||||
float32[3] (x, y, z offset)
|
float32[3] (x, y, z offset)
|
||||||
|
|
||||||
DMF format:
|
DMF format:
|
||||||
@@ -89,11 +89,11 @@ def tile_index(x, y, z):
|
|||||||
return x + y * CHUNK_WIDTH + z * CHUNK_WIDTH * CHUNK_HEIGHT
|
return x + y * CHUNK_WIDTH + z * CHUNK_WIDTH * CHUNK_HEIGHT
|
||||||
|
|
||||||
|
|
||||||
def find_mesh(filename):
|
def find_model(filename):
|
||||||
"""Search ASSETS_DIR/meshes/ recursively for filename.
|
"""Search ASSETS_DIR/models/ recursively for filename.
|
||||||
Returns the path relative to ASSETS_DIR (forward slashes), or None."""
|
Returns the path relative to ASSETS_DIR (forward slashes), or None."""
|
||||||
meshes_root = os.path.join(ASSETS_DIR, 'meshes')
|
models_root = os.path.join(ASSETS_DIR, 'models')
|
||||||
for dirpath, _, filenames in os.walk(meshes_root):
|
for dirpath, _, filenames in os.walk(models_root):
|
||||||
if filename in filenames:
|
if filename in filenames:
|
||||||
rel = os.path.relpath(
|
rel = os.path.relpath(
|
||||||
os.path.join(dirpath, filename), ASSETS_DIR
|
os.path.join(dirpath, filename), ASSETS_DIR
|
||||||
@@ -102,6 +102,17 @@ def find_mesh(filename):
|
|||||||
return None
|
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):
|
def derive_dcf_path(json_path):
|
||||||
"""assetsraw/chunks/chunk_X_Y_Z.json -> assets/chunks/X_Y_Z.dcf"""
|
"""assetsraw/chunks/chunk_X_Y_Z.json -> assets/chunks/X_Y_Z.dcf"""
|
||||||
base = os.path.splitext(os.path.basename(json_path))[0]
|
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)
|
terrain_verts = build_terrain_verts(tiles)
|
||||||
mesh_names = []
|
model_names = []
|
||||||
mesh_offsets = []
|
mesh_offsets = []
|
||||||
|
|
||||||
# Terrain mesh (only written if non-empty)
|
|
||||||
dcf_base = os.path.splitext(os.path.basename(dcf_path))[0]
|
dcf_base = os.path.splitext(os.path.basename(dcf_path))[0]
|
||||||
|
|
||||||
|
# Terrain mesh + model (only written if non-empty)
|
||||||
terrain_dir = os.path.join(ASSETS_DIR, 'meshes', 'chunks')
|
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(terrain_dir, exist_ok=True)
|
||||||
|
os.makedirs(models_dir, exist_ok=True)
|
||||||
if terrain_verts:
|
if terrain_verts:
|
||||||
dmf_name = f'chunk_{dcf_base}_0.dmf'
|
dmf_name = f'chunk_{dcf_base}_0.dmf'
|
||||||
write_dmf(os.path.join(terrain_dir, dmf_name), terrain_verts)
|
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))
|
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', []):
|
for mesh in data.get('meshes', []):
|
||||||
filename = mesh['file']
|
filename = mesh['file']
|
||||||
pos = mesh.get('pos', [0, 0, 0])
|
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:
|
if rel is None:
|
||||||
raise ValueError(f"Mesh not found under assets/meshes/: {filename}")
|
raise ValueError(
|
||||||
mesh_names.append(rel)
|
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])))
|
mesh_offsets.append((float(pos[0]), float(pos[1]), float(pos[2])))
|
||||||
print(f' Resolved {filename} -> {rel}')
|
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):
|
def process_json(json_path):
|
||||||
|
|||||||
Reference in New Issue
Block a user