#include "FRME.hpp" #include "DataSpec/DNACommon/TXTR.hpp" #include "hecl/Blender/Connection.hpp" namespace DataSpec::DNAMP1 { template <> void FRME::Enumerate(athena::io::IStreamReader& __dna_reader) { /* version */ version = __dna_reader.readUint32Big(); /* unk1 */ unk1 = __dna_reader.readUint32Big(); /* modelCount */ modelCount = __dna_reader.readUint32Big(); /* unk3 */ unk3 = __dna_reader.readUint32Big(); /* widgetCount */ widgetCount = __dna_reader.readUint32Big(); /* widgets */ __dna_reader.enumerate(widgets, widgetCount, [this](athena::io::IStreamReader& reader, Widget& w) { w.owner = this; w.read(reader); }); } template <> void FRME::Enumerate(athena::io::IStreamWriter& __dna_writer) { /* version */ __dna_writer.writeUint32Big(version); /* unk1 */ __dna_writer.writeUint32Big(unk1); /* modelCount */ __dna_writer.writeUint32Big(modelCount); /* unk3 */ __dna_writer.writeUint32Big(unk3); /* widgetCount */ __dna_writer.writeUint32Big(widgetCount); /* widgets */ __dna_writer.enumerate(widgets); } template <> void FRME::Enumerate(size_t& __isz) { for (const Widget& w : widgets) w.binarySize(__isz); __isz += 20; } template <> void FRME::Widget::Enumerate(athena::io::IStreamReader& __dna_reader) { /* type */ type.read(__dna_reader); /* header */ header.read(__dna_reader); switch (type.toUint32()) { case SBIG('BWIG'): widgetInfo = std::make_unique(); break; case SBIG('HWIG'): widgetInfo = std::make_unique(); break; case SBIG('CAMR'): widgetInfo = std::make_unique(); break; case SBIG('LITE'): widgetInfo = std::make_unique(); break; case SBIG('ENRG'): widgetInfo = std::make_unique(); break; case SBIG('MODL'): widgetInfo = std::make_unique(); break; case SBIG('METR'): widgetInfo = std::make_unique(); break; case SBIG('GRUP'): widgetInfo = std::make_unique(); break; case SBIG('PANE'): widgetInfo = std::make_unique(); break; case SBIG('TXPN'): widgetInfo = std::make_unique(owner->version); break; case SBIG('IMGP'): widgetInfo = std::make_unique(); break; case SBIG('TBGP'): widgetInfo = std::make_unique(); break; case SBIG('SLGP'): widgetInfo = std::make_unique(); break; default: Log.report(logvisor::Fatal, FMT_STRING("Unsupported FRME widget type {}"), type); } /* widgetInfo */ widgetInfo->read(__dna_reader); /* isWorker */ isWorker = __dna_reader.readBool(); if (isWorker) { /* workerId */ workerId = __dna_reader.readUint16Big(); } /* origin */ origin = __dna_reader.readVec3fBig(); /* basis[0] */ basis[0] = __dna_reader.readVec3fBig(); /* basis[1] */ basis[1] = __dna_reader.readVec3fBig(); /* basis[2] */ basis[2] = __dna_reader.readVec3fBig(); /* rotationCenter */ rotationCenter = __dna_reader.readVec3fBig(); /* unk1 */ unk1 = __dna_reader.readInt32Big(); /* unk2 */ unk2 = __dna_reader.readInt16Big(); } template <> void FRME::Widget::Enumerate(athena::io::IStreamWriter& __dna_writer) { /* type */ DNAFourCC _type = widgetInfo ? widgetInfo->fourcc() : FOURCC('BWIG'); _type.write(__dna_writer); /* header */ header.write(__dna_writer); /* widgetInfo */ if (widgetInfo) widgetInfo->write(__dna_writer); /* isWorker */ __dna_writer.writeBool(isWorker); if (isWorker) { /* workerId */ __dna_writer.writeUint16Big(workerId); } /* origin */ __dna_writer.writeVec3fBig(origin); /* basis[0] */ __dna_writer.writeVec3fBig(basis[0]); /* basis[1] */ __dna_writer.writeVec3fBig(basis[1]); /* basis[2] */ __dna_writer.writeVec3fBig(basis[2]); /* rotationCenter */ __dna_writer.writeVec3fBig(rotationCenter); /* unk1 */ __dna_writer.writeInt32Big(unk1); /* unk2 */ __dna_writer.writeInt16Big(unk2); } template <> void FRME::Widget::Enumerate(size_t& __isz) { type.binarySize(__isz); header.binarySize(__isz); if (widgetInfo) widgetInfo->binarySize(__isz); if (isWorker) __isz += 4; __isz += 67; } template <> void FRME::Widget::CAMRInfo::Enumerate(athena::io::IStreamReader& __dna_reader) { projectionType = ProjectionType(__dna_reader.readUint32Big()); if (projectionType == ProjectionType::Perspective) { projection = std::make_unique(); } else if (projectionType == ProjectionType::Orthographic) { projection = std::make_unique(); } else { Log.report(logvisor::Fatal, FMT_STRING("Invalid CAMR projection mode! {}"), int(projectionType)); } projection->read(__dna_reader); } template <> void FRME::Widget::CAMRInfo::Enumerate(athena::io::IStreamWriter& __dna_writer) { if (!projection) Log.report(logvisor::Fatal, FMT_STRING("Invalid CAMR projection object!")); if (projection->type != projectionType) Log.report(logvisor::Fatal, FMT_STRING("CAMR projection type does not match actual projection type!")); __dna_writer.writeUint32Big(atUint32(projectionType)); projection->write(__dna_writer); } template <> void FRME::Widget::CAMRInfo::Enumerate(size_t& __isz) { projection->binarySize(__isz); __isz += 4; } template <> void FRME::Widget::LITEInfo::Enumerate(athena::io::IStreamReader& __dna_reader) { /* type */ type = ELightType(__dna_reader.readUint32Big()); /* distC */ distC = __dna_reader.readFloatBig(); /* distL */ distL = __dna_reader.readFloatBig(); /* distQ */ distQ = __dna_reader.readFloatBig(); /* angC */ angC = __dna_reader.readFloatBig(); /* angL */ angL = __dna_reader.readFloatBig(); /* angQ */ angQ = __dna_reader.readFloatBig(); /* loadedIdx */ loadedIdx = __dna_reader.readUint32Big(); /* cutoff */ if (type == ELightType::Spot) cutoff = __dna_reader.readFloatBig(); } template <> void FRME::Widget::LITEInfo::Enumerate(athena::io::IStreamWriter& __dna_writer) { /* type */ __dna_writer.writeUint32Big(atUint32(type)); /* distC */ __dna_writer.writeFloatBig(distC); /* distL */ __dna_writer.writeFloatBig(distL); /* distQ */ __dna_writer.writeFloatBig(distQ); /* angC */ __dna_writer.writeFloatBig(angC); /* angL */ __dna_writer.writeFloatBig(angL); /* angQ */ __dna_writer.writeFloatBig(angQ); /* loadedIdx */ __dna_writer.writeUint32Big(loadedIdx); /* cutoff */ if (type == ELightType::Spot) __dna_writer.writeFloatBig(cutoff); } template <> void FRME::Widget::LITEInfo::Enumerate(size_t& __isz) { __isz += ((type == ELightType::Spot) ? 36 : 32); } template void FRME::Widget::TXPNInfo::Enumerate(typename Op::StreamT& s) { Do(athena::io::PropId{"xDim"}, xDim, s); Do(athena::io::PropId{"zDim"}, zDim, s); Do(athena::io::PropId{"scaleCenter"}, scaleCenter, s); Do(athena::io::PropId{"font"}, font, s); Do(athena::io::PropId{"wordWrap"}, wordWrap, s); Do(athena::io::PropId{"horizontal"}, horizontal, s); Do(athena::io::PropId{"justification"}, justification, s); Do(athena::io::PropId{"verticalJustification"}, verticalJustification, s); Do(athena::io::PropId{"fillColor"}, fillColor, s); Do(athena::io::PropId{"outlineColor"}, outlineColor, s); Do(athena::io::PropId{"blockExtent"}, blockExtent, s); if (version == 1) { Do(athena::io::PropId{"jpnFont"}, jpnFont, s); Do(athena::io::PropId{"jpnPointScale[0]"}, jpnPointScale[0], s); Do(athena::io::PropId{"jpnPointScale[1]"}, jpnPointScale[1], s); } } AT_SPECIALIZE_DNA(FRME::Widget::TXPNInfo) bool FRME::Extract(const SpecBase& dataSpec, PAKEntryReadStream& rs, const hecl::ProjectPath& outPath, PAKRouter& pakRouter, const PAK::Entry& entry, bool force, hecl::blender::Token& btok, std::function fileChanged) { if (!force && outPath.isFile()) return true; FRME frme; frme.read(rs); hecl::blender::Connection& conn = btok.getBlenderConnection(); if (!conn.createBlend(outPath, hecl::blender::BlendType::Frame)) return false; hecl::blender::PyOutStream os = conn.beginPythonOut(true); os << "import bpy, math, bmesh\n" "from mathutils import Matrix, Quaternion\n" "# Clear Scene\n" "if len(bpy.data.collections):\n" " bpy.data.collections.remove(bpy.data.collections[0])\n" "\n" "def duplicateObject(copy_obj):\n" " # Create new mesh\n" " mesh = bpy.data.meshes.new(copy_obj.name)\n" " # Create new object associated with the mesh\n" " ob_new = bpy.data.objects.new(copy_obj.name, mesh)\n" " # Copy data block from the old object into the new object\n" " ob_new.data = copy_obj.data\n" " ob_new.scale = copy_obj.scale\n" " ob_new.location = copy_obj.location\n" " # Link new object to the given scene and select it\n" " bpy.context.scene.collection.objects.link(ob_new)\n" " return ob_new\n"; os.format(FMT_STRING("bpy.context.scene.name = '{}'\n" "bpy.context.scene.render.resolution_x = 640\n" "bpy.context.scene.render.resolution_y = 480\n" "bpy.context.scene.world.use_nodes = True\n" "bg_node = bpy.context.scene.world.node_tree.nodes['Background']\n" "bg_node.inputs[1].default_value = 0.0\n"), pakRouter.getBestEntryName(entry)); int pIdx = 0; for (const FRME::Widget& w : frme.widgets) { os << "binding = None\n" "angle = Quaternion((1.0, 0.0, 0.0), 0)\n"; if (w.type == SBIG('CAMR')) { using CAMRInfo = Widget::CAMRInfo; os.format(FMT_STRING("cam = bpy.data.cameras.new(name='{}')\n" "binding = cam\n"), w.header.name); if (CAMRInfo* info = static_cast(w.widgetInfo.get())) { if (info->projectionType == CAMRInfo::ProjectionType::Orthographic) { CAMRInfo::OrthographicProjection* proj = static_cast(info->projection.get()); os.format(FMT_STRING("cam.type = 'ORTHO'\n" "cam.ortho_scale = {}\n" "cam.clip_start = {}\n" "cam.clip_end = {}\n"), std::fabs(proj->right - proj->left), proj->znear, proj->zfar); } else if (info->projectionType == CAMRInfo::ProjectionType::Perspective) { CAMRInfo::PerspectiveProjection* proj = static_cast(info->projection.get()); os.format(FMT_STRING("cam.type = 'PERSP'\n" "cam.lens_unit = 'FOV'\n" "cam.clip_start = {}\n" "cam.clip_end = {}\n" "bpy.context.scene.render.resolution_x = int(480 * {})\n"), proj->znear, proj->zfar, proj->aspect); if (proj->aspect > 1.f) os.format(FMT_STRING("cam.angle = math.atan2({}, 1.0 / math.tan(math.radians({} / 2.0))) * 2.0\n"), proj->aspect, proj->fov); else os.format(FMT_STRING("cam.angle = math.radians({})\n"), proj->fov); } } os << "angle = Quaternion((1.0, 0.0, 0.0), math.radians(90.0))\n"; } else if (w.type == SBIG('LITE')) { using LITEInfo = Widget::LITEInfo; if (LITEInfo* info = static_cast(w.widgetInfo.get())) { switch (info->type) { case LITEInfo::ELightType::LocalAmbient: { zeus::simd_floats colorF(w.header.color.simd); os.format(FMT_STRING("bg_node.inputs[0].default_value = ({},{},{},1.0)\n" "bg_node.inputs[1].default_value = {}\n"), colorF[0], colorF[1], colorF[2], info->distQ / 8.0); break; } case LITEInfo::ELightType::Spot: case LITEInfo::ELightType::Directional: os << "angle = Quaternion((1.0, 0.0, 0.0), math.radians(90.0))\n"; [[fallthrough]]; default: { zeus::simd_floats colorF(w.header.color.simd); os.format(FMT_STRING("lamp = bpy.data.lights.new(name='{}', type='POINT')\n" "lamp.color = ({}, {}, {})\n" "lamp.hecl_falloff_constant = {}\n" "lamp.hecl_falloff_linear = {}\n" "lamp.hecl_falloff_quadratic = {}\n" "lamp.retro_light_angle_constant = {}\n" "lamp.retro_light_angle_linear = {}\n" "lamp.retro_light_angle_quadratic = {}\n" "lamp.retro_light_index = {}\n" "binding = lamp\n"), w.header.name, colorF[0], colorF[1], colorF[2], info->distC, info->distL, info->distQ, info->angC, info->angL, info->angQ, info->loadedIdx); if (info->type == LITEInfo::ELightType::Spot) os.format(FMT_STRING("lamp.type = 'SPOT'\n" "lamp.spot_size = {}\n"), info->cutoff); else if (info->type == LITEInfo::ELightType::Directional) os << "lamp.type = 'SUN'\n"; } } } } else if (w.type == SBIG('IMGP')) { using IMGPInfo = Widget::IMGPInfo; if (IMGPInfo* info = static_cast(w.widgetInfo.get())) { std::string texName; std::string resPath; if (info->texture.isValid()) { texName = pakRouter.getBestEntryName(info->texture); const nod::Node* node; const PAKRouter::EntryType* texEntry = pakRouter.lookupEntry(info->texture, &node); hecl::ProjectPath txtrPath = pakRouter.getWorking(texEntry); if (txtrPath.isNone()) { txtrPath.makeDirChain(false); PAKEntryReadStream rs = texEntry->beginReadStream(*node); TXTR::Extract(rs, txtrPath); } resPath = pakRouter.getResourceRelativePath(entry, info->texture); } if (resPath.size()) { os.format(FMT_STRING("if '{}' in bpy.data.images:\n" " image = bpy.data.images['{}']\n" "else:\n" " image = bpy.data.images.load('''//{}''')\n" " image.name = '{}'\n"), texName, texName, resPath, texName); } else { os << "image = None\n"; } os.format(FMT_STRING("material = bpy.data.materials.new('{}')\n" "material.use_nodes = True\n" "new_nodetree = material.node_tree\n" "for n in new_nodetree.nodes:\n" " new_nodetree.nodes.remove(n)\n" "tex_node = new_nodetree.nodes.new('ShaderNodeTexImage')\n" "tex_node.image = image\n" "bm = bmesh.new()\n" "verts = []\n"), w.header.name); for (uint32_t i = 0; i < info->quadCoordCount; ++i) { int ti; if (i == 2) ti = 3; else if (i == 3) ti = 2; else ti = i; zeus::simd_floats f(info->quadCoords[ti].simd); os.format(FMT_STRING("verts.append(bm.verts.new(({},{},{})))\n"), f[0], f[1], f[2]); } os << "bm.faces.new(verts)\n" "bm.loops.layers.uv.new('UV')\n" "bm.verts.ensure_lookup_table()\n"; for (uint32_t i = 0; i < info->uvCoordCount; ++i) { int ti; if (i == 2) ti = 3; else if (i == 3) ti = 2; else ti = i; zeus::simd_floats f(info->uvCoords[ti].simd); os.format(FMT_STRING("bm.verts[{}].link_loops[0][bm.loops.layers.uv[0]].uv = ({},{})\n"), i, f[0], f[1]); } os.format(FMT_STRING("binding = bpy.data.meshes.new('{}')\n" "bm.to_mesh(binding)\n" "bm.free()\n" "binding.materials.append(material)\n"), w.header.name); } } zeus::simd_floats colorF(w.header.color.simd); os.format(FMT_STRING( "frme_obj = bpy.data.objects.new(name='{}', object_data=binding)\n" "frme_obj.pass_index = {}\n" "parentName = '{}'\n" "frme_obj.retro_widget_type = 'RETRO_{}'\n" "frme_obj.retro_widget_use_anim_controller = {}\n" "frme_obj.retro_widget_default_visible = {}\n" "frme_obj.retro_widget_default_active = {}\n" "frme_obj.retro_widget_cull_faces = {}\n" "frme_obj.retro_widget_color = ({},{},{},{})\n" "widget_model_draw_flags = {}\n" "if bpy.app.version >= (2, 93, 0):\n" " frme_obj.retro_widget_model_draw_flags = bpy.types.Object.retro_widget_model_draw_flags.keywords['items'][widget_model_draw_flags][0]\n" "else:\n" " frme_obj.retro_widget_model_draw_flags = bpy.types.Object.retro_widget_model_draw_flags[1]['items'][widget_model_draw_flags][0]\n" "frme_obj.retro_widget_is_worker = {}\n" "frme_obj.retro_widget_worker_id = {}\n" "if parentName not in bpy.data.objects:\n" " frme_obj.retro_widget_parent = parentName\n" "else:\n" " frme_obj.parent = bpy.data.objects[parentName]\n"), w.header.name, pIdx++, w.header.parent, w.type, w.header.useAnimController ? "True" : "False", w.header.defaultVisible ? "True" : "False", w.header.defaultActive ? "True" : "False", w.header.cullFaces ? "True" : "False", colorF[0], colorF[1], colorF[2], colorF[3], w.header.modelDrawFlags, w.isWorker ? "True" : "False", w.workerId); if (w.type == SBIG('MODL')) { using MODLInfo = FRME::Widget::MODLInfo; MODLInfo* info = static_cast(w.widgetInfo.get()); hecl::ProjectPath modelPath = pakRouter.getWorking(info->model); const PAKRouter::EntryType* cmdlE = pakRouter.lookupEntry(info->model, nullptr, true, true); os.linkMesh(modelPath.getAbsolutePath(), pakRouter.getBestEntryName(*cmdlE)); os.format(FMT_STRING("frme_obj.retro_model_light_mask = {}\n"), info->lightMask); os << "print(obj.name)\n" "copy_obj = duplicateObject(obj)\n" "copy_obj.parent = frme_obj\n" "copy_obj.hide_set(False)\n"; } else if (w.type == SBIG('CAMR')) { os << "bpy.context.scene.camera = frme_obj\n" "if 'Camera' in bpy.data.objects:\n" " cam = bpy.data.objects['Camera']\n" " #bpy.context.scene.objects.unlink(cam)\n" " bpy.data.objects.remove(cam)\n"; } else if (w.type == SBIG('PANE')) { using PANEInfo = Widget::PANEInfo; if (PANEInfo* info = static_cast(w.widgetInfo.get())) { zeus::simd_floats f(info->scaleCenter.simd); os.format(FMT_STRING("frme_obj.retro_pane_dimensions = ({},{})\n" "frme_obj.retro_pane_scale_center = ({},{},{})\n"), info->xDim, info->zDim, f[0], f[1], f[2]); } } else if (w.type == SBIG('TXPN')) { using TXPNInfo = Widget::TXPNInfo; if (TXPNInfo* info = static_cast(w.widgetInfo.get())) { hecl::ProjectPath fontPath = pakRouter.getWorking(info->font, true); hecl::ProjectPath jpFontPath; if (frme.version >= 1) jpFontPath = pakRouter.getWorking(info->jpnFont, true); zeus::simd_floats scaleF(info->scaleCenter.simd); zeus::simd_floats fillF(info->fillColor.simd); zeus::simd_floats outlineF(info->outlineColor.simd); zeus::simd_floats extentF(info->blockExtent.simd); os.format(FMT_STRING( "frme_obj.retro_pane_dimensions = ({},{})\n" "frme_obj.retro_pane_scale_center = ({},{},{})\n" "frme_obj.retro_textpane_font_path = '{}'\n" "frme_obj.retro_textpane_word_wrap = {}\n" "frme_obj.retro_textpane_horizontal = {}\n" "frme_obj.retro_textpane_fill_color = ({},{},{},{})\n" "frme_obj.retro_textpane_outline_color = ({},{},{},{})\n" "frme_obj.retro_textpane_block_extent = ({},{})\n" "frme_obj.retro_textpane_jp_font_path = '{}'\n" "frme_obj.retro_textpane_jp_font_scale = ({},{})\n" "textpane_hjustification = {}\n" "textpane_vjustification = {}\n" "if bpy.app.version >= (2, 93, 0):\n" " frme_obj.retro_textpane_hjustification = bpy.types.Object.retro_textpane_hjustification.keywords['items'][textpane_hjustification][0]\n" " frme_obj.retro_textpane_vjustification = bpy.types.Object.retro_textpane_vjustification.keywords['items'][textpane_vjustification][0]\n" "else:\n" " frme_obj.retro_textpane_hjustification = bpy.types.Object.retro_textpane_hjustification[1]['items'][textpane_hjustification][0]\n" " frme_obj.retro_textpane_vjustification = bpy.types.Object.retro_textpane_vjustification[1]['items'][textpane_vjustification][0]\n"), info->xDim, info->zDim, scaleF[0], scaleF[1], scaleF[2], fontPath.getRelativePath(), info->wordWrap ? "True" : "False", info->horizontal ? "True" : "False", fillF[0], fillF[1], fillF[2], fillF[3], outlineF[0], outlineF[1], outlineF[2], outlineF[3], extentF[0], extentF[1], jpFontPath.getRelativePath(), info->jpnPointScale[0], info->jpnPointScale[1], int(info->justification), int(info->verticalJustification)); } } else if (w.type == SBIG('TBGP')) { using TBGPInfo = Widget::TBGPInfo; if (TBGPInfo* info = static_cast(w.widgetInfo.get())) { os.format(FMT_STRING("frme_obj.retro_tablegroup_elem_count = {}\n" "frme_obj.retro_tablegroup_elem_default = {}\n" "frme_obj.retro_tablegroup_wraparound = {}\n"), info->elementCount, info->defaultSelection, info->selectWraparound ? "True" : "False"); } } else if (w.type == SBIG('GRUP')) { using GRUPInfo = Widget::GRUPInfo; if (GRUPInfo* info = static_cast(w.widgetInfo.get())) { os.format(FMT_STRING("frme_obj.retro_group_default_worker = {}\n"), info->defaultWorker); } } else if (w.type == SBIG('SLGP')) { using SLGPInfo = Widget::SLGPInfo; if (SLGPInfo* info = static_cast(w.widgetInfo.get())) { os.format(FMT_STRING("frme_obj.retro_slider_min = {}\n" "frme_obj.retro_slider_max = {}\n" "frme_obj.retro_slider_default = {}\n" "frme_obj.retro_slider_increment = {}\n"), info->min, info->max, info->cur, info->increment); } } else if (w.type == SBIG('ENRG')) { using ENRGInfo = Widget::ENRGInfo; if (ENRGInfo* info = static_cast(w.widgetInfo.get())) { hecl::ProjectPath txtrPath = pakRouter.getWorking(info->texture); if (txtrPath) os.format(FMT_STRING("frme_obj.retro_energybar_texture_path = '{}'\n"), txtrPath.getRelativePath()); } } else if (w.type == SBIG('METR')) { using METRInfo = Widget::METRInfo; if (METRInfo* info = static_cast(w.widgetInfo.get())) { os.format(FMT_STRING("frme_obj.retro_meter_no_round_up = {}\n" "frme_obj.retro_meter_max_capacity = {}\n" "frme_obj.retro_meter_worker_count = {}\n"), info->noRoundUp ? "True" : "False", info->maxCapacity, info->workerCount); } } zeus::simd_floats xfMtxF[3]; for (int i = 0; i < 3; ++i) w.basis[i].simd.copy_to(xfMtxF[i]); zeus::simd_floats originF(w.origin.simd); os.format(FMT_STRING("mtx = Matrix((({},{},{},{}),({},{},{},{}),({},{},{},{}),(0.0,0.0,0.0,1.0)))\n" "mtxd = mtx.decompose()\n" "frme_obj.rotation_mode = 'QUATERNION'\n" "frme_obj.location = mtxd[0]\n" "frme_obj.rotation_quaternion = mtxd[1] @ angle\n" "frme_obj.scale = mtxd[2]\n" "bpy.context.scene.collection.objects.link(frme_obj)\n"), xfMtxF[0][0], xfMtxF[0][1], xfMtxF[0][2], originF[0], xfMtxF[1][0], xfMtxF[1][1], xfMtxF[1][2], originF[1], xfMtxF[2][0], xfMtxF[2][1], xfMtxF[2][2], originF[2]); } os.centerView(); os.close(); conn.saveBlend(); return true; } } // namespace DataSpec::DNAMP1