From 43fda27475ee8e42f3ce5e90da616639e45896ea Mon Sep 17 00:00:00 2001 From: YellowAfterlife Date: Wed, 11 Nov 2020 15:08:26 +0200 Subject: [PATCH] Added GMS2.3 support. --- src/GenExt2.hx | 119 +++++++++++++++++-- src/YyBuf.hx | 61 ++++++++-- src/YyExtension.hx | 3 +- src/YyJsonParser.hx | 282 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 439 insertions(+), 26 deletions(-) create mode 100644 src/YyJsonParser.hx diff --git a/src/GenExt2.hx b/src/GenExt2.hx index be5fdd6..d79dc8e 100644 --- a/src/GenExt2.hx +++ b/src/GenExt2.hx @@ -1,6 +1,7 @@ package; import haxe.Json; import haxe.io.Path; +import sys.FileSystem; import sys.io.File; import file.*; import YyExtension; @@ -12,15 +13,17 @@ import YyExtension; class GenExt2 extends GenExt { public var json:String; public var yyExt:YyExtension; + public var v23:Bool; override public function proc(filter:Array) { var dir = Path.directory(path); if (FileSystem.exists(path + ".base")) { json = File.getContent(path + ".base"); } else json = File.getContent(path); + v23 = json.indexOf('"resourceType": "GMExtension"') >= 0; // GMS2 uses non-spec int64s in extensions JSON json = ~/("copyToTargets":\s*)(\d{12,32})/g.replace(json, '$1"$2"'); // - yyExt = Json.parse(json); + yyExt = YyJsonParser.parse(json); for (file in yyExt.files) { var q:GenFile; var filePath = Path.join([dir, file.filename]); @@ -39,7 +42,9 @@ class GenExt2 extends GenExt { q.functions.push(gf); } for (ym in file.constants) { - var gm = new GenMacro(ym.constantName, ym.value, ym.hidden, 0); + var ymName = ym.name; + if (ymName == null) ymName = ym.constantName; + var gm = new GenMacro(ymName, ym.value, ym.hidden, 0); q.macros.push(gm); } } @@ -48,15 +53,7 @@ class GenExt2 extends GenExt { files.push(q); } } - override public function flush():Void { - var out = new YyBuf(); - var filesStartStr = '"files": ['; - var filesStart = json.indexOf(filesStartStr); - if (filesStart < 0) throw "Your extension doesn't have an array of files in it."; - if (json.indexOf("\r\n") < 0) out.newLine = "\n"; - out.addString(json.substring(0, filesStart + filesStartStr.length)); - out.depth = 2; - out.addLine(0); + function flush22(out:YyBuf):Void { for (q in files) { out.addSep(); out.objectOpen(); @@ -124,8 +121,106 @@ class GenExt2 extends GenExt { out.addPair("uncompress", d.uncompress); out.objectClose(); } + } + function flush23(out:YyBuf):Void { + var extName:String = yyExt.name; + var extPath = 'extensions/$extName/$extName.yy'; + var fileSep = false; + for (q in files) { + if (fileSep) out.addLine(); else fileSep = true; + out.objectOpen(); + var d:YyExtensionFile = q.data; + out.addPair("filename", d.filename); + out.addPair("origname", d.origname); + out.addPair("init", d.init); + out.addPair("final", Reflect.field(d, "final")); + out.addPair("kind", d.kind); + out.addPair("uncompress", d.uncompress); + // + out.addField("functions"); + out.arrayOpen(); + var fkin = q.funcKind; + var funcSep = false; + for (qf in q.functions) { + if (funcSep) out.addLine(); else funcSep = true; + out.objectOpen(); + out.addPair("externalName", qf.extName); + out.addPair("kind", qf.comp == null ? 11 : fkin); + out.addPair("help", qf.comp != null ? qf.comp : ""); + out.addPair("hidden", qf.comp == null); + out.addPair("returnType", qf.retType); + out.addPair("argCount", qf.argCount); + out.addPair("args", qf.argTypes); + out.addPair("resourceVersion", "1.0"); + out.addPair("name", qf.name); + out.addPair("tags", []); + out.addPair("resourceType", "GMExtensionFunction"); + out.objectClose(); + out.add(","); + } + out.arrayClose(); + out.addFieldEnd(); + // + out.addField("constants"); + out.arrayOpen(); + var macroSep = false; + for (qm in q.macros) { + if (macroSep) out.addLine(); else macroSep = true; + out.objectOpen(); + out.addPair("value", qm.value); + out.addPair("hidden", qm.hide); + out.addPair("resourceVersion", "1.0"); + out.addPair("name", qm.name); + out.addPair("tags", []); + out.addPair("resourceType", "GMExtensionConstant"); + out.objectClose(); + out.add(","); + } + out.arrayClose(); + out.addFieldEnd(); + // + out.addPair("ProxyFiles", d.ProxyFiles); + out.addPair("copyToTargets", d.copyToTargets); + // + out.addField("order"); + out.arrayOpen(); + var orderSep = false; + for (qf in q.functions) { + if (orderSep) out.addLine(); else orderSep = true; + out.objectOpen(); + out.addPair("name", qf.name); + out.addPair("path", extPath); + out.objectClose(); + out.add(","); + } + out.arrayClose(); + out.addFieldEnd(); + // + out.addPair("resourceVersion", "1.0"); + out.addPair("name", ""); + out.addPair("tags", []); + out.addPair("resourceType", "GMExtensionFile"); + // + out.objectClose(); + out.addFieldEnd(); + } + } + override public function flush():Void { + var out = new YyBuf(v23); + var filesStartStr = '"files": ['; + var filesStart = json.indexOf(filesStartStr); + if (filesStart < 0) throw "Your extension doesn't have an array of files in it."; + // + if (json.indexOf("\r\n") < 0) out.newLine = "\n"; + out.addString(json.substring(0, filesStart + filesStartStr.length)); + out.depth = 2; + out.addLine(0); + // + if (v23) { + flush23(out); + } else flush22(out); // - var filesEnd = json.indexOf('\n ],', filesStart); + var filesEnd = json.indexOf(v23 ? '\n ],' : '\n ],', filesStart); if (filesEnd < 0) throw "Your extension doesn't have a well-balanced end of files array in it. It might be malformed."; out.addString(json.substring(filesEnd)); json = out.toString(); diff --git a/src/YyBuf.hx b/src/YyBuf.hx index 99a2d22..a153dfb 100644 --- a/src/YyBuf.hx +++ b/src/YyBuf.hx @@ -1,4 +1,5 @@ package; +import haxe.DynamicAccess; import haxe.Json; /** @@ -9,12 +10,21 @@ class YyBuf extends StringBuf { public var newLine:String = "\r\n"; public var depth:Int = 0; public var sep:Array = [false]; + public var v23:Bool; + public function new(v23:Bool) { + super(); + this.v23 = v23; + } public function addLine(d:Int = 0) { add(newLine); if (d > 0) sep.push(false); if (d < 0) sep.pop(); depth += d; - for (i in 0 ... depth) add(" "); + if (v23) { + for (i in 0 ... depth) add(" "); + } else { + for (i in 0 ... depth) add(" "); + } } // public inline function addString(s:String) { @@ -28,31 +38,52 @@ class YyBuf extends StringBuf { } else sep[i] = true; } public function addField(fd:String) { - addSep(); + if (!v23) addSep(); addChar('"'.code); addString(fd); - addString('": '); + addString(v23 ? '":' : '": '); + } + public function addFieldEnd() { + if (v23) addString(","); } public function addValue(val:Dynamic) { if (Std.is(val, Array)) { var arr:Array = val; - arrayOpen(); - for (v in arr) { - addSep(); - addValue(v); + if (v23 && arr.length == 0) { + addString("[]"); + } else { + arrayOpen(); + for (v in arr) { + addSep(); + addValue(v); + } + if (v23) add(","); + arrayClose(); } - arrayClose(); - } else if (Std.is(val, String)) { + } + else if (Std.is(val, String)) { var s = Json.stringify(val); - s = StringTools.replace(s, "/", "\\/"); // off-spec + if (!v23) s = StringTools.replace(s, "/", "\\/"); // off-spec addString(s); - } else { + } + else if (Reflect.isObject(val)) { + var fields = Reflect.fields(val); + if (fields.length != 0) { + objectOpen(); + for (f in fields) { + addPair(f, Reflect.field(val, f)); + } + objectClose(); + } else addString("{}"); + } + else { addString(Json.stringify(val)); } } public function addPair(fd:String, val:Dynamic) { addField(fd); addValue(val); + addFieldEnd(); } // public function arrayOpen() { @@ -66,10 +97,14 @@ class YyBuf extends StringBuf { // public function objectOpen() { add("{"); - addLine(1); + if (v23) { + depth++; + } else addLine(1); } public function objectClose() { - addLine(-1); + if (v23) { + depth--; + } else addLine(-1); add("}"); } } diff --git a/src/YyExtension.hx b/src/YyExtension.hx index 83652c8..e80e62c 100644 --- a/src/YyExtension.hx +++ b/src/YyExtension.hx @@ -36,7 +36,8 @@ typedef YyExtensionFunc = { } typedef YyExtensionMacro = { >YyBase, - constantName:String, + ?constantName:String, + ?name:String, hidden:Bool, value:String, } diff --git a/src/YyJsonParser.hx b/src/YyJsonParser.hx new file mode 100644 index 0000000..ea36efc --- /dev/null +++ b/src/YyJsonParser.hx @@ -0,0 +1,282 @@ +package; +using StringTools; +import haxe.Json; + +/** + * This is largely a copy of haxe.format.JsonParser, except: + * - Trailing commas are allowed + * - Int64 literals are preserved as Int64, not cast to Float with precision loss + * @author YellowAfterlife + */ +class YyJsonParser { + + /** + Parses given JSON-encoded `str` and returns the resulting object. + + JSON objects are parsed into anonymous structures and JSON arrays + are parsed into `Array`. + + If given `str` is not valid JSON, an exception will be thrown. + + If `str` is null, the result is unspecified. + **/ + static public inline function parse(str : String) : Dynamic { + return new YyJsonParser(str).doParse(); + } + + var str : String; + var pos : Int; + + function new( str : String ) { + this.str = str; + this.pos = 0; + } + + function doParse() : Dynamic { + var result = parseRec(); + var c; + while( !StringTools.isEof(c = nextChar()) ) { + switch( c ) { + case ' '.code, '\r'.code, '\n'.code, '\t'.code: + // allow trailing whitespace + default: + invalidChar(); + } + } + return result; + } + + function parseRec() : Dynamic { + while( true ) { + var c = nextChar(); + switch( c ) { + case ' '.code, '\r'.code, '\n'.code, '\t'.code: + // loop + case '{'.code: + var obj = {}, field = null, comma : Null = null; + while( true ) { + var c = nextChar(); + switch( c ) { + case ' '.code, '\r'.code, '\n'.code, '\t'.code: + // loop + case '}'.code: + // if( field != null || comma == false ) invalidChar(); // +y: allowed + return obj; + case ':'.code: + if( field == null ) + invalidChar(); + Reflect.setField(obj,field,parseRec()); + field = null; + comma = true; + case ','.code: + if( comma ) comma = false else invalidChar(); + case '"'.code: + if( field != null || comma ) invalidChar(); + field = parseString(); + default: + invalidChar(); + } + } + case '['.code: + var arr = [], comma : Null = null; + while( true ) { + var c = nextChar(); + switch( c ) { + case ' '.code, '\r'.code, '\n'.code, '\t'.code: + // loop + case ']'.code: + // if( comma == false ) invalidChar(); // +y: allowed + return arr; + case ','.code: + if( comma ) comma = false else invalidChar(); + default: + if( comma ) invalidChar(); + pos--; + arr.push(parseRec()); + comma = true; + } + } + case 't'.code: + var save = pos; + if( nextChar() != 'r'.code || nextChar() != 'u'.code || nextChar() != 'e'.code ) { + pos = save; + invalidChar(); + } + return true; + case 'f'.code: + var save = pos; + if( nextChar() != 'a'.code || nextChar() != 'l'.code || nextChar() != 's'.code || nextChar() != 'e'.code ) { + pos = save; + invalidChar(); + } + return false; + case 'n'.code: + var save = pos; + if( nextChar() != 'u'.code || nextChar() != 'l'.code || nextChar() != 'l'.code ) { + pos = save; + invalidChar(); + } + return null; + case '"'.code: + return parseString(); + case '0'.code, '1'.code,'2'.code,'3'.code,'4'.code,'5'.code,'6'.code,'7'.code,'8'.code,'9'.code,'-'.code: + return parseNumber(c); + default: + invalidChar(); + } + } + } + + function parseString() { + var start = pos; + var buf:StringBuf = null; + #if target.unicode + var prev = -1; + inline function cancelSurrogate() { + // invalid high surrogate (not followed by low surrogate) + buf.addChar(0xFFFD); + prev = -1; + } + #end + while( true ) { + var c = nextChar(); + if( c == '"'.code ) + break; + if( c == '\\'.code ) { + if (buf == null) { + buf = new StringBuf(); + } + buf.addSub(str,start, pos - start - 1); + c = nextChar(); + #if target.unicode + if( c != "u".code && prev != -1 ) cancelSurrogate(); + #end + switch( c ) { + case "r".code: buf.addChar("\r".code); + case "n".code: buf.addChar("\n".code); + case "t".code: buf.addChar("\t".code); + case "b".code: buf.addChar(8); + case "f".code: buf.addChar(12); + case "/".code, '\\'.code, '"'.code: buf.addChar(c); + case 'u'.code: + var uc:Int = Std.parseInt("0x" + str.substr(pos, 4)); + pos += 4; + #if !target.unicode + if( uc <= 0x7F ) + buf.addChar(uc); + else if( uc <= 0x7FF ) { + buf.addChar(0xC0 | (uc >> 6)); + buf.addChar(0x80 | (uc & 63)); + } else if( uc <= 0xFFFF ) { + buf.addChar(0xE0 | (uc >> 12)); + buf.addChar(0x80 | ((uc >> 6) & 63)); + buf.addChar(0x80 | (uc & 63)); + } else { + buf.addChar(0xF0 | (uc >> 18)); + buf.addChar(0x80 | ((uc >> 12) & 63)); + buf.addChar(0x80 | ((uc >> 6) & 63)); + buf.addChar(0x80 | (uc & 63)); + } + #else + if( prev != -1 ) { + if( uc < 0xDC00 || uc > 0xDFFF ) + cancelSurrogate(); + else { + buf.addChar(((prev - 0xD800) << 10) + (uc - 0xDC00) + 0x10000); + prev = -1; + } + } else if( uc >= 0xD800 && uc <= 0xDBFF ) + prev = uc; + else + buf.addChar(uc); + #end + default: + throw "Invalid escape sequence \\" + String.fromCharCode(c) + " at position " + (pos - 1); + } + start = pos; + } + #if !(target.unicode) + // ensure utf8 chars are not cut + else if( c >= 0x80 ) { + pos++; + if( c >= 0xFC ) pos += 4; + else if( c >= 0xF8 ) pos += 3; + else if( c >= 0xF0 ) pos += 2; + else if( c >= 0xE0 ) pos++; + } + #end + else if( StringTools.isEof(c) ) + throw "Unclosed string"; + } + #if target.unicode + if( prev != -1 ) + cancelSurrogate(); + #end + if (buf == null) { + return str.substr(start, pos - start - 1); + } + else { + buf.addSub(str,start, pos - start - 1); + return buf.toString(); + } + } + + #if (!debug) inline #end + function parseNumber( c : Int ) : Dynamic { + var start = pos - 1; + var minus = c == '-'.code, digit = !minus, zero = c == '0'.code; + var point = false, e = false, pm = false, end = false; + while( true ) { + c = nextChar(); + switch( c ) { + case '0'.code : + if (zero && !point) invalidNumber(start); + if (minus) { + minus = false; zero = true; + } + digit = true; + case '1'.code,'2'.code,'3'.code,'4'.code,'5'.code,'6'.code,'7'.code,'8'.code,'9'.code : + if (zero && !point) invalidNumber(start); + if (minus) minus = false; + digit = true; zero = false; + case '.'.code : + if (minus || point || e) invalidNumber(start); + digit = false; point = true; + case 'e'.code, 'E'.code : + if (minus || zero || e) invalidNumber(start); + digit = false; e = true; + case '+'.code, '-'.code : + if (!e || pm) invalidNumber(start); + digit = false; pm = true; + default : + if (!digit) invalidNumber(start); + pos--; + end = true; + } + if (end) break; + } + // +y: use Int64 if the number is too big for Float + var numstr = str.substr(start, pos - start); + var f = Std.parseFloat(numstr); + var i = Std.int(f); + if (i == f) { + return i; + } else if (!point && Std.string(f) != numstr) { + var i64 = haxe.Int64.parseString(numstr); + return Std.string(i64) == numstr ? i64 : f; + } else return f; + } + + inline function nextChar() { + return StringTools.fastCodeAt(str,pos++); + } + + function invalidChar() { + pos--; // rewind + throw "Invalid char "+StringTools.fastCodeAt(str,pos)+" at position "+pos; + } + + function invalidNumber( start : Int ) { + throw "Invalid number at position "+start+": " + str.substr(start, pos - start); + } +}