From 37a3eab15006a8398b7aef427b2b3d7ff12f5606 Mon Sep 17 00:00:00 2001
From: ZZYZX <zzyzx@virtual>
Date: Mon, 16 Jan 2017 13:18:46 +0200
Subject: [PATCH] Enabled prototype ZScript support

---
 Source/Core/Builder.csproj                  |   6 +
 Source/Core/ZDoom/DecorateActorStructure.cs | 432 +++++++++++
 Source/Core/ZDoom/DecorateParser.cs         | 779 --------------------
 Source/Core/ZDoom/DecorateStateGoto.cs      | 167 +++++
 Source/Core/ZDoom/DecorateStateStructure.cs | 209 ++++++
 Source/Core/ZDoom/StateGoto.cs              |   5 +
 Source/Core/ZDoom/ZScriptActorStructure.cs  | 499 +++++++++++++
 Source/Core/ZDoom/ZScriptParser.cs          | 535 ++------------
 Source/Core/ZDoom/ZScriptStateGoto.cs       |  75 ++
 Source/Core/ZDoom/ZScriptStateStructure.cs  | 279 +++++++
 Source/Core/ZDoom/ZScriptTokenizer.cs       |  39 +-
 11 files changed, 1751 insertions(+), 1274 deletions(-)
 create mode 100755 Source/Core/ZDoom/DecorateActorStructure.cs
 create mode 100755 Source/Core/ZDoom/DecorateStateGoto.cs
 create mode 100755 Source/Core/ZDoom/DecorateStateStructure.cs
 create mode 100755 Source/Core/ZDoom/ZScriptActorStructure.cs
 create mode 100755 Source/Core/ZDoom/ZScriptStateGoto.cs
 create mode 100755 Source/Core/ZDoom/ZScriptStateStructure.cs

diff --git a/Source/Core/Builder.csproj b/Source/Core/Builder.csproj
index 1a87cb30..11330636 100644
--- a/Source/Core/Builder.csproj
+++ b/Source/Core/Builder.csproj
@@ -520,7 +520,13 @@
     <Compile Include="VisualModes\VisualSector.cs" />
     <Compile Include="Rendering\World3DShader.cs" />
     <Compile Include="Rendering\WorldVertex.cs" />
+    <Compile Include="ZDoom\DecorateActorStructure.cs" />
+    <Compile Include="ZDoom\DecorateStateGoto.cs" />
+    <Compile Include="ZDoom\DecorateStateStructure.cs" />
+    <Compile Include="ZDoom\ZScriptActorStructure.cs" />
     <Compile Include="ZDoom\ZScriptParser.cs" />
+    <Compile Include="ZDoom\ZScriptStateGoto.cs" />
+    <Compile Include="ZDoom\ZScriptStateStructure.cs" />
     <Compile Include="ZDoom\ZScriptTokenizer.cs" />
   </ItemGroup>
   <ItemGroup>
