{"id":87,"date":"2025-09-03T12:52:02","date_gmt":"2025-09-03T12:52:02","guid":{"rendered":"https:\/\/thatuglydude.com\/?p=87"},"modified":"2025-09-03T12:52:02","modified_gmt":"2025-09-03T12:52:02","slug":"emerald-world-7","status":"publish","type":"post","link":"https:\/\/thatuglydude.com\/index.php\/2025\/09\/03\/emerald-world-7\/","title":{"rendered":"Emerald World #7"},"content":{"rendered":"\n<p>Woohoo, having asked on <a href=\"https:\/\/www.celestialheavens.com\/forum\/topic\/18822\">the Celestial Heavens<\/a> forum, I could use MMExtension to get the map data from the original game. I&#8217;ve managed to load it into Godot, a bare view: <\/p>\n\n\n\n<div class=\"wp-block-group is-layout-constrained wp-block-group-is-layout-constrained\">\n<figure class=\"wp-block-image is-style-default wp-duotone-unset-1\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"594\" src=\"https:\/\/thatuglydude.com\/wp-content\/uploads\/2025\/09\/harmondale_outside-1024x594.png\" alt=\"\" class=\"wp-image-88\" srcset=\"https:\/\/thatuglydude.com\/wp-content\/uploads\/2025\/09\/harmondale_outside-1024x594.png 1024w, https:\/\/thatuglydude.com\/wp-content\/uploads\/2025\/09\/harmondale_outside-300x174.png 300w, https:\/\/thatuglydude.com\/wp-content\/uploads\/2025\/09\/harmondale_outside-768x446.png 768w, https:\/\/thatuglydude.com\/wp-content\/uploads\/2025\/09\/harmondale_outside.png 1533w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>This is mean to be the Harmondale castle, as the geometry is relatively simple (walls and floors). Missing are of course textures, lights, contents, etc., but that&#8217;s the start!<\/p>\n<\/div>\n\n\n\n<p>To get it, I needed to use the MMExtension and MMEditor <a href=\"https:\/\/github.com\/GrayFace\/MMExtension\" data-type=\"link\" data-id=\"https:\/\/github.com\/GrayFace\/MMExtension\">made by GrayFace.<\/a> Start the game, get to the location you want to export, ALT+F1 to bring up the extension&#8217;s menu, then select Export (third row, third column). <\/p>\n\n\n\n<figure class=\"wp-block-image size-large wp-duotone-unset-2\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"700\" src=\"https:\/\/thatuglydude.com\/wp-content\/uploads\/2025\/09\/harmondale_mmextension-1024x700.png\" alt=\"\" class=\"wp-image-89\" srcset=\"https:\/\/thatuglydude.com\/wp-content\/uploads\/2025\/09\/harmondale_mmextension-1024x700.png 1024w, https:\/\/thatuglydude.com\/wp-content\/uploads\/2025\/09\/harmondale_mmextension-300x205.png 300w, https:\/\/thatuglydude.com\/wp-content\/uploads\/2025\/09\/harmondale_mmextension-768x525.png 768w, https:\/\/thatuglydude.com\/wp-content\/uploads\/2025\/09\/harmondale_mmextension.png 1537w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>The output is custom .obj file, listing rooms (&#8220;g&#8221;), vertices (&#8220;v&#8221;), textures (&#8220;vt&#8221;) and facets (&#8220;f&#8221;). Facets turn out to be triangle fans of the defined vertices.<\/p>\n\n\n\n<p>I could then write a very simple GDScript to parse parts of it:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>class_name DungeonLoader\nextends Script\n\nconst kScalingFactor: float = 0.01\n\n\nstatic func load_dungeon_map(path: String) -&gt; DungeonGeometry:\n\tvar file = FileAccess.open(path, FileAccess.READ)\n\tif file == null:\n\t\tLog.error(\"Could not load %s: %s\", &#091;path, error_string(FileAccess.get_open_error())])\n\t\treturn null\n\n\tvar geometry = DungeonGeometry.new()\n\twhile !file.eof_reached():\n\t\tvar line = file.get_line()\n\t\tif line.is_empty():\n\t\t\tcontinue\n\t\tmatch line.substr(0, 2):\n\t\t\t\"v \":\n\t\t\t\tgeometry.vertices.append(_parse_vertex_line(line))\n\t\t\t\"f \":\n\t\t\t\tgeometry.facets.append(_parse_facet_line(line))\n\treturn geometry\n\n\nstatic func _parse_vertex_line(line: String) -&gt; Vector3:\n\tvar parts = line.split(\" \")\n\treturn kScalingFactor * Vector3(float(parts&#091;1]), float(parts&#091;2]), float(parts&#091;3]))\n\n\nstatic func _parse_facet_line(line: String) -&gt; Array&#091;int]:\n\tvar parts = line.split(\" \")\n\tvar indexes: Array&#091;int] = &#091;]\n\tfor i in range(1, parts.size()):\n\t\tvar final_parts = parts&#091;i].split(\"\/\")\n\t\tindexes.append(int(final_parts&#091;0]) - 1)\n\treturn indexes<\/code><\/pre>\n\n\n\n<p>&#8230; and eventually consume it to generate the geometry:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>func _debug_mesh() -&gt; void:\n\tvar surface_tool = SurfaceTool.new()\n\tsurface_tool.begin(Mesh.PRIMITIVE_TRIANGLES)\n\tsurface_tool.set_color(Color(0, 0, 1, 1))\n\t\n\tvar geometry = DungeonLoader.load_dungeon_map(\"res:\/\/emerald_world\/scenes\/world\/d29.obj\")\n\tfor facet in geometry.facets:\n\t\tvar vertices = &#091;]\n\t\tfor index in facet:\n\t\t\tvertices.append(geometry.vertices&#091;index])\n\t\tsurface_tool.add_triangle_fan(vertices)\n\n\tsurface_tool.generate_normals(false)\n\t\n\t$DebugBody3D\/MeshInstance3D.mesh = surface_tool.commit()\n\t$DebugBody3D\/MeshInstance3D.global_position = Vector3(150, 0.1, 80)<\/code><\/pre>\n\n\n\n<p>Since Godot supports hot reloads, I could work on the script while debugging it, to see if everything&#8217;s loaded nicely.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Woohoo, having asked on the Celestial Heavens forum, I could use MMExtension to get the map data from the original game. I&#8217;ve managed to load it into Godot, a bare view: This is mean to be the Harmondale castle, as the geometry is relatively simple (walls and floors). Missing are of course textures, lights, contents, [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[9,6,5],"tags":[],"class_list":["post-87","post","type-post","status-publish","format-standard","hentry","category-gamedev","category-gdscript","category-mightandmagic"],"_links":{"self":[{"href":"https:\/\/thatuglydude.com\/index.php\/wp-json\/wp\/v2\/posts\/87","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/thatuglydude.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/thatuglydude.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/thatuglydude.com\/index.php\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/thatuglydude.com\/index.php\/wp-json\/wp\/v2\/comments?post=87"}],"version-history":[{"count":1,"href":"https:\/\/thatuglydude.com\/index.php\/wp-json\/wp\/v2\/posts\/87\/revisions"}],"predecessor-version":[{"id":90,"href":"https:\/\/thatuglydude.com\/index.php\/wp-json\/wp\/v2\/posts\/87\/revisions\/90"}],"wp:attachment":[{"href":"https:\/\/thatuglydude.com\/index.php\/wp-json\/wp\/v2\/media?parent=87"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/thatuglydude.com\/index.php\/wp-json\/wp\/v2\/categories?post=87"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/thatuglydude.com\/index.php\/wp-json\/wp\/v2\/tags?post=87"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}