diff --git a/src/Util.hx b/src/Util.hx index 59b02e55..70e0cfdc 100644 --- a/src/Util.hx +++ b/src/Util.hx @@ -77,4 +77,66 @@ class Util { } bitmap.unlock(); } + + public static function splitIgnoreStringLiterals(str:String, splitter:String, strLiteralToken = '"') { + var indices = []; + var inString = false; + for (i in 0...str.length) { + var c = str.charAt(i); + if (inString) { + if (c == strLiteralToken && str.charAt(i - 1) != '\\') + inString = false; + continue; + } + if (c == strLiteralToken) + inString = true; + else if (c == splitter) + indices.push(i); + } + var parts = []; + var remaining = str; + for (i in 0...indices.length) { + var index = indices[i] - (str.length - remaining.length); + var part = remaining.substring(0, index); + remaining = remaining.substring(index + 1); + parts.push(part); + } + parts.push(remaining); + return parts; + } + + public static function unescape(str:String) { + var specialCases = [ + '\\t' => '\t', + '\\v' => '\x0B', + '\\0' => '\x00', + '\\f' => '\x0C', + '\\n' => '\n', + '\\r' => '\r' + ]; + + for (obj => esc in specialCases) { + str = StringTools.replace(str, obj, esc); + } + + return str; + } + + /** Gets the index of a substring like String.prototype.indexOf, but only if that index lies outside of string literals. */ + public static function indexOfIgnoreStringLiterals(str:String, searchString:String, position = 0, strLiteralToken = '"') { + var inString = false; + for (i in position...str.length) { + var c = str.charAt(i); + if (inString) { + if (c == strLiteralToken && str.charAt(i - 1) != '\\') + inString = false; + continue; + } + if (c == strLiteralToken) + inString = true; + else if (StringTools.startsWith(str.substr(i), searchString)) + return i; + } + return -1; + } } diff --git a/src/mis/MisFile.hx b/src/mis/MisFile.hx new file mode 100644 index 00000000..9b6e520b --- /dev/null +++ b/src/mis/MisFile.hx @@ -0,0 +1,3 @@ +package mis; + +class MisFile {} diff --git a/src/mis/MisParser.hx b/src/mis/MisParser.hx new file mode 100644 index 00000000..5bdaab44 --- /dev/null +++ b/src/mis/MisParser.hx @@ -0,0 +1,178 @@ +package mis; + +import mis.MissionElement.MissionElementType; +import mis.MissionElement.MissionElementScriptObject; +import src.Util; +import h3d.Vector; +import h3d.Quat; + +final elementHeadRegEx = ~/new (\w+)\((\w*)\) *{/g; +final blockCommentRegEx = ~/\/\*(.|\n)*?\*\//g; +final lineCommentRegEx = ~/\/\/.*/g; +final assignmentRegEx = ~/(\$(?:\w|\d)+)\s*=\s*(.+?);/g; +final marbleAttributesRegEx = ~/setMarbleAttributes\("(\w+)",\s*(.+?)\);/g; +final activatePackageRegEx = ~/activatePackage\((.+?)\);/g; + +class MisParser { + var text:String; + var index = 0; + var currentElementId = 0; + var variables:Map; + + public function new(text:String) { + this.text = text; + } + + public function parse() {} + + function readValues() { + // Values are either strings or string arrays. + var obj:Map> = new Map(); + var endingBraceIndex = Util.indexOfIgnoreStringLiterals(this.text, '};', this.index); + if (endingBraceIndex == -1) + endingBraceIndex = this.text.length; + var section = StringTools.trim(this.text.substring(this.index, endingBraceIndex)); + var statements = Util.splitIgnoreStringLiterals(section, ';').map(x -> StringTools.trim(x)); // Get a list of all statements + for (statement in statements) { + if (statement == null || statement == "") + continue; + var splitIndex = statement.indexOf('='); + if (splitIndex == -1) + continue; + var parts = [statement.substring(0, splitIndex), statement.substring(splitIndex + 1)].map((part) -> StringTools.trim(part)); + if (parts.length != 2) + continue; + var key = parts[0]; + key = key.toLowerCase(); // TorqueScript is case-insensitive here + if (StringTools.endsWith(key, ']')) { + // The key is specifying array data, so handle that case. + var openingIndex = key.indexOf('['); + var arrayName = key.substring(0, openingIndex); + var array:Array; + if (obj.exists(arrayName)) + array = obj.get(arrayName); + else { + array = []; + obj.set(arrayName, array); + } // Create a new array or use the existing one + var index = Std.parseInt(key.substring(openingIndex + 1, -1)); + array[index] = this.resolveExpression(parts[1]); + } else { + obj.set(key, [this.resolveExpression(parts[1])]); + } + } + this.index = endingBraceIndex + 2; + return obj; + } + + function readScriptObject(name:String) { + var obj = new MissionElementScriptObject(); + obj._type = MissionElementType.ScriptObject; + obj._name = name; + var values = this.readValues(); + + for (key => value in values) { + if (value.length > 1) { + for (i in 0...value.length) { + Reflect.setField(obj, '${key}${i}', value[i]); + } + } else { + Reflect.setField(obj, key, value[0]); + } + } + return obj; + } + + /** Resolves a TorqueScript rvalue expression. Currently only supports the concatenation @ operator. */ + function resolveExpression(expr:String) { + var parts = Util.splitIgnoreStringLiterals(expr, ' @ ').map(x -> { + x = StringTools.trim(x); + if (StringTools.startsWith(x, '$ ') && this.variables[x] != null) { + // Replace the variable with its value + x = this.resolveExpression(this.variables[x]); + } else if (StringTools.startsWith(x, ' "') && StringTools.endsWith(x, '" ')) { + x = Util.unescape(x.substring(1, x.length - 2)); // It' s a string literal, so remove " " + } + return x; + }); + return parts.join(''); + } + + /** Parses a 4-component vector from a string of four numbers. */ + public static function parseVector3(string:String) { + if (string == null) + return new Vector(); + var parts = string.split(' ').map((part) -> Std.parseFloat(part)); + + if (parts.length < 3) + return new Vector(); + if (parts.filter(x -> !Math.isFinite(x)).length != 0) + return new Vector(); + return new Vector(parts[0], parts[1], parts[2]); + } + + /** Parses a 4-component vector from a string of four numbers. */ + public static function parseVector4(string:String) { + if (string == null) + return new Vector(); + var parts = string.split(' ').map((part) -> Std.parseFloat(part)); + + if (parts.length < 4) + return new Vector(); + if (parts.filter(x -> !Math.isFinite(x)).length != 0) + return new Vector(); + return new Vector(parts[0], parts[1], parts[2], parts[3]); + } + + /** Returns a quaternion based on a rotation specified from 4 numbers. */ + public static function parseRotation(string:String) { + if (string == null) + return new Quat(); + var parts = string.split(' ').map((part) -> Std.parseFloat(part)); + if (parts.length < 4) + return new Quat(); + if (parts.filter(x -> !Math.isFinite(x)).length != 0) + return new Quat(); + var quaternion = new Quat(); + // The first 3 values represent the axis to rotate on, the last represents the negative angle in degrees. + quaternion.initRotateAxis(parts[0], parts[1], parts[2], -parts[3] * Math.PI / 180); + return quaternion; + } + + /** Parses a numeric value. */ + public static function parseNumber(string:String):Float { + if (string == null) + return 0; + // Strange thing here, apparently you can supply lists of numbers. In this case tho, we just take the first value. + var val = Std.parseFloat(string.split(',')[0]); + if (Math.isNaN(val)) + return 0; + return val; + } + + /** Parses a list of space-separated numbers. */ + public static function parseNumberList(string:String) { + var parts = string.split(' '); + var result = []; + for (part in parts) { + var number = Std.parseFloat(part); + if (!Math.isNaN(number)) { + // The number parsed without issues; simply add it to the array. + result.push(number); + } else { + // Since we got NaN, we assume the number did not parse correctly and we have a case where the space between multiple numbers are missing. So "0.0000000 1.0000000" turning into "0.00000001.0000000". + final assumedDecimalPlaces = 7; // Reasonable assumption + // Scan the part to try to find all numbers contained in it + while (part.length > 0) { + var dotIndex = part.indexOf('.'); + if (dotIndex == -1) + break; + var section = part.substring(0, cast Math.min(dotIndex + assumedDecimalPlaces + 1, part.length)); + result.push(Std.parseFloat(section)); + part = part.substring(dotIndex + assumedDecimalPlaces + 1); + } + } + } + return result; + } +} diff --git a/src/mis/MissionElement.hx b/src/mis/MissionElement.hx new file mode 100644 index 00000000..c1e5c2a7 --- /dev/null +++ b/src/mis/MissionElement.hx @@ -0,0 +1,277 @@ +package mis; + +enum MissionElementType { + SimGroup; + ScriptObject; + MissionArea; + Sky; + Sun; + InteriorInstance; + StaticShape; + Item; + Path; + Marker; + PathedInterior; + Trigger; + AudioProfile; + MessageVector; + TSStatic; + ParticleEmitterNode; +} + +@:publicFields +class MissionElementBase { + // Underscore prefix to avoid name clashes + + /** The general type of the element. */ + var _type:MissionElementType; /** The object name; specified in the () of the "constructor". */ + + var _name:String; + + /** Is unique for every element in the mission file. */ + var _id:Int; +} + +@:publicFields +class MissionElementSimGroup extends MissionElementBase { + var elements:Array; + + public function new() { + _type = MissionElementType.SimGroup; + } +} + +@:publicFields +/** Stores metadata about the mission. */ +class MissionElementScriptObject extends MissionElementBase { + var time:String; + var name:String; + var desc:String; + var type:String; + var starthelptext:String; + var level:String; + var artist:String; + var goldtime:String; + + public function new() { + _type = MissionElementType.ScriptObject; + } +} + +@:publicFields +class MissionElementMissionArea extends MissionElementBase { + var area:String; + var flightceiling:String; + var flightceilingRange:String; + var locked:String; + + public function new() { + _type = MissionElementType.MissionArea; + } +} + +@:publicFields +class MissionElementSky extends MissionElementBase { + var position:String; + var rotation:String; + var scale:String; + var cloudheightper:Array; + var cloudspeed1:String; + var cloudspeed2:String; + var cloudspeed3:String; + var visibledistance:String; + var useskytextures:String; + var renderbottomtexture:String; + var skysolidcolor:String; + var fogdistance:String; + var fogcolor:String; + var fogvolume1:String; + var fogvolume2:String; + var fogvolume3:String; + var materiallist:String; + var windvelocity:String; + var windeffectprecipitation:String; + var norenderbans:String; + var fogvolumecolor1:String; + var fogvolumecolor2:String; + var fogvolumecolor3:String; + + public function new() { + _type = MissionElementType.Sky; + } +} + +@:publicFields +/** Stores information about the lighting direction and color. */ +class MissionElementSun extends MissionElementBase { + var direction:String; + var color:String; + var ambient:String; + + public function new() { + _type = MissionElementType.Sun; + } +} + +@:publicFields +/** Represents a static (non-moving) interior instance. */ +class MissionElementInteriorInstance extends MissionElementBase { + var position:String; + var rotation:String; + var scale:String; + var interiorfile:String; + var showterraininside:String; + + public function new() { + _type = MissionElementType.InteriorInstance; + } +} + +@:publicFields +/** Represents a static shape. */ +class MissionElementStaticShape extends MissionElementBase { + var position:String; + var rotation:String; + var scale:String; + var datablock:String; + var resettime:Null; + var timeout:Null; + + public function new() { + _type = MissionElementType.StaticShape; + } +} + +@:publicFields +/** Represents an item. */ +class MissionElementItem extends MissionElementBase { + var position:String; + var rotation:String; + var scale:String; + var datablock:String; + var collideable:String; + var isStatic:String; + var rotate:String; + var showhelponpickup:String; + var timebonus:Null; + + public function new() { + _type = MissionElementType.Item; + } +} + +@:publicFields +/** Holds the markers used for the path of a pathed interior. */ +class MissionElementPath extends MissionElementBase { + var markers:Array; + + public function new() { + _type = MissionElementType.Path; + } +} + +@:publicFields +/** One keyframe in a pathed interior path. */ +class MissionElementMarker extends MissionElementBase { + var position:String; + var rotation:String; + var scale:String; + var seqnum:String; + var mstonext:String; + + /** Either Linear; Accelerate or Spline. */ + var smoothingtype:String; + + public function new() { + _type = MissionElementType.Marker; + } +} + +@:publicFields +/** Represents a moving interior. */ +class MissionElementPathedInterior extends MissionElementBase { + var position:String; + var rotation:String; + var scale:String; + var datablock:String; + var interiorresource:String; + var interiorindex:String; + var baseposition:String; + var baserotation:String; + var basescale:String; + // These two following values are a bit weird. See usage for more explanation. + var initialtargetposition:String; + var initialposition:String; + + public function new() { + _type = MissionElementType.PathedInterior; + } +} + +@:publicFields +/** Represents a trigger area used for out-of-bounds and help. */ +class MissionElementTrigger extends MissionElementBase { + var position:String; + var rotation:String; + var scale:String; + var datablock:String; + + /** A list of 12 Strings representing 4 vectors. The first vector corresponds to the origin point of the cuboid; the other three are the side vectors. */ + var polyhedron:String; + + var text:Null; + var targettime:Null; + var instant:Null; + var icontinuetottime:Null; + + public function new() { + _type = MissionElementType.Trigger; + } +} + +@:publicFields +/** Represents the song choice. */ +class MissionElementAudioProfile extends MissionElementBase { + var filename:String; + var description:String; + var preload:String; + + public function new() { + _type = MissionElementType.AudioProfile; + } +} + +@:publicFields +class MissionElementMessageVector extends MissionElementBase { + public function new() { + _type = MissionElementType.MessageVector; + } +} + +@:publicFields +/** Represents a static; unmoving; unanimated DTS shape. They're pretty dumb; tbh. */ +class MissionElementTSStatic extends MissionElementBase { + var position:String; + var rotation:String; + var scale:String; + var shapename:String; + + public function new() { + _type = MissionElementType.TSStatic; + } +} + +@:publicFields +/** Represents a particle emitter. Currently unused by this port (these are really niche). */ +class MissionElementParticleEmitterNode extends MissionElementBase { + var position:String; + var rotation:String; + var scale:String; + var datablock:String; + var emitter:String; + var velocity:String; + + public function new() { + _type = MissionElementType.ParticleEmitterNode; + } +}