diff --git a/assets/chunks/-1_0_0.dcf b/assets/chunks/-1_0_0.dcf
index e35ec480..640ff6e5 100644
Binary files a/assets/chunks/-1_0_0.dcf and b/assets/chunks/-1_0_0.dcf differ
diff --git a/assets/chunks/0_0_0.dcf b/assets/chunks/0_0_0.dcf
index e6a1256f..aa2959c7 100644
Binary files a/assets/chunks/0_0_0.dcf and b/assets/chunks/0_0_0.dcf differ
diff --git a/assets/chunks/0_1_0.dcf b/assets/chunks/0_1_0.dcf
index 7e18574a..7429ed6c 100644
Binary files a/assets/chunks/0_1_0.dcf and b/assets/chunks/0_1_0.dcf differ
diff --git a/assets/chunks/1_0_0.dcf b/assets/chunks/1_0_0.dcf
index 7c10bc21..44ace2cb 100644
Binary files a/assets/chunks/1_0_0.dcf and b/assets/chunks/1_0_0.dcf differ
diff --git a/assets/chunks/1_1_0.dcf b/assets/chunks/1_1_0.dcf
index 3ff4b702..fb28d758 100644
Binary files a/assets/chunks/1_1_0.dcf and b/assets/chunks/1_1_0.dcf differ
diff --git a/assets/models/buildings/house_1_1.json b/assets/models/buildings/house_1_1.json
new file mode 100644
index 00000000..f3231bce
--- /dev/null
+++ b/assets/models/buildings/house_1_1.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_1_1.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_1_2.json b/assets/models/buildings/house_1_2.json
new file mode 100644
index 00000000..6962b659
--- /dev/null
+++ b/assets/models/buildings/house_1_2.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_1_2.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_1_3.json b/assets/models/buildings/house_1_3.json
new file mode 100644
index 00000000..3c369e8e
--- /dev/null
+++ b/assets/models/buildings/house_1_3.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_1_3.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_1_4.json b/assets/models/buildings/house_1_4.json
new file mode 100644
index 00000000..81b57861
--- /dev/null
+++ b/assets/models/buildings/house_1_4.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_1_4.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_1_5.json b/assets/models/buildings/house_1_5.json
new file mode 100644
index 00000000..36d93d6c
--- /dev/null
+++ b/assets/models/buildings/house_1_5.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_1_5.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_1_6.json b/assets/models/buildings/house_1_6.json
new file mode 100644
index 00000000..c544d562
--- /dev/null
+++ b/assets/models/buildings/house_1_6.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_1_6.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_1_7.json b/assets/models/buildings/house_1_7.json
new file mode 100644
index 00000000..7f419a27
--- /dev/null
+++ b/assets/models/buildings/house_1_7.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_1_7.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_1_8.json b/assets/models/buildings/house_1_8.json
new file mode 100644
index 00000000..b741c12f
--- /dev/null
+++ b/assets/models/buildings/house_1_8.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_1_8.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_2_1.json b/assets/models/buildings/house_2_1.json
new file mode 100644
index 00000000..693ee13f
--- /dev/null
+++ b/assets/models/buildings/house_2_1.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_2_1.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_2_2.json b/assets/models/buildings/house_2_2.json
new file mode 100644
index 00000000..83080cd5
--- /dev/null
+++ b/assets/models/buildings/house_2_2.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_2_2.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_2_3.json b/assets/models/buildings/house_2_3.json
new file mode 100644
index 00000000..172b0784
--- /dev/null
+++ b/assets/models/buildings/house_2_3.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_2_3.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_2_4.json b/assets/models/buildings/house_2_4.json
new file mode 100644
index 00000000..c0b4a34b
--- /dev/null
+++ b/assets/models/buildings/house_2_4.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_2_4.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_2_5.json b/assets/models/buildings/house_2_5.json
new file mode 100644
index 00000000..a450c254
--- /dev/null
+++ b/assets/models/buildings/house_2_5.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_2_5.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_2_6.json b/assets/models/buildings/house_2_6.json
new file mode 100644
index 00000000..6b9e98ab
--- /dev/null
+++ b/assets/models/buildings/house_2_6.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_2_6.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_2_7.json b/assets/models/buildings/house_2_7.json
new file mode 100644
index 00000000..b93f7400
--- /dev/null
+++ b/assets/models/buildings/house_2_7.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_2_7.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_2_8.json b/assets/models/buildings/house_2_8.json
new file mode 100644
index 00000000..a306084d
--- /dev/null
+++ b/assets/models/buildings/house_2_8.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_2_8.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_3_1.json b/assets/models/buildings/house_3_1.json
new file mode 100644
index 00000000..8a51936f
--- /dev/null
+++ b/assets/models/buildings/house_3_1.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_3_1.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_3_2.json b/assets/models/buildings/house_3_2.json
new file mode 100644
index 00000000..91a189a9
--- /dev/null
+++ b/assets/models/buildings/house_3_2.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_3_2.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_3_3.json b/assets/models/buildings/house_3_3.json
new file mode 100644
index 00000000..98f13cca
--- /dev/null
+++ b/assets/models/buildings/house_3_3.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_3_3.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_3_4.json b/assets/models/buildings/house_3_4.json
new file mode 100644
index 00000000..f9413bde
--- /dev/null
+++ b/assets/models/buildings/house_3_4.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_3_4.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_3_5.json b/assets/models/buildings/house_3_5.json
new file mode 100644
index 00000000..71507711
--- /dev/null
+++ b/assets/models/buildings/house_3_5.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_3_5.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_3_6.json b/assets/models/buildings/house_3_6.json
new file mode 100644
index 00000000..5eb7865d
--- /dev/null
+++ b/assets/models/buildings/house_3_6.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_3_6.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_3_7.json b/assets/models/buildings/house_3_7.json
new file mode 100644
index 00000000..7bcc8b5a
--- /dev/null
+++ b/assets/models/buildings/house_3_7.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_3_7.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_3_8.json b/assets/models/buildings/house_3_8.json
new file mode 100644
index 00000000..0643ca47
--- /dev/null
+++ b/assets/models/buildings/house_3_8.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_3_8.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_4_1.json b/assets/models/buildings/house_4_1.json
new file mode 100644
index 00000000..73a911cd
--- /dev/null
+++ b/assets/models/buildings/house_4_1.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_4_1.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_4_2.json b/assets/models/buildings/house_4_2.json
new file mode 100644
index 00000000..db055224
--- /dev/null
+++ b/assets/models/buildings/house_4_2.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_4_2.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_4_3.json b/assets/models/buildings/house_4_3.json
new file mode 100644
index 00000000..64e39039
--- /dev/null
+++ b/assets/models/buildings/house_4_3.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_4_3.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_4_4.json b/assets/models/buildings/house_4_4.json
new file mode 100644
index 00000000..8963ba12
--- /dev/null
+++ b/assets/models/buildings/house_4_4.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_4_4.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_4_5.json b/assets/models/buildings/house_4_5.json
new file mode 100644
index 00000000..c13ba9e3
--- /dev/null
+++ b/assets/models/buildings/house_4_5.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_4_5.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_4_6.json b/assets/models/buildings/house_4_6.json
new file mode 100644
index 00000000..9fc76393
--- /dev/null
+++ b/assets/models/buildings/house_4_6.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_4_6.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_4_7.json b/assets/models/buildings/house_4_7.json
new file mode 100644
index 00000000..c7545cac
--- /dev/null
+++ b/assets/models/buildings/house_4_7.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_4_7.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_4_8.json b/assets/models/buildings/house_4_8.json
new file mode 100644
index 00000000..a470127f
--- /dev/null
+++ b/assets/models/buildings/house_4_8.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_4_8.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_5_1.json b/assets/models/buildings/house_5_1.json
new file mode 100644
index 00000000..7966a3e8
--- /dev/null
+++ b/assets/models/buildings/house_5_1.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_5_1.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_5_2.json b/assets/models/buildings/house_5_2.json
new file mode 100644
index 00000000..cdbc0a7c
--- /dev/null
+++ b/assets/models/buildings/house_5_2.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_5_2.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_5_3.json b/assets/models/buildings/house_5_3.json
new file mode 100644
index 00000000..657900a1
--- /dev/null
+++ b/assets/models/buildings/house_5_3.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_5_3.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_5_4.json b/assets/models/buildings/house_5_4.json
new file mode 100644
index 00000000..ce52eac1
--- /dev/null
+++ b/assets/models/buildings/house_5_4.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_5_4.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_5_5.json b/assets/models/buildings/house_5_5.json
new file mode 100644
index 00000000..0223c690
--- /dev/null
+++ b/assets/models/buildings/house_5_5.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_5_5.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_5_6.json b/assets/models/buildings/house_5_6.json
new file mode 100644
index 00000000..769373be
--- /dev/null
+++ b/assets/models/buildings/house_5_6.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_5_6.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_5_7.json b/assets/models/buildings/house_5_7.json
new file mode 100644
index 00000000..c428dbc2
--- /dev/null
+++ b/assets/models/buildings/house_5_7.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_5_7.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_5_8.json b/assets/models/buildings/house_5_8.json
new file mode 100644
index 00000000..2efa8d5b
--- /dev/null
+++ b/assets/models/buildings/house_5_8.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_5_8.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_6_1.json b/assets/models/buildings/house_6_1.json
new file mode 100644
index 00000000..d2a4d5be
--- /dev/null
+++ b/assets/models/buildings/house_6_1.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_6_1.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_6_2.json b/assets/models/buildings/house_6_2.json
new file mode 100644
index 00000000..c3f0a02f
--- /dev/null
+++ b/assets/models/buildings/house_6_2.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_6_2.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_6_3.json b/assets/models/buildings/house_6_3.json
new file mode 100644
index 00000000..f145bf86
--- /dev/null
+++ b/assets/models/buildings/house_6_3.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_6_3.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_6_4.json b/assets/models/buildings/house_6_4.json
new file mode 100644
index 00000000..6c37797c
--- /dev/null
+++ b/assets/models/buildings/house_6_4.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_6_4.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_6_5.json b/assets/models/buildings/house_6_5.json
new file mode 100644
index 00000000..b2d252ce
--- /dev/null
+++ b/assets/models/buildings/house_6_5.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_6_5.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_6_6.json b/assets/models/buildings/house_6_6.json
new file mode 100644
index 00000000..e50bba0c
--- /dev/null
+++ b/assets/models/buildings/house_6_6.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_6_6.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_6_7.json b/assets/models/buildings/house_6_7.json
new file mode 100644
index 00000000..21ff16fd
--- /dev/null
+++ b/assets/models/buildings/house_6_7.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_6_7.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_6_8.json b/assets/models/buildings/house_6_8.json
new file mode 100644
index 00000000..a2c66829
--- /dev/null
+++ b/assets/models/buildings/house_6_8.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_6_8.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_7_1.json b/assets/models/buildings/house_7_1.json
new file mode 100644
index 00000000..0017a2f4
--- /dev/null
+++ b/assets/models/buildings/house_7_1.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_7_1.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_7_2.json b/assets/models/buildings/house_7_2.json
new file mode 100644
index 00000000..d62cfc35
--- /dev/null
+++ b/assets/models/buildings/house_7_2.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_7_2.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_7_3.json b/assets/models/buildings/house_7_3.json
new file mode 100644
index 00000000..112826b2
--- /dev/null
+++ b/assets/models/buildings/house_7_3.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_7_3.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_7_4.json b/assets/models/buildings/house_7_4.json
new file mode 100644
index 00000000..3a68ee2e
--- /dev/null
+++ b/assets/models/buildings/house_7_4.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_7_4.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_7_5.json b/assets/models/buildings/house_7_5.json
new file mode 100644
index 00000000..11a4aba9
--- /dev/null
+++ b/assets/models/buildings/house_7_5.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_7_5.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_7_6.json b/assets/models/buildings/house_7_6.json
new file mode 100644
index 00000000..ab1f64e2
--- /dev/null
+++ b/assets/models/buildings/house_7_6.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_7_6.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_7_7.json b/assets/models/buildings/house_7_7.json
new file mode 100644
index 00000000..22262621
--- /dev/null
+++ b/assets/models/buildings/house_7_7.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_7_7.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_7_8.json b/assets/models/buildings/house_7_8.json
new file mode 100644
index 00000000..8797c640
--- /dev/null
+++ b/assets/models/buildings/house_7_8.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_7_8.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_8_1.json b/assets/models/buildings/house_8_1.json
new file mode 100644
index 00000000..dc1860fa
--- /dev/null
+++ b/assets/models/buildings/house_8_1.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_8_1.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_8_2.json b/assets/models/buildings/house_8_2.json
new file mode 100644
index 00000000..4a32f105
--- /dev/null
+++ b/assets/models/buildings/house_8_2.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_8_2.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_8_3.json b/assets/models/buildings/house_8_3.json
new file mode 100644
index 00000000..2b7c73b7
--- /dev/null
+++ b/assets/models/buildings/house_8_3.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_8_3.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_8_4.json b/assets/models/buildings/house_8_4.json
new file mode 100644
index 00000000..29a2a49c
--- /dev/null
+++ b/assets/models/buildings/house_8_4.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_8_4.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_8_5.json b/assets/models/buildings/house_8_5.json
new file mode 100644
index 00000000..54788a80
--- /dev/null
+++ b/assets/models/buildings/house_8_5.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_8_5.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_8_6.json b/assets/models/buildings/house_8_6.json
new file mode 100644
index 00000000..6d2e35b6
--- /dev/null
+++ b/assets/models/buildings/house_8_6.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_8_6.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_8_7.json b/assets/models/buildings/house_8_7.json
new file mode 100644
index 00000000..41009c69
--- /dev/null
+++ b/assets/models/buildings/house_8_7.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_8_7.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/buildings/house_8_8.json b/assets/models/buildings/house_8_8.json
new file mode 100644
index 00000000..800d2758
--- /dev/null
+++ b/assets/models/buildings/house_8_8.json
@@ -0,0 +1,4 @@
+{
+ "mesh": "meshes/buildings/house_8_8.dmf",
+ "color": [255, 0, 255, 255]
+}
diff --git a/assets/models/chunks/chunk_-1_0_0_0.json b/assets/models/chunks/chunk_-1_0_0_0.json
new file mode 100644
index 00000000..e2757646
--- /dev/null
+++ b/assets/models/chunks/chunk_-1_0_0_0.json
@@ -0,0 +1,10 @@
+{
+ "mesh": "meshes/chunks/chunk_-1_0_0_0.dmf",
+ "color": [
+ 255,
+ 255,
+ 255,
+ 255
+ ],
+ "texture": "tiles.png"
+}
\ No newline at end of file
diff --git a/assets/models/chunks/chunk_0_0_0_0.json b/assets/models/chunks/chunk_0_0_0_0.json
new file mode 100644
index 00000000..7715800a
--- /dev/null
+++ b/assets/models/chunks/chunk_0_0_0_0.json
@@ -0,0 +1,10 @@
+{
+ "mesh": "meshes/chunks/chunk_0_0_0_0.dmf",
+ "color": [
+ 255,
+ 255,
+ 255,
+ 255
+ ],
+ "texture": "tiles.png"
+}
\ No newline at end of file
diff --git a/assets/models/chunks/chunk_0_1_0_0.json b/assets/models/chunks/chunk_0_1_0_0.json
new file mode 100644
index 00000000..e2da8643
--- /dev/null
+++ b/assets/models/chunks/chunk_0_1_0_0.json
@@ -0,0 +1,10 @@
+{
+ "mesh": "meshes/chunks/chunk_0_1_0_0.dmf",
+ "color": [
+ 255,
+ 255,
+ 255,
+ 255
+ ],
+ "texture": "tiles.png"
+}
\ No newline at end of file
diff --git a/assets/models/chunks/chunk_1_0_0_0.json b/assets/models/chunks/chunk_1_0_0_0.json
new file mode 100644
index 00000000..1c5947f8
--- /dev/null
+++ b/assets/models/chunks/chunk_1_0_0_0.json
@@ -0,0 +1,10 @@
+{
+ "mesh": "meshes/chunks/chunk_1_0_0_0.dmf",
+ "color": [
+ 255,
+ 255,
+ 255,
+ 255
+ ],
+ "texture": "tiles.png"
+}
\ No newline at end of file
diff --git a/assets/models/chunks/chunk_1_1_0_0.json b/assets/models/chunks/chunk_1_1_0_0.json
new file mode 100644
index 00000000..b68b098c
--- /dev/null
+++ b/assets/models/chunks/chunk_1_1_0_0.json
@@ -0,0 +1,10 @@
+{
+ "mesh": "meshes/chunks/chunk_1_1_0_0.dmf",
+ "color": [
+ 255,
+ 255,
+ 255,
+ 255
+ ],
+ "texture": "tiles.png"
+}
\ No newline at end of file
diff --git a/assets/tiles.png b/assets/tiles.png
new file mode 100644
index 00000000..bc170af1
Binary files /dev/null and b/assets/tiles.png differ
diff --git a/editor/client/public/index.html b/editor/client/public/index.html
new file mode 100644
index 00000000..3507e45b
--- /dev/null
+++ b/editor/client/public/index.html
@@ -0,0 +1 @@
+
Hello Homepage
diff --git a/editor/client/public/template.html b/editor/client/public/template.html
new file mode 100644
index 00000000..976e6fe4
--- /dev/null
+++ b/editor/client/public/template.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Dusk Map Editor
+
+
+
+
+
diff --git a/editor/server/server.py b/editor/server/server.py
new file mode 100644
index 00000000..07fe3052
--- /dev/null
+++ b/editor/server/server.py
@@ -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= directory listing
+ GET /api/read?path= file contents
+ POST /api/write?path= write file (JSON body: {"content":"..."})
+ POST /api/mkdir?path= 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(
+ '',
+ f'{fragment}
',
+ 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.")
diff --git a/editor/start.sh b/editor/start.sh
new file mode 100755
index 00000000..e1871d04
--- /dev/null
+++ b/editor/start.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+set -e
+cd "$(dirname "$0")"
+python3 server/server.py
diff --git a/src/dusk/asset/asset.c b/src/dusk/asset/asset.c
index 55cf4933..6ac7a4e9 100644
--- a/src/dusk/asset/asset.c
+++ b/src/dusk/asset/asset.c
@@ -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) {
diff --git a/src/dusk/asset/loader/assetloader.c b/src/dusk/asset/loader/assetloader.c
index 902731b6..ab1ba41b 100644
--- a/src/dusk/asset/loader/assetloader.c
+++ b/src/dusk/asset/loader/assetloader.c
@@ -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,
diff --git a/src/dusk/asset/loader/assetloader.h b/src/dusk/asset/loader/assetloader.h
index 7a96963c..a9edd1b1 100644
--- a/src/dusk/asset/loader/assetloader.h
+++ b/src/dusk/asset/loader/assetloader.h
@@ -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;
diff --git a/src/dusk/asset/loader/chunk/assetchunkloader.c b/src/dusk/asset/loader/chunk/assetchunkloader.c
index 56fadf49..408be108 100644
--- a/src/dusk/asset/loader/chunk/assetchunkloader.c
+++ b/src/dusk/asset/loader/chunk/assetchunkloader.c
@@ -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(
diff --git a/src/dusk/asset/loader/chunk/assetchunkloader.h b/src/dusk/asset/loader/chunk/assetchunkloader.h
index 8504b2c2..775a28cd 100644
--- a/src/dusk/asset/loader/chunk/assetchunkloader.h
+++ b/src/dusk/asset/loader/chunk/assetchunkloader.h
@@ -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.
diff --git a/src/dusk/asset/loader/dmf/CMakeLists.txt b/src/dusk/asset/loader/dmf/CMakeLists.txt
index c767bc3f..7210daa0 100644
--- a/src/dusk/asset/loader/dmf/CMakeLists.txt
+++ b/src/dusk/asset/loader/dmf/CMakeLists.txt
@@ -6,4 +6,5 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
assetmeshloader.c
+ assetmodelloader.c
)
diff --git a/src/dusk/asset/loader/dmf/assetmodelloader.c b/src/dusk/asset/loader/dmf/assetmodelloader.c
new file mode 100644
index 00000000..ce87f96d
--- /dev/null
+++ b/src/dusk/asset/loader/dmf/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();
+}
diff --git a/src/dusk/asset/loader/dmf/assetmodelloader.h b/src/dusk/asset/loader/dmf/assetmodelloader.h
new file mode 100644
index 00000000..555407c6
--- /dev/null
+++ b/src/dusk/asset/loader/dmf/assetmodelloader.h
@@ -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);
diff --git a/src/dusk/asset/loader/json/assetjsonloader.c b/src/dusk/asset/loader/json/assetjsonloader.c
index c1d3ea9a..c8505cca 100644
--- a/src/dusk/asset/loader/json/assetjsonloader.c
+++ b/src/dusk/asset/loader/json/assetjsonloader.c
@@ -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");
diff --git a/src/dusk/asset/loader/json/assetjsonloader.h b/src/dusk/asset/loader/json/assetjsonloader.h
index 4508064d..e3c2d251 100644
--- a/src/dusk/asset/loader/json/assetjsonloader.h
+++ b/src/dusk/asset/loader/json/assetjsonloader.h
@@ -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,
diff --git a/src/dusk/console/console.c b/src/dusk/console/console.c
index e0acabba..fe6e6119 100644
--- a/src/dusk/console/console.c
+++ b/src/dusk/console/console.c
@@ -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);
diff --git a/src/dusk/rpg/overworld/chunk.h b/src/dusk/rpg/overworld/chunk.h
index 23348819..38cc9abe 100644
--- a/src/dusk/rpg/overworld/chunk.h
+++ b/src/dusk/rpg/overworld/chunk.h
@@ -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;
diff --git a/src/dusk/rpg/overworld/map.c b/src/dusk/rpg/overworld/map.c
index 1f279a12..db12600f 100644
--- a/src/dusk/rpg/overworld/map.c
+++ b/src/dusk/rpg/overworld/map.c
@@ -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();
}
diff --git a/src/dusk/scene/overworld/sceneoverworld.c b/src/dusk/scene/overworld/sceneoverworld.c
index 9bb91bd3..fc3bcca6 100644
--- a/src/dusk/scene/overworld/sceneoverworld.c
+++ b/src/dusk/scene/overworld/sceneoverworld.c
@@ -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();
}
diff --git a/tools/asset/chunk/__main__.py b/tools/asset/chunk/__main__.py
index 36111e44..a0d01b93 100644
--- a/tools/asset/chunk/__main__.py
+++ b/tools/asset/chunk/__main__.py
@@ -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):