Bläddra i källkod

Merge remote-tracking branch 'Steuchs_bug_hunt/QSRC2TW'

Kevin_Smarts 3 månader sedan
förälder
incheckning
6487248242

+ 1 - 1
locations/music_actions.qsrc

@@ -331,7 +331,7 @@ if $ARGS[0] = 'willpower_cost':
         else
             $diff = 'easy'
         end
-        gs 'willpower', 'skill', 'self', pcs_perform, $diff
+        gs 'willpower', 'skill', 'self', 'pcs_perform', $diff
     end
 end
 

+ 10 - 2
qsrc2tw/package-lock.json

@@ -5,6 +5,7 @@
   "packages": {
     "": {
       "dependencies": {
+        "@types/jqueryui": "^1.12.23",
         "gridstack": "^10.3.1"
       },
       "devDependencies": {
@@ -35,17 +36,24 @@
       "version": "3.5.30",
       "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.30.tgz",
       "integrity": "sha512-nbWKkkyb919DOUxjmRVk8vwtDb0/k8FKncmUKFi+NY+QXqWltooxTrswvz4LspQwxvLdvzBN1TImr6cw3aQx2A==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@types/sizzle": "*"
       }
     },
+    "node_modules/@types/jqueryui": {
+      "version": "1.12.23",
+      "resolved": "https://registry.npmjs.org/@types/jqueryui/-/jqueryui-1.12.23.tgz",
+      "integrity": "sha512-pm1yVNVI29B9IGw41anCEzA5eR2r1pYc7flqD471ZT7B0yUXIY7YNe/zq7LGpihIGXNzWyG+Q4YQSzv2AF3fNA==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/jquery": "*"
+      }
+    },
     "node_modules/@types/sizzle": {
       "version": "2.3.8",
       "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz",
       "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/@types/twine-sugarcube": {

+ 1 - 0
qsrc2tw/package.json

@@ -7,6 +7,7 @@
   },
   "types": "twine-code/interfaces.d.ts",
   "dependencies": {
+    "@types/jqueryui": "^1.12.23",
     "gridstack": "^10.3.1"
   }
 }

+ 4 - 3
qsrc2tw/tools/QSRC2TW/index.js

@@ -9,7 +9,7 @@ import os from 'node:os';
 
 import { execSync } from 'node:child_process';
 
-const VERSION = 6;
+const VERSION = 8;
 const generatedFilesPrefix = '-generated';
 const resourcesFilesPrefix = '+resources';
 
@@ -17,7 +17,7 @@ const program = new Command();
 program
   .name('QSP TO Sugarcube')
   .description('CLI to Convert Quest Soft sourcecode to Twine Sugarcube')
-  .version('0.0.6')
+  .version('0.0.8')
   .option('-in, --input-file-path <path>','the path where the qsrc-files are')
   .option('-out, --output-file-path <path>','the path where the tw-files go')
   .option('-f, --single-file <path>','only converts the specified file')
@@ -126,7 +126,8 @@ fs.cpSync("./resources", resourcesPath, {recursive: true});
 
 
 const executionTime =  (new Date()).getTime() - startTime;
-console.log('ENDED CONVERSION'.padEnd(30,'.')+ ' '+executionTime+' ms'+` (${filePaths.length-failedFiles.length} of ${filePaths.length} successful)`);
+const executionTimeString = `${Math.floor(executionTime/3600000).toString().padStart(2,'0')}:${Math.floor(executionTime % 3600000 / 60000).toString().padStart(2,'0')}:${Math.floor(executionTime % 60000 /1000).toString().padStart(2,'0')}.${(executionTime % 1000).toString().padStart(4,'0')}`;
+console.log('ENDED CONVERSION'.padEnd(30,'.')+ ' '+executionTimeString+` (${filePaths.length-failedFiles.length} of ${filePaths.length} successful)`);
 
 //#region Versioning File
     const pathOfVersioningFile = path.join(outPath,generatedFilesPrefix,'version.js');

+ 5 - 0
qsrc2tw/tools/QSRC2TW/resources/QSP-functions/strpos.js

@@ -0,0 +1,5 @@
+setup.qsp_strpos = (s, search, ...additionalArgs) => {
+    if(additionalArgs[0])
+        throw new Error("setup.qsp_strpos with more than 2 arguments is not implemented.");
+    return s.indexOf(search)+1;
+}

