FieldDecompiler.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  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 <boost/test/unit_test.hpp>
  16. #include "installer/decompiler/field/FieldDecompiler.h"
  17. BOOST_AUTO_TEST_CASE(TestFieldDecompiler){
  18. std::vector<unsigned char> script(104, 0);
  19. script[0] = 0x02;
  20. script[1] = 0x05;
  21. script[2] = 1;
  22. script[9] = 2;
  23. script[32] = 'D';
  24. script[33] = 'o';
  25. script[34] = 'o';
  26. script[35] = 'r';
  27. for (size_t index = 0; index < 32; ++index) script[40 + index * 2] = 104;
  28. for (unsigned char type = 0; type <= 2; ++type){
  29. const unsigned char instruction[] = {0xc3, 0, 0, type, 128, 0, 0, 0, 0, 0, 30, 0};
  30. script.insert(script.end(), std::begin(instruction), std::end(instruction));
  31. }
  32. script.push_back(0);
  33. script[4] = static_cast<unsigned char>(script.size());
  34. script[5] = static_cast<unsigned char>(script.size() >> 8);
  35. FieldScriptFormatter formatter;
  36. const std::string output = FieldDecompiler::Decompile("synthetic", script, formatter).luaScript;
  37. BOOST_CHECK(output.find("offset_to_position(1, 0, 0, Entity.NONE, 1)") != std::string::npos);
  38. BOOST_CHECK(output.find("offset_to_position(1, 0, 0, Entity.LINEAR, 1)") != std::string::npos);
  39. BOOST_CHECK(output.find("offset_to_position(1, 0, 0, Entity.SMOOTH, 1)") != std::string::npos);
  40. }
  41. static std::string DecompileInstructions(const std::vector<unsigned char>& instructions){
  42. std::vector<unsigned char> script(104, 0);
  43. script[0] = 0x02;
  44. script[1] = 0x05;
  45. script[2] = 1;
  46. script[9] = 2;
  47. script[32] = 'D';
  48. script[33] = 'o';
  49. script[34] = 'o';
  50. script[35] = 'r';
  51. for (size_t index = 0; index < 32; ++index) script[40 + index * 2] = 104;
  52. script.insert(script.end(), instructions.begin(), instructions.end());
  53. script.push_back(0); // RET
  54. script[4] = static_cast<unsigned char>(script.size());
  55. script[5] = static_cast<unsigned char>(script.size() >> 8);
  56. FieldScriptFormatter formatter;
  57. return FieldDecompiler::Decompile("synthetic", script, formatter).luaScript;
  58. }
  59. namespace{
  60. class DefaultAnimationFormatter : public FieldScriptFormatter{
  61. public:
  62. std::string GetDefaultAnimationName(int char_id) override{
  63. return char_id == 2 ? "stand" : "";
  64. }
  65. };
  66. }
  67. static FieldDecompiler::DecompiledScript DecompileFully(
  68. const std::vector<unsigned char>& instructions, FieldScriptFormatter& formatter
  69. ){
  70. std::vector<unsigned char> script(104, 0);
  71. script[0] = 0x02;
  72. script[1] = 0x05;
  73. script[2] = 1;
  74. script[9] = 2;
  75. script[32] = 'D';
  76. script[33] = 'o';
  77. script[34] = 'o';
  78. script[35] = 'r';
  79. for (size_t index = 0; index < 32; ++index) script[40 + index * 2] = 104;
  80. script.insert(script.end(), instructions.begin(), instructions.end());
  81. script.push_back(0); // RET
  82. script[4] = static_cast<unsigned char>(script.size());
  83. script[5] = static_cast<unsigned char>(script.size() >> 8);
  84. return FieldDecompiler::Decompile("synthetic", script, formatter);
  85. }
  86. static std::string DecompileWith(
  87. const std::vector<unsigned char>& instructions, FieldScriptFormatter& formatter
  88. ){return DecompileFully(instructions, formatter).luaScript;}
  89. BOOST_AUTO_TEST_CASE(TestFieldDecompilerModelWithoutIdleGetsDefaultAnimation){
  90. DefaultAnimationFormatter formatter;
  91. // CHAR 2: the formatter has an animation for it.
  92. const std::string with_animation = DecompileWith({0xA1, 2}, formatter);
  93. BOOST_CHECK(
  94. with_animation.find("self.Door:set_default_animation(\"stand\") -- No Idle animation")
  95. != std::string::npos
  96. );
  97. BOOST_CHECK(with_animation.find("self.Door:play_animation(\"stand\")") != std::string::npos);
  98. // CHAR 1: the formatter has none, as when the model already has an Idle animation.
  99. const std::string without_animation = DecompileWith({0xA1, 1}, formatter);
  100. BOOST_CHECK(without_animation.find("self.Door = entity_manager") != std::string::npos);
  101. BOOST_CHECK(without_animation.find("set_default_animation") == std::string::npos);
  102. // The default formatter never adds one.
  103. FieldScriptFormatter plain_formatter;
  104. BOOST_CHECK(
  105. DecompileWith({0xA1, 2}, plain_formatter).find("set_default_animation") == std::string::npos
  106. );
  107. }
  108. namespace{
  109. class NamedAnimationFormatter : public FieldScriptFormatter{
  110. public:
  111. std::string GetFriendlyAnimationName(int char_id, int id) override{
  112. return "anim" + std::to_string(id);
  113. }
  114. };
  115. }
  116. BOOST_AUTO_TEST_CASE(TestFieldDecompilerNonWaitingAnimationsDontWait){
  117. NamedAnimationFormatter formatter;
  118. // ANIM!1 3, 1 - ANIME2 2, 1 - CANIM1 4, 0, 14, 1 - CANM!1 5, 15, 29, 1.
  119. const std::string output = DecompileWith(
  120. {0xAF, 3, 1, 0xAE, 2, 1, 0xB0, 4, 0, 14, 1, 0xB1, 5, 15, 29, 1}, formatter
  121. );
  122. BOOST_CHECK(output.find("self.Door:play_animation_stop(\"anim3\") -- speed 1") != std::string::npos);
  123. BOOST_CHECK(output.find("self.Door:play_animation(\"anim2\") -- speed 1") != std::string::npos);
  124. BOOST_CHECK(
  125. output.find("self.Door:play_animation(\"anim4\", 0, 0.466667) -- speed 1") != std::string::npos
  126. );
  127. BOOST_CHECK(
  128. output.find("self.Door:play_animation_stop(\"anim5\", 0.5, 0.966667) -- speed 1")
  129. != std::string::npos
  130. );
  131. BOOST_CHECK(output.find("UNIMPLMENTED") == std::string::npos);
  132. // None of them waits.
  133. BOOST_CHECK(output.find("animation_sync") == std::string::npos);
  134. }
  135. BOOST_AUTO_TEST_CASE(TestFieldDecompilerMovaWalksToTheEntityAndWaits){
  136. FieldScriptFormatter formatter;
  137. // MOVA 0: the only entity of the synthetic field.
  138. const std::string output = DecompileWith({0xAA, 0}, formatter);
  139. const size_t walk = output.find("self.Door:move_to_entity(entity_manager:get_entity(\"Door\"))");
  140. const size_t wait = output.find("self.Door:move_sync()");
  141. BOOST_REQUIRE(walk != std::string::npos && wait != std::string::npos);
  142. BOOST_CHECK(walk < wait);
  143. BOOST_CHECK(output.find("UNIMPLMENTED") == std::string::npos);
  144. }
  145. BOOST_AUTO_TEST_CASE(TestFieldDecompilerScreenFades){
  146. FieldScriptFormatter formatter;
  147. // FADE 255, 0, 0, speed 8, type 5 - NFADE type 11, 10, 20, 30, 15 frames - FADEW.
  148. const std::string output = DecompileWith(
  149. {0x6B, 0, 0, 255, 0, 0, 8, 5, 0, 0x25, 0, 0, 11, 10, 20, 30, 15, 0, 0x6C}, formatter
  150. );
  151. BOOST_CHECK(output.find("screen_fade:fade(5, 255, 0, 0, 8)") != std::string::npos);
  152. BOOST_CHECK(output.find("screen_fade:nfade(11, 10, 20, 30, 15)") != std::string::npos);
  153. BOOST_CHECK(output.find("screen_fade:wait()") != std::string::npos);
  154. // They used to be comments.
  155. BOOST_CHECK(output.find("-- fade:") == std::string::npos);
  156. BOOST_CHECK(output.find("UNIMPLMENTED") == std::string::npos);
  157. }
  158. BOOST_AUTO_TEST_CASE(TestFieldDecompilerWindowCloseTalkRangeAndFacing){
  159. FieldScriptFormatter formatter;
  160. // WCLS 2 - TALKR 64 - TLKR2 128 - DIRA 0.
  161. const std::string output = DecompileWith(
  162. {0x2E, 2, 0xC5, 0, 64, 0xD6, 0, 128, 0, 0xB6, 0}, formatter
  163. );
  164. BOOST_CHECK(output.find("dialog:dialog_close(\"2\")") != std::string::npos);
  165. BOOST_CHECK(output.find("self.Door:set_talk_radius(0.5)") != std::string::npos);
  166. BOOST_CHECK(output.find("self.Door:set_talk_radius(1)") != std::string::npos);
  167. BOOST_CHECK(
  168. output.find("self.Door:face_entity(entity_manager:get_entity(\"Door\"))") != std::string::npos
  169. );
  170. BOOST_CHECK(output.find("UNIMPLMENTED") == std::string::npos);
  171. }
  172. BOOST_AUTO_TEST_CASE(TestFieldDecompilerAspedSetsTheAnimationSpeed){
  173. FieldScriptFormatter formatter;
  174. // ASPED 16 (normal speed), ASPED 32 (double) and ASPED 8 (half).
  175. const std::string output = DecompileWith(
  176. {0xBD, 0, 16, 0, 0xBD, 0, 32, 0, 0xBD, 0, 8, 0}, formatter
  177. );
  178. BOOST_CHECK(output.find("self.Door:set_animation_speed(1)") != std::string::npos);
  179. BOOST_CHECK(output.find("self.Door:set_animation_speed(2)") != std::string::npos);
  180. BOOST_CHECK(output.find("self.Door:set_animation_speed(0.5)") != std::string::npos);
  181. BOOST_CHECK(output.find("UNIMPLMENTED") == std::string::npos);
  182. }
  183. BOOST_AUTO_TEST_CASE(TestFieldDecompilerShakeKeepsItsParameters){
  184. FieldScriptFormatter formatter;
  185. // SHAKE 0, 0, 2, 0, 0, 5, 2 (shake the Y axis) and SHAKE 0, 0, 0, 0, 1, 0, 1 (stop).
  186. const std::string output = DecompileWith(
  187. {0x5E, 0, 0, 2, 0, 0, 5, 2, 0x5E, 0, 0, 0, 0, 1, 0, 1}, formatter
  188. );
  189. BOOST_CHECK(output.find("background2d:shake(2, 0, 0, 5, 2)") != std::string::npos);
  190. BOOST_CHECK(output.find("background2d:shake(0, 0, 1, 0, 1)") != std::string::npos);
  191. BOOST_CHECK(output.find("UNIMPLMENTED") == std::string::npos);
  192. }
  193. BOOST_AUTO_TEST_CASE(TestFieldDecompilerAnimwWaitsForTheAnimation){
  194. NamedAnimationFormatter formatter;
  195. // ANIM!1 3, 1 and then ANIMW: the script waits after starting the animation.
  196. const std::string output = DecompileWith({0xAF, 3, 1, 0xAC}, formatter);
  197. const size_t start = output.find("self.Door:play_animation_stop(\"anim3\")");
  198. const size_t wait = output.find("self.Door:animation_sync()");
  199. BOOST_REQUIRE(start != std::string::npos && wait != std::string::npos);
  200. BOOST_CHECK(start < wait);
  201. }
  202. BOOST_AUTO_TEST_CASE(TestFieldDecompilerBackgroundAreas){
  203. FieldScriptFormatter formatter;
  204. // BGON 0, 0, 2, 0 - BGOFF 0, 0, 8, 2 - BGCLR 0, 8.
  205. const std::string output = DecompileWith(
  206. {0xE0, 0, 2, 0, 0xE1, 0, 8, 2, 0xE4, 0, 8}, formatter
  207. );
  208. BOOST_CHECK(output.find("background2d:area_on(2, 0)") != std::string::npos);
  209. BOOST_CHECK(output.find("background2d:area_off(8, 2)") != std::string::npos);
  210. BOOST_CHECK(output.find("background2d:area_clear(8)") != std::string::npos);
  211. // They used to be comments.
  212. BOOST_CHECK(output.find("-- field:background_") == std::string::npos);
  213. }
  214. BOOST_AUTO_TEST_CASE(TestFieldDecompilerReportsTheBackgroundAreasItControls){
  215. FieldScriptFormatter formatter;
  216. const auto literal = DecompileFully({0xE0, 0, 2, 0, 0xE1, 0, 8, 2, 0xE4, 0, 5}, formatter);
  217. BOOST_CHECK(literal.background_areas == std::set<int>({2, 5, 8}));
  218. BOOST_CHECK(!literal.background_areas_dynamic);
  219. // BGON with the area read from bank 1, address 3: any area could be controlled.
  220. const auto from_variable = DecompileFully({0xE0, 0x10, 3, 0}, formatter);
  221. BOOST_CHECK(from_variable.background_areas_dynamic);
  222. // A script that doesn't touch the background controls no area.
  223. const auto none = DecompileFully({}, formatter);
  224. BOOST_CHECK(none.background_areas.empty());
  225. BOOST_CHECK(!none.background_areas_dynamic);
  226. }
  227. BOOST_AUTO_TEST_CASE(TestFieldDecompilerFmoveWaitsAndKeepsTheAnimation){
  228. FieldScriptFormatter formatter;
  229. // FMOVE 0, 0, 128, -256: both bank nibbles zero, then X and Y as signed shorts.
  230. const std::string output = DecompileWith({0xAD, 0, 0x80, 0x00, 0x00, 0xFF}, formatter);
  231. BOOST_CHECK(output.find("UNIMPLMENTED INSTRUCTION: \"FMOVE\"") == std::string::npos);
  232. const size_t off = output.find("self.Door:move_auto_animation(false)");
  233. const size_t move = output.find("self.Door:move_to_position(");
  234. const size_t sync = output.find("self.Door:move_sync()");
  235. const size_t on = output.find("self.Door:move_auto_animation(true)");
  236. BOOST_REQUIRE(off != std::string::npos && move != std::string::npos);
  237. BOOST_REQUIRE(sync != std::string::npos && on != std::string::npos);
  238. // The move must end before the automatic animation comes back.
  239. BOOST_CHECK(off < move);
  240. BOOST_CHECK(move < sync);
  241. BOOST_CHECK(sync < on);
  242. }
  243. BOOST_AUTO_TEST_CASE(TestFieldDecompilerBattleKeepsItsId){
  244. FieldScriptFormatter formatter;
  245. // BATTLE 0, 0, 328 and BATTLE 0, 0, 7.
  246. const std::string output = DecompileWith({0x70, 0, 0x48, 0x01, 0x70, 0, 0x07, 0x00}, formatter);
  247. BOOST_CHECK(output.find("Battle.start(328)") != std::string::npos);
  248. BOOST_CHECK(output.find("Battle.start(7)") != std::string::npos);
  249. BOOST_CHECK(output.find("Battle.start(0)") == std::string::npos);
  250. }
  251. BOOST_AUTO_TEST_CASE(TestFieldDecompilerMessageInUndefinedWindow){
  252. // MESSAGE 0, 0 and MESSAGE 1, 0, where no WINDOW defines any of them.
  253. const std::string output = DecompileInstructions({0x40, 0, 0, 0x40, 1, 0});
  254. BOOST_CHECK(
  255. output.find("dialog:dialog_open(\"0\", 16, 176, 288, 56) -- Default, no WINDOW")
  256. != std::string::npos
  257. );
  258. BOOST_CHECK(
  259. output.find("dialog:dialog_open(\"1\", 16, 176, 288, 56) -- Default, no WINDOW")
  260. != std::string::npos
  261. );
  262. // The window must be opened before the text is set.
  263. BOOST_CHECK(
  264. output.find("dialog_open(\"0\"") < output.find("dialog:dialog_set_text(\"0\", \"synthetic_0\")")
  265. );
  266. }
  267. BOOST_AUTO_TEST_CASE(TestFieldDecompilerMessageInDefinedWindow){
  268. // WINDOW 1, 83, 120, 138, 25 and then MESSAGE 1, 0. Window 0 is not defined.
  269. const std::string output = DecompileInstructions(
  270. {0x50, 1, 83, 0, 120, 0, 138, 0, 25, 0, 0x40, 1, 0, 0x40, 0, 0}
  271. );
  272. BOOST_CHECK(output.find("dialog:dialog_open(\"1\", 83, 120, 138, 25)") != std::string::npos);
  273. BOOST_CHECK(output.find("dialog_open(\"1\", 16, 176") == std::string::npos);
  274. BOOST_CHECK(output.find("dialog_open(\"0\", 16, 176, 288, 56)") != std::string::npos);
  275. }
  276. BOOST_AUTO_TEST_CASE(TestFieldDecompilerOnUpdateRunsOnce){
  277. std::vector<unsigned char> script(104, 0);
  278. script[0] = 0x02;
  279. script[1] = 0x05;
  280. script[2] = 1;
  281. script[9] = 2;
  282. script[32] = 'D';
  283. script[33] = 'o';
  284. script[34] = 'o';
  285. script[35] = 'r';
  286. for (size_t index = 0; index < 32; ++index) script[40 + index * 2] = 104;
  287. // The first RET ends the init script (on_start), the code after it is the main script
  288. // (on_update).
  289. script.push_back(0); // RET
  290. script.push_back(0); // RET
  291. script[4] = static_cast<unsigned char>(script.size());
  292. script[5] = static_cast<unsigned char>(script.size() >> 8);
  293. FieldScriptFormatter formatter;
  294. const std::string output = FieldDecompiler::Decompile("synthetic", script, formatter).luaScript;
  295. const size_t on_update = output.find("on_update = function(self)");
  296. BOOST_REQUIRE(on_update != std::string::npos);
  297. const std::string body = output.substr(on_update);
  298. // The engine runs on_update again once it ends, but a script that returns is over.
  299. const size_t guard = body.find("if self.on_update_finished then");
  300. const size_t mark = body.find("self.on_update_finished = true");
  301. BOOST_CHECK(guard != std::string::npos);
  302. BOOST_CHECK(mark != std::string::npos);
  303. BOOST_CHECK(guard < mark);
  304. // Other functions must keep running normally.
  305. BOOST_CHECK(output.substr(0, on_update).find("on_update_finished") == std::string::npos);
  306. }
  307. BOOST_AUTO_TEST_CASE(TestFieldDecompilerScrollInstructions){
  308. std::vector<unsigned char> script(104, 0);
  309. script[0] = 0x02;
  310. script[1] = 0x05;
  311. script[2] = 1;
  312. script[9] = 2;
  313. script[32] = 'D';
  314. script[33] = 'o';
  315. script[34] = 'o';
  316. script[35] = 'r';
  317. for (size_t index = 0; index < 32; ++index) script[40 + index * 2] = 104;
  318. const unsigned char instructions[] = {
  319. 0x63, 0, 128, 0, 0, 3, // SCRLA: Smooth scroll to entity 0, in 128 frames.
  320. 0x63, 0, 0, 0, 0, 1, // SCRLA: Instant scroll to entity 0.
  321. 0x67, // SCRLW
  322. 0x62, 0, 30, 0, 2, // SCRLC: Linear scroll to the player, in 30 frames.
  323. 0x62, 0, 0, 0, 1, // SCRLC: This type has no effect.
  324. 0x65 // SCRCC
  325. };
  326. script.insert(script.end(), std::begin(instructions), std::end(instructions));
  327. script.push_back(0);
  328. script[4] = static_cast<unsigned char>(script.size());
  329. script[5] = static_cast<unsigned char>(script.size() >> 8);
  330. FieldScriptFormatter formatter;
  331. const std::string output = FieldDecompiler::Decompile("synthetic", script, formatter).luaScript;
  332. const std::string get_door = "entity_manager:get_entity(\"Door\")";
  333. BOOST_CHECK(
  334. output.find("scroll_to_entity(" + get_door + ", Background2D.SMOOTH, 4.266667)")
  335. != std::string::npos
  336. );
  337. BOOST_CHECK(
  338. output.find("scroll_to_entity(" + get_door + ", Background2D.NONE, 0.000000)")
  339. != std::string::npos
  340. );
  341. BOOST_CHECK(output.find("background2d:scroll_sync()") != std::string::npos);
  342. BOOST_CHECK(
  343. output.find("scroll_to_player(Background2D.LINEAR, 1.000000)") != std::string::npos
  344. );
  345. BOOST_CHECK(output.find("-- SCRLC without effect") != std::string::npos);
  346. BOOST_CHECK(output.find("scroll_to_player(Background2D.NONE, 0)") != std::string::npos);
  347. }