diff --git a/source/flx3d/Flx3DUtil.hx b/source/flx3d/Flx3DUtil.hx index 9a5e0b53..029a06f3 100644 --- a/source/flx3d/Flx3DUtil.hx +++ b/source/flx3d/Flx3DUtil.hx @@ -13,6 +13,7 @@ import flixel.FlxG; * Help from Ne_Eo * @see https://twitter.com/Ne_Eo_Twitch */ +@:access(away3d.core.managers.Stage3DManager) class Flx3DUtil { #if THREE_D_SUPPORT @@ -25,18 +26,25 @@ class Flx3DUtil return FlxG.stage.stage3Ds.length; } + /** + * Returns the total amount of 3D stages that are in use + * @return Int + */ + public static inline function getUsed3D():Int + { + return Stage3DManager._numStageProxies; + } + /** * Returns if a Stage3D is available * @return Bool */ public static inline function is3DAvailable():Bool { - @:privateAccess { - if (Stage3DManager._stageProxies == null) - return true; + if (Stage3DManager._stageProxies == null) + return true; - return Stage3DManager._numStageProxies < Stage3DManager._stageProxies.length; - } + return Stage3DManager._numStageProxies < Stage3DManager._stageProxies.length; } /** diff --git a/source/flx3d/Flx3DView.hx b/source/flx3d/Flx3DView.hx index b034190d..24763bac 100644 --- a/source/flx3d/Flx3DView.hx +++ b/source/flx3d/Flx3DView.hx @@ -36,7 +36,7 @@ class Flx3DView extends FlxView3D { var meshes:Array = []; public function new(x:Float = 0, y:Float = 0, width:Int = -1, height:Int = -1) { if (!Flx3DUtil.is3DAvailable()) - throw "[Flx3DView] 3D is not available on this platform. Stages in use: " + Flx3DUtil.getTotal3D() + ", Max stages allowed: " + FlxG.stage.stage3Ds.length + "."; + throw "[Flx3DView] 3D is not available on this platform. Stages in use: " + Flx3DUtil.getUsed3D() + ", Max stages allowed: " + Flx3DUtil.getTotal3D() + "."; super(x, y, width, height); __cur3DStageID = __3DIDS++; } diff --git a/source/funkin/backend/MusicBeatState.hx b/source/funkin/backend/MusicBeatState.hx index ce37405e..c89728a9 100644 --- a/source/funkin/backend/MusicBeatState.hx +++ b/source/funkin/backend/MusicBeatState.hx @@ -123,7 +123,7 @@ class MusicBeatState extends FlxState implements IBeatReceiver var path = Paths.script('data/states/${scriptName}/LIB_$i'); var script = Script.create(path); if (script is DummyScript) continue; - script.fileName = '$i:$scriptName'; + script.remappedNames.set(script.fileName, '$i:${script.fileName}'); stateScripts.add(script); script.load(); } diff --git a/source/funkin/backend/MusicBeatSubstate.hx b/source/funkin/backend/MusicBeatSubstate.hx index 4c96f46b..d8940afc 100644 --- a/source/funkin/backend/MusicBeatSubstate.hx +++ b/source/funkin/backend/MusicBeatSubstate.hx @@ -110,7 +110,7 @@ class MusicBeatSubstate extends FlxSubState implements IBeatReceiver var path = Paths.script('data/states/${scriptName}/LIB_$i'); var script = Script.create(path); if (script is DummyScript) continue; - script.fileName = '$i:$scriptName'; + script.remappedNames.set(script.fileName, '$i:${script.fileName}'); stateScripts.add(script); script.load(); } diff --git a/source/funkin/backend/assets/AssetsLibraryList.hx b/source/funkin/backend/assets/AssetsLibraryList.hx index 744feed2..49447997 100644 --- a/source/funkin/backend/assets/AssetsLibraryList.hx +++ b/source/funkin/backend/assets/AssetsLibraryList.hx @@ -6,9 +6,6 @@ import lime.utils.AssetLibrary; class AssetsLibraryList extends AssetLibrary { public var libraries:Array = []; - public var rootLibs:Array = []; - public var assetsLibs:Array = []; - @:allow(funkin.backend.system.Main) @:allow(funkin.backend.system.MainState) private var __defaultLibraries:Array = []; @@ -19,7 +16,6 @@ class AssetsLibraryList extends AssetLibrary { libraries.remove(lib); return lib; } - public function existsSpecific(id:String, type:String, source:AssetSource = BOTH) { if (!id.startsWith("assets/") && exists('assets/$id', type)) return true; @@ -98,29 +94,23 @@ class AssetsLibraryList extends AssetLibrary { return content; } - public function getSpecificAsset(id:String, type:String, source:AssetSource = BOTH, ?avoidRoot:Bool = false):Dynamic { + public function getSpecificAsset(id:String, type:String, source:AssetSource = BOTH):Dynamic { try { if (!id.startsWith("assets/")) { var ass = getSpecificAsset('assets/$id', type, source); - if (ass != null) + if (ass != null) { return ass; - } else { - for (lib in assetsLibs) { - if (shouldSkipLib(libraries.indexOf(lib), source)) continue; - - var asset = lib.getAsset(id, type); - if (asset != null) - return asset; } } + for(k=>e in libraries) { + if (shouldSkipLib(k, source)) continue; - if (!avoidRoot) { - for (lib in rootLibs) { - if (shouldSkipLib(libraries.indexOf(lib), source)) continue; - - var asset = lib.getAsset(id, type); - if (asset != null) + @:privateAccess + if (e.exists(id, e.types.get(id))) { + var asset = e.getAsset(id, type); + if (asset != null) { return asset; + } } } return null; @@ -140,9 +130,6 @@ class AssetsLibraryList extends AssetLibrary { public override inline function getAsset(id:String, type:String):Dynamic return getSpecificAsset(id, type, BOTH); - public inline function getAssetSafe(id:String, type:String):Dynamic - return getSpecificAsset(id, type, BOTH, true); - public override function isLocal(id:String, type:String) { return true; } @@ -171,17 +158,8 @@ class AssetsLibraryList extends AssetLibrary { addLibrary(d); } - public function addLibrary(lib:AssetLibrary) @:privateAccess { + public function addLibrary(lib:AssetLibrary) { libraries.insert(0, lib); - - var finalLib:AssetLibrary = lib; - if (finalLib is openfl.utils.AssetLibrary) - finalLib = cast(finalLib, openfl.utils.AssetLibrary).__proxy; - - if (finalLib is IModsAssetLibrary) - assetsLibs.insert(0, finalLib); - else - rootLibs.insert(0, finalLib); return lib; } } diff --git a/source/funkin/backend/assets/Paths.hx b/source/funkin/backend/assets/Paths.hx index 6a6efc08..00aae99d 100644 --- a/source/funkin/backend/assets/Paths.hx +++ b/source/funkin/backend/assets/Paths.hx @@ -302,6 +302,23 @@ class Paths return content; */ } + + // Used in Script.hx + @:noCompletion public static function getFilenameFromLibFile(path:String) { + var file = new haxe.io.Path(path); + if(file.file.startsWith("LIB_")) { + return file.dir + "." + file.ext; + } + return path; + } + + @:noCompletion public static function getLibFromLibFile(path:String) { + var file = new haxe.io.Path(path); + if(file.file.startsWith("LIB_")) { + return file.file.substr(4); + } + return ""; + } } class ScriptPathInfo { diff --git a/source/funkin/backend/scripting/GlobalScript.hx b/source/funkin/backend/scripting/GlobalScript.hx index 9c6c15dd..34da69b5 100644 --- a/source/funkin/backend/scripting/GlobalScript.hx +++ b/source/funkin/backend/scripting/GlobalScript.hx @@ -94,7 +94,7 @@ class GlobalScript { var path = Paths.script('data/global/LIB_$i'); var script = Script.create(path); if (script is DummyScript) continue; - script.fileName = '$i:global.hx'; + script.remappedNames.set(script.fileName, '$i:${script.fileName}'); scripts.add(script); script.load(); } diff --git a/source/funkin/backend/scripting/HScript.hx b/source/funkin/backend/scripting/HScript.hx index a43055fd..be108c5f 100644 --- a/source/funkin/backend/scripting/HScript.hx +++ b/source/funkin/backend/scripting/HScript.hx @@ -1,21 +1,17 @@ package funkin.backend.scripting; import haxe.io.Path; -import hscript.Expr.ClassDecl; -import hscript.Expr.ModuleDecl; import hscript.Expr.Error; import hscript.Parser; import openfl.Assets; -import lime.utils.AssetType; import hscript.*; class HScript extends Script { public var interp:Interp; public var parser:Parser; public var expr:Expr; - public var decls:Array = null; - public var code:String; - public var folderlessPath:String; + public var code:String = null; + //public var folderlessPath:String; var __importedPaths:Array; public static function initParser() { @@ -30,10 +26,12 @@ class HScript extends Script { interp = new Interp(); - // We use getAssetSafe to prevent crash from empty file (null which would check root (big crash)) -lunar - code = Assets.exists(path) ? Paths.assetsTree.getAssetSafe(path, TEXT) : null; + try { + if(Assets.exists(rawPath)) code = Assets.getText(rawPath); + } catch(e) Logs.trace('Error while reading $path: ${Std.string(e)}', ERROR); + parser = initParser(); - folderlessPath = Path.directory(path); + //folderlessPath = Path.directory(path); __importedPaths = [path]; interp.errorHandler = _errorHandler; @@ -48,14 +46,13 @@ class HScript extends Script { })); funkin.backend.scripting.GlobalScript.call("onScriptCreated", [this, "hscript"]); - - if (code != null && code.trim() != "") - loadFromString(code); + loadFromString(code); } public override function loadFromString(code:String) { try { - expr = parser.parseString(code, fileName); + if (code != null && code.trim() != "") + expr = parser.parseString(code, fileName); } catch(e:Error) { _errorHandler(e); } catch(e) { @@ -94,7 +91,9 @@ class HScript extends Script { } private function _errorHandler(error:Error) { - + var fileName = error.origin; + if(remappedNames.exists(fileName)) + fileName = remappedNames.get(fileName); var fn = '$fileName:${error.line}: '; var err = error.toString(); if (err.startsWith(fn)) err = err.substr(fn.length); diff --git a/source/funkin/backend/scripting/Script.hx b/source/funkin/backend/scripting/Script.hx index 49c79a00..7d141349 100644 --- a/source/funkin/backend/scripting/Script.hx +++ b/source/funkin/backend/scripting/Script.hx @@ -124,13 +124,22 @@ class Script extends FlxBasic implements IFlxDestroyable { */ public var fileName:String; + /** + * Script Extension + */ + public var extension:String; + /** * Path to the script. */ public var path:String = null; + private var rawPath:String = null; + private var didLoad:Bool = false; + public var remappedNames:Map = []; + /** * Creates a script from the specified asset path. The language is automatically determined. * @param path Path in assets @@ -177,7 +186,11 @@ class Script extends FlxBasic implements IFlxDestroyable { public function new(path:String) { super(); + rawPath = path; + path = Paths.getFilenameFromLibFile(path); + fileName = Path.withoutDirectory(path); + extension = Path.extension(path); this.path = path; onCreate(path); for(k=>e in getDefaultVariables(this)) { @@ -223,6 +236,9 @@ class Script extends FlxBasic implements IFlxDestroyable { * Traces something as this script. */ public function trace(v:Dynamic) { + var fileName = this.fileName; + if(remappedNames.exists(fileName)) + fileName = remappedNames.get(fileName); Logs.traceColored([ Logs.logText('${fileName}: ', GREEN), Logs.logText(Std.string(v)) @@ -280,6 +296,9 @@ class Script extends FlxBasic implements IFlxDestroyable { * @param additionalInfo Additional information you could provide. */ public function error(text:String, ?additionalInfo:Dynamic):Void { + var fileName = this.fileName; + if(remappedNames.exists(fileName)) + fileName = remappedNames.get(fileName); Logs.traceColored([ Logs.logText(fileName, RED), Logs.logText(text) diff --git a/source/funkin/backend/scripting/events/GameOverCreationEvent.hx b/source/funkin/backend/scripting/events/GameOverCreationEvent.hx index 8536e16e..bb9b04c0 100644 --- a/source/funkin/backend/scripting/events/GameOverCreationEvent.hx +++ b/source/funkin/backend/scripting/events/GameOverCreationEvent.hx @@ -16,7 +16,7 @@ final class GameOverCreationEvent extends CancellableEvent public var gameOverSong:String; - public var bpm:Int; + public var bpm:Float; public var lossSFX:String; diff --git a/source/funkin/backend/utils/BitmapUtil.hx b/source/funkin/backend/utils/BitmapUtil.hx index 298b4843..367d0123 100644 --- a/source/funkin/backend/utils/BitmapUtil.hx +++ b/source/funkin/backend/utils/BitmapUtil.hx @@ -19,7 +19,7 @@ class BitmapUtil { for(x in 0...bmap.width) { color = bmap.getPixel32(x, y); fixedColor = 0xFF000000 + (color % 0x1000000); - if (colorMap[fixedColor] == null) + if (!colorMap.exists(fixedColor)) colorMap[fixedColor] = 0; colorMap[fixedColor] += color.alphaFloat; } @@ -50,7 +50,7 @@ class BitmapUtil { for(x in 0...bmap.width) { color = bmap.getPixel32(x, y); fixedColor = 0xFF000000 + (color % 0x1000000); - if (colorMap[fixedColor] == null) + if (!colorMap.exists(fixedColor)) colorMap[fixedColor] = 0; colorMap[fixedColor] += color.alphaFloat * 0.33 + (0.67 * (color.saturation * (2 * (color.lightness > 0.5 ? 0.5 - (color.lightness) : color.lightness)))); } diff --git a/source/funkin/backend/utils/CoolUtil.hx b/source/funkin/backend/utils/CoolUtil.hx index 01c7f309..d73304c1 100644 --- a/source/funkin/backend/utils/CoolUtil.hx +++ b/source/funkin/backend/utils/CoolUtil.hx @@ -618,7 +618,7 @@ class CoolUtil frontEnd.music = music; } - public static inline function flxeaseFromString(mainEase:String, suffix:String) + @:noUsing public static inline function flxeaseFromString(mainEase:String, suffix:String) return Reflect.field(FlxEase, mainEase + (mainEase == "linear" ? "" : suffix)); } diff --git a/source/funkin/game/GameOverSubstate.hx b/source/funkin/game/GameOverSubstate.hx index b2f7636f..c7d6c44e 100644 --- a/source/funkin/game/GameOverSubstate.hx +++ b/source/funkin/game/GameOverSubstate.hx @@ -20,7 +20,7 @@ class GameOverSubstate extends MusicBeatSubstate public var characterName:String; public var gameOverSong:String; - public var gameOverSongBPM:Int; + public var gameOverSongBPM:Float; public var lossSFXName:String; public var retrySFX:String; public var player:Bool; diff --git a/source/haxe/format/JsonParser.hx b/source/haxe/format/JsonParser.hx new file mode 100644 index 00000000..9be63189 --- /dev/null +++ b/source/haxe/format/JsonParser.hx @@ -0,0 +1,394 @@ +/* + * Copyright (C)2005-2019 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +package haxe.format; + +/** + An implementation of JSON parser in Haxe. + + This class is used by `haxe.Json` when native JSON implementation + is not available. + + @see https://haxe.org/manual/std-Json-parsing.html +**/ +class JsonParser { + /** + 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 JsonParser(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())) { + if (checkComments(c)) { + continue; + } + switch (c) { + case ' '.code, '\r'.code, '\n'.code, '\t'.code: + // allow trailing whitespace + default: + invalidChar(); + } + } + return result; + } + + var inComment = false; + var blockComment = false; + + inline function checkComments(c:Int) { + var ret = false; + if (!inComment) { // Starting Comment + if (c == '/'.code) { + var d = nextChar(); + if (d == '/'.code || (blockComment = (d == '*'.code))) { + inComment = true; + } else { + pos--; + } + } + } else { // In Comment + if (blockComment) { // Block Comment + if (c == '*'.code) { + var d = nextChar(); + if (d == '/'.code) { + inComment = blockComment = false; + ret = true; + } else { + pos--; + } + } + } else { // Line Comment + if (c == '\n'.code) { + inComment = false; + } + } + } + return ret || inComment; + } + + function parseRec():Dynamic { + while (true) { + var c = nextChar(); + if (checkComments(c)) { + continue; + } + 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(); + if (checkComments(c)) { + continue; + } + switch (c) { + case ' '.code, '\r'.code, '\n'.code, '\t'.code: + // loop + case '}'.code: + if (field != null || comma == false) + invalidChar(); + 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(); + if (checkComments(c)) { + continue; + } + switch (c) { + case ' '.code, '\r'.code, '\n'.code, '\t'.code: + // loop + case ']'.code: + if (comma == false) invalidChar(); + 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(); + } + } + + inline 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; + } + + var f = Std.parseFloat(str.substr(start, pos - start)); + if (point) { + return f; + } else { + var i = Std.int(f); + return if (i == f) i else f; + } + } + + inline function nextChar() { + return StringTools.fastCodeAt(str, pos++); + } + + function invalidChar() { + pos--; // rewind + var col = 1; + var line = 1; + for (i in 0...pos) { + var c = StringTools.fastCodeAt(str, i); + if (c == '\n'.code) { + col = 1; + line++; + } + else { + col++; + } + } + throw "Invalid char " + StringTools.fastCodeAt(str, pos) + " ('" + str.charAt(pos) + "')" + " at line " + line + " col " + col; + } + + function invalidNumber(start:Int) { + var col = 1; + var line = 1; + for (i in 0...start) { + var c = StringTools.fastCodeAt(str, i); + if (c == '\n'.code) { + col = 1; + line++; + } + else { + col++; + } + } + throw "Invalid number at line " + line + " col " + col + ": " + str.substr(start, pos - start); + } +}