diff --git a/Source/Core/ZDoom/DecorateActorStructure.cs b/Source/Core/ZDoom/DecorateActorStructure.cs
new file mode 100755
index 00000000..e5487b94
--- /dev/null
+++ b/Source/Core/ZDoom/DecorateActorStructure.cs
@@ -0,0 +1,432 @@
+using CodeImp.DoomBuilder.Config;
+using CodeImp.DoomBuilder.Types;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+
+namespace CodeImp.DoomBuilder.ZDoom
+{
+
+    public sealed class DecorateActorStructure : ActorStructure
+    {
+        #region ================== DECORATE Actor Structure parsing
+
+        internal DecorateActorStructure(ZDTextParser zdparser, DecorateCategoryInfo catinfo) : base(zdparser, catinfo)
+        {
+            DecorateParser parser = (DecorateParser)zdparser;
+            bool done = false; //mxd
+
+            // First next token is the class name
+            parser.SkipWhitespace(true);
+            classname = parser.StripTokenQuotes(parser.ReadToken(ACTOR_CLASS_SPECIAL_TOKENS));
+
+            if (string.IsNullOrEmpty(classname))
+            {
+                parser.ReportError("Expected actor class name");
+                return;
+            }
+
+            //mxd. Fail on duplicates
+            if (parser.ActorsByClass.ContainsKey(classname.ToLowerInvariant()))
+            {
+                parser.ReportError("Actor \"" + classname + "\" is double-defined");
+                return;
+            }
+
+            // Parse tokens before entering the actor scope
+            while (parser.SkipWhitespace(true))
+            {
+                string token = parser.ReadToken();
+                if (!string.IsNullOrEmpty(token))
+                {
+                    token = token.ToLowerInvariant();
+
+                    switch (token)
+                    {
+                        case ":":
+                            // The next token must be the class to inherit from
+                            parser.SkipWhitespace(true);
+                            inheritclass = parser.StripTokenQuotes(parser.ReadToken());
+                            if (string.IsNullOrEmpty(inheritclass))
+                            {
+                                parser.ReportError("Expected class name to inherit from");
+                                return;
+                            }
+
+                            // Find the actor to inherit from
+                            baseclass = parser.GetArchivedActorByName(inheritclass);
+                            break;
+
+                        case "replaces":
+                            // The next token must be the class to replace
+                            parser.SkipWhitespace(true);
+                            replaceclass = parser.StripTokenQuotes(parser.ReadToken());
+                            if (string.IsNullOrEmpty(replaceclass))
+                            {
+                                parser.ReportError("Expected class name to replace");
+                                return;
+                            }
+                            break;
+
+                        case "native":
+                            // Igore this token
+                            break;
+
+                        case "{":
+                            // Actor scope begins here,
+                            // break out of this parse loop
+                            done = true;
+                            break;
+
+                        case "-":
+                            // This could be a negative doomednum (but our parser sees the - as separate token)
+                            // So read whatever is after this token and ignore it (negative doomednum indicates no doomednum)
+                            parser.ReadToken();
+                            break;
+
+                        default:
+                            //mxd. Property begins with $? Then the whole line is a single value
+                            if (token.StartsWith("$"))
+                            {
+                                // This is for editor-only properties such as $sprite and $category
+                                props[token] = new List<string> { (parser.SkipWhitespace(false) ? parser.ReadLine() : "") };
+                                continue;
+                            }
+
+                            if (!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out doomednum)) // Check if numeric
+                            {
+                                // Not numeric!
+                                parser.ReportError("Expected editor number or start of actor scope while parsing \"" + classname + "\"");
+                                return;
+                            }
+
+                            //mxd. Range check
+                            if ((doomednum < General.Map.FormatInterface.MinThingType) || (doomednum > General.Map.FormatInterface.MaxThingType))
+                            {
+                                // Out of bounds!
+                                parser.ReportError("Actor \"" + classname + "\" has invalid editor number. Editor number must be between "
+                                    + General.Map.FormatInterface.MinThingType + " and " + General.Map.FormatInterface.MaxThingType);
+                                return;
+                            }
+                            break;
+                    }
+
+                    if (done) break; //mxd
+                }
+                else
+                {
+                    parser.ReportError("Unexpected end of structure");
+                    return;
+                }
+            }
+
+            // Now parse the contents of actor structure
+            string previoustoken = "";
+            done = false; //mxd
+            while (parser.SkipWhitespace(true))
+            {
+                string token = parser.ReadToken();
+                token = token.ToLowerInvariant();
+
+                switch (token)
+                {
+                    case "+":
+                    case "-":
+                        // Next token is a flag (option) to set or remove
+                        bool flagvalue = (token == "+");
+                        parser.SkipWhitespace(true);
+                        string flagname = parser.ReadToken();
+                        if (!string.IsNullOrEmpty(flagname))
+                        {
+                            // Add the flag with its value
+                            flagname = flagname.ToLowerInvariant();
+                            flags[flagname] = flagvalue;
+                        }
+                        else
+                        {
+                            parser.ReportError("Expected flag name");
+                            return;
+                        }
+                        break;
+
+                    case "action":
+                    case "native":
+                        // We don't need this, ignore up to the first next ;
+                        while (parser.SkipWhitespace(true))
+                        {
+                            string t = parser.ReadToken();
+                            if (string.IsNullOrEmpty(t) || t == ";") break;
+                        }
+                        break;
+
+                    case "skip_super":
+                        skipsuper = true;
+                        break;
+
+                    case "states":
+                        // Now parse actor states until we reach the end of the states structure
+                        while (parser.SkipWhitespace(true))
+                        {
+                            string statetoken = parser.ReadToken();
+                            if (!string.IsNullOrEmpty(statetoken))
+                            {
+                                // Start of scope?
+                                if (statetoken == "{")
+                                {
+                                    // This is fine
+                                }
+                                // End of scope?
+                                else if (statetoken == "}")
+                                {
+                                    // Done with the states,
+                                    // break out of this parse loop
+                                    break;
+                                }
+                                // State label?
+                                else if (statetoken == ":")
+                                {
+                                    if (!string.IsNullOrEmpty(previoustoken))
+                                    {
+                                        // Parse actor state
+                                        StateStructure st = new DecorateStateStructure(this, parser, parser.DataManager);
+                                        if (parser.HasError) return;
+                                        states[previoustoken.ToLowerInvariant()] = st;
+                                    }
+                                    else
+                                    {
+                                        parser.ReportError("Expected actor state name");
+                                        return;
+                                    }
+                                }
+                                else
+                                {
+                                    // Keep token
+                                    previoustoken = statetoken;
+                                }
+                            }
+                            else
+                            {
+                                parser.ReportError("Unexpected end of structure");
+                                return;
+                            }
+                        }
+                        break;
+
+                    case "var": //mxd
+                                // Type
+                        parser.SkipWhitespace(true);
+                        string typestr = parser.ReadToken().ToUpperInvariant();
+                        UniversalType type = UniversalType.EnumOption; // There is no Unknown type, so let's use something impossiburu...
+                        switch (typestr)
+                        {
+                            case "INT": type = UniversalType.Integer; break;
+                            case "FLOAT": type = UniversalType.Float; break;
+                            default: parser.LogWarning("Unknown user variable type"); break;
+                        }
+
+                        // Name
+                        parser.SkipWhitespace(true);
+                        string name = parser.ReadToken();
+                        if (string.IsNullOrEmpty(name))
+                        {
+                            parser.ReportError("Expected User Variable name");
+                            return;
+                        }
+                        if (!name.StartsWith("user_", StringComparison.OrdinalIgnoreCase))
+                        {
+                            parser.ReportError("User Variable name must start with \"user_\" prefix");
+                            return;
+                        }
+                        if (uservars.ContainsKey(name))
+                        {
+                            parser.ReportError("User Variable \"" + name + "\" is double defined");
+                            return;
+                        }
+                        if (!skipsuper && baseclass != null && baseclass.uservars.ContainsKey(name))
+                        {
+                            parser.ReportError("User variable \"" + name + "\" is already defined in one of the parent classes");
+                            return;
+                        }
+
+                        // Rest
+                        parser.SkipWhitespace(true);
+                        string next = parser.ReadToken();
+                        if (next == "[") // that's User Array. Let's skip it...
+                        {
+                            int arrlen = -1;
+                            if (!parser.ReadSignedInt(ref arrlen))
+                            {
+                                parser.ReportError("Expected User Array length");
+                                return;
+                            }
+                            if (arrlen < 1)
+                            {
+                                parser.ReportError("User Array length must be a positive value");
+                                return;
+                            }
+                            if (!parser.NextTokenIs("]") || !parser.NextTokenIs(";"))
+                            {
+                                return;
+                            }
+                        }
+                        else if (next != ";")
+                        {
+                            parser.ReportError("Expected \";\", but got \"" + next + "\"");
+                            return;
+                        }
+                        else
+                        {
+                            // Add to collection
+                            uservars.Add(name, type);
+                        }
+                        break;
+
+                    case "}":
+                        //mxd. Get user vars from the BaseClass, if we have one
+                        if (!skipsuper && baseclass != null && baseclass.uservars.Count > 0)
+                        {
+                            foreach (var group in baseclass.uservars)
+                                uservars.Add(group.Key, group.Value);
+                        }
+
+                        // Actor scope ends here, break out of this parse loop
+                        done = true;
+                        break;
+
+                    // Monster property?
+                    case "monster":
+                        // This sets certain flags we are interested in
+                        flags["shootable"] = true;
+                        flags["countkill"] = true;
+                        flags["solid"] = true;
+                        flags["canpushwalls"] = true;
+                        flags["canusewalls"] = true;
+                        flags["activatemcross"] = true;
+                        flags["canpass"] = true;
+                        flags["ismonster"] = true;
+                        break;
+
+                    // Projectile property?
+                    case "projectile":
+                        // This sets certain flags we are interested in
+                        flags["noblockmap"] = true;
+                        flags["nogravity"] = true;
+                        flags["dropoff"] = true;
+                        flags["missile"] = true;
+                        flags["activateimpact"] = true;
+                        flags["activatepcross"] = true;
+                        flags["noteleport"] = true;
+                        break;
+
+                    // Clearflags property?
+                    case "clearflags":
+                        // Clear all flags
+                        flags.Clear();
+                        break;
+
+                    // Game property?
+                    case "game":
+                        // Include all tokens on the same line
+                        List<string> games = new List<string>();
+                        while (parser.SkipWhitespace(false))
+                        {
+                            string v = parser.ReadToken();
+                            if (string.IsNullOrEmpty(v))
+                            {
+                                parser.ReportError("Expected \"Game\" property value");
+                                return;
+                            }
+                            if (v == "\n") break;
+                            if (v == "}") return; //mxd
+                            if (v != ",") games.Add(v.ToLowerInvariant());
+                        }
+                        props[token] = games;
+                        break;
+
+                    // Property
+                    default:
+                        // Property begins with $? Then the whole line is a single value
+                        if (token.StartsWith("$"))
+                        {
+                            // This is for editor-only properties such as $sprite and $category
+                            props[token] = new List<string> { (parser.SkipWhitespace(false) ? parser.ReadLine() : "") };
+                        }
+                        else
+                        {
+                            // Next tokens up until the next newline are values
+                            List<string> values = new List<string>();
+                            while (parser.SkipWhitespace(false))
+                            {
+                                string v = parser.ReadToken();
+                                if (string.IsNullOrEmpty(v))
+                                {
+                                    parser.ReportError("Unexpected end of structure");
+                                    return;
+                                }
+                                if (v == "\n") break;
+                                if (v == "}") return; //mxd
+                                if (v != ",") values.Add(v);
+                            }
+
+                            //mxd. Translate scale to xscale and yscale
+                            if (token == "scale")
+                            {
+                                props["xscale"] = values;
+                                props["yscale"] = values;
+                            }
+                            else
+                            {
+                                props[token] = values;
+                            }
+                        }
+                        break;
+                }
+
+                if (done) break; //mxd
+
+                // Keep token
+                previoustoken = token;
+            }
+
+            //mxd. Check if baseclass is valid
+            if (inheritclass.ToLowerInvariant() != "actor" && doomednum > -1 && baseclass == null)
+            {
+                //check if this class inherits from a class defined in game configuration
+                Dictionary<int, ThingTypeInfo> things = General.Map.Config.GetThingTypes();
+                string inheritclasscheck = inheritclass.ToLowerInvariant();
+
+                foreach (KeyValuePair<int, ThingTypeInfo> ti in things)
+                {
+                    if (!string.IsNullOrEmpty(ti.Value.ClassName) && ti.Value.ClassName.ToLowerInvariant() == inheritclasscheck)
+                    {
+                        //states
+                        if (states.Count == 0 && !string.IsNullOrEmpty(ti.Value.Sprite))
+                            states.Add("spawn", new StateStructure(ti.Value.Sprite.Substring(0, 5)));
+
+                        //flags
+                        if (ti.Value.Hangs && !flags.ContainsKey("spawnceiling"))
+                            flags["spawnceiling"] = true;
+
+                        if (ti.Value.Blocking > 0 && !flags.ContainsKey("solid"))
+                            flags["solid"] = true;
+
+                        //properties
+                        if (!props.ContainsKey("height"))
+                            props["height"] = new List<string> { ti.Value.Height.ToString() };
+
+                        if (!props.ContainsKey("radius"))
+                            props["radius"] = new List<string> { ti.Value.Radius.ToString() };
+
+                        return;
+                    }
+                }
+
+                parser.LogWarning("Unable to find \"" + inheritclass + "\" class to inherit from, while parsing \"" + classname + ":" + doomednum + "\"");
+            }
+        }
+
+        #endregion
+    }
+}
diff --git a/Source/Core/ZDoom/DecorateParser.cs b/Source/Core/ZDoom/DecorateParser.cs
index 4fcb9c15..7e76f586 100755
--- a/Source/Core/ZDoom/DecorateParser.cs
+++ b/Source/Core/ZDoom/DecorateParser.cs
@@ -28,785 +28,6 @@ using CodeImp.DoomBuilder.Types;
 
 namespace CodeImp.DoomBuilder.ZDoom
 {
-    internal sealed class DecorateStateGoto : StateGoto
-    {
-        #region ================== DECORATE State Goto parsing
-
-        internal DecorateStateGoto(ActorStructure actor, ZDTextParser zdparser) : base(actor, zdparser)
-        {
-            DecorateParser parser = (DecorateParser)zdparser;
-
-            string firsttarget = "";
-            string secondtarget = "";
-            bool commentreached = false;
-            bool offsetreached = false;
-            string offsetstr = "";
-            int cindex = 0;
-
-            // This is a bitch to parse because for some bizarre reason someone thought it
-            // was funny to allow quotes here. Read the whole line and start parsing this manually.
-            string line = parser.ReadLine();
-
-            // Skip whitespace
-            while ((cindex < line.Length) && ((line[cindex] == ' ') || (line[cindex] == '\t')))
-                cindex++;
-
-            // Parse first target
-            while ((cindex < line.Length) && (line[cindex] != ':'))
-            {
-                // When a comment is reached, we're done here
-                if (line[cindex] == '/')
-                {
-                    if ((cindex + 1 < line.Length) && ((line[cindex + 1] == '/') || (line[cindex + 1] == '*')))
-                    {
-                        commentreached = true;
-                        break;
-                    }
-                }
-
-                // Whitespace ends the string
-                if ((line[cindex] == ' ') || (line[cindex] == '\t'))
-                    break;
-
-                // + sign indicates offset start
-                if (line[cindex] == '+')
-                {
-                    cindex++;
-                    offsetreached = true;
-                    break;
-                }
-
-                // Ignore quotes
-                if (line[cindex] != '"')
-                    firsttarget += line[cindex];
-
-                cindex++;
-            }
-
-            if (!commentreached && !offsetreached)
-            {
-                // Skip whitespace
-                while ((cindex < line.Length) && ((line[cindex] == ' ') || (line[cindex] == '\t')))
-                    cindex++;
-
-                // Parse second target
-                while (cindex < line.Length)
-                {
-                    // When a comment is reached, we're done here
-                    if (line[cindex] == '/')
-                    {
-                        if ((cindex + 1 < line.Length) && ((line[cindex + 1] == '/') || (line[cindex + 1] == '*')))
-                        {
-                            commentreached = true;
-                            break;
-                        }
-                    }
-
-                    // Whitespace ends the string
-                    if ((line[cindex] == ' ') || (line[cindex] == '\t'))
-                        break;
-
-                    // + sign indicates offset start
-                    if (line[cindex] == '+')
-                    {
-                        cindex++;
-                        offsetreached = true;
-                        break;
-                    }
-
-                    // Ignore quotes and semicolons
-                    if ((line[cindex] != '"') && (line[cindex] != ':'))
-                        secondtarget += line[cindex];
-
-                    cindex++;
-                }
-            }
-
-            // Try to find the offset if we still haven't found it yet
-            if (!offsetreached)
-            {
-                // Skip whitespace
-                while ((cindex < line.Length) && ((line[cindex] == ' ') || (line[cindex] == '\t')))
-                    cindex++;
-
-                if ((cindex < line.Length) && (line[cindex] == '+'))
-                {
-                    cindex++;
-                    offsetreached = true;
-                }
-            }
-
-            if (offsetreached)
-            {
-                // Parse offset
-                while (cindex < line.Length)
-                {
-                    // When a comment is reached, we're done here
-                    if (line[cindex] == '/')
-                    {
-                        if ((cindex + 1 < line.Length) && ((line[cindex + 1] == '/') || (line[cindex + 1] == '*')))
-                        {
-                            commentreached = true;
-                            break;
-                        }
-                    }
-
-                    // Whitespace ends the string
-                    if ((line[cindex] == ' ') || (line[cindex] == '\t'))
-                        break;
-
-                    // Ignore quotes and semicolons
-                    if ((line[cindex] != '"') && (line[cindex] != ':'))
-                        offsetstr += line[cindex];
-
-                    cindex++;
-                }
-            }
-
-            // We should now have a first target, optionally a second target and optionally a sprite offset
-
-            // Check if we don't have the class specified
-            if (string.IsNullOrEmpty(secondtarget))
-            {
-                // First target is the state to go to
-                classname = actor.ClassName;
-                statename = firsttarget.ToLowerInvariant().Trim();
-            }
-            else
-            {
-                // First target is the base class to use
-                // Second target is the state to go to
-                classname = firsttarget.ToLowerInvariant().Trim();
-                statename = secondtarget.ToLowerInvariant().Trim();
-            }
-
-            if (offsetstr.Length > 0)
-                int.TryParse(offsetstr, out spriteoffset);
-
-            if ((classname == "super") && (actor.BaseClass != null))
-                classname = actor.BaseClass.ClassName;
-        }
-
-        #endregion
-    }
-
-    public sealed class DecorateStateStructure : StateStructure
-    {
-        #region ================== DECORATE State Structure parsing
-
-        internal DecorateStateStructure(ActorStructure actor, ZDTextParser zdparser, DataManager dataman) : base(actor, zdparser, dataman)
-        {
-            DecorateParser parser = (DecorateParser)zdparser;
-            string lasttoken = "";
-
-            // Skip whitespace
-            while (parser.SkipWhitespace(true))
-            {
-                // Read first token
-                string token = parser.ReadToken().ToLowerInvariant();
-
-                // One of the flow control statements?
-                if ((token == "loop") || (token == "stop") || (token == "wait") || (token == "fail"))
-                {
-                    // Ignore flow control
-                }
-                // Goto?
-                else if (token == "goto")
-                {
-                    gotostate = new DecorateStateGoto(actor, parser);
-                    if (parser.HasError) return;
-                }
-                // Label?
-                else if (token == ":")
-                {
-                    // Rewind so that this label can be read again
-                    if (!string.IsNullOrEmpty(lasttoken))
-                        parser.DataStream.Seek(-(lasttoken.Length + 1), SeekOrigin.Current);
-
-                    // Done here
-                    return;
-                }
-                //mxd. Start of inner scope?
-                else if (token == "{")
-                {
-                    int bracelevel = 1;
-                    while (!string.IsNullOrEmpty(token) && bracelevel > 0)
-                    {
-                        parser.SkipWhitespace(false);
-                        token = parser.ReadToken();
-                        switch (token)
-                        {
-                            case "{": bracelevel++; break;
-                            case "}": bracelevel--; break;
-                        }
-                    }
-                }
-                // End of scope?
-                else if (token == "}")
-                {
-                    // Rewind so that this scope end can be read again
-                    parser.DataStream.Seek(-1, SeekOrigin.Current);
-
-                    // Done here
-                    return;
-                }
-                else
-                {
-                    // First part of the sprite name
-                    token = parser.StripTokenQuotes(token); //mxd. First part of the sprite name can be quoted
-                    if (string.IsNullOrEmpty(token))
-                    {
-                        parser.ReportError("Expected sprite name");
-                        return;
-                    }
-
-                    // Frames of the sprite name
-                    parser.SkipWhitespace(true);
-                    string spriteframes = parser.StripTokenQuotes(parser.ReadToken()); //mxd. Frames can be quoted
-                    if (string.IsNullOrEmpty(spriteframes))
-                    {
-                        parser.ReportError("Expected sprite frame");
-                        return;
-                    }
-
-                    // Label?
-                    if (spriteframes == ":")
-                    {
-                        // Rewind so that this label can be read again
-                        parser.DataStream.Seek(-(token.Length + 1), SeekOrigin.Current);
-
-                        // Done here
-                        return;
-                    }
-
-                    // No first sprite yet?
-                    FrameInfo info = new FrameInfo(); //mxd
-                    if (spriteframes.Length > 0)
-                    {
-                        //mxd. I'm not even 50% sure the parser handles all bizzare cases without shifting sprite name / frame blocks,
-                        // so let's log it as a warning, not an error...
-                        if (token.Length != 4)
-                        {
-                            parser.LogWarning("Invalid sprite name \"" + token.ToUpperInvariant() + "\". Sprite names must be exactly 4 characters long");
-                        }
-                        else
-                        {
-                            // Make the sprite name
-                            string spritename = (token + spriteframes[0]).ToUpperInvariant();
-
-                            // Ignore some odd ZDoom things
-                            if (!spritename.StartsWith("TNT1") && !spritename.StartsWith("----") && !spritename.Contains("#"))
-                            {
-                                info.Sprite = spritename; //mxd
-                                sprites.Add(info);
-                            }
-                        }
-                    }
-
-                    // Continue until the end of the line
-                    parser.SkipWhitespace(false);
-                    string t = parser.ReadToken();
-                    while (!string.IsNullOrEmpty(t) && t != "\n")
-                    {
-                        //mxd. Bright keyword support...
-                        if (t == "bright")
-                        {
-                            info.Bright = true;
-                        }
-                        //mxd. Light() expression support...
-                        else if (t == "light")
-                        {
-                            if (!parser.NextTokenIs("(")) return;
-
-                            if (!parser.SkipWhitespace(true))
-                            {
-                                parser.ReportError("Unexpected end of the structure");
-                                return;
-                            }
-
-                            info.LightName = parser.StripTokenQuotes(parser.ReadToken());
-                            if (string.IsNullOrEmpty(info.LightName))
-                            {
-                                parser.ReportError("Expected dynamic light name");
-                                return;
-                            }
-
-                            if (!parser.SkipWhitespace(true))
-                            {
-                                parser.ReportError("Unexpected end of the structure");
-                                return;
-                            }
-
-                            if (!parser.NextTokenIs(")")) return;
-                        }
-                        //mxd. Inner scope start. Step back and reparse using parent loop
-                        else if (t == "{")
-                        {
-                            // Rewind so that this scope end can be read again
-                            parser.DataStream.Seek(-1, SeekOrigin.Current);
-
-                            // Break out of this loop
-                            break;
-                        }
-                        //mxd. Function params start (those can span multiple lines)
-                        else if (t == "(")
-                        {
-                            int bracelevel = 1;
-                            while (!string.IsNullOrEmpty(token) && bracelevel > 0)
-                            {
-                                parser.SkipWhitespace(true);
-                                token = parser.ReadToken();
-                                switch (token)
-                                {
-                                    case "(": bracelevel++; break;
-                                    case ")": bracelevel--; break;
-                                }
-                            }
-                        }
-                        //mxd. Because stuff like this is also valid: "Actor Oneliner { States { Spawn: WOOT A 1 A_FadeOut(0.1) Loop }}"
-                        else if (t == "}")
-                        {
-                            // Rewind so that this scope end can be read again
-                            parser.DataStream.Seek(-1, SeekOrigin.Current);
-
-                            // Done here
-                            return;
-                        }
-
-                        // Read next token
-                        parser.SkipWhitespace(false);
-                        t = parser.ReadToken().ToLowerInvariant();
-                    }
-                }
-
-                lasttoken = token;
-            }
-        }
-
-        #endregion
-    }
-
-    public sealed class DecorateActorStructure : ActorStructure
-    {
-        #region ================== DECORATE Actor Structure parsing
-
-        internal DecorateActorStructure(ZDTextParser zdparser, DecorateCategoryInfo catinfo) : base(zdparser, catinfo)
-        {
-            DecorateParser parser = (DecorateParser)zdparser;
-            bool done = false; //mxd
-
-            // First next token is the class name
-            parser.SkipWhitespace(true);
-            classname = parser.StripTokenQuotes(parser.ReadToken(ACTOR_CLASS_SPECIAL_TOKENS));
-
-            if (string.IsNullOrEmpty(classname))
-            {
-                parser.ReportError("Expected actor class name");
-                return;
-            }
-
-            //mxd. Fail on duplicates
-            if (parser.ActorsByClass.ContainsKey(classname.ToLowerInvariant()))
-            {
-                parser.ReportError("Actor \"" + classname + "\" is double-defined");
-                return;
-            }
-
-            // Parse tokens before entering the actor scope
-            while (parser.SkipWhitespace(true))
-            {
-                string token = parser.ReadToken();
-                if (!string.IsNullOrEmpty(token))
-                {
-                    token = token.ToLowerInvariant();
-
-                    switch (token)
-                    {
-                        case ":":
-                            // The next token must be the class to inherit from
-                            parser.SkipWhitespace(true);
-                            inheritclass = parser.StripTokenQuotes(parser.ReadToken());
-                            if (string.IsNullOrEmpty(inheritclass))
-                            {
-                                parser.ReportError("Expected class name to inherit from");
-                                return;
-                            }
-
-                            // Find the actor to inherit from
-                            baseclass = parser.GetArchivedActorByName(inheritclass);
-                            break;
-
-                        case "replaces":
-                            // The next token must be the class to replace
-                            parser.SkipWhitespace(true);
-                            replaceclass = parser.StripTokenQuotes(parser.ReadToken());
-                            if (string.IsNullOrEmpty(replaceclass))
-                            {
-                                parser.ReportError("Expected class name to replace");
-                                return;
-                            }
-                            break;
-
-                        case "native":
-                            // Igore this token
-                            break;
-
-                        case "{":
-                            // Actor scope begins here,
-                            // break out of this parse loop
-                            done = true;
-                            break;
-
-                        case "-":
-                            // This could be a negative doomednum (but our parser sees the - as separate token)
-                            // So read whatever is after this token and ignore it (negative doomednum indicates no doomednum)
-                            parser.ReadToken();
-                            break;
-
-                        default:
-                            //mxd. Property begins with $? Then the whole line is a single value
-                            if (token.StartsWith("$"))
-                            {
-                                // This is for editor-only properties such as $sprite and $category
-                                props[token] = new List<string> { (parser.SkipWhitespace(false) ? parser.ReadLine() : "") };
-                                continue;
-                            }
-
-                            if (!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out doomednum)) // Check if numeric
-                            {
-                                // Not numeric!
-                                parser.ReportError("Expected editor number or start of actor scope while parsing \"" + classname + "\"");
-                                return;
-                            }
-
-                            //mxd. Range check
-                            if ((doomednum < General.Map.FormatInterface.MinThingType) || (doomednum > General.Map.FormatInterface.MaxThingType))
-                            {
-                                // Out of bounds!
-                                parser.ReportError("Actor \"" + classname + "\" has invalid editor number. Editor number must be between "
-                                    + General.Map.FormatInterface.MinThingType + " and " + General.Map.FormatInterface.MaxThingType);
-                                return;
-                            }
-                            break;
-                    }
-
-                    if (done) break; //mxd
-                }
-                else
-                {
-                    parser.ReportError("Unexpected end of structure");
-                    return;
-                }
-            }
-
-            // Now parse the contents of actor structure
-            string previoustoken = "";
-            done = false; //mxd
-            while (parser.SkipWhitespace(true))
-            {
-                string token = parser.ReadToken();
-                token = token.ToLowerInvariant();
-
-                switch (token)
-                {
-                    case "+":
-                    case "-":
-                        // Next token is a flag (option) to set or remove
-                        bool flagvalue = (token == "+");
-                        parser.SkipWhitespace(true);
-                        string flagname = parser.ReadToken();
-                        if (!string.IsNullOrEmpty(flagname))
-                        {
-                            // Add the flag with its value
-                            flagname = flagname.ToLowerInvariant();
-                            flags[flagname] = flagvalue;
-                        }
-                        else
-                        {
-                            parser.ReportError("Expected flag name");
-                            return;
-                        }
-                        break;
-
-                    case "action":
-                    case "native":
-                        // We don't need this, ignore up to the first next ;
-                        while (parser.SkipWhitespace(true))
-                        {
-                            string t = parser.ReadToken();
-                            if (string.IsNullOrEmpty(t) || t == ";") break;
-                        }
-                        break;
-
-                    case "skip_super":
-                        skipsuper = true;
-                        break;
-
-                    case "states":
-                        // Now parse actor states until we reach the end of the states structure
-                        while (parser.SkipWhitespace(true))
-                        {
-                            string statetoken = parser.ReadToken();
-                            if (!string.IsNullOrEmpty(statetoken))
-                            {
-                                // Start of scope?
-                                if (statetoken == "{")
-                                {
-                                    // This is fine
-                                }
-                                // End of scope?
-                                else if (statetoken == "}")
-                                {
-                                    // Done with the states,
-                                    // break out of this parse loop
-                                    break;
-                                }
-                                // State label?
-                                else if (statetoken == ":")
-                                {
-                                    if (!string.IsNullOrEmpty(previoustoken))
-                                    {
-                                        // Parse actor state
-                                        StateStructure st = new DecorateStateStructure(this, parser, parser.DataManager);
-                                        if (parser.HasError) return;
-                                        states[previoustoken.ToLowerInvariant()] = st;
-                                    }
-                                    else
-                                    {
-                                        parser.ReportError("Expected actor state name");
-                                        return;
-                                    }
-                                }
-                                else
-                                {
-                                    // Keep token
-                                    previoustoken = statetoken;
-                                }
-                            }
-                            else
-                            {
-                                parser.ReportError("Unexpected end of structure");
-                                return;
-                            }
-                        }
-                        break;
-
-                    case "var": //mxd
-                                // Type
-                        parser.SkipWhitespace(true);
-                        string typestr = parser.ReadToken().ToUpperInvariant();
-                        UniversalType type = UniversalType.EnumOption; // There is no Unknown type, so let's use something impossiburu...
-                        switch (typestr)
-                        {
-                            case "INT": type = UniversalType.Integer; break;
-                            case "FLOAT": type = UniversalType.Float; break;
-                            default: parser.LogWarning("Unknown user variable type"); break;
-                        }
-
-                        // Name
-                        parser.SkipWhitespace(true);
-                        string name = parser.ReadToken();
-                        if (string.IsNullOrEmpty(name))
-                        {
-                            parser.ReportError("Expected User Variable name");
-                            return;
-                        }
-                        if (!name.StartsWith("user_", StringComparison.OrdinalIgnoreCase))
-                        {
-                            parser.ReportError("User Variable name must start with \"user_\" prefix");
-                            return;
-                        }
-                        if (uservars.ContainsKey(name))
-                        {
-                            parser.ReportError("User Variable \"" + name + "\" is double defined");
-                            return;
-                        }
-                        if (!skipsuper && baseclass != null && baseclass.uservars.ContainsKey(name))
-                        {
-                            parser.ReportError("User variable \"" + name + "\" is already defined in one of the parent classes");
-                            return;
-                        }
-
-                        // Rest
-                        parser.SkipWhitespace(true);
-                        string next = parser.ReadToken();
-                        if (next == "[") // that's User Array. Let's skip it...
-                        {
-                            int arrlen = -1;
-                            if (!parser.ReadSignedInt(ref arrlen))
-                            {
-                                parser.ReportError("Expected User Array length");
-                                return;
-                            }
-                            if (arrlen < 1)
-                            {
-                                parser.ReportError("User Array length must be a positive value");
-                                return;
-                            }
-                            if (!parser.NextTokenIs("]") || !parser.NextTokenIs(";"))
-                            {
-                                return;
-                            }
-                        }
-                        else if (next != ";")
-                        {
-                            parser.ReportError("Expected \";\", but got \"" + next + "\"");
-                            return;
-                        }
-                        else
-                        {
-                            // Add to collection
-                            uservars.Add(name, type);
-                        }
-                        break;
-
-                    case "}":
-                        //mxd. Get user vars from the BaseClass, if we have one
-                        if (!skipsuper && baseclass != null && baseclass.uservars.Count > 0)
-                        {
-                            foreach (var group in baseclass.uservars)
-                                uservars.Add(group.Key, group.Value);
-                        }
-
-                        // Actor scope ends here, break out of this parse loop
-                        done = true;
-                        break;
-
-                    // Monster property?
-                    case "monster":
-                        // This sets certain flags we are interested in
-                        flags["shootable"] = true;
-                        flags["countkill"] = true;
-                        flags["solid"] = true;
-                        flags["canpushwalls"] = true;
-                        flags["canusewalls"] = true;
-                        flags["activatemcross"] = true;
-                        flags["canpass"] = true;
-                        flags["ismonster"] = true;
-                        break;
-
-                    // Projectile property?
-                    case "projectile":
-                        // This sets certain flags we are interested in
-                        flags["noblockmap"] = true;
-                        flags["nogravity"] = true;
-                        flags["dropoff"] = true;
-                        flags["missile"] = true;
-                        flags["activateimpact"] = true;
-                        flags["activatepcross"] = true;
-                        flags["noteleport"] = true;
-                        break;
-
-                    // Clearflags property?
-                    case "clearflags":
-                        // Clear all flags
-                        flags.Clear();
-                        break;
-
-                    // Game property?
-                    case "game":
-                        // Include all tokens on the same line
-                        List<string> games = new List<string>();
-                        while (parser.SkipWhitespace(false))
-                        {
-                            string v = parser.ReadToken();
-                            if (string.IsNullOrEmpty(v))
-                            {
-                                parser.ReportError("Expected \"Game\" property value");
-                                return;
-                            }
-                            if (v == "\n") break;
-                            if (v == "}") return; //mxd
-                            if (v != ",") games.Add(v.ToLowerInvariant());
-                        }
-                        props[token] = games;
-                        break;
-
-                    // Property
-                    default:
-                        // Property begins with $? Then the whole line is a single value
-                        if (token.StartsWith("$"))
-                        {
-                            // This is for editor-only properties such as $sprite and $category
-                            props[token] = new List<string> { (parser.SkipWhitespace(false) ? parser.ReadLine() : "") };
-                        }
-                        else
-                        {
-                            // Next tokens up until the next newline are values
-                            List<string> values = new List<string>();
-                            while (parser.SkipWhitespace(false))
-                            {
-                                string v = parser.ReadToken();
-                                if (string.IsNullOrEmpty(v))
-                                {
-                                    parser.ReportError("Unexpected end of structure");
-                                    return;
-                                }
-                                if (v == "\n") break;
-                                if (v == "}") return; //mxd
-                                if (v != ",") values.Add(v);
-                            }
-
-                            //mxd. Translate scale to xscale and yscale
-                            if (token == "scale")
-                            {
-                                props["xscale"] = values;
-                                props["yscale"] = values;
-                            }
-                            else
-                            {
-                                props[token] = values;
-                            }
-                        }
-                        break;
-                }
-
-                if (done) break; //mxd
-
-                // Keep token
-                previoustoken = token;
-            }
-
-            //mxd. Check if baseclass is valid
-            if (inheritclass.ToLowerInvariant() != "actor" && doomednum > -1 && baseclass == null)
-            {
-                //check if this class inherits from a class defined in game configuration
-                Dictionary<int, ThingTypeInfo> things = General.Map.Config.GetThingTypes();
-                string inheritclasscheck = inheritclass.ToLowerInvariant();
-
-                foreach (KeyValuePair<int, ThingTypeInfo> ti in things)
-                {
-                    if (!string.IsNullOrEmpty(ti.Value.ClassName) && ti.Value.ClassName.ToLowerInvariant() == inheritclasscheck)
-                    {
-                        //states
-                        if (states.Count == 0 && !string.IsNullOrEmpty(ti.Value.Sprite))
-                            states.Add("spawn", new StateStructure(ti.Value.Sprite.Substring(0, 5)));
-
-                        //flags
-                        if (ti.Value.Hangs && !flags.ContainsKey("spawnceiling"))
-                            flags["spawnceiling"] = true;
-
-                        if (ti.Value.Blocking > 0 && !flags.ContainsKey("solid"))
-                            flags["solid"] = true;
-
-                        //properties
-                        if (!props.ContainsKey("height"))
-                            props["height"] = new List<string> { ti.Value.Height.ToString() };
-
-                        if (!props.ContainsKey("radius"))
-                            props["radius"] = new List<string> { ti.Value.Radius.ToString() };
-
-                        return;
-                    }
-                }
-
-                parser.LogWarning("Unable to find \"" + inheritclass + "\" class to inherit from, while parsing \"" + classname + ":" + doomednum + "\"");
-            }
-        }
-
-        #endregion
-    }
-
 	public sealed class DecorateParser : ZDTextParser
 	{
 		#region ================== Delegates
diff --git a/Source/Core/ZDoom/DecorateStateGoto.cs b/Source/Core/ZDoom/DecorateStateGoto.cs
new file mode 100755
index 00000000..df44747a
--- /dev/null
+++ b/Source/Core/ZDoom/DecorateStateGoto.cs
@@ -0,0 +1,167 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace CodeImp.DoomBuilder.ZDoom
+{
+    internal sealed class DecorateStateGoto : StateGoto
+    {
+        #region ================== DECORATE State Goto parsing
+
+        internal DecorateStateGoto(ActorStructure actor, ZDTextParser parser) : base(actor, parser)
+        {
+            string firsttarget = "";
+            string secondtarget = "";
+            bool commentreached = false;
+            bool offsetreached = false;
+            string offsetstr = "";
+            int cindex = 0;
+
+            // This is a bitch to parse because for some bizarre reason someone thought it
+            // was funny to allow quotes here. Read the whole line and start parsing this manually.
+            string line = parser.ReadLine();
+
+            // Skip whitespace
+            while ((cindex < line.Length) && ((line[cindex] == ' ') || (line[cindex] == '\t')))
+                cindex++;
+
+            // Parse first target
+            while ((cindex < line.Length) && (line[cindex] != ':'))
+            {
+                // When a comment is reached, we're done here
+                if (line[cindex] == '/')
+                {
+                    if ((cindex + 1 < line.Length) && ((line[cindex + 1] == '/') || (line[cindex + 1] == '*')))
+                    {
+                        commentreached = true;
+                        break;
+                    }
+                }
+
+                // Whitespace ends the string
+                if ((line[cindex] == ' ') || (line[cindex] == '\t'))
+                    break;
+
+                // + sign indicates offset start
+                if (line[cindex] == '+')
+                {
+                    cindex++;
+                    offsetreached = true;
+                    break;
+                }
+
+                // Ignore quotes
+                if (line[cindex] != '"')
+                    firsttarget += line[cindex];
+
+                cindex++;
+            }
+
+            if (!commentreached && !offsetreached)
+            {
+                // Skip whitespace
+                while ((cindex < line.Length) && ((line[cindex] == ' ') || (line[cindex] == '\t')))
+                    cindex++;
+
+                // Parse second target
+                while (cindex < line.Length)
+                {
+                    // When a comment is reached, we're done here
+                    if (line[cindex] == '/')
+                    {
+                        if ((cindex + 1 < line.Length) && ((line[cindex + 1] == '/') || (line[cindex + 1] == '*')))
+                        {
+                            commentreached = true;
+                            break;
+                        }
+                    }
+
+                    // Whitespace ends the string
+                    if ((line[cindex] == ' ') || (line[cindex] == '\t'))
+                        break;
+
+                    // + sign indicates offset start
+                    if (line[cindex] == '+')
+                    {
+                        cindex++;
+                        offsetreached = true;
+                        break;
+                    }
+
+                    // Ignore quotes and semicolons
+                    if ((line[cindex] != '"') && (line[cindex] != ':'))
+                        secondtarget += line[cindex];
+
+                    cindex++;
+                }
+            }
+
+            // Try to find the offset if we still haven't found it yet
+            if (!offsetreached)
+            {
+                // Skip whitespace
+                while ((cindex < line.Length) && ((line[cindex] == ' ') || (line[cindex] == '\t')))
+                    cindex++;
+
+                if ((cindex < line.Length) && (line[cindex] == '+'))
+                {
+                    cindex++;
+                    offsetreached = true;
+                }
+            }
+
+            if (offsetreached)
+            {
+                // Parse offset
+                while (cindex < line.Length)
+                {
+                    // When a comment is reached, we're done here
+                    if (line[cindex] == '/')
+                    {
+                        if ((cindex + 1 < line.Length) && ((line[cindex + 1] == '/') || (line[cindex + 1] == '*')))
+                        {
+                            commentreached = true;
+                            break;
+                        }
+                    }
+
+                    // Whitespace ends the string
+                    if ((line[cindex] == ' ') || (line[cindex] == '\t'))
+                        break;
+
+                    // Ignore quotes and semicolons
+                    if ((line[cindex] != '"') && (line[cindex] != ':'))
+                        offsetstr += line[cindex];
+
+                    cindex++;
+                }
+            }
+
+            // We should now have a first target, optionally a second target and optionally a sprite offset
+
+            // Check if we don't have the class specified
+            if (string.IsNullOrEmpty(secondtarget))
+            {
+                // First target is the state to go to
+                classname = actor.ClassName;
+                statename = firsttarget.ToLowerInvariant().Trim();
+            }
+            else
+            {
+                // First target is the base class to use
+                // Second target is the state to go to
+                classname = firsttarget.ToLowerInvariant().Trim();
+                statename = secondtarget.ToLowerInvariant().Trim();
+            }
+
+            if (offsetstr.Length > 0)
+                int.TryParse(offsetstr, out spriteoffset);
+
+            if ((classname == "super") && (actor.BaseClass != null))
+                classname = actor.BaseClass.ClassName;
+        }
+
+        #endregion
+    }
+}
diff --git a/Source/Core/ZDoom/DecorateStateStructure.cs b/Source/Core/ZDoom/DecorateStateStructure.cs
new file mode 100755
index 00000000..79899ab4
--- /dev/null
+++ b/Source/Core/ZDoom/DecorateStateStructure.cs
@@ -0,0 +1,209 @@
+using CodeImp.DoomBuilder.Config;
+using CodeImp.DoomBuilder.Data;
+using CodeImp.DoomBuilder.Types;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace CodeImp.DoomBuilder.ZDoom
+{
+
+    public sealed class DecorateStateStructure : StateStructure
+    {
+        #region ================== DECORATE State Structure parsing
+
+        internal DecorateStateStructure(ActorStructure actor, ZDTextParser zdparser, DataManager dataman) : base(actor, zdparser, dataman)
+        {
+            DecorateParser parser = (DecorateParser)zdparser;
+            string lasttoken = "";
+
+            // Skip whitespace
+            while (parser.SkipWhitespace(true))
+            {
+                // Read first token
+                string token = parser.ReadToken().ToLowerInvariant();
+
+                // One of the flow control statements?
+                if ((token == "loop") || (token == "stop") || (token == "wait") || (token == "fail"))
+                {
+                    // Ignore flow control
+                }
+                // Goto?
+                else if (token == "goto")
+                {
+                    gotostate = new DecorateStateGoto(actor, parser);
+                    if (parser.HasError) return;
+                }
+                // Label?
+                else if (token == ":")
+                {
+                    // Rewind so that this label can be read again
+                    if (!string.IsNullOrEmpty(lasttoken))
+                        parser.DataStream.Seek(-(lasttoken.Length + 1), SeekOrigin.Current);
+
+                    // Done here
+                    return;
+                }
+                //mxd. Start of inner scope?
+                else if (token == "{")
+                {
+                    int bracelevel = 1;
+                    while (!string.IsNullOrEmpty(token) && bracelevel > 0)
+                    {
+                        parser.SkipWhitespace(false);
+                        token = parser.ReadToken();
+                        switch (token)
+                        {
+                            case "{": bracelevel++; break;
+                            case "}": bracelevel--; break;
+                        }
+                    }
+                }
+                // End of scope?
+                else if (token == "}")
+                {
+                    // Rewind so that this scope end can be read again
+                    parser.DataStream.Seek(-1, SeekOrigin.Current);
+
+                    // Done here
+                    return;
+                }
+                else
+                {
+                    // First part of the sprite name
+                    token = parser.StripTokenQuotes(token); //mxd. First part of the sprite name can be quoted
+                    if (string.IsNullOrEmpty(token))
+                    {
+                        parser.ReportError("Expected sprite name");
+                        return;
+                    }
+
+                    // Frames of the sprite name
+                    parser.SkipWhitespace(true);
+                    string spriteframes = parser.StripTokenQuotes(parser.ReadToken()); //mxd. Frames can be quoted
+                    if (string.IsNullOrEmpty(spriteframes))
+                    {
+                        parser.ReportError("Expected sprite frame");
+                        return;
+                    }
+
+                    // Label?
+                    if (spriteframes == ":")
+                    {
+                        // Rewind so that this label can be read again
+                        parser.DataStream.Seek(-(token.Length + 1), SeekOrigin.Current);
+
+                        // Done here
+                        return;
+                    }
+
+                    // No first sprite yet?
+                    FrameInfo info = new FrameInfo(); //mxd
+                    if (spriteframes.Length > 0)
+                    {
+                        //mxd. I'm not even 50% sure the parser handles all bizzare cases without shifting sprite name / frame blocks,
+                        // so let's log it as a warning, not an error...
+                        if (token.Length != 4)
+                        {
+                            parser.LogWarning("Invalid sprite name \"" + token.ToUpperInvariant() + "\". Sprite names must be exactly 4 characters long");
+                        }
+                        else
+                        {
+                            // Make the sprite name
+                            string spritename = (token + spriteframes[0]).ToUpperInvariant();
+
+                            // Ignore some odd ZDoom things
+                            if (!spritename.StartsWith("TNT1") && !spritename.StartsWith("----") && !spritename.Contains("#"))
+                            {
+                                info.Sprite = spritename; //mxd
+                                sprites.Add(info);
+                            }
+                        }
+                    }
+
+                    // Continue until the end of the line
+                    parser.SkipWhitespace(false);
+                    string t = parser.ReadToken();
+                    while (!string.IsNullOrEmpty(t) && t != "\n")
+                    {
+                        //mxd. Bright keyword support...
+                        if (t == "bright")
+                        {
+                            info.Bright = true;
+                        }
+                        //mxd. Light() expression support...
+                        else if (t == "light")
+                        {
+                            if (!parser.NextTokenIs("(")) return;
+
+                            if (!parser.SkipWhitespace(true))
+                            {
+                                parser.ReportError("Unexpected end of the structure");
+                                return;
+                            }
+
+                            info.LightName = parser.StripTokenQuotes(parser.ReadToken());
+                            if (string.IsNullOrEmpty(info.LightName))
+                            {
+                                parser.ReportError("Expected dynamic light name");
+                                return;
+                            }
+
+                            if (!parser.SkipWhitespace(true))
+                            {
+                                parser.ReportError("Unexpected end of the structure");
+                                return;
+                            }
+
+                            if (!parser.NextTokenIs(")")) return;
+                        }
+                        //mxd. Inner scope start. Step back and reparse using parent loop
+                        else if (t == "{")
+                        {
+                            // Rewind so that this scope end can be read again
+                            parser.DataStream.Seek(-1, SeekOrigin.Current);
+
+                            // Break out of this loop
+                            break;
+                        }
+                        //mxd. Function params start (those can span multiple lines)
+                        else if (t == "(")
+                        {
+                            int bracelevel = 1;
+                            while (!string.IsNullOrEmpty(token) && bracelevel > 0)
+                            {
+                                parser.SkipWhitespace(true);
+                                token = parser.ReadToken();
+                                switch (token)
+                                {
+                                    case "(": bracelevel++; break;
+                                    case ")": bracelevel--; break;
+                                }
+                            }
+                        }
+                        //mxd. Because stuff like this is also valid: "Actor Oneliner { States { Spawn: WOOT A 1 A_FadeOut(0.1) Loop }}"
+                        else if (t == "}")
+                        {
+                            // Rewind so that this scope end can be read again
+                            parser.DataStream.Seek(-1, SeekOrigin.Current);
+
+                            // Done here
+                            return;
+                        }
+
+                        // Read next token
+                        parser.SkipWhitespace(false);
+                        t = parser.ReadToken().ToLowerInvariant();
+                    }
+                }
+
+                lasttoken = token;
+            }
+        }
+
+        #endregion
+    }
+}
diff --git a/Source/Core/ZDoom/StateGoto.cs b/Source/Core/ZDoom/StateGoto.cs
index f12e7e16..5c3a3964 100755
--- a/Source/Core/ZDoom/StateGoto.cs
+++ b/Source/Core/ZDoom/StateGoto.cs
@@ -45,6 +45,11 @@ namespace CodeImp.DoomBuilder.ZDoom
 		#region ================== Constructor / Disposer
 		
 		// Constructor
+        internal StateGoto()
+        {
+
+        }
+
 		internal StateGoto(ActorStructure actor, ZDTextParser parser)
 		{
 
diff --git a/Source/Core/ZDoom/ZScriptActorStructure.cs b/Source/Core/ZDoom/ZScriptActorStructure.cs
new file mode 100755
index 00000000..4836b844
--- /dev/null
+++ b/Source/Core/ZDoom/ZScriptActorStructure.cs
@@ -0,0 +1,499 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace CodeImp.DoomBuilder.ZDoom
+{
+    public sealed class ZScriptActorStructure : ActorStructure
+    {
+        private bool ParseDefaultBlock(ZScriptParser parser, Stream stream, ZScriptTokenizer tokenizer)
+        {
+            tokenizer.SkipWhitespace();
+            ZScriptToken token = tokenizer.ExpectToken(ZScriptTokenType.OpenCurly);
+            if (token == null || !token.IsValid)
+            {
+                parser.ReportError("Expected {, got " + ((Object)token ?? "<null>").ToString());
+                return false;
+            }
+
+            ZScriptTokenType[] whitespacetypes = new ZScriptTokenType[] { ZScriptTokenType.Newline, ZScriptTokenType.Whitespace, ZScriptTokenType.BlockComment, ZScriptTokenType.LineComment };
+
+            // todo parse defaults block
+            while (true)
+            {
+                tokenizer.SkipWhitespace();
+                long cpos = stream.Position;
+                token = tokenizer.ReadToken();
+                if (token == null)
+                {
+                    parser.ReportError("Expected a token");
+                    return false;
+                }
+
+                if (token.Type == ZScriptTokenType.CloseCurly)
+                    break;
+
+                switch (token.Type)
+                {
+                    case ZScriptTokenType.Whitespace:
+                    case ZScriptTokenType.BlockComment:
+                    case ZScriptTokenType.Newline:
+                        break;
+
+                    // flag definition (+/-)
+                    case ZScriptTokenType.OpAdd:
+                    case ZScriptTokenType.OpSubtract:
+                        {
+                            bool flagset = (token.Type == ZScriptTokenType.OpAdd);
+                            string flagname = parser.ParseDottedIdentifier();
+                            if (flagname == null) return false;
+
+                            //parser.LogWarning(string.Format("{0}{1}", (flagset ? '+' : '-'), flagname));
+                            // set flag
+                            flags[flagname] = flagset;
+                            break;
+                        }
+
+                    // property or combo definition
+                    case ZScriptTokenType.Identifier:
+                        {
+                            stream.Position = cpos;
+                            string propertyname = parser.ParseDottedIdentifier();
+                            if (propertyname == null) return false;
+                            List<string> propertyvalues = new List<string>();
+
+                            // read in property values, until semicolon reached
+                            while (true)
+                            {
+                                tokenizer.SkipWhitespace();
+                                List<ZScriptToken> expr = parser.ParseExpression();
+                                string exprstring = ZScriptTokenizer.TokensToString(expr);
+
+                                token = tokenizer.ExpectToken(ZScriptTokenType.Comma, ZScriptTokenType.Semicolon);
+                                if (token == null || !token.IsValid)
+                                {
+                                    parser.ReportError("Expected comma or ;, got " + ((Object)token ?? "<null>").ToString());
+                                    return false;
+                                }
+
+                                propertyvalues.Add(exprstring);
+                                if (token.Type == ZScriptTokenType.Semicolon)
+                                    break;
+                            }
+
+                            //parser.LogWarning(string.Format("{0} = [{1}]", propertyname, string.Join(", ", propertyvalues.ToArray())));
+                            // set property
+                            props[propertyname] = propertyvalues;
+                            break;
+                        }
+                }
+
+            }
+
+            return true;
+        }
+
+        private bool ParseStatesBlock(ZScriptParser parser, Stream stream, ZScriptTokenizer tokenizer)
+        {
+            tokenizer.SkipWhitespace();
+            ZScriptToken token = tokenizer.ExpectToken(ZScriptTokenType.OpenParen, ZScriptTokenType.OpenCurly);
+            if (token == null || !token.IsValid)
+            {
+                parser.ReportError("Expected ( or {, got " + ((Object)token ?? "<null>").ToString());
+                return false;
+            }
+
+            // we can have some weirdass class name list after States keyword. handle that here.
+            if (token.Type == ZScriptTokenType.OpenParen)
+            {
+                parser.ParseExpression(true);
+                tokenizer.SkipWhitespace();
+                token = tokenizer.ExpectToken(ZScriptTokenType.CloseParen);
+                if (token == null || !token.IsValid)
+                {
+                    parser.ReportError("Expected ), got " + ((Object)token ?? "<null>").ToString());
+                    return false;
+                }
+
+                tokenizer.SkipWhitespace();
+                token = tokenizer.ExpectToken(ZScriptTokenType.OpenCurly);
+                if (token == null || !token.IsValid)
+                {
+                    parser.ReportError("Expected {, got " + ((Object)token ?? "<null>").ToString());
+                    return false;
+                }
+            }
+
+            // todo parse states block
+            stream.Position--;
+            token = tokenizer.ExpectToken(ZScriptTokenType.OpenCurly);
+            if (token == null || !token.IsValid)
+            {
+                parser.ReportError("Expected {, got " + ((Object)token ?? "<null>").ToString());
+                return false;
+            }
+
+            string statelabel = "";
+            while (true)
+            {
+                // parse a state block.
+                // this is a seriously broken approach, but let it be for now.
+                StateStructure st = new ZScriptStateStructure(this, parser, parser.DataManager);
+                parser.tokenizer = tokenizer;
+                if (parser.HasError) return false;
+                states[statelabel] = st;
+
+                tokenizer.SkipWhitespace();
+                long cpos = stream.Position;
+                token = tokenizer.ExpectToken(ZScriptTokenType.Identifier, ZScriptTokenType.CloseCurly);
+                if (token == null || !token.IsValid)
+                {
+                    parser.ReportError("Expected state label or }, got " + ((Object)token ?? "<null>").ToString());
+                    return false;
+                }
+
+                if (token.Type == ZScriptTokenType.CloseCurly)
+                    break;
+
+                stream.Position = cpos;
+                statelabel = parser.ParseDottedIdentifier();
+                if (statelabel == null)
+                    return false;
+
+                // otherwise expect a colon
+                token = tokenizer.ExpectToken(ZScriptTokenType.Colon);
+                if (token == null || !token.IsValid)
+                {
+                    parser.ReportError("Expected :, got " + ((Object)token ?? "<null>").ToString());
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        private string ParseTypeName(ZScriptParser parser, Stream stream, ZScriptTokenizer tokenizer)
+        {
+            ZScriptToken token = tokenizer.ExpectToken(ZScriptTokenType.Identifier);
+            if (token == null || !token.IsValid)
+            {
+                parser.ReportError("Expected type name, got " + ((Object)token ?? "<null>").ToString());
+                return null;
+            }
+
+            string outs = token.Value;
+
+            long cpos = stream.Position;
+            tokenizer.SkipWhitespace();
+            token = tokenizer.ReadToken();
+            if (token != null && token.Type == ZScriptTokenType.OpLessThan) // <
+            {
+                string internal_type = ParseTypeName(parser, stream, tokenizer);
+                if (internal_type == null)
+                    return null;
+                token = tokenizer.ExpectToken(ZScriptTokenType.OpGreaterThan);
+                if (token == null || !token.IsValid)
+                {
+                    parser.ReportError("Expected >, got " + ((Object)token ?? "<null>").ToString());
+                    return null;
+                }
+                return outs + "<" + internal_type + ">";
+            }
+            else
+            {
+                stream.Position = cpos;
+                return outs;
+            }
+        }
+
+        internal ZScriptActorStructure(ZDTextParser zdparser, DecorateCategoryInfo catinfo, string _classname, string _replacesname, string _parentname) : base(zdparser, catinfo)
+        {
+            ZScriptParser parser = (ZScriptParser)zdparser;
+            Stream stream = parser.DataStream;
+            ZScriptTokenizer tokenizer = new ZScriptTokenizer(parser.DataReader);
+            parser.tokenizer = tokenizer;
+
+            classname = _classname;
+            replaceclass = _replacesname;
+            baseclass = parser.GetArchivedActorByName(_parentname);
+
+            ZScriptToken cls_open = tokenizer.ExpectToken(ZScriptTokenType.OpenCurly);
+            if (cls_open == null || !cls_open.IsValid)
+            {
+                parser.ReportError("Expected {, got " + ((Object)cls_open ?? "<null>").ToString());
+                return;
+            }
+
+            // in the class definition, we can have the following:
+            // - Defaults block
+            // - States block
+            // - method signature: [native] [action] <type [, type [...]]> <name> (<arguments>);
+            // - method: <method signature (except native)> <block>
+            // - field declaration: [native] <type> <name>;
+            // - enum definition: enum <name> <block>;
+            // we are skipping everything, except Defaults and States.
+            while (true)
+            {
+                tokenizer.SkipWhitespace();
+                long ocpos = stream.Position;
+                ZScriptToken token = tokenizer.ExpectToken(ZScriptTokenType.Identifier, ZScriptTokenType.CloseCurly);
+                if (token == null || !token.IsValid)
+                {
+                    parser.ReportError("Expected identifier, got " + ((Object)cls_open ?? "<null>").ToString());
+                    return;
+                }
+                if (token.Type == ZScriptTokenType.CloseCurly) // end of class
+                    break;
+
+                string b_lower = token.Value.ToLowerInvariant();
+                switch (b_lower)
+                {
+                    case "default":
+                        if (!ParseDefaultBlock(parser, stream, tokenizer))
+                            return;
+                        continue;
+
+                    case "states":
+                        if (!ParseStatesBlock(parser, stream, tokenizer))
+                            return;
+                        continue;
+
+                    case "enum":
+                        if (!parser.ParseEnum())
+                            return;
+                        continue;
+
+                    case "const":
+                        if (!parser.ParseConst())
+                            return;
+                        continue;
+
+                    // apparently we can have a struct inside a class, but not another class.
+                    case "struct":
+                        if (!parser.ParseClassOrStruct(true, false))
+                            return;
+                        continue;
+
+                    default:
+                        stream.Position = ocpos;
+                        break;
+                }
+
+                // try to read in a variable/method.
+                bool bmethod = false;
+                string[] availablemodifiers = new string[] { "static", "native", "action", "readonly", "protected", "private", "virtual", "override", "meta", "deprecated", "final" };
+                string[] methodmodifiers = new string[] { "action", "virtual", "override", "final" };
+                HashSet<string> modifiers = new HashSet<string>();
+                List<string> types = new List<string>();
+                List<string> names = new List<string>();
+                List<int> arraylens = new List<int>();
+                List<ZScriptToken> args = null; // this is for the future
+                List<ZScriptToken> body = null;
+
+                while (true)
+                {
+                    tokenizer.SkipWhitespace();
+                    long cpos = stream.Position;
+                    token = tokenizer.ExpectToken(ZScriptTokenType.Identifier);
+                    if (token == null || !token.IsValid)
+                    {
+                        parser.ReportError("Expected modifier or name, got " + ((Object)cls_open ?? "<null>").ToString());
+                        return;
+                    }
+
+                    b_lower = token.Value.ToLowerInvariant();
+                    if (availablemodifiers.Contains(b_lower))
+                    {
+                        if (modifiers.Contains(b_lower))
+                        {
+                            parser.ReportError("Field/method modifier '" + b_lower + "' was specified twice");
+                            return;
+                        }
+
+                        if (methodmodifiers.Contains(b_lower))
+                            bmethod = true;
+
+                        modifiers.Add(b_lower);
+                    }
+                    else
+                    {
+                        stream.Position = cpos;
+                        break;
+                    }
+                }
+
+                // read in the type name(s)
+                // type name can be:
+                //  - identifier
+                //  - identifier<identifier>
+                while (true)
+                {
+                    tokenizer.SkipWhitespace();
+                    string typename = ParseTypeName(parser, stream, tokenizer);
+                    if (typename == null)
+                        return;
+                    types.Add(typename.ToLowerInvariant());
+                    long cpos = stream.Position;
+                    tokenizer.SkipWhitespace();
+                    token = tokenizer.ReadToken();
+                    if (token == null || token.Type != ZScriptTokenType.Comma)
+                    {
+                        stream.Position = cpos;
+                        break;
+                    }
+                }
+
+                while (true)
+                {
+                    string name = null;
+                    int arraylen = 0;
+
+                    // read in the method/field name
+                    tokenizer.SkipWhitespace();
+                    token = tokenizer.ExpectToken(ZScriptTokenType.Identifier);
+                    if (token == null || !token.IsValid)
+                    {
+                        parser.ReportError("Expected field/method name, got " + ((Object)token ?? "<null>").ToString());
+                        return;
+                    }
+                    name = token.Value.ToLowerInvariant();
+
+                    // check the token. if it's a (, then it's a method. if it's a ;, then it's a field, if it's a [ it's an array field.
+                    // if it's a field and bmethod=true, report error.
+                    tokenizer.SkipWhitespace();
+                    token = tokenizer.ExpectToken(ZScriptTokenType.Comma, ZScriptTokenType.OpenParen, ZScriptTokenType.OpenSquare, ZScriptTokenType.Semicolon);
+                    if (token == null || !token.IsValid)
+                    {
+                        parser.ReportError("Expected comma, ;, [, or argument list, got " + ((Object)token ?? "<null>").ToString());
+                        return;
+                    }
+
+                    if (token.Type == ZScriptTokenType.OpenParen)
+                    {
+                        // if we have multiple names
+                        if (names.Count > 1)
+                        {
+                            parser.ReportError("Cannot have multiple names in a method");
+                            return;
+                        }
+
+                        bmethod = true;
+                        // now, I could parse this properly, but it won't be used anyway, so I'll do it as a fake expression.
+                        args = parser.ParseExpression(true);
+                        token = tokenizer.ExpectToken(ZScriptTokenType.CloseParen);
+                        if (token == null || !token.IsValid)
+                        {
+                            parser.ReportError("Expected ), got " + ((Object)token ?? "<null>").ToString());
+                            return;
+                        }
+
+                        // also get the body block, if any.
+                        tokenizer.SkipWhitespace();
+                        long cpos = stream.Position;
+                        token = tokenizer.ExpectToken(ZScriptTokenType.Semicolon, ZScriptTokenType.OpenCurly);
+                        if (token == null || !token.IsValid)
+                        {
+                            parser.ReportError("Expected ; or {, got " + ((Object)token ?? "<null>").ToString());
+                            return;
+                        }
+
+                        //
+                        if (token.Type == ZScriptTokenType.OpenCurly)
+                        {
+                            stream.Position = cpos;
+                            body = parser.ParseBlock(false);
+                        }
+                    }
+                    else
+                    {
+                        if (bmethod)
+                        {
+                            parser.ReportError("Cannot have virtual, override or action fields");
+                            return;
+                        }
+
+                        // array
+                        if (token.Type == ZScriptTokenType.OpenSquare)
+                        {
+                            // read in the size or a constant.
+                            tokenizer.SkipWhitespace();
+                            token = tokenizer.ExpectToken(ZScriptTokenType.Integer, ZScriptTokenType.Identifier);
+                            if (token == null || !token.IsValid)
+                            {
+                                parser.ReportError("Expected integer or constant, got " + ((Object)token ?? "<null>").ToString());
+                                return;
+                            }
+
+                            // 
+                            if (token.Type == ZScriptTokenType.Integer)
+                                arraylen = token.ValueInt;
+                            else arraylen = -1;
+
+                            tokenizer.SkipWhitespace();
+                            token = tokenizer.ExpectToken(ZScriptTokenType.CloseSquare);
+                            if (token == null || !token.IsValid)
+                            {
+                                parser.ReportError("Expected ], got " + ((Object)token ?? "<null>").ToString());
+                                return;
+                            }
+
+                            tokenizer.SkipWhitespace();
+                            token = tokenizer.ExpectToken(ZScriptTokenType.Semicolon, ZScriptTokenType.Comma);
+                            if (token == null || !token.IsValid)
+                            {
+                                parser.ReportError("Expected ;, got " + ((Object)token ?? "<null>").ToString());
+                                return;
+                            }
+                        }
+                    }
+
+                    names.Add(name);
+                    arraylens.Add(arraylen);
+
+                    if (token.Type != ZScriptTokenType.Comma) // next name
+                        break;
+                }
+
+                // validate modifiers here.
+                // protected and private cannot be combined.
+                if (bmethod)
+                {
+                    if (modifiers.Contains("protected") && modifiers.Contains("private"))
+                    {
+                        parser.ReportError("Cannot have protected and private on the same method");
+                        return;
+                    }
+                    // virtual and override cannot be combined.
+                    int cvirtual = modifiers.Contains("virtual") ? 1 : 0;
+                    cvirtual += modifiers.Contains("override") ? 1 : 0;
+                    cvirtual += modifiers.Contains("final") ? 1 : 0;
+                    if (cvirtual > 1)
+                    {
+                        parser.ReportError("Cannot have virtual, override and final on the same method");
+                        return;
+                    }
+                    // meta (what the fuck is that?) probably cant be on a method
+                    if (modifiers.Contains("meta"))
+                    {
+                        parser.ReportError("Cannot have meta on a method");
+                        return;
+                    }
+                }
+
+                // finished method or field parsing.
+                /*for (int i = 0; i < names.Count; i++)
+                {
+                    string name = names[i];
+                    int arraylen = arraylens[i];
+
+                    string _args = "";
+                    if (args != null) _args = " (" + ZScriptTokenizer.TokensToString(args) + ")";
+                    else if (arraylen != 0) _args = " [" + arraylen.ToString() + "]";
+                    parser.LogWarning(string.Format("{0} {1} {2}{3}", string.Join(" ", modifiers.ToArray()), string.Join(", ", types.ToArray()), name, _args));
+                }*/
+            }
+        }
+    }
+}
diff --git a/Source/Core/ZDoom/ZScriptParser.cs b/Source/Core/ZDoom/ZScriptParser.cs
index 9c66117a..695abc1f 100755
--- a/Source/Core/ZDoom/ZScriptParser.cs
+++ b/Source/Core/ZDoom/ZScriptParser.cs
@@ -9,482 +9,6 @@ using System.Text;
 
 namespace CodeImp.DoomBuilder.ZDoom
 {
-    public sealed class ZScriptActorStructure : ActorStructure
-    {
-        private string ParseDottedIdentifier(ZScriptParser parser, Stream stream, ZScriptTokenizer tokenizer)
-        {
-            string name = "";
-            while (true)
-            {
-                ZScriptToken token = tokenizer.ExpectToken(ZScriptTokenType.Identifier);
-                if (token == null || !token.IsValid)
-                {
-                    parser.ReportError("Expected identifier, got " + ((Object)token ?? "<null>").ToString());
-                    return null;
-                }
-
-                if (name.Length > 0) name += '.';
-                name += token.Value.ToLowerInvariant();
-
-                long cpos = stream.Position;
-                token = tokenizer.ReadToken();
-                if (token.Type != ZScriptTokenType.Dot)
-                {
-                    stream.Position = cpos;
-                    break;
-                }
-            }
-
-            return name;
-        }
-
-        private bool ParseDefaultBlock(ZScriptParser parser, Stream stream, ZScriptTokenizer tokenizer)
-        {
-            tokenizer.SkipWhitespace();
-            ZScriptToken token = tokenizer.ExpectToken(ZScriptTokenType.OpenCurly);
-            if (token == null || !token.IsValid)
-            {
-                parser.ReportError("Expected {, got " + ((Object)token ?? "<null>").ToString());
-                return false;
-            }
-
-            ZScriptTokenType[] whitespacetypes = new ZScriptTokenType[] {ZScriptTokenType.Newline, ZScriptTokenType.Whitespace, ZScriptTokenType.BlockComment, ZScriptTokenType.LineComment};
-
-            // todo parse defaults block
-            while (true)
-            {
-                tokenizer.SkipWhitespace();
-                long cpos = stream.Position;
-                token = tokenizer.ReadToken();
-                if (token == null)
-                {
-                    parser.ReportError("Expected a token");
-                    return false;
-                }
-
-                if (token.Type == ZScriptTokenType.CloseCurly)
-                    break;
-
-                switch (token.Type)
-                {
-                    case ZScriptTokenType.Whitespace:
-                    case ZScriptTokenType.BlockComment:
-                    case ZScriptTokenType.Newline:
-                        break;
-
-                    // flag definition (+/-)
-                    case ZScriptTokenType.OpAdd:
-                    case ZScriptTokenType.OpSubtract:
-                        {
-                            bool flagset = (token.Type == ZScriptTokenType.OpAdd);
-                            string flagname = ParseDottedIdentifier(parser, stream, tokenizer);
-                            if (flagname == null) return false;
-
-                            //parser.LogWarning(string.Format("{0}{1}", (flagset ? '+' : '-'), flagname));
-                            // set flag
-                            flags[flagname] = flagset;
-                            break;
-                        }
-
-                    // property or combo definition
-                    case ZScriptTokenType.Identifier:
-                        {
-                            stream.Position = cpos;
-                            string propertyname = ParseDottedIdentifier(parser, stream, tokenizer);
-                            if (propertyname == null) return false;
-                            List<string> propertyvalues = new List<string>();
-
-                            // read in property values, until semicolon reached
-                            while (true)
-                            {
-                                tokenizer.SkipWhitespace();
-                                List<ZScriptToken> expr = parser.ParseExpression();
-                                string exprstring = ZScriptTokenizer.TokensToString(expr);
-
-                                token = tokenizer.ExpectToken(ZScriptTokenType.Comma, ZScriptTokenType.Semicolon);
-                                if (token == null || !token.IsValid)
-                                {
-                                    parser.ReportError("Expected comma or ;, got " + ((Object)token ?? "<null>").ToString());
-                                    return false;
-                                }
-
-                                propertyvalues.Add(exprstring);
-                                if (token.Type == ZScriptTokenType.Semicolon)
-                                    break;
-                            }
-
-                            //parser.LogWarning(string.Format("{0} = [{1}]", propertyname, string.Join(", ", propertyvalues.ToArray())));
-                            // set property
-                            props[propertyname] = propertyvalues;
-                            break;
-                        }
-                }
-                
-            }
-
-            return true;
-        }
-
-        private bool ParseStatesBlock(ZScriptParser parser, Stream stream, ZScriptTokenizer tokenizer)
-        {
-            tokenizer.SkipWhitespace();
-            ZScriptToken token = tokenizer.ExpectToken(ZScriptTokenType.OpenParen, ZScriptTokenType.OpenCurly);
-            if (token == null || !token.IsValid)
-            {
-                parser.ReportError("Expected ( or {, got " + ((Object)token ?? "<null>").ToString());
-                return false;
-            }
-            
-            // we can have some weirdass class name list after States keyword. handle that here.
-            if (token.Type == ZScriptTokenType.OpenParen)
-            {
-                parser.ParseExpression(true);
-                tokenizer.SkipWhitespace();
-                token = tokenizer.ExpectToken(ZScriptTokenType.CloseParen);
-                if (token == null || !token.IsValid)
-                {
-                    parser.ReportError("Expected ), got " + ((Object)token ?? "<null>").ToString());
-                    return false;
-                }
-
-                tokenizer.SkipWhitespace();
-                token = tokenizer.ExpectToken(ZScriptTokenType.OpenCurly);
-                if (token == null || !token.IsValid)
-                {
-                    parser.ReportError("Expected {, got " + ((Object)token ?? "<null>").ToString());
-                    return false;
-                }
-            }
-
-            // todo parse states block
-            stream.Position--;
-            List<ZScriptToken> tokens = parser.ParseBlock(false); // do nothing for now
-
-            return (tokens != null);
-        }
-
-        private string ParseTypeName(ZScriptParser parser, Stream stream, ZScriptTokenizer tokenizer)
-        {
-            ZScriptToken token = tokenizer.ExpectToken(ZScriptTokenType.Identifier);
-            if (token == null || !token.IsValid)
-            {
-                parser.ReportError("Expected type name, got " + ((Object)token ?? "<null>").ToString());
-                return null;
-            }
-
-            string outs = token.Value;
-
-            long cpos = stream.Position;
-            tokenizer.SkipWhitespace();
-            token = tokenizer.ReadToken();
-            if (token != null && token.Type == ZScriptTokenType.OpLessThan) // <
-            {
-                string internal_type = ParseTypeName(parser, stream, tokenizer);
-                if (internal_type == null)
-                    return null;
-                token = tokenizer.ExpectToken(ZScriptTokenType.OpGreaterThan);
-                if (token == null || !token.IsValid)
-                {
-                    parser.ReportError("Expected >, got " + ((Object)token ?? "<null>").ToString());
-                    return null;
-                }
-                return outs + "<" + internal_type + ">";
-            }
-            else
-            {
-                stream.Position = cpos;
-                return outs;
-            }
-        }
-
-        internal ZScriptActorStructure(ZDTextParser zdparser, DecorateCategoryInfo catinfo, string _classname, string _replacesname, string _parentname) : base(zdparser, catinfo)
-        {
-            ZScriptParser parser = (ZScriptParser)zdparser;
-            Stream stream = parser.DataStream;
-            ZScriptTokenizer tokenizer = new ZScriptTokenizer(parser.DataReader);
-            parser.tokenizer = tokenizer;
-
-            classname = _classname;
-            replaceclass = _replacesname;
-
-            ZScriptToken cls_open = tokenizer.ExpectToken(ZScriptTokenType.OpenCurly);
-            if (cls_open == null || !cls_open.IsValid)
-            {
-                parser.ReportError("Expected {, got " + ((Object)cls_open ?? "<null>").ToString());
-                return;
-            }
-
-            // in the class definition, we can have the following:
-            // - Defaults block
-            // - States block
-            // - method signature: [native] [action] <type [, type [...]]> <name> (<arguments>);
-            // - method: <method signature (except native)> <block>
-            // - field declaration: [native] <type> <name>;
-            // - enum definition: enum <name> <block>;
-            // we are skipping everything, except Defaults and States.
-            while (true)
-            {
-                tokenizer.SkipWhitespace();
-                long ocpos = stream.Position;
-                ZScriptToken token = tokenizer.ExpectToken(ZScriptTokenType.Identifier, ZScriptTokenType.CloseCurly);
-                if (token == null || !token.IsValid)
-                {
-                    parser.ReportError("Expected identifier, got " + ((Object)cls_open ?? "<null>").ToString());
-                    return;
-                }
-                if (token.Type == ZScriptTokenType.CloseCurly) // end of class
-                    break;
-
-                string b_lower = token.Value.ToLowerInvariant();
-                switch (b_lower)
-                {
-                    case "default":
-                        if (!ParseDefaultBlock(parser, stream, tokenizer))
-                            return;
-                        continue;
-
-                    case "states":
-                        if (!ParseStatesBlock(parser, stream, tokenizer))
-                            return;
-                        continue;
-
-                    case "enum":
-                        if (!parser.ParseEnum())
-                            return;
-                        continue;
-
-                    case "const":
-                        if (!parser.ParseConst())
-                            return;
-                        continue;
-
-                        // apparently we can have a struct inside a class, but not another class.
-                    case "struct":
-                        if (!parser.ParseClassOrStruct(true, false))
-                            return;
-                        continue;
-
-                    default:
-                        stream.Position = ocpos;
-                        break;
-                }
-
-                // try to read in a variable/method.
-                bool bmethod = false;
-                string[] availablemodifiers = new string[] { "static", "native", "action", "readonly", "protected", "private", "virtual", "override", "meta", "deprecated", "final" };
-                string[] methodmodifiers = new string[] { "action", "virtual", "override", "final" };
-                HashSet<string> modifiers = new HashSet<string>();
-                List<string> types = new List<string>();
-                List<string> names = new List<string>();
-                List<int> arraylens = new List<int>();
-                List<ZScriptToken> args = null; // this is for the future
-                List<ZScriptToken> body = null;
-
-                while (true)
-                {
-                    tokenizer.SkipWhitespace();
-                    long cpos = stream.Position;
-                    token = tokenizer.ExpectToken(ZScriptTokenType.Identifier);
-                    if (token == null || !token.IsValid)
-                    {
-                        parser.ReportError("Expected modifier or name, got " + ((Object)cls_open ?? "<null>").ToString());
-                        return;
-                    }
-
-                    b_lower = token.Value.ToLowerInvariant();
-                    if (availablemodifiers.Contains(b_lower))
-                    {
-                        if (modifiers.Contains(b_lower))
-                        {
-                            parser.ReportError("Field/method modifier '" + b_lower + "' was specified twice");
-                            return;
-                        }
-
-                        if (methodmodifiers.Contains(b_lower))
-                            bmethod = true;
-
-                        modifiers.Add(b_lower);
-                    }
-                    else
-                    {
-                        stream.Position = cpos;
-                        break;
-                    }
-                }
-
-                // read in the type name(s)
-                // type name can be:
-                //  - identifier
-                //  - identifier<identifier>
-                while (true)
-                {
-                    tokenizer.SkipWhitespace();
-                    string typename = ParseTypeName(parser, stream, tokenizer);
-                    if (typename == null)
-                        return;
-                    types.Add(typename.ToLowerInvariant());
-                    long cpos = stream.Position;
-                    tokenizer.SkipWhitespace();
-                    token = tokenizer.ReadToken();
-                    if (token == null || token.Type != ZScriptTokenType.Comma)
-                    {
-                        stream.Position = cpos;
-                        break;
-                    }
-                }
-
-                while (true)
-                {
-                    string name = null;
-                    int arraylen = 0;
-
-                    // read in the method/field name
-                    tokenizer.SkipWhitespace();
-                    token = tokenizer.ExpectToken(ZScriptTokenType.Identifier);
-                    if (token == null || !token.IsValid)
-                    {
-                        parser.ReportError("Expected field/method name, got " + ((Object)token ?? "<null>").ToString());
-                        return;
-                    }
-                    name = token.Value.ToLowerInvariant();
-
-                    // check the token. if it's a (, then it's a method. if it's a ;, then it's a field, if it's a [ it's an array field.
-                    // if it's a field and bmethod=true, report error.
-                    tokenizer.SkipWhitespace();
-                    token = tokenizer.ExpectToken(ZScriptTokenType.Comma, ZScriptTokenType.OpenParen, ZScriptTokenType.OpenSquare, ZScriptTokenType.Semicolon);
-                    if (token == null || !token.IsValid)
-                    {
-                        parser.ReportError("Expected comma, ;, [, or argument list, got " + ((Object)token ?? "<null>").ToString());
-                        return;
-                    }
-
-                    if (token.Type == ZScriptTokenType.OpenParen)
-                    {
-                        // if we have multiple names
-                        if (names.Count > 1)
-                        {
-                            parser.ReportError("Cannot have multiple names in a method");
-                            return;
-                        }
-
-                        bmethod = true;
-                        // now, I could parse this properly, but it won't be used anyway, so I'll do it as a fake expression.
-                        args = parser.ParseExpression(true);
-                        token = tokenizer.ExpectToken(ZScriptTokenType.CloseParen);
-                        if (token == null || !token.IsValid)
-                        {
-                            parser.ReportError("Expected ), got " + ((Object)token ?? "<null>").ToString());
-                            return;
-                        }
-
-                        // also get the body block, if any.
-                        tokenizer.SkipWhitespace();
-                        long cpos = stream.Position;
-                        token = tokenizer.ExpectToken(ZScriptTokenType.Semicolon, ZScriptTokenType.OpenCurly);
-                        if (token == null || !token.IsValid)
-                        {
-                            parser.ReportError("Expected ; or {, got " + ((Object)token ?? "<null>").ToString());
-                            return;
-                        }
-
-                        //
-                        if (token.Type == ZScriptTokenType.OpenCurly)
-                        {
-                            stream.Position = cpos;
-                            body = parser.ParseBlock(false);
-                        }
-                    }
-                    else
-                    {
-                        if (bmethod)
-                        {
-                            parser.ReportError("Cannot have virtual, override or action fields");
-                            return;
-                        }
-
-                        // array
-                        if (token.Type == ZScriptTokenType.OpenSquare)
-                        {
-                            // read in the size or a constant.
-                            tokenizer.SkipWhitespace();
-                            token = tokenizer.ExpectToken(ZScriptTokenType.Integer, ZScriptTokenType.Identifier);
-                            if (token == null || !token.IsValid)
-                            {
-                                parser.ReportError("Expected integer or constant, got " + ((Object)token ?? "<null>").ToString());
-                                return;
-                            }
-
-                            // 
-                            if (token.Type == ZScriptTokenType.Integer)
-                                arraylen = token.ValueInt;
-                            else arraylen = -1;
-
-                            tokenizer.SkipWhitespace();
-                            token = tokenizer.ExpectToken(ZScriptTokenType.CloseSquare);
-                            if (token == null || !token.IsValid)
-                            {
-                                parser.ReportError("Expected ], got " + ((Object)token ?? "<null>").ToString());
-                                return;
-                            }
-
-                            tokenizer.SkipWhitespace();
-                            token = tokenizer.ExpectToken(ZScriptTokenType.Semicolon, ZScriptTokenType.Comma);
-                            if (token == null || !token.IsValid)
-                            {
-                                parser.ReportError("Expected ;, got " + ((Object)token ?? "<null>").ToString());
-                                return;
-                            }
-                        }
-                    }
-
-                    names.Add(name);
-                    arraylens.Add(arraylen);
-
-                    if (token.Type != ZScriptTokenType.Comma) // next name
-                        break;
-                }
-
-                // validate modifiers here.
-                // protected and private cannot be combined.
-                if (bmethod)
-                {
-                    if (modifiers.Contains("protected") && modifiers.Contains("private"))
-                    {
-                        parser.ReportError("Cannot have protected and private on the same method");
-                        return;
-                    }
-                    // virtual and override cannot be combined.
-                    int cvirtual = modifiers.Contains("virtual")?1:0;
-                    cvirtual += modifiers.Contains("override")?1:0;
-                    cvirtual += modifiers.Contains("final")?1:0;
-                    if (cvirtual > 1)
-                    {
-                        parser.ReportError("Cannot have virtual, override and final on the same method");
-                        return;
-                    }
-                    // meta (what the fuck is that?) probably cant be on a method
-                    if (modifiers.Contains("meta"))
-                    {
-                        parser.ReportError("Cannot have meta on a method");
-                        return;
-                    }
-                }
-
-                // finished method or field parsing.
-                /*for (int i = 0; i < names.Count; i++)
-                {
-                    string name = names[i];
-                    int arraylen = arraylens[i];
-
-                    string _args = "";
-                    if (args != null) _args = " (" + ZScriptTokenizer.TokensToString(args) + ")";
-                    else if (arraylen != 0) _args = " [" + arraylen.ToString() + "]";
-                    parser.LogWarning(string.Format("{0} {1} {2}{3}", string.Join(" ", modifiers.ToArray()), string.Join(", ", types.ToArray()), name, _args));
-                }*/
-            }
-        }
-    }
-
     public sealed class ZScriptParser : ZDTextParser
     {
         #region ================== Internal classes
@@ -982,6 +506,65 @@ namespace CodeImp.DoomBuilder.ZDoom
             return true;
         }
 
+        internal bool ParseInteger(out int value)
+        {
+            value = 1;
+
+            ZScriptToken token = tokenizer.ExpectToken(ZScriptTokenType.Integer, ZScriptTokenType.OpAdd, ZScriptTokenType.OpSubtract);
+            if (token == null || !token.IsValid)
+            {
+                ReportError("Expected integer, got " + ((Object)token ?? "<null>").ToString());
+                return false;
+            }
+
+            if (token.Type == ZScriptTokenType.OpSubtract) value = -1;
+
+            if (token.Type != ZScriptTokenType.Integer)
+            {
+                token = tokenizer.ExpectToken(ZScriptTokenType.Integer);
+                if (token == null || !token.IsValid)
+                {
+                    ReportError("Expected integer, got " + ((Object)token ?? "<null>").ToString());
+                    return false;
+                }
+
+                value *= token.ValueInt;
+                return true;
+            }
+            else
+            {
+                value = token.ValueInt;
+                return true;
+            }
+        }
+
+        internal string ParseDottedIdentifier()
+        {
+            string name = "";
+            while (true)
+            {
+                ZScriptToken token = tokenizer.ExpectToken(ZScriptTokenType.Identifier);
+                if (token == null || !token.IsValid)
+                {
+                    ReportError("Expected identifier, got " + ((Object)token ?? "<null>").ToString());
+                    return null;
+                }
+
+                if (name.Length > 0) name += '.';
+                name += token.Value.ToLowerInvariant();
+
+                long cpos = datastream.Position;
+                token = tokenizer.ReadToken();
+                if (token.Type != ZScriptTokenType.Dot)
+                {
+                    datastream.Position = cpos;
+                    break;
+                }
+            }
+
+            return name;
+        }
+
         internal bool ParseClassOrStruct(bool isstruct, bool extend)
         {
             // 'class' keyword is already parsed
diff --git a/Source/Core/ZDoom/ZScriptStateGoto.cs b/Source/Core/ZDoom/ZScriptStateGoto.cs
new file mode 100755
index 00000000..7394eda0
--- /dev/null
+++ b/Source/Core/ZDoom/ZScriptStateGoto.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace CodeImp.DoomBuilder.ZDoom
+{
+    internal sealed class ZScriptStateGoto : StateGoto
+    {
+        internal ZScriptStateGoto(ActorStructure actor, ZDTextParser zdparser) : base(actor, zdparser)
+        {
+            // goto syntax that is accepted by GZDB is [classname::]statename[+offset]
+
+            ZScriptParser parser = (ZScriptParser)zdparser;
+            Stream stream = parser.DataStream;
+            ZScriptTokenizer tokenizer = new ZScriptTokenizer(parser.DataReader);
+            parser.tokenizer = tokenizer;
+
+            tokenizer.SkipWhitespace();
+            string firsttarget = parser.ParseDottedIdentifier();
+            if (firsttarget == null)
+                return;
+
+            ZScriptToken token;
+
+            string secondtarget = null;
+            int offset = 0;
+
+            tokenizer.SkipWhitespace();
+            token = tokenizer.ExpectToken(ZScriptTokenType.DoubleColon);
+            if (token != null && token.IsValid)
+            {
+                secondtarget = parser.ParseDottedIdentifier();
+                if (secondtarget == null)
+                    return;
+            }
+
+            tokenizer.SkipWhitespace();
+            token = tokenizer.ExpectToken(ZScriptTokenType.OpAdd);
+            if (token != null && token.IsValid)
+            {
+                tokenizer.SkipWhitespace();
+                token = tokenizer.ExpectToken(ZScriptTokenType.Integer);
+                if (token == null || !token.IsValid)
+                {
+                    parser.ReportError("Expected state offset, got " + ((Object)token ?? "<null>").ToString());
+                    return;
+                }
+
+                offset = token.ValueInt;
+            }
+
+            // Check if we don't have the class specified
+            if (string.IsNullOrEmpty(secondtarget))
+            {
+                // First target is the state to go to
+                classname = actor.ClassName;
+                statename = firsttarget.ToLowerInvariant().Trim();
+            }
+            else
+            {
+                // First target is the base class to use
+                // Second target is the state to go to
+                classname = firsttarget.ToLowerInvariant().Trim();
+                statename = secondtarget.ToLowerInvariant().Trim();
+            }
+
+            spriteoffset = offset;
+
+            if ((classname == "super") && (actor.BaseClass != null))
+                classname = actor.BaseClass.ClassName;
+        }
+    }
+}
diff --git a/Source/Core/ZDoom/ZScriptStateStructure.cs b/Source/Core/ZDoom/ZScriptStateStructure.cs
new file mode 100755
index 00000000..836f334a
--- /dev/null
+++ b/Source/Core/ZDoom/ZScriptStateStructure.cs
@@ -0,0 +1,279 @@
+using CodeImp.DoomBuilder.Data;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace CodeImp.DoomBuilder.ZDoom
+{
+    public sealed class ZScriptStateStructure : StateStructure
+    {
+        ZScriptToken TryReadSprite(ZScriptParser parser, Stream stream, ZScriptTokenizer tokenizer)
+        {
+            // this is a hack to read #### and ----.
+            // also [\]
+            string specialinvalid = "-#";
+            string outs = "";
+            long cpos = stream.Position;
+            for (int i = 0; ; i++)
+            {
+                cpos = stream.Position;
+                ZScriptToken token = tokenizer.ReadToken(true);
+
+                if ((token == null) ||
+                    (token.Type != ZScriptTokenType.Invalid) ||
+                    (token.Value == ";") ||
+                    ((outs.Length > 0 && token.Value[0] != outs[0]) && specialinvalid.Contains(token.Value[0])))
+                {
+                    stream.Position = cpos;
+                    break;
+                }
+
+                outs += token.Value[0];
+            }
+
+            if (outs.Length > 0)
+            {
+                ZScriptToken tok = new ZScriptToken();
+                tok.Type = ZScriptTokenType.String;
+                tok.Value = outs;
+                return tok;
+            }
+
+            stream.Position = cpos;
+            return null;
+        }
+
+        internal ZScriptStateStructure(ActorStructure actor, ZDTextParser zdparser, DataManager dataman) : base(actor, zdparser, dataman)
+        {
+            ZScriptParser parser = (ZScriptParser)zdparser;
+            Stream stream = parser.DataStream;
+            ZScriptTokenizer tokenizer = new ZScriptTokenizer(parser.DataReader);
+            parser.tokenizer = tokenizer;
+
+            // todo: parse stuff
+            // 
+            string[] control_keywords = new string[] { "goto", "loop", "wait", "fail", "stop" };
+            
+            while (true)
+            {
+                // expect identifier or string.
+                // if it's an identifier, it can be goto/loop/wait/fail/stop.
+                // if it's a string, it's always a sprite name.
+                tokenizer.SkipWhitespace();
+                long cpos = stream.Position;
+                ZScriptToken token = tokenizer.ExpectToken(ZScriptTokenType.Identifier, ZScriptTokenType.String, ZScriptTokenType.CloseCurly);
+                if (token == null || !token.IsValid)
+                {
+                    ZScriptToken _token = TryReadSprite(parser, stream, tokenizer);
+                    if (_token == null)
+                    {
+                        parser.ReportError("Expected identifier or string, got " + ((Object)token ?? "<null>").ToString());
+                        return;
+                    }
+
+                    token = _token;
+                }
+
+                if (token.Type == ZScriptTokenType.CloseCurly)
+                {
+                    stream.Position--;
+                    return; // done
+                }
+                else if (token.Type == ZScriptTokenType.Identifier)
+                {
+                    string s_keyword = token.Value.ToLowerInvariant();
+                    if (control_keywords.Contains(s_keyword))
+                    {
+                        if (s_keyword == "goto") // just use decorate goto here. should work. but check for semicolon!
+                        {
+                            gotostate = new ZScriptStateGoto(actor, parser);
+                            parser.tokenizer = tokenizer;
+                        }
+
+                        //parser.LogWarning(string.Format("keyword {0}", s_keyword));
+
+                        tokenizer.SkipWhitespace();
+                        token = tokenizer.ExpectToken(ZScriptTokenType.Semicolon);
+                        if (token == null || !token.IsValid)
+                        {
+                            parser.ReportError("Expected ;, got " + ((Object)token ?? "<null>").ToString());
+                            return;
+                        }
+
+                        continue;
+                    }
+                }
+
+                // make sure it's not a label of the next state. if it is, break out.
+                long cpos2 = stream.Position; // local rewind point
+                ZScriptToken token2 = tokenizer.ExpectToken(ZScriptTokenType.Colon, ZScriptTokenType.Dot);
+                bool nextstate = (token2 != null && token2.IsValid);
+                stream.Position = cpos2; // rewind to before state label read
+                if (nextstate)
+                {
+                    stream.Position = cpos;
+                    break;
+                }
+
+                // it's a frame definition. read it.
+                string spritename = token.Value.ToLowerInvariant();
+                if (spritename.Length != 4)
+                {
+                    parser.ReportError("Sprite name should be exactly 4 characters long (got " + spritename + ")");
+                    return;
+                }
+
+                tokenizer.SkipWhitespace();
+
+                token = TryReadSprite(parser, stream, tokenizer);
+                if (token == null)
+                {
+                    parser.ReportError("Expected sprite frame(s)");
+                    return;
+                }
+
+                string spriteframes = token.Value;
+
+                //parser.LogWarning(string.Format("sprite {0} {1}", spritename, spriteframes));
+
+                // duration
+                int duration;
+                tokenizer.SkipWhitespace();
+                if (!parser.ParseInteger(out duration))
+                    return;
+
+                // now, it can also contain BRIGHT, LIGHT(), OFFSET()
+                string[] allspecials = new string[] { "bright", "light", "offset", "fast", "slow" };
+                HashSet<string> specials = new HashSet<string>();
+                // maybe something else. I don't know.
+                FrameInfo info = new FrameInfo();
+
+                // Make the sprite name
+                string realspritename = (spritename + spriteframes[0]).ToUpperInvariant();
+
+                // Ignore some odd ZDoom things
+                if (!realspritename.StartsWith("TNT1") && !realspritename.StartsWith("----") && !realspritename.Contains("#"))
+                {
+                    info.Sprite = realspritename; //mxd
+                    sprites.Add(info);
+                }
+
+                while (true)
+                {
+                    tokenizer.SkipWhitespace();
+                    cpos2 = stream.Position;
+                    token = tokenizer.ExpectToken(ZScriptTokenType.Identifier, ZScriptTokenType.Semicolon, ZScriptTokenType.OpenCurly);
+                    if (token == null || !token.IsValid)
+                    {
+                        parser.ReportError("Expected identifier, ;, or {, got " + ((Object)token ?? "<null>").ToString());
+                        return;
+                    }
+
+                    // if it's opencurly, it means that everything else is an anonymous block.
+                    // skip/parse that.
+                    // if it's semicolon, it means end of the frame.
+                    // if it's BRIGHT, LIGHT() or OFFSET(), it should be processed.
+                    // if it's something else (but identifier), then it's an action function call, process it.
+                    if (token.Type == ZScriptTokenType.OpenCurly)
+                    {
+                        stream.Position--;
+                        if (!parser.SkipBlock())
+                            return;
+                        break; // this block is done
+                    }
+                    else if (token.Type == ZScriptTokenType.Semicolon)
+                    {
+                        break; // done
+                    }
+                    else // identifier
+                    {
+                        string special = token.Value.ToLowerInvariant();
+                        if (allspecials.Contains(special))
+                        {
+                            if (specials.Contains(special))
+                            {
+                                parser.ReportError("'" + special + "' cannot be used twice");
+                                return;
+                            }
+
+                            specials.Add(special);
+
+                            if (special == "bright")
+                            {
+                                info.Bright = true;
+                            }
+                            else if (special == "light" || special == "offset")
+                            {
+                                tokenizer.SkipWhitespace();
+                                token = tokenizer.ExpectToken(ZScriptTokenType.OpenParen);
+                                if (token == null || !token.IsValid)
+                                {
+                                    parser.ReportError("Expected (, got " + ((Object)token ?? "<null>").ToString());
+                                    return;
+                                }
+
+                                List<ZScriptToken> tokens = parser.ParseExpression(true);
+                                tokenizer.SkipWhitespace();
+                                token = tokenizer.ExpectToken(ZScriptTokenType.CloseParen);
+                                if (token == null || !token.IsValid)
+                                {
+                                    parser.ReportError("Expected ), got " + ((Object)token ?? "<null>").ToString());
+                                    return;
+                                }
+
+                                // parse the light expression.
+                                if (special == "light")
+                                {
+                                    if (tokens.Count != 1 || (tokens[0].Type != ZScriptTokenType.String && tokens[0].Type != ZScriptTokenType.Identifier))
+                                    {
+                                        parser.ReportError("Light() special takes one string argument");
+                                        return;
+                                    }
+
+                                    info.LightName = tokens[0].Value;
+                                }
+                            }
+                        }
+                        else
+                        {
+                            // 
+                            stream.Position = cpos2;
+                            string actionfunction = parser.ParseDottedIdentifier();
+                            //parser.LogWarning("actionfunction = " + actionfunction);
+                            if (actionfunction == null)
+                                return;
+                            // 
+                            tokenizer.SkipWhitespace();
+                            token = tokenizer.ExpectToken(ZScriptTokenType.OpenParen);
+                            if (token != null && token.IsValid)
+                            {
+                                List<ZScriptToken> tokens = parser.ParseExpression(true);
+                                tokenizer.SkipWhitespace();
+                                token = tokenizer.ExpectToken(ZScriptTokenType.CloseParen);
+                                if (token == null || !token.IsValid)
+                                {
+                                    parser.ReportError("Expected ), got " + ((Object)token ?? "<null>").ToString());
+                                    return;
+                                }
+
+                                // possibly do something with the arguments? not now though.
+                            }
+                                
+                            // expect semicolon and break.
+                            token = tokenizer.ExpectToken(ZScriptTokenType.Semicolon);
+                            if (token == null || !token.IsValid)
+                            {
+                                parser.ReportError("Expected ;, got " + ((Object)token ?? "<null>").ToString());
+                                return;
+                            }
+
+                            break;
+                        } // if not special
+                    } // if identifier
+                } // frame parsing loop (inner)
+            } // state parsing loop (outer)
+        }
+    }
+}
diff --git a/Source/Core/ZDoom/ZScriptTokenizer.cs b/Source/Core/ZDoom/ZScriptTokenizer.cs
index e8906bd6..06d22b52 100755
--- a/Source/Core/ZDoom/ZScriptTokenizer.cs
+++ b/Source/Core/ZDoom/ZScriptTokenizer.cs
@@ -60,6 +60,7 @@ namespace CodeImp.DoomBuilder.ZDoom
         // ternary operator (x ? y : z), also the colon after state labels
         [ZScriptTokenString("?")] Questionmark,
         [ZScriptTokenString(":")] Colon,
+        [ZScriptTokenString("::")] DoubleColon,
 
         // + - * / << >> ~ ^ & |
         [ZScriptTokenString("+")] OpAdd,
@@ -183,32 +184,32 @@ namespace CodeImp.DoomBuilder.ZDoom
                 // 
                 string whitespace = " \r\t\u00A0";
 
-                if (!short_circuit)
+                // check whitespace
+                if (whitespace.Contains(c))
                 {
-                    // check whitespace
-                    if (whitespace.Contains(c))
+                    string ws_content = "";
+                    ws_content += c;
+                    while (true)
                     {
-                        string ws_content = "";
-                        ws_content += c;
-                        while (true)
+                        char cnext = reader.ReadChar();
+                        if (whitespace.Contains(cnext))
                         {
-                            char cnext = reader.ReadChar();
-                            if (whitespace.Contains(cnext))
-                            {
-                                ws_content += cnext;
-                                continue;
-                            }
-
-                            reader.BaseStream.Position--;
-                            break;
+                            ws_content += cnext;
+                            continue;
                         }
 
-                        tok = new ZScriptToken();
-                        tok.Type = ZScriptTokenType.Whitespace;
-                        tok.Value = ws_content;
-                        return tok;
+                        reader.BaseStream.Position--;
+                        break;
                     }
 
+                    tok = new ZScriptToken();
+                    tok.Type = ZScriptTokenType.Whitespace;
+                    tok.Value = ws_content;
+                    return tok;
+                }
+
+                if (!short_circuit)
+                {
                     // check identifier
                     if ((c >= 'a' && c <= 'z') ||
                         (c >= 'A' && c <= 'Z') ||