FieldDataInstaller.cpp 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933
  1. /*
  2. * Copyright (C) 2022 The V-Gears Team
  3. *
  4. * This file is part of V-Gears
  5. *
  6. * V-Gears is free software: you can redistribute it and/or modify it under
  7. * terms of the GNU General Public License as published by the Free Software
  8. * Foundation, version 3.0 (GPLv3) of the License.
  9. *
  10. * V-Gears is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. */
  15. #include <string>
  16. #include <algorithm>
  17. #include <boost/filesystem.hpp>
  18. #include <QtCore/QDir>
  19. #include "DiskImage.h"
  20. #include "FieldDataInstaller.h"
  21. #include "ModelMaterials.h"
  22. #include "TexFile.h"
  23. #include "common/File.h"
  24. #include "data/VGearsMapListFile.h"
  25. #include "data/VGearsLZSFLevelFileManager.h"
  26. #include "data/VGearsHRCFileManager.h"
  27. #include "data/VGearsLGPArchive.h"
  28. #include "data/VGearsAFileManager.h"
  29. float FieldDataInstaller::LINE_SCALE_FACTOR(0.0078124970964f);
  30. std::string FieldDataInstaller::FIELD_MODELS_DIR("models/fields");
  31. std::string FieldDataInstaller::FIELD_MAPS_DIR("fields");
  32. int FieldDataInstaller::INACTIVE_GATEWAY_ID(32767);
  33. const std::vector<int> FieldDataInstaller::TEMPORARY_BANKS{5};
  34. std::string FieldDataInstaller::FieldScriptHeader(){
  35. std::string header = "EntityContainer = {}\n\n";
  36. for (const int bank : TEMPORARY_BANKS){
  37. header += "-- Temporary variables start at zero in every field.\n"
  38. "for address = 0, Banks.SIZE do\n"
  39. " Banks[" + std::to_string(bank) + "][address] = 0\n"
  40. "end\n\n";
  41. }
  42. return header;
  43. }
  44. std::string FieldDataInstaller::CreateGateWayScript(
  45. const std::string& gateway_entity_name, const std::string& target_map_name,
  46. const std::string& source_spawn_point_name
  47. ){
  48. return
  49. "\nEntityContainer[ \"" + gateway_entity_name + "\" ] = {\n\n"
  50. " on_start = function(self)\n"
  51. " return 0\n"
  52. " end,\n\n"
  53. " on_approach = function(self, entity)\n"
  54. " return 0\n"
  55. " end,\n\n"
  56. " on_cross = function(self, entity)\n"
  57. " return 0\n"
  58. " end,\n\n"
  59. " on_near = function(self, entity)\n"
  60. " if entity == \"Cloud\" then\n"
  61. " if not Data.DisableGateways then\n"
  62. " load_field_map_request(\"" + target_map_name + "\", \"" + source_spawn_point_name + "\")\n"
  63. " end\n"
  64. " end\n"
  65. " return 0\n"
  66. " end,\n\n"
  67. " on_leave = function(self, entity)\n"
  68. " return 0\n"
  69. " end,\n"
  70. "}\n\n";
  71. }
  72. size_t FieldDataInstaller::GetFieldId(
  73. const std::string& name, const std::vector<std::string>& field_id_to_name_lookup
  74. ){
  75. for (size_t i = 0; i < field_id_to_name_lookup.size(); i ++){
  76. if (field_id_to_name_lookup[i] == name) return i;
  77. }
  78. throw std::runtime_error("No Id found for field name");
  79. }
  80. bool FieldDataInstaller::IsAFieldFile(const Ogre::String& resource_name){
  81. return (
  82. !VGears::StringUtil::endsWith(resource_name, ".tex")
  83. && !VGears::StringUtil::endsWith(resource_name, ".tut")
  84. && !VGears::StringUtil::endsWith(resource_name, ".siz")
  85. && resource_name != "maplist"
  86. );
  87. }
  88. bool FieldDataInstaller::WillCrash(const Ogre::String& resource_name){
  89. if (
  90. resource_name == "bugin1a" // crashes in back ground image generation
  91. || resource_name == "frcyo" // Hangs in decompliation
  92. || resource_name == "fr_e" // Background image crash
  93. || resource_name == "las4_2" // Background image crash
  94. || resource_name == "las4_3" // Background image crash
  95. || resource_name == "lastmap" // Background image crash
  96. || resource_name == "md_e1" // Background image crash
  97. || resource_name == "trnad_3" // Background image crash
  98. || resource_name == "bonevil2" // stof
  99. || resource_name == "coloin1" // boost::bad_format_string: format-string is ill-formed
  100. || resource_name == "del3" // Segmentation fault.
  101. || resource_name == "elmin4_2" // boost::bad_format_string: format-string is ill-formed
  102. ) return true;
  103. return false;
  104. }
  105. bool FieldDataInstaller::IsTestField(const Ogre::String& resource_name){
  106. if (
  107. resource_name == "startmap"
  108. || resource_name == "md1stin"
  109. || resource_name == "md1_1"
  110. || resource_name == "md1_2"
  111. || resource_name == "nrthmk"
  112. || resource_name == "nmkin_1"
  113. || resource_name == "elevtr1"
  114. || resource_name == "nmkin_2"
  115. || resource_name == "nmkin_3"
  116. || resource_name == "nmkin_4"
  117. || resource_name == "nmkin_5"
  118. || resource_name == "southmk1"
  119. || resource_name == "southmk2"
  120. || resource_name == "smkin_1"
  121. || resource_name == "smkin_2"
  122. || resource_name == "smkin_3"
  123. || resource_name == "smkin_4"
  124. || resource_name == "smkin_5"
  125. || resource_name == "md8_1"
  126. || resource_name == "md8_2"
  127. || resource_name == "md8_3"
  128. || resource_name == "md8_4"
  129. || resource_name == "md8brdg"
  130. || resource_name == "cargoin"
  131. || resource_name == "tin_1"
  132. || resource_name == "tin_2"
  133. || resource_name == "tin_3"
  134. || resource_name == "tin_4"
  135. || resource_name == "rootmap"
  136. || resource_name == "mds7st1"
  137. || resource_name == "mds7st2"
  138. || resource_name == "mds7st3"
  139. || resource_name == "mds7st32"
  140. || resource_name == "mds7_w1"
  141. || resource_name == "mds7_w2"
  142. || resource_name == "mds7_w3"
  143. || resource_name == "mds7"
  144. )return true;
  145. return false;
  146. }
  147. void FieldDataInstaller::CreateDir(const std::string& path){
  148. QString target = QString::fromStdString(data_dir_ + path);
  149. QDir dir(target);
  150. if (!dir.mkpath(".")) throw std::runtime_error("Failed to mkpath");
  151. }
  152. void FieldDataInstaller::CollectFieldScaleFactors(
  153. VGears::FLevelFilePtr& field, FieldScaleFactorMap& scale_factors,
  154. const std::vector<std::string>& field_id_to_name_lookup
  155. ){
  156. scale_factors[GetFieldId(field->getName(), field_id_to_name_lookup)]
  157. = FieldDecompiler::ScaleFactor(field->GetRawScript());
  158. }
  159. bool FieldDataInstaller::CollectSpawnPoints(
  160. VGears::FLevelFilePtr& field, const std::vector<std::string>& field_id_to_name_lookup,
  161. FieldSpawnPointsMap& spawn_points
  162. ){
  163. const size_t this_field_id = GetFieldId(field->getName(), field_id_to_name_lookup);
  164. // Decompile the script just so the MAPJUMPs can be collected. This is needed to construct the
  165. // full list of entries to each field.
  166. try{
  167. // Get the raw script bytes.
  168. FieldDecompiler::DecompiledScript decompiled;
  169. const std::vector<u8> raw_field_data = field->GetRawScript();
  170. // Decompile to LUA.
  171. FF7FieldScriptGatewayCollector formatter(
  172. field->getName(), field_id_to_name_lookup, spawn_points, this_field_id
  173. );
  174. decompiled = FieldDecompiler::Decompile(
  175. field->getName(), raw_field_data, formatter, "", "EntityContainer = {}\n\n"
  176. );
  177. }
  178. catch (const ::DecompilerException& ex){
  179. std::cerr << "[WARNING] CollectSpawnPoints: decompiler failed for " << field->getName()
  180. << ": " << ex.what() << std::endl;
  181. return false;
  182. }
  183. const VGears::TriggersFilePtr& triggers = field->GetTriggers();
  184. if (triggers.isNull()){
  185. std::cerr << "CollectSpawnPoints: missing trigger data for " << field->getName() << std::endl;
  186. return false;
  187. }
  188. const auto& gateways = triggers->GetGateways();
  189. for (size_t i = 0; i < gateways.size(); i ++){
  190. const VGears::TriggersFile::Gateway& gateway = gateways[i];
  191. if (gateway.destination_field_id != INACTIVE_GATEWAY_ID){
  192. auto it = spawn_points.find(gateway.destination_field_id);
  193. if (it != std::end(spawn_points)){
  194. // Add to the list of gateways that link to
  195. // destination_field_id.
  196. SpawnPointDb::Record rec;
  197. rec.field_id = this_field_id;
  198. rec.gateway = gateway;
  199. rec.gateway_index_or_map_jump_address = i;
  200. it->second.gateways_to_this_field.push_back(rec);
  201. }
  202. else{
  203. // Create a new record for destination_field_id.
  204. SpawnPointDb db;
  205. db.target_field_id = gateway.destination_field_id;
  206. SpawnPointDb::Record rec;
  207. rec.field_id = this_field_id;
  208. rec.gateway = gateway;
  209. rec.gateway_index_or_map_jump_address = i;
  210. db.gateways_to_this_field.push_back(rec);
  211. spawn_points.insert(std::make_pair(db.target_field_id, db));
  212. }
  213. }
  214. }
  215. return true;
  216. }
  217. FieldDataInstaller::FieldDataInstaller(
  218. const DiskImage& disk_image, const std::string output_dir,
  219. std::function<void(std::string)> log_output_line
  220. ):
  221. disk_image_(disk_image), output_dir_(output_dir), data_dir_(output_dir + "data/"),
  222. write_output_line_(log_output_line)
  223. {}
  224. FieldDataInstaller::~FieldDataInstaller(){}
  225. int FieldDataInstaller::CollectSpawnAndScaleFactorsInit(Ogre::ResourceGroupManager* res_mgr){
  226. // List what's in the LGP archive.
  227. //flevel_file_list_ = application_.ResMgr()->listResourceNames("FFVIIFields", "*");
  228. flevel_file_list_ = res_mgr->listResourceNames("FFVIIFields", "*");
  229. // Load the map list field.
  230. VGears::MapListFilePtr map_list = VGears::MapListFileManager::GetSingleton().load(
  231. "maplist", "FFVIIFields"
  232. ).staticCast<VGears::MapListFile>();
  233. map_list_ = map_list->GetMapList();
  234. return flevel_file_list_->size();
  235. }
  236. void FieldDataInstaller::CollectSpawnAndScaleFactors(int field_index){
  237. // On the first pass collate required field information.
  238. progress_step_num_elements_ = flevel_file_list_->size();
  239. if (field_index >= flevel_file_list_->size()){
  240. // TODO: Show invalid index.
  241. return;
  242. }
  243. auto resource_name = (*flevel_file_list_)[field_index];
  244. // Exclude things that are not fields.
  245. if (IsAFieldFile(resource_name) && !WillCrash(resource_name)){
  246. try{
  247. VGears::FLevelFilePtr field = VGears::LZSFLevelFileManager::GetSingleton()
  248. .load(resource_name, "FFVIIFields").staticCast<VGears::FLevelFile>();
  249. if (!CollectSpawnPoints(field, map_list_, spawn_points_)){
  250. failed_fields_.push_back(resource_name);
  251. return;
  252. }
  253. CollectFieldScaleFactors(field, scale_factors_, map_list_);
  254. }
  255. catch (const std::exception& exception){
  256. failed_fields_.push_back(resource_name);
  257. std::cerr << "[WARNING] CollectSpawnAndScaleFactors: skipped " << resource_name
  258. << " after error: " << exception.what() << std::endl;
  259. }
  260. }
  261. }
  262. void FieldDataInstaller::LoadInstalledFields(){
  263. converted_map_list_.clear();
  264. for (const std::string& name : map_list_){
  265. if (boost::filesystem::exists(data_dir_ + FIELD_MAPS_DIR + "/" + name + "/map.xml")){
  266. converted_map_list_.push_back(name);
  267. }
  268. }
  269. }
  270. void FieldDataInstaller::Convert(int field_index){
  271. // Do the full conversion with the collated data.
  272. if (field_index >= flevel_file_list_->size()){
  273. // TODO: Show invalid index.
  274. return;
  275. }
  276. auto resource_name = (*flevel_file_list_)[field_index];
  277. // Exclude things that are not fields.
  278. if (IsAFieldFile(resource_name)){
  279. if (std::find(failed_fields_.begin(), failed_fields_.end(), resource_name)
  280. != failed_fields_.end()){
  281. std::cerr << "[ERROR] Skip field: " << resource_name
  282. << " because its scripts or scale factor could not be collected." << std::endl;
  283. }
  284. else if (!WillCrash(resource_name)){
  285. //write_output_line_("Converting field " + resource_name);
  286. CreateDir(FIELD_MAPS_DIR + "/" + resource_name);
  287. VGears::FLevelFilePtr field = VGears::LZSFLevelFileManager::GetSingleton()
  288. .load(resource_name, "FFVIIFields").staticCast<VGears::FLevelFile>();
  289. PcFieldToVGearsField(field);
  290. }
  291. else{
  292. std::cerr << "[ERROR] Skip field: " << resource_name
  293. << " due to a known crash or hang issue." << std::endl;
  294. }
  295. }
  296. }
  297. int FieldDataInstaller::WriteInit(){
  298. doc_ = std::make_unique<TiXmlDocument>();
  299. element_ = std::make_unique<TiXmlElement>("maps");
  300. return converted_map_list_.size();
  301. }
  302. void FieldDataInstaller::Write(int field_index){
  303. // TODO: Probably need to inject "empty" and "test" fields.
  304. if (field_index >= flevel_file_list_->size()){
  305. // TODO: Show invalid index.
  306. return;
  307. }
  308. auto map = converted_map_list_[field_index];
  309. //write_output_line_("Writing field " + FieldName(map));
  310. std::unique_ptr<TiXmlElement> xml_element(new TiXmlElement("map"));
  311. xml_element->SetAttribute("name", map);
  312. xml_element->SetAttribute("file_name", map + "/map.xml");
  313. element_->LinkEndChild(xml_element.release());
  314. }
  315. void FieldDataInstaller::WriteEnd(){
  316. doc_->LinkEndChild(element_.release());
  317. doc_->SaveFile(data_dir_ + "/fields/_fields.xml");
  318. }
  319. std::vector<std::string> FieldDataInstaller::ConvertModelsInit(){
  320. std::vector<std::string> models;
  321. // Open char_lgp as a lgp archive
  322. VGears::LGPArchive char_lgp(*disk_image_.filePath("data/field/char.lgp"), "LGP");
  323. char_lgp.open(*disk_image_.filePath("data/field/char.lgp"), true);
  324. char_lgp.load();
  325. // Also, open it as a file for reading
  326. File char_file(*disk_image_.filePath("data/field/char.lgp"));
  327. //Ogre::StringVectorPtr file_list = char_lgp.list(true, true);
  328. field_model_file_list_ = char_lgp.list(true, true);
  329. VGears::LGPArchive::FileList files = char_lgp.GetFiles();
  330. for (int i = 0; i < field_model_file_list_->size(); i ++){
  331. VGears::LGPArchive::FileEntry f = files.at(i);
  332. // Save the TEX File
  333. std::fstream out;
  334. out.open(output_dir_ + "temp/char/" + f.file_name, std::ios::out);
  335. char_file.SetOffset(f.data_offset);
  336. for (int j = 0; j < f.data_size; j ++) out << char_file.readU8();
  337. out.close();
  338. //field_model_file_list_->push_back(f.file_name);
  339. }
  340. // Ogre indexes a resource location when it is added, so files written afterwards are not found until the
  341. // location is registered again. On a clean install the folder was empty when it was first added.
  342. Ogre::ResourceGroupManager& res_mgr = Ogre::ResourceGroupManager::getSingleton();
  343. res_mgr.removeResourceLocation(output_dir_ + "temp/char/", "FFVII");
  344. res_mgr.addResourceLocation(output_dir_ + "temp/char/", "FileSystem", "FFVII", true, true);
  345. for (auto it = used_models_and_anims_.map.begin(); it != used_models_and_anims_.map.end(); it ++){
  346. models.push_back(it->first);
  347. }
  348. return models;
  349. }
  350. void FieldDataInstaller::ConvertModels(std::string model_name){
  351. auto model = used_models_and_anims_.map.find(model_name);
  352. try{
  353. Ogre::ResourcePtr hrc = VGears::HRCFileManager::GetSingleton().load(model->first, "FFVII");
  354. //Ogre::ResourcePtr hrc = VGears::HRCFile();
  355. Ogre::String base_name;
  356. VGears::StringUtil::splitBase(model->first, base_name);
  357. auto mesh_name = VGears::NameLookup::model(base_name) + ".mesh";
  358. Ogre::MeshPtr mesh(Ogre::MeshManager::getSingleton().load(mesh_name, "FFVII"));
  359. Ogre::SkeletonPtr skeleton(mesh->getSkeleton());
  360. const Ogre::AxisAlignedBox bind_bounds = mesh->getBounds();
  361. Ogre::AxisAlignedBox translation_bounds(Ogre::Vector3::ZERO, Ogre::Vector3::ZERO);
  362. for (auto& anim : model->second){
  363. VGears::AFileManager &afl_mgr(VGears::AFileManager::GetSingleton());
  364. VGears::AFilePtr a = afl_mgr.load(anim, "FFVII").staticCast<VGears::AFile>();
  365. // Convert the FF7 name to a more readable name set in the meta data.
  366. VGears::StringUtil::splitBase(anim, base_name);
  367. a->AddTo(skeleton, VGears::NameLookup::Animation(base_name), true);
  368. translation_bounds.merge(a->GetRootTranslationBounds(true));
  369. }
  370. if (bind_bounds.isFinite()){
  371. const Ogre::AxisAlignedBox animated_bounds(
  372. bind_bounds.getMinimum() + translation_bounds.getMinimum(),
  373. bind_bounds.getMaximum() + translation_bounds.getMaximum()
  374. );
  375. mesh->_setBounds(animated_bounds, false);
  376. mesh->_setBoundingSphereRadius(std::max(
  377. mesh->getBoundingSphereRadius(),
  378. animated_bounds.getCenter().length() + animated_bounds.getHalfSize().length()
  379. ));
  380. }
  381. ExportMesh(data_dir_ + FIELD_MODELS_DIR + "/", mesh);
  382. // Models can share a skeleton name with different bones. Drop the model (and its skeleton and mesh) so the
  383. // next one doesn't reuse them.
  384. VGears::HRCFileManager::GetSingleton().remove(model->first, "FFVII");
  385. }
  386. catch (const Ogre::Exception& ex){
  387. if (VGears::HRCFileManager::GetSingleton().resourceExists(model->first, "FFVII"))
  388. VGears::HRCFileManager::GetSingleton().remove(model->first, "FFVII");
  389. write_output_line_("[ERROR] Ogre exception converting model " + model->first + ": " + ex.what());
  390. std::cerr << "[ERROR] Ogre exception converting model " << model->first <<": " << ex.what() << std::endl;
  391. }
  392. catch (const std::exception& ex){
  393. if (VGears::HRCFileManager::GetSingleton().resourceExists(model->first, "FFVII"))
  394. VGears::HRCFileManager::GetSingleton().remove(model->first, "FFVII");
  395. write_output_line_("[ERROR] Exception converting model " + model->first + ": " + ex.what());
  396. std::cerr << "[ERROR] Exception converting model " << model->first << ": " << ex.what() << std::endl;
  397. }
  398. }
  399. void FieldDataInstaller::ExportMesh(const std::string outdir, const Ogre::MeshPtr &mesh){
  400. // TODO: Share function with pc model exporter
  401. VGears::String base_mesh_name;
  402. VGears::StringUtil::splitFull(mesh->getName(), base_mesh_name);
  403. MakeMaterialsUnique(mesh, base_mesh_name);
  404. Ogre::MeshSerializer mesh_serializer;
  405. Ogre::SkeletonPtr skeleton(mesh->getSkeleton());
  406. Ogre::SkeletonSerializer skeleton_serializer;
  407. skeleton_serializer.exportSkeleton(skeleton.getPointer(), outdir + base_mesh_name + ".skeleton");
  408. mesh->setSkeletonName(base_mesh_name + ".skeleton");
  409. mesh_serializer.exportMesh(mesh.getPointer(), outdir + mesh->getName());
  410. Ogre::Mesh::SubMeshIterator it(mesh->getSubMeshIterator());
  411. Ogre::MaterialSerializer mat_ser;
  412. size_t i(0);
  413. while (it.hasMoreElements()){
  414. Ogre::SubMesh *sub_mesh(it.getNext());
  415. Ogre::MaterialPtr mat(Ogre::MaterialManager::getSingleton().getByName(sub_mesh->getMaterialName()));
  416. if (mat != nullptr){
  417. for (size_t techs = 0; techs < mat->getNumTechniques(); techs ++){
  418. Ogre::Technique* tech = mat->getTechnique(techs);
  419. if (tech){
  420. for (size_t pass_num = 0; pass_num < tech->getNumPasses(); pass_num ++){
  421. Ogre::Pass* pass = tech->getPass(pass_num);
  422. if (pass){
  423. for (
  424. size_t texture_unit_num = 0;
  425. texture_unit_num < pass->getNumTextureUnitStates();
  426. texture_unit_num ++
  427. ){
  428. Ogre::TextureUnitState* unit = pass->getTextureUnitState(texture_unit_num);
  429. if (unit && unit->getTextureName().empty() == false){
  430. // Materials are shared by name across all model types, so this one may have
  431. // already been converted for a battle model (its texture is then a png in
  432. // another folder). There is nothing to convert in that case.
  433. if (!boost::filesystem::exists(output_dir_ + "temp/char/" + unit->getTextureName())){
  434. write_output_line_(
  435. "[WARNING] Texture " + unit->getTextureName() + " of model " + base_mesh_name
  436. + " not found; skipping it."
  437. );
  438. continue;
  439. }
  440. // Convert the texture from .tex to .png.
  441. TexFile tex(output_dir_ + "temp/char/" + unit->getTextureName());
  442. // Ensure the output material script references png files
  443. // rather than tex files.
  444. Ogre::String base_name;
  445. VGears::StringUtil::splitBase(unit->getTextureName(), base_name);
  446. unit->setTextureName(base_mesh_name + "_" + base_name + ".png");
  447. tex.SavePng(data_dir_ + FIELD_MODELS_DIR + "/" + base_name + ".png", 0);
  448. // Copy subtexture (xxxx.png) to model_xxxx.png
  449. // TODO: obtain the "data" folder
  450. // programatically.
  451. boost::filesystem::copy_file(
  452. data_dir_ + FIELD_MODELS_DIR + "/" + base_name + ".png",
  453. data_dir_ + FIELD_MODELS_DIR + "/"
  454. + base_mesh_name + "_" + base_name + ".png",
  455. boost::filesystem::copy_options::overwrite_existing
  456. );
  457. }
  458. }
  459. }
  460. }
  461. }
  462. }
  463. if (std::count(materials_.begin(), materials_.end(), sub_mesh->getMaterialName()) == 0){
  464. mat_ser.queueForExport(mat);
  465. materials_.push_back(sub_mesh->getMaterialName());
  466. }
  467. }
  468. ++ i;
  469. }
  470. mat_ser.exportQueued(outdir + base_mesh_name + VGears::EXT_MATERIAL);
  471. }
  472. float FieldDataInstaller::GetFieldScaleFactor(size_t field_id){
  473. auto it = scale_factors_.find(field_id);
  474. if (it == std::end(scale_factors_)) throw std::runtime_error("Scale factor not found for field id");
  475. return it->second;
  476. }
  477. std::vector<int> FieldDataInstaller::ExtractMusicTrackIds(VGears::FLevelFilePtr& field){
  478. // Search for the "AKAO" string in the raw field data.
  479. // The next byte is a track id.
  480. const std::vector<u8> raw = field->GetRawScript();
  481. std::vector<int> tracks;
  482. for (int i = 0; i < raw.size() - 5; i ++){
  483. if (raw[i] == 0x41){ // A
  484. if (raw[i + 1] == 0x4B){ // K
  485. if (raw[i + 2] == 0x41){ // A
  486. if (raw[i + 3] == 0x4f){ // O
  487. tracks.push_back(static_cast<int>(raw[i + 4]) - 2);
  488. }
  489. }
  490. }
  491. }
  492. }
  493. return tracks;
  494. }
  495. void FieldDataInstaller::PcFieldToVGearsField(VGears::FLevelFilePtr& field){
  496. // Generate triggers script to insert into main
  497. // decompiled FF7 field -> LUA script.
  498. std::string gateway_script_data;
  499. const VGears::TriggersFilePtr& triggers = field->GetTriggers();
  500. const auto& gateways = triggers->GetGateways();
  501. for (size_t i = 0; i < gateways.size(); i ++){
  502. const VGears::TriggersFile::Gateway& gateway = gateways[i];
  503. if (gateway.destination_field_id != INACTIVE_GATEWAY_ID){
  504. const std::string gateway_entity_name = "Gateway" + std::to_string(i);
  505. gateway_script_data += CreateGateWayScript(
  506. gateway_entity_name, map_list_.at(gateway.destination_field_id),
  507. "Spawn_" + field->getName() + "_" + std::to_string(i)
  508. );
  509. }
  510. }
  511. const VGears::ModelListFilePtr& models = field->GetModelList();
  512. FieldDecompiler::DecompiledScript decompiled;
  513. FF7FieldScriptFormatter formatter(field->getName(), models, map_list_, spawn_points_);
  514. try{
  515. // Get the raw script bytes.
  516. const std::vector<u8> raw_field_data = field->GetRawScript();
  517. // Decompile to LUA.
  518. decompiled = FieldDecompiler::Decompile(
  519. field->getName(), raw_field_data, formatter, gateway_script_data, FieldScriptHeader()
  520. );
  521. std::ofstream script_file(data_dir_ + "/" + FIELD_MAPS_DIR + "/" + field->getName() + "/script.lua");
  522. if (script_file.is_open()){
  523. script_file << decompiled.luaScript;
  524. field_text_writer_.Begin(data_dir_ + "/" + FIELD_MAPS_DIR + "/" + field->getName() + "/text.xml");
  525. try{field_text_writer_.Write(raw_field_data, field->getName());}
  526. catch (const std::out_of_range& ex){
  527. write_output_line_("[ERROR] Failed to read texts from field " + field->getName() + ": " + ex.what());
  528. std::cerr << "[ERROR] Failed to read texts from field "
  529. << field->getName() << ": " << ex.what() << std::endl;
  530. }
  531. field_text_writer_.End();
  532. }
  533. else{
  534. write_output_line_("[ERROR] Failed to open script file from field " + field->getName() + " for writing.");
  535. std::cerr << "[ERROR] Failed to open script file from field "
  536. << field->getName() << "for writing." << std::endl;
  537. }
  538. }
  539. catch (const ::DecompilerException& ex){
  540. write_output_line_("[ERROR] Internal decompiler error in field " + field->getName() + ": " + ex.what());
  541. std::cerr << "[ERROR] Internal decompiler error in field "
  542. << field->getName() << ": " << ex.what() << std::endl;
  543. }
  544. // Write out the map file which links all the other files together for a given field.
  545. {
  546. TiXmlDocument doc;
  547. std::unique_ptr<TiXmlElement> element(new TiXmlElement("map"));
  548. // Script.
  549. std::unique_ptr<TiXmlElement> xml_script_element(new TiXmlElement("script"));
  550. xml_script_element->SetAttribute("file_name", field->getName() + "/script.lua");
  551. element->LinkEndChild(xml_script_element.release());
  552. // Background.
  553. std::unique_ptr<TiXmlElement> xml_background_2d(new TiXmlElement("background2d"));
  554. xml_background_2d->SetAttribute("file_name", field->getName() + "/bg.xml");
  555. element->LinkEndChild(xml_background_2d.release());
  556. // Texts.
  557. std::unique_ptr<TiXmlElement> xml_texts(new TiXmlElement("texts"));
  558. xml_texts->SetAttribute("file_name", field->getName() + "/text.xml");
  559. element->LinkEndChild(xml_texts.release());
  560. // Walkmesh.
  561. std::unique_ptr<TiXmlElement> xml_walkmesh(new TiXmlElement("walkmesh"));
  562. xml_walkmesh->SetAttribute("file_name", field->getName() + "/wm.xml");
  563. element->LinkEndChild(xml_walkmesh.release());
  564. // Forward direction (angle player moves when "up" is pressed)
  565. std::unique_ptr<TiXmlElement> xml_forward_direction(new TiXmlElement("movement_rotation"));
  566. xml_forward_direction->SetAttribute("degree", std::to_string(triggers->MovementRotation()));
  567. element->LinkEndChild(xml_forward_direction.release());
  568. // Random encounters. The section has two tables of 24 bytes: enabled (1 byte), rate
  569. // (1 byte), six standard and four special encounters (16 bits each, battle ID in the lower
  570. // 10 bits and probability, out of 64, in the upper 6), and 2 bytes of padding.
  571. {
  572. const std::vector<u8>& raw_encounter = field->GetRawEncounter();
  573. std::unique_ptr<TiXmlElement> xml_encounters(new TiXmlElement("encounters"));
  574. bool any_table = false;
  575. for (int table = 0; table < 2 && raw_encounter.size() >= static_cast<size_t>(24 * (table + 1)); table ++){
  576. const u8* data = raw_encounter.data() + 24 * table;
  577. if (data[0] == 0) continue;
  578. std::unique_ptr<TiXmlElement> xml_table(new TiXmlElement("table"));
  579. xml_table->SetAttribute("id", table + 1);
  580. xml_table->SetAttribute("rate", static_cast<int>(data[1]));
  581. for (int e = 0; e < 10; e ++){
  582. const unsigned int value = data[2 + 2 * e] | (data[3 + 2 * e] << 8);
  583. const int battle = value & 0x3FF;
  584. const int probability = value >> 10;
  585. if (probability == 0) continue;
  586. std::unique_ptr<TiXmlElement> xml_encounter(new TiXmlElement(e < 6 ? "battle" : "special"));
  587. xml_encounter->SetAttribute("id", battle);
  588. xml_encounter->SetAttribute("probability", probability);
  589. xml_table->LinkEndChild(xml_encounter.release());
  590. }
  591. xml_encounters->LinkEndChild(xml_table.release());
  592. any_table = true;
  593. }
  594. if (any_table) element->LinkEndChild(xml_encounters.release());
  595. }
  596. // Get this fields Id.
  597. const size_t this_field_id = GetFieldId(field->getName(), map_list_);
  598. // Use the ID to find the pre-computed list of gateways in all other fields that link to
  599. // this field.
  600. auto spawn_iterator = spawn_points_.find(this_field_id);
  601. Ogre::Vector3 first_entity_point = Ogre::Vector3::ZERO;
  602. // If none found it probably just means no other fields have doors to this one.
  603. if (spawn_iterator != std::end(spawn_points_)){
  604. const std::vector<SpawnPointDb::Record>& spawn_point_records
  605. = spawn_iterator->second.gateways_to_this_field;
  606. for (size_t i = 0; i < spawn_point_records.size(); i ++){
  607. const VGears::TriggersFile::Gateway& gateway = spawn_point_records[i].gateway;
  608. // Entity_point:
  609. std::unique_ptr<TiXmlElement> xml_entity_point(new TiXmlElement("entity_point"));
  610. if (spawn_point_records[i].from_script){
  611. // Spawn points from map jumps have a another algorithm.
  612. xml_entity_point->SetAttribute(
  613. "name",
  614. map_list_.at(
  615. spawn_point_records[i].field_id) + "_" + spawn_point_records[i].entity_name
  616. + "_" + spawn_point_records[i].script_function_name + "_addr_"
  617. + std::to_string(spawn_point_records[i].gateway_index_or_map_jump_address)
  618. );
  619. }
  620. else{
  621. // Must also include the gateway index for the case where 2
  622. // fields have more than one door linking to each other.
  623. xml_entity_point->SetAttribute(
  624. "name",
  625. "Spawn_" + map_list_.at(spawn_point_records[i].field_id) + "_"
  626. + std::to_string(spawn_point_records[i].gateway_index_or_map_jump_address)
  627. );
  628. }
  629. const float downscaler_next = 128.0f * GetFieldScaleFactor(gateway.destination_field_id);
  630. // Position Z is actually the target walkmesh triangle ID, so this is tiny bit more
  631. // complex. Now "get the Z value of the triangle with that ID". Note that ID
  632. // actually just means index.
  633. unsigned int triangle_index = static_cast<unsigned int>(gateway.destination.z);
  634. if (triangle_index >= field->GetWalkmesh()->GetTriangles().size()){
  635. write_output_line_(
  636. "[WARNING] In field " + field->getName() + ": Map jump triangle ("
  637. + std::to_string(triangle_index) + ") out of bounds ("
  638. + std::to_string(field->GetWalkmesh()->GetTriangles().size()) + ")"
  639. );
  640. std::cerr << "[WARNING] In field " << field->getName()
  641. << ": Map jump triangle (" << triangle_index << ") out of bounds ("
  642. << field->GetWalkmesh()->GetTriangles().size() << ")" << std::endl;
  643. triangle_index = 0;
  644. }
  645. const float z_of_triangle_with_id
  646. = field->GetWalkmesh()->GetTriangles().at(triangle_index).a.z / GetFieldScaleFactor(this_field_id);
  647. const Ogre::Vector3 position(
  648. gateway.destination.x / downscaler_next, gateway.destination.y / downscaler_next,
  649. z_of_triangle_with_id
  650. );
  651. if (position != Ogre::Vector3::ZERO && first_entity_point == Ogre::Vector3::ZERO){
  652. first_entity_point = position;
  653. }
  654. xml_entity_point->SetAttribute("position", Ogre::StringConverter::toString(position));
  655. const float rotation = (360.0f * static_cast<float>(gateway.dir)) / 255.0f;
  656. xml_entity_point->SetAttribute("rotation", std::to_string(rotation));
  657. element->LinkEndChild(xml_entity_point.release());
  658. }
  659. }
  660. // Get lines. Add them to a list, so they aren't processed later as regular entities.
  661. std::vector<std::string> line_entities;
  662. const float line_scale = LINE_SCALE_FACTOR / GetFieldScaleFactor(this_field_id);
  663. for (FieldDecompiler::Line line : decompiled.lines){
  664. std::unique_ptr<TiXmlElement> xml_entity_trigger(new TiXmlElement("entity_trigger"));
  665. line_entities.push_back(line.name);
  666. xml_entity_trigger->SetAttribute("name", line.name);
  667. xml_entity_trigger->SetAttribute(
  668. "point1",
  669. std::to_string(line.point_a[0] * line_scale)
  670. + " " + std::to_string(line.point_a[1] * line_scale)
  671. + " " + std::to_string(line.point_a[2] * line_scale)
  672. );
  673. xml_entity_trigger->SetAttribute(
  674. "point2",
  675. std::to_string(line.point_b[0] * line_scale)
  676. + " " + std::to_string(line.point_b[1] * line_scale)
  677. + " " + std::to_string(line.point_b[2] * line_scale)
  678. );
  679. xml_entity_trigger->SetAttribute("enabled", "true");
  680. element->LinkEndChild(xml_entity_trigger.release());
  681. }
  682. // Get entities.
  683. for (FieldDecompiler::FieldEntity entity : decompiled.entities){
  684. // If the entity has been added as a line, skip.
  685. if (line_entities.size() > 0){
  686. if (std::find(line_entities.begin(), line_entities.end(), entity.name)
  687. != line_entities.end()) continue;
  688. }
  689. const int char_id = entity.char_id;
  690. if (char_id != -1){
  691. const VGears::ModelListFile::ModelDescription& desc = models->GetModels().at(char_id);
  692. auto& animations = used_models_and_anims_.ModelAnimations(desc.hrc_name);
  693. for (const auto& anim : desc.animations){
  694. animations.insert(used_models_and_anims_.NormalizeAnimationName(anim.name));
  695. }
  696. std::unique_ptr<TiXmlElement> xml_entity_script(new TiXmlElement("entity_model"));
  697. xml_entity_script->SetAttribute("name", entity.name);
  698. // TODO: Add to list of HRC's to convert, obtain name of converted .mesh file.
  699. auto lower_case_hrc_name = desc.hrc_name;
  700. VGears::StringUtil::toLowerCase(lower_case_hrc_name);
  701. xml_entity_script->SetAttribute(
  702. "file_name", used_models_and_anims_.ModelMetaDataName(lower_case_hrc_name)
  703. );
  704. xml_entity_script->SetAttribute("index", entity.index);
  705. if (desc.type == VGears::ModelListFile::PLAYER){
  706. // For player models the position is set manually in the xml because if a map
  707. // is loaded manually this is where the player will end up. Hence we set the
  708. // position to the first entity_point that we have.
  709. xml_entity_script->SetAttribute("position", Ogre::StringConverter::toString(first_entity_point));
  710. }
  711. else{
  712. xml_entity_script->SetAttribute("position", Ogre::StringConverter::toString(Ogre::Vector3::ZERO));
  713. }
  714. xml_entity_script->SetAttribute("direction", "0");
  715. // TODO: This isn't quite right, the models animation
  716. // translation seems to be inverted?
  717. xml_entity_script->SetAttribute("scale", "0.03125 0.03125 0.03125");
  718. xml_entity_script->SetAttribute("root_orientation", "0.7071067811865476 0.7071067811865476 0 0");
  719. element->LinkEndChild(xml_entity_script.release());
  720. }
  721. else{
  722. std::unique_ptr<TiXmlElement> xml_entity_script(new TiXmlElement("entity_script"));
  723. xml_entity_script->SetAttribute("name", entity.name);//it.first);
  724. element->LinkEndChild(xml_entity_script.release());
  725. }
  726. }
  727. const float downscaler_this = 128.0f * GetFieldScaleFactor(this_field_id);
  728. for (size_t i = 0; i < gateways.size(); i ++){
  729. const VGears::TriggersFile::Gateway& gateway = gateways[i];
  730. // If non-inactive gateway:
  731. if (gateway.destination_field_id != INACTIVE_GATEWAY_ID){
  732. std::unique_ptr<TiXmlElement> xml_entity_trigger(new TiXmlElement("entity_trigger"));
  733. xml_entity_trigger->SetAttribute("name", "Gateway" + std::to_string(i));
  734. xml_entity_trigger->SetAttribute(
  735. "point1",
  736. Ogre::StringConverter::toString(
  737. Ogre::Vector3(
  738. gateway.exit_line[0].x, gateway.exit_line[0].y, gateway.exit_line[0].z) / downscaler_this
  739. )
  740. );
  741. xml_entity_trigger->SetAttribute(
  742. "point2",
  743. Ogre::StringConverter::toString(
  744. Ogre::Vector3(
  745. gateway.exit_line[1].x, gateway.exit_line[1].y, gateway.exit_line[1].z) / downscaler_this
  746. )
  747. );
  748. // Enabled is hard coded to true.
  749. xml_entity_trigger->SetAttribute("enabled", "true");
  750. element->LinkEndChild(xml_entity_trigger.release());
  751. }
  752. }
  753. // Get music tracks
  754. std::vector<int> tracks = ExtractMusicTrackIds(field);
  755. std::unique_ptr<TiXmlElement> xml_tracks(new TiXmlElement("tracks"));
  756. for (int t = 0; t < tracks.size(); t ++){
  757. std::unique_ptr<TiXmlElement> xml_track(new TiXmlElement("track"));
  758. xml_track->SetAttribute("id", t);
  759. xml_track->SetAttribute("track_id", tracks[t]);
  760. xml_tracks->LinkEndChild(xml_track.release());
  761. }
  762. element->LinkEndChild(xml_tracks.release());
  763. doc.LinkEndChild(element.release());
  764. doc.SaveFile(data_dir_ + "/" + FIELD_MAPS_DIR + "/" + field->getName() + "/map.xml");
  765. const VGears::PaletteFilePtr& pal = field->GetPalette();
  766. const VGears::BackgroundFilePtr& bg = field->GetBackground();
  767. std::unique_ptr<Ogre::Image> bg_image(bg->CreateImage(pal));
  768. bg_image->encode("png");
  769. bg_image->save(data_dir_ + "/" + FIELD_MAPS_DIR + "/" + field->getName() + "/tiles.png");
  770. {
  771. TiXmlDocument bg_doc;
  772. std::unique_ptr<TiXmlElement> bg_element(new TiXmlElement("background2d"));
  773. // Magic constants.
  774. const int BG_SCALE_UP_FACTOR = 3;
  775. const int BG_PSX_SCREEN_WIDTH = 320;
  776. const int BG_PSX_SCREEN_HEIGHT = 240;
  777. // Get texture atlas size.
  778. const int width = bg_image->getWidth();
  779. const int height = bg_image->getHeight();
  780. const VGears::CameraMatrixFilePtr& camera_matrix = field->GetCameraMatrix();
  781. const Ogre::Vector3 position = camera_matrix->GetPosition() / GetFieldScaleFactor(this_field_id);
  782. const Ogre::Quaternion orientation = camera_matrix->GetOrientation();
  783. const Ogre::Degree fov = camera_matrix->GetFov(static_cast<float>(BG_PSX_SCREEN_HEIGHT));
  784. const int min_x = triggers->GetCameraRange().left * BG_SCALE_UP_FACTOR;
  785. const int min_y = triggers->GetCameraRange().top * BG_SCALE_UP_FACTOR;
  786. const int max_x = triggers->GetCameraRange().right * BG_SCALE_UP_FACTOR;
  787. const int max_y = triggers->GetCameraRange().bottom * BG_SCALE_UP_FACTOR;
  788. bg_element->SetAttribute("image", field->getName() + "/tiles.png");
  789. bg_element->SetAttribute("position", Ogre::StringConverter::toString(position));
  790. bg_element->SetAttribute("orientation", Ogre::StringConverter::toString(orientation));
  791. bg_element->SetAttribute("fov", Ogre::StringConverter::toString(fov));
  792. bg_element->SetAttribute(
  793. "range",
  794. std::to_string(min_x) + " " + std::to_string(min_y) + " "
  795. + std::to_string(max_x) + " " + std::to_string(max_y)
  796. );
  797. bg_element->SetAttribute(
  798. "clip",
  799. std::to_string(BG_PSX_SCREEN_WIDTH * BG_SCALE_UP_FACTOR) + " "
  800. + std::to_string(BG_PSX_SCREEN_HEIGHT * BG_SCALE_UP_FACTOR)
  801. );
  802. // Write out the *_BG.XML data.
  803. auto& layers = bg->GetLayers();
  804. for (const auto& layer : layers){
  805. // TODO: Should only the tiles to construct enabled layers be outputted?
  806. // if (layer.enabled){
  807. for (const VGears::BackgroundFile::SpriteData& sprite : layer.sprites){
  808. std::unique_ptr<TiXmlElement> xml_element(new TiXmlElement("tile"));
  809. xml_element->SetAttribute(
  810. "destination",
  811. Ogre::StringConverter::toString(sprite.dst.x * BG_SCALE_UP_FACTOR) + " "
  812. + Ogre::StringConverter::toString(sprite.dst.y * BG_SCALE_UP_FACTOR)
  813. );
  814. xml_element->SetAttribute(
  815. "width", Ogre::StringConverter::toString(sprite.width * BG_SCALE_UP_FACTOR)
  816. );
  817. xml_element->SetAttribute(
  818. "height", Ogre::StringConverter::toString(sprite.height * BG_SCALE_UP_FACTOR)
  819. );
  820. // Each tile is added to a big texture atlas with hard coded size of 1024x1024,
  821. // convert UV's to the 0.0f to 1.0f range.
  822. const float u0 = static_cast<float>(sprite.src.x) / width;
  823. const float v0 = static_cast<float>(sprite.src.y) / height;
  824. const float u1 = static_cast<float>(sprite.src.x + sprite.width) / width;
  825. const float v1 = static_cast<float>(sprite.src.y + sprite.height) / height;
  826. xml_element->SetAttribute(
  827. "uv",
  828. Ogre::StringConverter::toString(u0) + " " + Ogre::StringConverter::toString(v0) + " "
  829. + Ogre::StringConverter::toString(u1) + " " + Ogre::StringConverter::toString(v1)
  830. );
  831. // TODO: It works (on FFVII PC), but why this number?
  832. xml_element->SetAttribute(
  833. "depth", Ogre::StringConverter::toString(static_cast<float>(sprite.depth) * (0.03125f))
  834. );
  835. // TODO: Copied from DatFile::AddTile.
  836. // Add to common method.
  837. Ogre::String blending_str = "";
  838. if (sprite.blending == 0) blending_str = "alpha";
  839. // 3 is source + 0.25 * destination:
  840. else if (sprite.blending == 1 || sprite.blending == 3) blending_str = "add";
  841. else if (sprite.blending == 2) blending_str = "subtract";
  842. // TODO: Should probably throw to fail conversion:
  843. else blending_str = "unknown";
  844. xml_element->SetAttribute("blending", blending_str);
  845. // Sprites in an area the script controls (BGON, BGOFF, BGCLR) are drawn only
  846. // while it turns on one of their layers. The rest are always drawn.
  847. if (
  848. sprite.animation_id != 0
  849. && (
  850. decompiled.background_areas_dynamic
  851. || decompiled.background_areas.count(sprite.animation_id) > 0
  852. )
  853. ){
  854. xml_element->SetAttribute("area", static_cast<int>(sprite.animation_id));
  855. xml_element->SetAttribute("state", static_cast<int>(sprite.animation_frame));
  856. }
  857. bg_element->LinkEndChild(xml_element.release());
  858. }
  859. //}
  860. }
  861. bg_doc.LinkEndChild(bg_element.release());
  862. bg_doc.SaveFile(data_dir_ + "/" + FIELD_MAPS_DIR + "/" + field->getName() + "/bg.xml");
  863. }
  864. }
  865. {
  866. // Save out the walk mesh as XML.
  867. const VGears::WalkmeshFilePtr& walkmesh = field->GetWalkmesh();
  868. // Scripts, gateways and the camera use the field's scale, so the walkmesh must too.
  869. const float field_scale = GetFieldScaleFactor(GetFieldId(field->getName(), map_list_));
  870. TiXmlDocument doc;
  871. std::unique_ptr<TiXmlElement> wmesh_element(new TiXmlElement("walkmesh"));
  872. for (VGears::WalkmeshFile::Triangle& tri : walkmesh->GetTriangles()){
  873. std::unique_ptr<TiXmlElement> xml_element(new TiXmlElement("triangle"));
  874. xml_element->SetAttribute("a", Ogre::StringConverter::toString(tri.a / field_scale));
  875. xml_element->SetAttribute("b", Ogre::StringConverter::toString(tri.b / field_scale));
  876. xml_element->SetAttribute("c", Ogre::StringConverter::toString(tri.c / field_scale));
  877. xml_element->SetAttribute("a_b", std::to_string(tri.access_side[0]));
  878. xml_element->SetAttribute("b_c", std::to_string(tri.access_side[1]));
  879. xml_element->SetAttribute("c_a", std::to_string(tri.access_side[2]));
  880. wmesh_element->LinkEndChild(xml_element.release());
  881. }
  882. doc.LinkEndChild(wmesh_element.release());
  883. doc.SaveFile(data_dir_ + "/" + FIELD_MAPS_DIR + "/" + field->getName() + "/wm.xml");
  884. }
  885. converted_map_list_.push_back(field->getName());
  886. }