diff --git a/__init__.py b/__init__.py index b6441e429..5399cee8e 100644 --- a/__init__.py +++ b/__init__.py @@ -79,7 +79,7 @@ ) gameEditorEnum = ( - # ("SM64", "SM64", "Super Mario 64", 0), + ("SM64", "SM64", "Super Mario 64", 0), ("OOT", "OOT", "Ocarina Of Time", 1), ("MM", "MM", "Majora's Mask", 4), ("MK64", "MK64", "Mario Kart 64", 3), diff --git a/fast64_internal/hm64/sm64/__init__.py b/fast64_internal/hm64/sm64/__init__.py new file mode 100644 index 000000000..f90cbbd1a --- /dev/null +++ b/fast64_internal/hm64/sm64/__init__.py @@ -0,0 +1,15 @@ +from .ghostship_geolayout import ( + draw_panel as draw_ghostship_geolayout_panel, + export_armature_from_context as export_ghostship_geolayout_armature, + export_object_from_context as export_ghostship_geolayout_object, + register_scene_props as register_ghostship_geolayout_props, + unregister_scene_props as unregister_ghostship_geolayout_props, +) + +__all__ = ( + "draw_ghostship_geolayout_panel", + "export_ghostship_geolayout_armature", + "export_ghostship_geolayout_object", + "register_ghostship_geolayout_props", + "unregister_ghostship_geolayout_props", +) diff --git a/fast64_internal/hm64/sm64/ghostship_geolayout.py b/fast64_internal/hm64/sm64/ghostship_geolayout.py new file mode 100644 index 000000000..ac3ee71c9 --- /dev/null +++ b/fast64_internal/hm64/sm64/ghostship_geolayout.py @@ -0,0 +1,505 @@ +from __future__ import annotations + +import os +import struct + +import bpy + +from ...f3d.f3d_gbi import ( + DPSetTextureImage, + FModel, + GfxList, + SPBranchList, + SPDisplayList, + SPSetLights, + SPVertex, + get_F3D_GBI, +) +from ...sm64 import sm64_geolayout_classes as geo +from ...sm64.sm64_geolayout_constants import ( + GEO_BILLBOARD, + GEO_BRANCH, + GEO_CALL_ASM, + GEO_CAMERA, + GEO_END, + GEO_HELD_OBJECT, + GEO_LOAD_DL, + GEO_LOAD_DL_W_OFFSET, + GEO_NODE_CLOSE, + GEO_NODE_OPEN, + GEO_RETURN, + GEO_ROTATE, + GEO_SCALE, + GEO_SET_BG, + GEO_SET_CAMERA_FRUSTRUM, + GEO_SET_ORTHO, + GEO_SET_RENDER_AREA, + GEO_SET_RENDER_RANGE, + GEO_SET_Z_BUF, + GEO_SETUP_OBJ_RENDER, + GEO_START, + GEO_START_W_RENDERAREA, + GEO_START_W_SHADOW, + GEO_SWITCH, + GEO_TRANSLATE, + GEO_TRANSLATE_ROTATE, +) +from ...utility import ( + PluginError, + convertEulerFloatToShort, + convertFloatToShort, + crc64, + geoNodeRotateOrder, + prop_split, + toAlnum, +) + +GHOSTSHIP_RESOURCE_TYPE_BLOB = 0x4F424C42 + + +def _ghostship_asset_hash(folder_path: str, name: str | None): + if name in {None, "", "NULL"}: + return 0 + asset_path = f"{folder_path}/{name}".replace("\\", "/") + return int(crc64(asset_path), 16) + + +def _ghostship_resource_header(resource_type: int, version: int = 0): + data = bytearray() + data.extend(struct.pack(" 0: + if isinstance(node.node, geo.FunctionNode): + raise PluginError("An FunctionNode cannot have children.") + if node.groups and not skip_node: + _write_u8(data, GEO_NODE_OPEN) + for child in node.children: + data.extend(_transform_node_to_ghostship_binary(child, folder_path)) + if node.groups and not skip_node: + _write_u8(data, GEO_NODE_CLOSE) + elif isinstance(node.node, geo.SwitchNode): + raise PluginError("A switch bone must have at least one child bone.") + return data + + +def _node_to_ghostship_binary(node, folder_path: str): + data = bytearray() + + if isinstance(node, geo.JumpNode): + _write_u8(data, GEO_BRANCH) + _write_u8(data, 1 if node.storeReturn else 0) + _write_u64(data, _geo_asset_hash(node.geolayout, node.geoRef, folder_path)) + elif isinstance(node, geo.FunctionNode): + _write_u8(data, GEO_CALL_ASM) + _write_s16(data, int(node.func_param)) + _write_u32(data, _func_u32(node.geo_func)) + elif isinstance(node, geo.HeldObjectNode): + _write_u8(data, GEO_HELD_OBJECT) + _write_u32(data, _func_u32(node.geo_func)) + _write_u8(data, 0) + _write_vec3s(data, [convertFloatToShort(value) for value in node.translate]) + elif isinstance(node, geo.StartNode): + _write_u8(data, GEO_START) + elif isinstance(node, geo.EndNode): + _write_u8(data, GEO_END) + elif isinstance(node, geo.SwitchNode): + _write_u8(data, GEO_SWITCH) + _write_s16(data, int(node.defaultCase)) + _write_u32(data, _func_u32(node.switchFunc)) + elif isinstance(node, geo.TranslateRotateNode): + params = ((1 if node.hasDL else 0) << 7) | (node.fieldLayout << 4) | int(node.drawLayer) + rotation = node.rotate.to_euler(geoNodeRotateOrder) + _write_u8(data, GEO_TRANSLATE_ROTATE) + _write_u8(data, params) + if node.fieldLayout == 0: + _write_vec3s(data, [convertFloatToShort(value) for value in node.translate]) + _write_vec3s(data, [convertEulerFloatToShort(value) for value in rotation]) + elif node.fieldLayout == 1: + _write_vec3s(data, [convertFloatToShort(value) for value in node.translate]) + elif node.fieldLayout == 2: + _write_vec3s(data, [convertEulerFloatToShort(value) for value in rotation]) + elif node.fieldLayout == 3: + _write_s16(data, convertEulerFloatToShort(rotation.y)) + if node.hasDL: + _write_u64(data, _dl_asset_hash(node, folder_path)) + elif isinstance(node, geo.TranslateNode): + _write_u8(data, GEO_TRANSLATE) + _write_u8(data, ((1 if node.hasDL else 0) << 7) | int(node.drawLayer)) + _write_vec3s(data, [convertFloatToShort(value) for value in node.translate]) + if node.hasDL: + _write_u64(data, _dl_asset_hash(node, folder_path)) + elif isinstance(node, geo.RotateNode): + _write_u8(data, GEO_ROTATE) + _write_u8(data, ((1 if node.hasDL else 0) << 7) | int(node.drawLayer)) + _write_vec3s(data, [convertEulerFloatToShort(value) for value in node.rotate.to_euler(geoNodeRotateOrder)]) + if node.hasDL: + _write_u64(data, _dl_asset_hash(node, folder_path)) + elif isinstance(node, geo.BillboardNode): + _write_u8(data, GEO_BILLBOARD) + _write_u8(data, ((1 if node.hasDL else 0) << 7) | int(node.drawLayer)) + _write_vec3s(data, [convertFloatToShort(value) for value in node.translate]) + if node.hasDL: + _write_u64(data, _dl_asset_hash(node, folder_path)) + elif isinstance(node, geo.DisplayListNode): + _write_u8(data, GEO_LOAD_DL) + _write_u8(data, int(node.drawLayer)) + _write_u64(data, _dl_asset_hash(node, folder_path)) + elif isinstance(node, geo.ShadowNode): + _write_u8(data, GEO_START_W_SHADOW) + _write_s16(data, node.shadowType) + _write_s16(data, node.shadowSolidity) + _write_s16(data, node.shadowScale) + elif isinstance(node, geo.ScaleNode): + _write_u8(data, GEO_SCALE) + _write_u8(data, ((1 if node.hasDL else 0) << 7) | int(node.drawLayer)) + _write_u32(data, int(node.scaleValue * 0x10000)) + if node.hasDL: + _write_u64(data, _dl_asset_hash(node, folder_path)) + elif isinstance(node, geo.StartRenderAreaNode): + _write_u8(data, GEO_START_W_RENDERAREA) + _write_s16(data, convertFloatToShort(node.cullingRadius)) + elif isinstance(node, geo.RenderRangeNode): + _write_u8(data, GEO_SET_RENDER_RANGE) + _write_s16(data, convertFloatToShort(node.minDist)) + _write_s16(data, convertFloatToShort(node.maxDist)) + elif isinstance(node, geo.DisplayListWithOffsetNode): + _write_u8(data, GEO_LOAD_DL_W_OFFSET) + _write_u8(data, int(node.drawLayer)) + _write_vec3s(data, [convertFloatToShort(value) for value in node.translate]) + _write_u64(data, _dl_asset_hash(node, folder_path)) + elif isinstance(node, geo.ScreenAreaNode): + position = [160, 120] if node.useDefaults else node.position + dimensions = [160, 120] if node.useDefaults else node.dimensions + entry_count = 0xA if node.useDefaults else node.entryMinus2Count + _write_u8(data, GEO_SET_RENDER_AREA) + _write_s16(data, entry_count) + _write_s16(data, position[0]) + _write_s16(data, position[1]) + _write_s16(data, dimensions[0]) + _write_s16(data, dimensions[1]) + elif isinstance(node, geo.OrthoNode): + _write_u8(data, GEO_SET_ORTHO) + _write_s16(data, int(node.scale)) + elif isinstance(node, geo.FrustumNode): + _write_u8(data, GEO_SET_CAMERA_FRUSTRUM) + _write_u8(data, 1 if node.useFunc else 0) + _write_s16(data, int(node.fov)) + _write_s16(data, node.near) + _write_s16(data, node.far) + if node.useFunc: + _write_u32(data, 0x8029AA3C) + elif isinstance(node, geo.ZBufferNode): + _write_u8(data, GEO_SET_Z_BUF) + _write_u8(data, 1 if node.enable else 0) + elif isinstance(node, geo.CameraNode): + _write_u8(data, GEO_CAMERA) + _write_s16(data, node.camType) + _write_vec3f(data, node.position) + _write_vec3f(data, node.lookAt) + _write_u32(data, _func_u32(node.geo_func)) + elif isinstance(node, geo.RenderObjNode): + _write_u8(data, GEO_SETUP_OBJ_RENDER) + elif isinstance(node, geo.BackgroundNode): + _write_u8(data, GEO_SET_BG) + _write_s16(data, node.backgroundValue) + _write_u32(data, 0 if node.isColor else _func_u32(node.geo_func)) + else: + raise PluginError(f"Ghostship export does not support {type(node).__name__}.") + + return data + + +def _geolayout_to_ghostship_otr(geolayout: geo.Geolayout, folder_path: str): + payload = bytearray() + for node in geolayout.nodes: + payload.extend(_transform_node_to_ghostship_binary(node, folder_path)) + _write_u8(payload, GEO_END if geolayout.isStartGeo else GEO_RETURN) + return _ghostship_blob_resource(payload) + + +def _geolayout_graph_to_ghostship_otr(geolayout_graph: geo.GeolayoutGraph, folder_path: str): + geolayout_graph.checkListSorted() + return {geolayout.name: _geolayout_to_ghostship_otr(geolayout, folder_path) for geolayout in geolayout_graph.sortedList} + + +def _write_ghostship_resource(export_folder_path: str, name: str, data: bytes): + with open(os.path.join(export_folder_path, name), "wb") as resource_file: + resource_file.write(data) + + +def _ghostship_hash_bytes(asset_path: str): + hash_val = int(crc64(asset_path.replace("\\", "/")), 16) + return struct.pack("> 32, hash_val & 0xFFFFFFFF) + + +def _ghostship_native_words_from_big_endian(data: bytes): + fixed = bytearray() + for offset in range(0, len(data), 4): + fixed.extend(data[offset : offset + 4][::-1]) + return fixed + + +def _ghostship_pointer_command(command, folder_path: str): + return _ghostship_native_words_from_big_endian(command.toO2R(folder_path)) + + +def _ghostship_gbi_command(command, folder_path: str, f3d): + if isinstance(command, (SPVertex, SPDisplayList, SPBranchList, DPSetTextureImage)): + return _ghostship_pointer_command(command, folder_path) + + data = command.to_binary(f3d, {}) + if len(data) >= 8 and data[0] >= 0xE4: + data = _ghostship_native_words_from_big_endian(data) + return data + + +def _ghostship_vtx_list(folder_path: str, vtx_list): + data = _ghostship_resource_header(0x4F565458, 0) + data.extend(struct.pack("> 8) & 0xFF + length = (w0 & 0xFF) + 1 + data += f'\t\n' + else: + raise PluginError(f"Ghostship XML export does not support command {command.__class__.__name__}.") + data += "\n\n" + return data.encode("utf-8") + + +def _ghostship_mario_root_geo(folder_path: str, body_geo_name: str): + body_hash = _ghostship_asset_hash(folder_path, body_geo_name) + data = bytearray() + data.extend(struct.pack("