+ 1 - 1
qsrc2tw/tools/QSRC2TW/resources/QSP-macros/GT.js

@@ -88,7 +88,7 @@ Macro.add('gt', {
 	skipArgs : false,
 	handler  : function () {
 		try{
-			setup.gt(...this.args);
+			setup.qsp_gt(...this.args);
 		}
 		catch (ex) {
 			return this.error('ERROR in gt-widget: ' + ex.message);

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 0
qsrc2tw/tools/QSRC2TW/resources/resources/Topbar.css


+ 8 - 0
qsrc2tw/tools/QSRC2TW/resources/resources/Topbar.tw

@@ -42,4 +42,12 @@
 			<<run UI.restart();>>
 		<</button>>
 	<</id>>
+
+	<<id 'buttonEditor'>>
+		<<button 'E'>>
+			<<set _returnArgs = $args ?? [QSP.$ARGS[0]]>>
+			<<set $editor={"passage":State.passage, "returnPassage":State.passage, "returnArgs": _returnArgs}>>
+			<<goto 'editor'>>
+		<</button>>
+	<</id>>
 </div>

+ 4 - 2
qsrc2tw/tools/QSRC2TW/src/visitor/QsrcVisitor.js

@@ -269,9 +269,11 @@ export default class QsrcVisitor extends qsrcParserVisitor{
     }
 
     visitNumberLiteralWithOptionalSign(ctx){
+        const numberText = ctx.NumberLiteral().getText();
+        const number = Number(numberText); //Catch the edge cases where a number is prefixed with 0
         if(ctx.MINUS())
-            return `-${ctx.NumberLiteral().getText()}`;
-        return ctx.NumberLiteral().getText();
+            return `-${number}`;
+        return `${number}`;
     }
 
 

+ 81 - 0
qsrc2tw/tools/QSRC2TW/src/wardrobeItems.js

@@ -0,0 +1,81 @@
+//Don't forget to add the d-flag to your regex! It's needed for the use of indices.
+function splitByRegex(text,regex, callback){
+	let result = [];
+
+	let regexResult;
+	let previousEndIndex;
+	let previousRegexResult;
+
+	while(regexResult = regex.exec(text))
+	{
+        let thisEndIndex = regexResult.indices[0][1];
+        let thisStartIndex = regexResult.indices[0][0];
+        if(previousEndIndex){
+            const previousCodeSegment = text.substring(previousEndIndex,thisStartIndex);
+			result.push(callback(previousRegexResult,previousCodeSegment));
+
+        }
+
+        previousRegexResult = regexResult;
+        previousEndIndex = thisEndIndex;
+	}
+
+
+	let thisStartIndex = text.length;
+	const previousCodeSegment = text.substring(previousEndIndex,thisStartIndex);
+	result.push(callback(previousRegexResult,previousCodeSegment));
+
+	return result;
+}
+
+function stripPrefixFromObjectKeys(obj, prefix){
+	return Object.fromEntries(Object.entries(obj).map(([key,val]) => {
+		if(key.startsWith(prefix) && key.length > prefix.length)
+			return [key.substring(prefix.length),val];
+		return [key,val];
+	}));
+}
+
+export default function wardrobeItems(code,options){
+	let firstLine = code.split("\n")[0];
+	let twCode = firstLine.replace("#","::")+`\n`;
+
+	const itemIdPrefix = firstLine.trim().replace("# $attributes_","");
+
+	let tsCode = "setup.wardrobeStatic ??= {};\n";
+
+	const propertyRegex = /^\s*(\w+)\s*=\s*(\d+)\s*$/gm;
+	const stringPropertyRegex = /^\s*\$(\w+)\s*=\s*('|")(.*?)\2\s*$/gm;
+
+	const itemData = splitByRegex(code, /(?:else)?if\s+args\[1\]\s*=\s*(\d+):/gmid,
+		(regexResult,code) => {
+			const itemId = `${itemIdPrefix}_${regexResult[1]}`;
+
+			let properties = {};
+			let propertyRegexResult;
+			while(propertyRegexResult = propertyRegex.exec(code))
+				properties[propertyRegexResult[1]] = Number(propertyRegexResult[2]);
+			while(propertyRegexResult = stringPropertyRegex.exec(code))
+				properties[propertyRegexResult[1]] = propertyRegexResult[3];
+
+			if(properties['CloQuality'])
+				properties = Object.assign({type:'clothes'},stripPrefixFromObjectKeys(properties,'Clo'));
+			else if(properties['ShoQuality'])
+				properties = Object.assign({type:'shoes'},stripPrefixFromObjectKeys(properties,'Sho'));
+			else if(properties['BraQuality'])
+				properties = Object.assign({type:'bra'},stripPrefixFromObjectKeys(properties,'Bra'));
+			else if(properties['PanQuality'])
+				properties = Object.assign({type:'bra'},stripPrefixFromObjectKeys(properties,'Pan'));
+			else if(properties['CoatQuality'])
+				properties = Object.assign({type:'coat'},stripPrefixFromObjectKeys(properties,'Coat'));
+
+			if(options.shop)
+				properties.shop = options.shop;
+
+			return `setup.wardrobeStatic['${itemId}'] = ${JSON.stringify(properties)}\n`
+
+		});
+	tsCode += itemData.join("");
+
+	return [twCode,tsCode];
+}

+ 22 - 9
qsrc2tw/tools/QSRC2TW/task_processor.js

@@ -7,6 +7,7 @@ import path from "path";
 import defaultProcess from "./src/defaultProcess.js";
 import npcInit from "./src/npcInit.js";
 import skillDefinitions from "./src/skillDefinitions.js";
+import wardrobeItems from "./src/wardrobeItems.js";
 
 
 
@@ -64,20 +65,32 @@ function convertFile(task){
 		//#endregion
 
 		var convertMode = "default";
-
-		if(qsp2twOptions.startsWith("!! QSRC2TW_module")){
+		var convertModeOptions = {};
+		/*if(qsp2twOptions.startsWith("!! QSRC2TW_module")){
 			convertMode = qsp2twOptions.trim().split(" ").toReversed()[0];
+		}*/
+		const modeLookupRegex = /\s*!!\s*QSRC2TW_module\s+(\w+)(\s+{[^}]+})?/;
+		let modeLookupResult;
+		try{
+			if(modeLookupResult = modeLookupRegex.exec(qsp2twOptions)){
+				convertMode = modeLookupResult[1];
+				if(modeLookupResult[2])
+					convertModeOptions = JSON.parse(modeLookupResult[2]);
+			}
+		}
+		catch(e){
+			console.log(e.toString());
 		}
-
 
 		/**
 		 * Return value is Array [TwineCode, TSCode]. TwineCode must not be null.
 		 */
-		var convertFunction = (code)=>[null,null];
+		var convertFunction = (code,options)=>[null,null];
 		switch (convertMode) {
-			case "default": convertFunction = (code) => [defaultProcess(code),null]; break;
-			case "npcInit": convertFunction = (code) => npcInit(code); break;
-			case "stat_sklattrib_lvlset": convertFunction = (code) => skillDefinitions(code); break;
+			case "default": convertFunction = (code,options) => [defaultProcess(code),null]; break;
+			case "npcInit": convertFunction = (code,options) => npcInit(code); break;
+			case "stat_sklattrib_lvlset": convertFunction = (code,options) => skillDefinitions(code); break;
+			case "wardrobeItems": convertFunction = (code,options) => wardrobeItems(code,options); break;
 			default:
 				console.warn("Unreckognized Convert Mode");
 				break;
@@ -93,7 +106,7 @@ function convertFile(task){
 			if(!options.verboseErrors)
 				console = {log:(...args)=>{}, debug: (...args)=>{},warn:(...args)=>{}, error: (...args)=>{}};
 			try{
-				[twineCodeRaw,tsCodeRaw] = convertFunction(data);
+				[twineCodeRaw,tsCodeRaw] = convertFunction(data,convertModeOptions);
 			}
 			catch(e){
 				throw e;
@@ -102,7 +115,7 @@ function convertFile(task){
 				console = defaultConsole;
 			}
 			if(!twineCodeRaw){
-				console.error("Twine Code must be generated by every converMode");
+				console.error("Twine Code must be generated by every convertMode");
 				return [2,"Invalid convertFunction"];
 			}else{
 				twineCode = twineCodeRaw.split('\n')

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
qsrc2tw/tools/tweeGo/storyFormats/sugarcube-2/format.js


+ 81 - 0
qsrc2tw/twine-code/editor/Editor.css

@@ -0,0 +1,81 @@
+#passages:has(> #passage-editor){
+	grid-column: 1 / -1;
+	grid-row: 2 / -1;
+	z-index: 10;
+	padding: 0;
+	position: relative;
+}
+
+#passage-editor{
+	position: absolute;
+	display: grid;
+	grid-template-rows: auto 1fr;
+	grid-template-columns: 1fr repeat(3, min-content);
+	width: 100%;
+	height: 100%;
+}
+
+#passage-editor #commonTasks div{
+	margin-bottom: 1em;
+}
+
+#passage-editor #commonTasks div:hover {
+	background: #ccc;
+}
+
+#editorToolbar {
+	grid-column: 1 / -1;
+}
+
+
+.collapsable>h2{
+	cursor: pointer;
+}
+
+.collapsable h3 {
+	margin: 1em 0 0 0;
+	display: inline-block;
+	min-width: 6vw;
+}
+
+.collapsable{
+	--transitionSpeed: 0.3s;
+	transition: var(--transitionSpeed);
+	width: 30vw;
+	width: calc(30vw - 1em);
+	padding: 0 1em;
+	overflow: auto;
+}
+
+.collapsable.collapsed{
+	width: 2rem;
+	padding: 0;
+}
+
+.collapsable.collapsed>*{
+	display: none;
+}
+
+.collapsable.collapsed > h2:first-of-type {
+	font-size: 1em;
+	writing-mode: vertical-rl;
+	margin: 0;
+	transition: var(--transitionSpeed);
+	display: block;
+}
+
+.diff{
+	font-size: 0.7em;
+}
+
+.diff p{
+	margin: 0;
+}
+
+.diff p.addition{
+	color: rgb(0, 160, 0);
+}
+
+.diff p.substraction {
+	color: rgb(160,0, 0);
+}

+ 15 - 0
qsrc2tw/twine-code/editor/Editor.js

@@ -0,0 +1,15 @@
+
+setup.startEditor = (domId, text) =>{
+	setup.externalCodePromise('ace').then(()=>{
+		const editor = ace.edit(domId);
+		editor.setOption("showInvisibles", true);
+		editor.setTheme("ace/theme/chrome");
+		editor.session.setMode("ace/mode/html");
+		editor.session.setUseWorker(false);
+		editor.setValue(text);
+
+		setup.editor = editor;
+	});
+
+
+}

+ 121 - 0
qsrc2tw/twine-code/editor/Editor.tw

@@ -0,0 +1,121 @@
+:: editor
+
+<<set _changedFiles = JSON.parse(localStorage.getItem("SweetCube.Modifications") ?? '{}')>>
+
+<div id="editorToolbar">
+	<<=$editor.passage>>
+
+	<<button 'Save'>>
+		<<run setup.editorSave($editor.passage,setup.editor.getValue())>>
+	<</button>>
+	<<button 'Return'>>
+		<<gt $editor.returnPassage $editor.returnArgs[0]>>
+	<</button>>
+
+	<<set _allPassageNames = Story.lookupWith((p)=>true).map((p)=>p.title)>>
+	<<autocomplete "_openPassageName" _allPassageNames>>
+		<<set $editor.passage = _openPassageName>>
+		<<goto 'editor'>>
+	<</autocomplete>>
+</div>
+
+<div id="editor">
+
+</div>
+
+<div id="commonTasks" class="collapsable collapsed">
+	<h2>Common Tasks</h2>
+	<div class="comment">
+		This is an example of help that could be provided here.
+	</div>
+	<h3>Passage Links</h3>
+	<h4>Goto</h4>
+	<div>
+		You use a goto-command to get from one passage to another.
+		Each goto-command creates a new entry in the history.
+		The player can navigate between these steps using the arrow-buttons in the Topbar (if this isn't disabled).
+		This system breaks if goto-commands are chained.
+		<b>You should therefore avoid executing a goto-commands without user-input!</b>
+	</div>
+	<div>
+		<code>&lt;&lt;goto 'passage2'&gt;&gt;</code><br/>
+		This is the SugarCube-native way to execute a goto-command. Use it if no arguments have to be sent to the next passage.
+	</div>
+	<div>
+		<code>&lt;&lt;gt 'passage2' 'arg0' 'arg1'&gt;&gt;</code><br/>
+		This command behaves as it does in QSP: send the user to a passage and set some arguments for the receiving passage.
+		Not that the <b>arguments are not separated by commas</b>.
+	</div>
+	<div>
+		<code>&lt;&lt;run setup.qsp_gt('passage2' , 'arg0' , 'arg1')&gt;&gt;</code><br/>
+		This is the syntax you'll find in the auto-translated files.
+		It provides support for some weird edge cases (mostly concerning dynamic code).
+		If you don't need that, use one of the two previous approaches.
+	</div>
+</div>
+
+<div id="passageLinks" class="collapsable collapsed">
+	<h2>Passage Links</h2>
+	<<set _passageLinks = setup.getPassageLinks($editor.passage)>>
+
+	<<for _passageId, _passageLinkArray range _passageLinks>>
+		<<capture _passageId>>
+		<div>
+			<<link _passageId>>
+				<<set $editor.passage = _passageId>>
+				<<goto 'editor'>>
+			<</link>>: <<=_passageLinkArray.length>>x
+		</div>
+		<</capture>>
+	<</for>>
+
+	<h2>Links to this passage</h2>
+	<<set _passageLinksToCurrentPassage = setup.getPassageLinksToPassage($editor.passage)>>
+	<<for _passageId range _passageLinksToCurrentPassage>>
+		<<capture _passageId>>
+		<div>
+			<<link _passageId>>
+				<<set $editor.passage = _passageId>>
+				<<goto 'editor'>>
+			<</link>>
+		</div>
+		<</capture>>
+	<</for>>
+
+	<div class="comment">
+		These lists might be incomplete. Some connections can not be detected automatically or it isn't efficient to do so.
+	</div>
+</div>
+
+<div id="changedFiles" class="collapsable collapsed">
+	<h2>Changed Files</h2>
+	<div class="changedPassages">
+		<<for _passage, _changes range _changedFiles>>
+			<<set _diffId = "diff_"+_passage>>
+			<<capture _passage _changes _diffId>>
+				<div class="changedPassage">
+					<h3 class="passageName">_passage</h3>
+					<<link "Open">>
+						<<set $editor.passage=_passage>>
+						<<goto 'editor'>>
+					<</link>>
+					<<linkreplace "Diff">>
+						<<run setup.placeDiffInDom(_passage,window.SweetCube.originalPassageContents[_passage],_changes,"#"+_diffId)>>
+					<</linkreplace>>
+					<<link "Delete">>
+						<<run delete _changedFiles[_passage]>>
+						<<run localStorage.setItem("SweetCube.Modifications",JSON.stringify(_changedFiles))>>
+						<<run location.reload()>>
+					<</link>>
+					<code @id=_diffId class="diff"></code>
+				</div>
+			<</capture>>
+		<</for>>
+	</div>
+</div>
+
+<<done>>
+	<<run setup.startEditor('editor',Story.get($editor.passage).text)>>
+	<<run $(".collapsable > h2:first-of-type").on("click",function() {$(this).parent('.collapsable').toggleClass('collapsed');})>>
+<</done>>
+

+ 35 - 0
qsrc2tw/twine-code/editor/diff.js

@@ -0,0 +1,35 @@
+setup.placeDiffInDom = (title, oldContent, newContent, domSelector) => {
+	setup.externalCodePromise('diff').then(()=>{
+
+		const escapedChars = {
+			/*'&' : '&amp;',
+			'<' : '&lt;',
+			'>' : '&gt;',*/
+			'"' : '&quot;',
+			"'" : '&#39;',
+			'`' : '&#96;'
+		};
+
+		for(const [toBeEscaped, asEscaped] of Object.entries(escapedChars))
+        	oldContent = oldContent.replaceAll(toBeEscaped,asEscaped);
+
+		const unifiedDiffPatch = window.Diff.createTwoFilesPatch(title,title,oldContent,newContent,undefined,undefined,{ignoreWhitespace: true, stripTrailingCr: true});
+
+		let resultingHTML = "";
+
+		unifiedDiffPatch.split('\n').forEach((line)=>{
+			if(line.startsWith('@@')){
+				resultingHTML += `<p>${line}</p>`;
+			}else if(line.startsWith('---')){
+			}else if(line.startsWith('+++')){
+			}else if(line.startsWith('-')){
+				resultingHTML += `<p class="substraction">${line}</p>`;
+			}else if(line.startsWith('+')){
+				resultingHTML += `<p class="addition">${line}</p>`;
+			}
+
+		});
+
+		$(domSelector).html(resultingHTML);
+	});
+}

+ 30 - 0
qsrc2tw/twine-code/editor/save.js

@@ -0,0 +1,30 @@
+
+setup.editorSave = (passage,newContent) => {
+    const escapedChars = {
+        '&' : '&amp;',
+		'<' : '&lt;',
+		'>' : '&gt;',
+		'"' : '&quot;',
+		"'" : '&#39;',
+		'`' : '&#96;'
+    };
+
+    let sanitizedNewContent = newContent;
+
+    for(const [toBeEscaped, asEscaped] of Object.entries(escapedChars))
+        sanitizedNewContent = sanitizedNewContent.replaceAll(toBeEscaped,asEscaped);
+
+
+    const localStorageModsKey = "SweetCube.Modifications";
+
+    const updateObject = {};
+    updateObject[passage] = sanitizedNewContent;
+
+    const currentModificationsString = localStorage.getItem(localStorageModsKey) ?? "{}";
+    const currentModifications = JSON.parse(currentModificationsString);
+    const newModifications = Object.assign({},currentModifications,updateObject);
+    const newModificationsString = JSON.stringify(newModifications);
+    localStorage.setItem(localStorageModsKey, newModificationsString);
+
+    location.reload();
+}

+ 14 - 0
qsrc2tw/twine-code/external/_external.ts

@@ -0,0 +1,14 @@
+setup.externalCodeURLs ??= {};
+setup.externalCodePromises ??= {};
+
+setup.externalCodePromise = (promiseID: string)=>{
+	if(!setup.externalCodeURLs[promiseID])
+		return Promise.resolve();
+	setup.externalCodePromises[promiseID] ??= Promise.all(
+		[
+			importScripts(setup.externalCodeURLs[promiseID].code as unknown as string), // Casting to unknown because of a weird bug in the types
+			importStyles(...setup.externalCodeURLs[promiseID].style),
+		]
+	);
+	return setup.externalCodePromises[promiseID];
+}

+ 8 - 0
qsrc2tw/twine-code/external/ace.ts

@@ -0,0 +1,8 @@
+/// <reference path="./_external.ts" />
+setup.externalCodeURLs.ace = {
+    code: [
+        "https://cdn.jsdelivr.net/npm/[email protected]/src-min-noconflict/ace.js",
+        "https://cdn.jsdelivr.net/npm/[email protected]/src-min-noconflict/theme-chrome.min.js",
+    ],
+    style:["https://cdn.jsdelivr.net/npm/[email protected]/css/ace.min.css"],
+};

+ 5 - 0
qsrc2tw/twine-code/external/diff.ts

@@ -0,0 +1,5 @@
+/// <reference path="./_external.ts" />
+setup.externalCodeURLs.diff = {
+    code: ["https://cdn.jsdelivr.net/npm/[email protected]/dist/diff.min.js"],
+    style:[],
+};

+ 5 - 0
qsrc2tw/twine-code/external/jQueryUI.ts

@@ -0,0 +1,5 @@
+/// <reference path="./_external.ts" />
+setup.externalCodeURLs.jQueryUI = {
+    code: ["https://code.jquery.com/ui/1.14.0/jquery-ui.min.js"],
+    style:["https://code.jquery.com/ui/1.14.0/themes/base/jquery-ui.css"],
+};

+ 40 - 0
qsrc2tw/twine-code/misc/getPassageLinks.ts

@@ -0,0 +1,40 @@
+const passageLinkMarkups:{[key:string]:{type:string, regex: RegExp}} = {
+	'gs-macro': 	{type: 'gs',	regex: /<<gs\s+`\['([^']+)'[^\]]*\]`\s*>>/gd},
+	'gt-function':	{type: 'gt',	regex: /setup\.qsp_gt\('([^']+)'[^\)]*\s*\)/gd},
+
+	'goto':			{type: 'gt',	regex: /<<goto\s+'([^']+)'\s*>>/gd},
+	'include':		{type: 'gs',	regex: /<<include\s+'([^']+)'\s*>>/gd},
+};
+
+setup.getPassageLinks = function(passageId:string){
+	let result:{[passageId: string]: {type: string;line: number;}[];} = {};
+
+	const passageCode = Story.get(passageId).text;
+
+	for(const[key, markupSettings] of Object.entries(passageLinkMarkups)){
+		const regex = markupSettings.regex;
+		let regexMatch;
+		while(regexMatch = regex.exec(passageCode)) {
+			const passageId = regexMatch[1];
+			result[passageId] ??= [];
+			result[passageId].push({line:-1, type: markupSettings.type});
+		}
+	}
+
+	return result;
+}
+
+setup.getPassageLinksToPassage = function(passageId:string){
+	function anyPassageLinkMarkupMatches(text:string):boolean{
+		for(const[key, markupSettings] of Object.entries(passageLinkMarkups)){
+			let regexMatch;
+			while(regexMatch = markupSettings.regex.exec(text))
+				if(regexMatch[1] == passageId)
+					return true;
+		}
+		return false;
+	}
+
+	return Story.lookupWith(function(p){return anyPassageLinkMarkupMatches(p.text)}).map((passage)=>passage.title);
+
+}

+ 30 - 0
qsrc2tw/twine-code/misc/head.txt

@@ -1 +1,31 @@
 <meta name="rating" content="adult" />
+<script>
+	window.SweetCube ??= {passageInitOverwrites:[], originalPassageContents:{}};
+
+    function applyModifications(modifications={}){
+		function applyModification(passage, modification){
+			passage.element.innerHTML = modification;
+			return passage;
+		}
+
+
+		window.SweetCube.passageInitOverwrites.push((pid,passage)=>{
+			const passageName = passage?.element?.attributes?.name?.value;
+			if(!passageName || !modifications[passageName])
+				return passage;
+
+			window.SweetCube.originalPassageContents[passageName] = passage.element.innerHTML;
+
+			const modification = modifications[passageName];
+
+			return applyModification(passage,modification);
+		});
+    }
+
+	const modificationsString = localStorage.getItem("SweetCube.Modifications");
+	if(modificationsString){
+		const modifications = JSON.parse(modificationsString);
+		applyModifications(modifications);
+	}
+
+</script>

+ 1 - 1
qsrc2tw/twine-code/start/begin.tw

@@ -15,7 +15,7 @@
 	<<SHOWSTAT 1>>
 	<h2>CHOOSE GAME START</h2>
 	<div id="beginScenarioSelection" style="display: flex;justify-content: center;">
-		<<gs `['beginScenarioSelection']`>>
+		<<gs `['beginscenarioselection']`>>
 	</div>
 	<p>
 		There are three main start types:

+ 10 - 0
qsrc2tw/twine-code/twine-code.d.ts

@@ -63,6 +63,16 @@ declare module "twine-sugarcube" {
 
         qsp_dyneval: (code:string, ...args : (string|number)[]) => string|number;
         qsp_func: (passage:string,...arguments:(string|number)[]) => string|number;
+
+
+        //#region External Code
+            externalCodeURLs: {[key:string]:{code:string[],style:string[]}};
+            externalCodePromises: {[key:string]:Promise<any>};
+            externalCodePromise: (promiseId:string)=>Promise<any>;
+        //#endregion
+
+        getPassageLinks: (passageId:string) => {[passageId:string]:{type:string;line:number}[]};
+        getPassageLinksToPassage: (passageId:string) => string[];
     }
 
     export interface SugarCubeStoryVariables{

+ 36 - 0
qsrc2tw/twine-code/ui/autoComplete.ts

@@ -0,0 +1,36 @@
+Macro.add('autocomplete', {
+	skipArgs : false,
+    tags: [],
+	handler  : function () {
+		try{
+
+            const varname = this.args[0];
+            const domID = `autoComplete_${varname.replaceAll("$","--")}`;
+            const tags = this.args[1];
+
+            const innerCode = this.payload[0].contents;
+
+            $(this.output).wiki(`<<id '${domID}'>><<textbox "${varname}" "">><</id>>`);
+
+            $(document).one(':passagedisplay', function (ev) {
+                setup.externalCodePromise('jQueryUI').then(function () {
+                    $(`#${domID}`).autocomplete({
+                        source: tags,
+                        select: (event, ui) => {
+                            const v = ui.item.value;
+                            State.setVar(varname,v);
+                            $.wiki(innerCode);
+                        }
+                    });
+
+                });
+            });
+		}
+		catch (ex) {
+			return this.error('ERROR in gt-widget: ' + ex.message);
+		}
+	}
+});
+
+
+

Vissa filer visades inte eftersom för många filer har ändrats