index.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. import qsrc2tw from "./src/qsrc2tw.js";
  2. import npcInit from "./src/npcInit.js";
  3. import skillDefinitions from "./src/skillDefinitions.js";
  4. import {glob} from 'glob';
  5. import fs from 'node:fs';
  6. import path from "path";
  7. import { Command } from "commander";
  8. import md5 from "./src/misc/md5.js";
  9. const VERSION = 1;
  10. const generatedFilesPrefix = '-generated';
  11. const resourcesFilesPrefix = '+resources';
  12. const program = new Command();
  13. program
  14. .name('QSP TO Sugarcube')
  15. .description('CLI to Convert Quest Soft sourcecode to Twine Sugarcube')
  16. .version('0.0.1')
  17. .option('-in, --input-file-path <path>','the path where the qsrc-files are')
  18. .option('-out, --output-file-path <path>','the path where the tw-files go')
  19. .option('-f, --single-file <path>','only converts the specified file')
  20. .option('-fs, --multiple-files-from-file <path>','only converts the files specified in the input file')
  21. .option('-outff, --failed-files-output-path <path>','path of the failed files file')
  22. .option('-sff, --skip-failedfiles-file','skips creation of the failed files file')
  23. .option('-ve, --verbose-errors','prints out complete error messages where possible')
  24. ;
  25. program.parse(process.argv);
  26. const options = program.opts();
  27. console.log('START CONVERTION');
  28. const startTime = (new Date()).getTime();
  29. const inPath = options.inputFilePath ?? './in';
  30. const outPath = options.outputFilePath ?? './out';
  31. const failedFilesPath = options.failedFilesOutputPath ?? "./failedFiles.log";
  32. var filePaths = [];
  33. try{
  34. if(options.singleFile)
  35. filePaths = [options.singleFile];
  36. else if(options.multipleFilesFromFile){
  37. let multipleFilesFromFilePath = options.multipleFilesFromFile;
  38. if(multipleFilesFromFilePath == "ff")
  39. multipleFilesFromFilePath = failedFilesPath;
  40. const data = fs.readFileSync(multipleFilesFromFilePath, 'utf8');
  41. filePaths = data.split('\n');
  42. }
  43. else{
  44. const searchArgument = path.join(inPath,'**/*.qsrc');
  45. filePaths = await glob(searchArgument.replace(/\\/g,'/'));
  46. }
  47. }catch(e){
  48. console.error("Error retrieving filePaths:",e);
  49. }
  50. fs.mkdir(path.join(outPath,generatedFilesPrefix), { recursive: true }, (err) => {
  51. if (err) throw err;
  52. });
  53. //https://stackoverflow.com/a/25221100/7200161
  54. function baseFileName(fullpath){
  55. return fullpath.split('\\').pop().split('/').pop();
  56. }
  57. let consoleActive = true;
  58. consoleOverwrite();
  59. function consoleOverwrite(){
  60. //https://stackoverflow.com/a/30197438/7200161
  61. // define a new console
  62. var consoleOverwrite=(function(oldCons){
  63. return {
  64. log: function(text){
  65. if(consoleActive)
  66. oldCons.log(text);
  67. },
  68. info: function (text) {
  69. if(consoleActive)
  70. oldCons.info(text);
  71. },
  72. warn: function (text) {
  73. if(consoleActive)
  74. oldCons.warn(text);
  75. },
  76. error: function (text) {
  77. if(consoleActive)
  78. oldCons.error(text);
  79. }
  80. };
  81. }(console));
  82. //Then redefine the old console
  83. console = consoleOverwrite;
  84. }
  85. async function convertFile(filePath){
  86. return new Promise((resolve, reject) => {
  87. fs.readFile(filePath, 'utf8', (err, data) => {
  88. if (err) {
  89. console.error(err);
  90. reject(err);
  91. return;
  92. }
  93. const startTime = (new Date()).getTime();
  94. const baseFileNameStr = baseFileName(filePath);
  95. const outFilePath = path.join(outPath,generatedFilesPrefix,baseFileNameStr.split('.')[0]+'.tw');
  96. const outFilePathTS = path.join(outPath,generatedFilesPrefix,baseFileNameStr.split('.')[0]+'.ts');
  97. const qsp2twOptions = data.split('\n')?.[1];
  98. if(qsp2twOptions.startsWith("!! SKIP_QSRC2TW")){
  99. if(fs.existsSync(outFilePath))
  100. fs.unlink(outFilePath,(err) => {if (err) throw err;});
  101. if(fs.existsSync(outFilePathTS))
  102. fs.unlink(outFilePathTS,(err) => {if (err) throw err;});
  103. resolve("SKIP");
  104. return;
  105. }
  106. //#region Skip File Output if outfile exists, is based on the same codebase (determined by hash) and has used the same compier-version
  107. const codeHash = md5(data);
  108. if (fs.existsSync(outFilePath)) {
  109. try{
  110. const secondLineData = fs.readFileSync(outFilePath, "utf-8").split('\n')[1];
  111. const qsrc2twResultMatch = secondLineData.match(/<!--\s*qsrc2twResult=({.*})\s*-->/);
  112. if(qsrc2twResultMatch){
  113. const qsrc2twResult = JSON.parse(qsrc2twResultMatch[1]);
  114. if((qsrc2twResult.code && codeHash == qsrc2twResult.code) &&
  115. (qsrc2twResult.version && VERSION == qsrc2twResult.version)){
  116. resolve("EXISTS");
  117. return;
  118. }
  119. }
  120. }
  121. catch(e){
  122. //Something unexpected happens. No need to handle this, because we'll just run the default file-processing.
  123. }
  124. }
  125. //#endregion
  126. var convertMode = "default";
  127. if(qsp2twOptions.startsWith("!! QSRC2TW_module")){
  128. convertMode = qsp2twOptions.trim().split(" ").toReversed()[0];
  129. }
  130. /**
  131. * Return value is Array [TwineCode, TSCode]. TwineCode must not be null.
  132. */
  133. var convertFunction = (code)=>[null,null];
  134. switch (convertMode) {
  135. case "default": convertFunction = (code) => [qsrc2tw(code, true),null]; break;
  136. case "npcInit": convertFunction = (code) => npcInit(code); break;
  137. case "stat_sklattrib_lvlset": convertFunction = (code) => skillDefinitions(code); break;
  138. default:
  139. console.warn("Unreckognized Convert Mode");
  140. break;
  141. }
  142. try{
  143. let twineCode = "";
  144. consoleActive = options.verboseErrors;
  145. let twineCodeRaw = undefined;
  146. let tsCodeRaw = undefined;
  147. try{
  148. [twineCodeRaw,tsCodeRaw] = convertFunction(data);
  149. }
  150. catch(e){
  151. throw e;
  152. }
  153. finally{
  154. consoleActive = true;
  155. }
  156. if(!twineCodeRaw){
  157. console.error("Twine Code must be generated by every converMode");
  158. reject("Invalid convertFunction");
  159. return;
  160. }else{
  161. twineCode = twineCodeRaw.split('\n')
  162. .toSpliced(1,0,`<!--qsrc2twResult={"version":${VERSION},"code":"${codeHash}","time":"${(new Date().toISOString())}"}-->`)
  163. .join('\n');
  164. fs.writeFile(outFilePath, twineCode, err => {
  165. if (err) {
  166. console.error(err);
  167. } else {
  168. }
  169. });
  170. }
  171. if(!tsCodeRaw){
  172. if(fs.existsSync(outFilePathTS))
  173. fs.unlink(outFilePathTS,(err) => {if (err) throw err;});
  174. }else{
  175. fs.writeFile(outFilePathTS, tsCodeRaw, err => {
  176. if (err) {
  177. console.error(err);
  178. } else {
  179. }
  180. });
  181. }
  182. const executionTime = (new Date()).getTime() - startTime;
  183. console.log(`${baseFileNameStr.padEnd(30,'.')} ${executionTime} ms`);
  184. resolve("SUCCESS");
  185. }catch(e){
  186. console.error(`Error in "${baseFileNameStr}". No output was generated`, e);
  187. reject(e);
  188. }
  189. });
  190. });
  191. }
  192. const failedFiles = [];
  193. var promises = [];
  194. for(let filePath of filePaths){
  195. const convertPromise = convertFile(filePath);
  196. promises.push(convertPromise);
  197. convertPromise.catch((e)=>{
  198. failedFiles.push(filePath);
  199. });
  200. }
  201. const allPromises = Promise.allSettled(promises);
  202. await allPromises;
  203. if(!options.skipFailedfilesFile){
  204. const contentsOfFailedFilesFile = failedFiles.sort((a, b) => a.localeCompare(b)).join("\n");
  205. fs.writeFile(failedFilesPath, contentsOfFailedFilesFile , err => {
  206. if (err) {
  207. console.error(err);
  208. } else {
  209. // file written successfully
  210. }
  211. });
  212. }
  213. const resourcesPath = path.join(outPath,resourcesFilesPrefix);
  214. fs.rmSync(resourcesPath, { recursive: true, force: true });
  215. fs.cpSync("./resources", resourcesPath, {recursive: true});
  216. const executionTime = (new Date()).getTime() - startTime;
  217. console.log('ENDED CONVERSION'.padEnd(30,'.')+ ' '+executionTime+' ms');