protected virtual void WriteActor(Actor actor)
        {
            this.WriteActorHeader(actor);

            this.writer.WriteLine("{");

            this.WriteActorCombos(actor);
            this.WriteActorFlags(actor);
            this.WriteActorProperties(actor);
            this.writer.WriteLine("States{");
            this.WriteActorStateMachine(actor);
            this.writer.WriteLine("}");

            this.writer.WriteLine("}");
        }
        protected virtual void WriteActorCombos(Actor actor)
        {
            if (actor.HasMonsterCombo)
            {
                this.writer.WriteLine("MONSTER");
            }

            if (actor.HasProjectileCombo)
            {
                this.writer.WriteLine("PROJECTILE");
            }
        }
        protected virtual void WriteActorProperties(Actor actor)
        {
            var keyValPairs =
                from property in actor.Properties
                from value in actor.Properties[property.Key]
                select new { Key = property.Key, Value = value };

            foreach (var pair in keyValPairs)
            {
                this.writer.WriteLine(pair.Key + " " + pair.Value);
            }
        }
        protected virtual void WriteActorStateMachine(Actor actor)
        {
            Random r = new Random();

            var jumpTargetLabelsByState = actor.StateMachine.States.OfType<GoToState>().ToDictionary(p => p, p => "__j" + r.Next(0, int.MaxValue));
            var labelsFromJumpTargets = jumpTargetLabelsByState.ToLookup(p => p.Key.Target, p => p.Value);

            ILookup<State, string> labelsByState = actor.StateMachine.Labels.ToLookup(p => p.Value, p => p.Key).Union(labelsFromJumpTargets);

            foreach (State state in actor.StateMachine.States)
            {
                if (labelsByState.Contains(state))
                {
                    foreach (string label in labelsByState[state].Where(p => !p.Contains(':')))
                    {
                        this.writer.WriteLine(label + ":");
                    }
                }

                if (state is FrameState)
                {
                    FrameState frameState = state as FrameState;
                    this.writer.WriteLine(frameState.Sprite + " " + frameState.Frame + " " + frameState.Duration + (frameState.IsBright ? " BRIGHT " : " ") + frameState.CodePointer);
                }
                else if (state is GoToState)
                {
                    this.writer.WriteLine("goto " + jumpTargetLabelsByState[state as GoToState]);
                }
                else if (state is FailState)
                {
                    this.writer.WriteLine("fail");
                }
                else if (state is StopState)
                {
                    this.writer.WriteLine("stop");
                }
            }
        }
        protected virtual void WriteActorHeader(Actor actor)
        {
            this.writer.WriteLine("actor \"" + actor.Name + "\" : \"" + actor.Parent + "\" ");

            if (actor.ReplacesName != string.Empty)
            {
                this.writer.WriteLine("replaces \"" + actor.ReplacesName + "\" ");
            }

            if (actor.IsNative)
            {
                this.writer.WriteLine("native ");
            }

            if (actor.DoomEdNum != 0)
            {
                this.writer.WriteLine(actor.DoomEdNum.ToString() + " ");
            }
        }
        protected virtual void WriteActorFlags(Actor actor)
        {
            foreach (string flag in actor.EnabledFlags)
            {
                this.writer.WriteLine("+" + flag);
            }

            foreach (string flag in actor.DisabledFlags)
            {
                this.writer.WriteLine("-" + flag);
            }
        }
        public static int Main(string[] args)
        {
            DateTime startTime = DateTime.Now;

            try
            {
                // parse command line options
                List<string> decoratePaths = new List<string>();
                List<string> acsFiles = new List<string>();
                string otputDecorateFile = "decorate.txt";
                string otputAcsFile = "acs.txt";
                string libraryName = "DEFAULTLIB";
                string mode = "zdoom";
                OptionSet options = new OptionSet()
                {
                    {
                        "a|acs=",  "ACS source files.", p => acsFiles.Add(p)
                    },
                    {
                        "d|decorate=", "Directory with user DECORATE files.", p => decoratePaths.Add(p)
                    },
                    {
                        "o|acsOut=",  "Output ACS file. acs.txt is default.", p => otputAcsFile = p
                    },
                    {
                        "p|decorateOut=",  "Output DECORATE file. decorate.txt is default.", p => otputDecorateFile = p
                    },
                    {
                        "m|mode=", "Mode (ZDoom or Skulltag), ZDoom is default.", p => mode = p
                    },
                                        {
                        "l|library=", "Name of the ACS library.", p => libraryName = p
                    }
                };

                options.Parse(args);

                if (decoratePaths.Count == 0)
                {
                    throw new ArgumentException("No input decorate path specified.");
                }

                if (acsFiles.Count == 0)
                {
                    throw new ArgumentException("No input ACS file specified.");
                }

                DecorateParser parser = new DecorateParser();

                // add native decorate files
                string applicationDirectory = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) + "/";
                if (mode.Equals("ZDoom", StringComparison.OrdinalIgnoreCase))
                {
                    parser.AddFiles(new DirectoryInfo(applicationDirectory + "decorate/zdoom").GetFilesRecursive(), DecorateFileType.Native);
                }
                else
                {
                    parser.AddFiles(new DirectoryInfo(applicationDirectory + "decorate/skulltag").GetFilesRecursive(), DecorateFileType.Native);
                }

                // add user decorate files
                var files =
                    from path in decoratePaths
                    from file in new DirectoryInfo(path).GetFilesRecursive()
                    select file;

                parser.AddFiles(files, DecorateFileType.User);

                Console.WriteLine("Parsing DECORATE files...");

                // parse the files
                var actors = parser.GetActors();

                // print some stats
                var localActors = from actor in actors where !actor.IsFromNativeFile select actor;
                Console.WriteLine("Found " + actors.Count() + " actors (" + localActors.Count() + " user + " + (actors.Count() - localActors.Count()) + " native)");

                // add the classId actor to each monster
                Actor classIdActor = new Actor()
                {
                    Name = "__classId",
                    Parent = actors.OfName("inventory").First()
                };

                actors = actors.Append(classIdActor);

                var monsters =
                    from actor in actors
                    where actor.HasFlagEnabled("ISMONSTER")
                    select actor;

                foreach (var monster in monsters)
                {
                    monster.StateMachine.InsertStatesBeforeState(
                        new List<State>
                        {
                            new FrameState()
                            {
                            },
                            new FrameState()
                            {
                                CodePointer = "A_TakeInventory(\"__classId\", 9999)"
                            },
                            new FrameState()
                            {
                                CodePointer = "A_GiveInventory(\"__classId\", " + monster.ID + ")"
                            }
                        },
                        monster.StateMachine.Labels["spawn"]);
                }

                // generate ACS
                Console.WriteLine("Generating output ACS...");

                using (var textWriter = new StreamWriter(otputAcsFile))
                {
                    var writer = new AcsWriter(textWriter, libraryName);
                    writer.WriteCheckActorClass2(actors);
                    writer.WriteGetWeapon(actors);
                    writer.WriteGetCustomProperty(actors);
                    writer.WriteGetWeaponCustomProperty(actors);
                    writer.WriteOriginalContent(File.ReadAllText(acsFiles[0]));
                }

                // generate DECORATE
                Console.WriteLine("Generating output DECORATE...");

                using (var textWriter = new StreamWriter(otputDecorateFile))
                {
                    var writer = new DecorateWriter(textWriter);
                    writer.WriteActors(actors);
                }

                // print time stat
                TimeSpan timeDelta = DateTime.Now - startTime;
                Console.WriteLine("Done after " + Math.Round((double)timeDelta.TotalMilliseconds / 1000d, 2) + " seconds!");

                return 0;
            }
            catch (IOException e)
            {
                Console.WriteLine(e.GetType().Name + ": " + e.Message);
                Console.WriteLine(string.Empty);
                Console.WriteLine("Stack trace:");
                Console.WriteLine(e.StackTrace);

                return -1;
            }
        }
        protected StateMachine ParseStateMachine(Actor actor)
        {
            string[] lines = actor.StatesText.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None);

            var states = new List<State>();
            var labels = new Dictionary<string, State>();

            // non-root actors inheirit state machine from their parents
            if (actor.Parent != null)
            {
                StateMachine parentMachine = actor.Parent.StateMachine.CloneForChild(actor.Parent.Name);
                states = parentMachine.States.ToList();
                labels = parentMachine.Labels.ToDictionary(p => p.Key, p => p.Value);
            }

            var openGoTos = new Dictionary<GoToState, KeyValuePair<string, int>>();

            List<string> openLabels = new List<string>();

            string lastLabel = string.Empty;

            foreach (string line in lines)
            {
                string trimmed = line.Trim();

                if (trimmed == string.Empty)
                {
                    continue;
                }

                string framePattern = @"
                    ([A-Za-z0-9_\-""#]{4,6})    # sprite name
                    \s+
                    ([A-Za-z\[\]\\""#]+)         # frame list
                    \s+
                    (-?\d+)                     # duration
                    (\s+bright\s+)              # bright yes/no
                    ?(.*)                       # code pointer
                ";

                Match labelMatch = Regex.Match(line, "([A-Za-z0-9_.]+):", RegexOptions.IgnoreCase);
                Match gotoMatch = Regex.Match(line, @"goto\s+([A-Za-z0-9_:.]+)?(\s*\+\s*(\d+))?", RegexOptions.IgnoreCase);
                Match frameMatch = Regex.Match(line, framePattern, RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);

                List<State> currentLineStates = new List<State>();

                if (trimmed.Equals("fail", StringComparison.OrdinalIgnoreCase))
                {
                    currentLineStates.Add(new FailState());
                }
                else if (trimmed.Equals("stop", StringComparison.OrdinalIgnoreCase))
                {
                    currentLineStates.Add(new StopState());
                }
                else if (trimmed.Equals("loop", StringComparison.OrdinalIgnoreCase))
                {
                    GoToState state = new GoToState();
                    openGoTos.Add(state, new KeyValuePair<string, int>(lastLabel, 0));
                    currentLineStates.Add(state);
                }
                else if (trimmed.Equals("wait", StringComparison.OrdinalIgnoreCase))
                {
                    GoToState state = new GoToState();
                    state.Target = states.Last();
                    currentLineStates.Add(state);
                }
                else if (gotoMatch.Success)
                {
                    int offset = 0;

                    int.TryParse(gotoMatch.Groups[3].Value, out offset);

                    State state = new GoToState();
                    openGoTos.Add(state as GoToState, new KeyValuePair<string, int>(gotoMatch.Groups[1].Value.ToLower(), offset));
                    currentLineStates.Add(state);
                }
                else if (labelMatch.Success)
                {
                    openLabels.Add(labelMatch.Groups[1].Value.ToLower());
                    lastLabel = labelMatch.Groups[1].Value.ToLower();
                }
                else if (frameMatch.Success)
                {
                    foreach (char frame in frameMatch.Groups[2].Value.TrimQuotes())
                    {
                        FrameState newState = new FrameState()
                        {
                            Sprite = frameMatch.Groups[1].Value,
                            Frame = frame,
                            Duration = int.Parse(frameMatch.Groups[3].Value),
                            IsBright = frameMatch.Groups[4].Value != string.Empty,
                            CodePointer = frameMatch.Groups[5].Value
                        };

                        currentLineStates.Add(newState);
                    }
                }
                else
                {
                    throw new Exception("Could not parse state line \"" + trimmed + "\" in actor \"" + actor.Name + "\" in file \"" + actor.File.FullName + "\".");
                }

                if (currentLineStates.Count > 0)
                {
                    foreach (string label in openLabels)
                    {
                        labels[label] = currentLineStates[0];
                    }

                    openLabels.Clear();
                }

                states.AddRange(currentLineStates);
            }

            foreach (var openGoTo in openGoTos)
            {
                GoToState state = openGoTo.Key;
                string targetLabel = openGoTo.Value.Key;
                int offset = openGoTo.Value.Value;

                State targetState = null;

                if (targetLabel == string.Empty)
                {
                    targetState = state;
                }
                else
                {
                    if (!labels.ContainsKey(targetLabel))
                    {
                        throw new Exception("Jump target \"" + targetLabel + "\" not found in actor \"" + actor.Name + "\" in file \"" + actor.File.FullName + "\".");
                    }

                    targetState = labels[targetLabel];
                }

                int stateIndex = states.IndexOf(targetState);

                Debug.Assert(stateIndex >= 0, "Goto target state not found!");

                state.Target = states[stateIndex + offset];

                // only FrameStates count towards the offset
                int numStatesToSkip = offset;
                int i = stateIndex;
                state.Target = states[i];
                while (numStatesToSkip >= 1)
                {
                    i++;
                    state.Target = states[i];

                    if (state.Target.CountsAsState)
                    {
                        numStatesToSkip--;
                    }
                }
            }

            return new StateMachine(states, labels);
        }
        protected IEnumerable<Actor> GetActorsFromFile(DecorateFileInfo file)
        {
            Console.WriteLine("    Parsing file " + file.FileInfo.FullName);
            //// if(cached && cacheValid){
            ////      load from cache
            //// }
            //// else
            //// {
            string text = string.Empty;
            using (var reader = file.FileInfo.OpenText())
            {
                text = reader.ReadToEnd();
            }

            // remove comments
            text = Regex.Replace(text, "//(.*?)\\n", Environment.NewLine, RegexOptions.Singleline | RegexOptions.IgnoreCase);
            text = Regex.Replace(text, "/\\*(.*?)\\*/", string.Empty, RegexOptions.Singleline | RegexOptions.IgnoreCase);

            // remove action and const definitions
            if (file.Origin == DecorateFileType.Native)
            {
                text = Regex.Replace(text, "(action)?\\s*(native|const)?(.*?);", string.Empty, RegexOptions.IgnoreCase);
            }

            string actorPattern = @"
                actor\s+
                    ([A-Za-z0-9_]+|"".*?"")                     # actor name (with or without quotes)
                    \s*
                    (:\s*([A-Za-z0-9_]+|"".*?"")\s*)?           # parent name (with or without quotes)
                    (replaces\s*([A-Za-z0-9_]+|"".*?"")\s*)?    # replaced actor name (with or without quotes)
                    (\d*\s*)?                                   # doomEdNum
                    (native\s*)?                                # native yes/no
                {
                    (.*?)                                       # properties and flags
                    (states\s*{(.*?)}\s*)?                      # state machine
                }
            ";

            var matches = Regex.Matches(text, actorPattern, RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);

            foreach (Match match in matches)
            {
                string fullText = match.Groups[0].Value;
                string name = match.Groups[1].Value;
                string parent = match.Groups[3].Value;
                string replaces = match.Groups[5].Value;
                string doomednum = match.Groups[6].Value;
                string native = match.Groups[7].Value;
                string content = match.Groups[8].Value;
                string statesContent = match.Groups[10].Value;

                Actor actor = null;

                actor = new Actor()
                {
                    File = file.FileInfo,
                    Name = name,
                    ParentName = parent == string.Empty ? (name.ToLower() == "actor" ? string.Empty : "actor") : parent,
                    ReplacesName = replaces,
                    IsNative = native != string.Empty,
                    IsFromNativeFile = file.Origin == DecorateFileType.Native,
                    DoomEdNum = doomednum != string.Empty ? Int32.Parse(doomednum) : 0,
                    PropertiesAndFlagsText = content,
                    StatesText = statesContent,
                    OriginalText = fullText
                };

                yield return actor;
            }
        }