11
\$\begingroup\$

I've written a small library of objects for parsing of Valve Map Files (VMF). These files are always generated and exported from either the Hammer World Editor, or a Source engine built in map making program like the PTI in Portal 2. So I can trust them to always look roughly like the following, except of course that these are just bits of what is usually a much more massive file.

in.vmf

visgroups
{
    visgroup
    {
        "name" "Tree_1"
        "visgroupid" "5"
        "color" "65 45 0"
    }
    visgroup
    {
        "name" "Tree_2"
        "visgroupid" "1"
        "color" "60 35 0"
        visgroup
        {
            "name" "Branch_1"
            "visgroupid" "2"
            "color" "0 192 0"
        }
        visgroup
        {
            "name" "Branch_2"
            "visgroupid" "3"
            "color" "0 255 0"
            visgroup
            {
                "name" "Leaf"
                "visgroupid" "4"
                "color" "255 0 0"
            }
        }
    }
}
viewsettings
{
    "bSnapToGrid" "1"
    "bShowGrid" "1"
    "bShowLogicalGrid" "0"
    "nGridSpacing" "64"
    "bShow3DGrid" "0"
}
side
{
    "id" "6"
    "plane" "(512 -512 -512) (-512 -512 -512) (-512 -512 512)"
    "material" "BRICK/BRICKFLOOR001A"
    "uaxis" "[1 0 0 0] 0.25"
    "vaxis" "[0 0 -1 0] 0.25"
    "rotation" "0"
    "lightmapscale" "16"
    "smoothing_groups" "0"
    dispinfo
    {
    }
}

I'm looking for feedback on anything relating to structure of my tree, or how I programmed it. If you have any tips, or see some place where I've taken a longer route than necessary, I'd like to know.

I'm not looking for criticism on how my if-else chains look, or how I only include brackets when necessary instead of all the time to maintain consistency. I am more interested in functionality feedback.

One thing I can point out right now, which I believe is inefficient is that when a block is being created, it is looped over just to be collected and passed into another block constructor. This means if you have a block nested 10 blocks deep that block is iterated over 10 times, and only processed the last time. I could remedy this by creating new properties and blocks as I go, the deeper I go. Using a recursive approach, and that might work faster. However as far as I've seen blocks are never nested 10 deep, they may get as deep as 5. Anyway here is the code.

IVNode

public interface IVNode
{
    string Name { get; }
}

VProperty

public class VProperty : IVNode
{
    public string Name { get; private set; }
    public string Value { get; set; }

    public VProperty(string name, string value = "")
    {
        Name = name;
        Value = value;
    }

    public VProperty(string text)
    {
        var texts = text.Trim().Split(new char[] { ' ', '\t' }, 2);
        Name = texts[0].Trim('\"');
        Value = texts[1].Trim('\"');
    }

    public string ToVMFString()
    {
        return string.Format("\"{0}\" \"{1}\"", Name, Value);
    }

    public override string ToString()
    {
        return base.ToString() + " (" + Name + ")";
    }
}

VBlock

public class VBlock : IVNode
{
    public string Name { get; private set; }
    public IList<IVNode> Body { get; private set; }

    public VBlock(string name, IList<IVNode> body = null)
    {
        Name = name;
        if (body == null)
            body = new List<IVNode>();
    }

    public VBlock(string[] text)
    {
        Name = text[0].Trim();
        Body = Utils.ParseToBody(text.SubArray(2, text.Length - 3));
    }

    public string[] ToVMFStrings(bool useTabs = true)
    {
        var text = Utils.BodyToString(Body);
        if (useTabs)
            text = text.Select(t => t.Insert(0, "\t")).ToList();
        text.Insert(0, Name);
        text.Insert(1, "{");
        text.Add("}");
        return text.ToArray();
    }

    public override string ToString()
    {
        return base.ToString() + " (" + Name + ")";
    }
}

VMF

public class VMF 
{
    public IList<IVNode> Body { get; private set; }

    public VMF(string[] text)
    {
        Body = Utils.ParseToBody(text);
    }

    public string[] ToVMFStrings()
    {
        return Utils.BodyToString(Body).ToArray();
    }
}

Extensions

static class Extensions
{
    public static T[] SubArray<T>(this T[] data, int index, int length)
    {
        T[] result = new T[length];
        Array.Copy(data, index, result, 0, length);
        return result;
    }
}

Utils

internal static class Utils
{
    internal static Type GetNodeType(string line)
    {
        return line.Trim().StartsWith("\"") ? typeof(VProperty) : typeof(VBlock);
    }

    internal static IList<IVNode> ParseToBody(string[] body)
    {
        IList<IVNode> nBody = new List<IVNode>();
        int depth = 0;
        var wasDeep = false;
        IList<string> nextBlock = null;
        for (int i = 0; i < body.Length; i++)
        {
            var line = body[i].Trim();

            if (string.IsNullOrWhiteSpace(line) || line.StartsWith("//"))
                continue;

            var readable = line.FirstOrDefault() != default(char);

            if (readable && line.First() == '{')
                depth++;

            if (depth == 0)
                if (Utils.GetNodeType(line) == typeof(VProperty))
                    nBody.Add(new VProperty(line));
                else
                {
                    nextBlock = new List<string>();
                    nextBlock.Add(line);
                }
            else
                nextBlock.Add(line);

            wasDeep = depth > 0;

            if (readable && line.First() == '}')
                depth--;

            if (wasDeep && depth == 0)
            {
                nBody.Add(new VBlock(nextBlock.ToArray()));
                nextBlock = null;
            }
        }
        return nBody;
    }

    internal static IList<string> BodyToString(IList<IVNode> body)
    {
        IList<string> text = new List<string>();
        foreach (var node in body)
            if (node.GetType() == typeof(VProperty))
                text.Add(((VProperty)node).ToVMFString());
            else
                foreach (string s in ((VBlock)node).ToVMFStrings())
                    text.Add(s);
        return text;
    }
}

If you want to actually run this program, here is the very simple code that it takes to load up a vmf and write it back.

Main

static void Main()
{
    string fileName = "in.vmf";
    VMF vmf = new VMF(File.ReadAllLines(fileName));

    // Manipulate the vmf as you desire

    File.WriteAllLines("out.vmf", vmf.ToVMFStrings());
}
\$\endgroup\$
  • \$\begingroup\$ Isn't there a format specification for the language? Just to make sure that you covered every possible edge case correctly. \$\endgroup\$ – svick Mar 20 '14 at 23:19
  • \$\begingroup\$ not as far as I've seen, and as long as it works with hammer and the compiler, it doesn't matter... it doesn't really throw curveballs \$\endgroup\$ – BenVlodgi Mar 21 '14 at 0:13
  • \$\begingroup\$ @svick There is a wiki page on this, however some examples have invalid syntax to save space developer.valvesoftware.com/wiki/Valve_Map_Format like anywhere you have a block name followed by empty {} \$\endgroup\$ – BenVlodgi Mar 21 '14 at 0:53
  • \$\begingroup\$ That's the problem when you don't have a formal spec: is that actually invalid syntax, or are the newlines that I think you are expecting there optional? \$\endgroup\$ – svick Mar 21 '14 at 1:24
8
\$\begingroup\$
public VProperty(string name, string value = "")
public VProperty(string text)

This means that the default value will never be used. If you write new VProperty("propertyName"), it will call the text overload.

To avoid the confusion, consider using some way of differentiate parsing from normal constructor, like using a static factory method (e.g. public static VProperty Parse(string text)), or even have the parsing and possibly also ToVMFString() in a separate class.


'\"'

You don't need the backslash there, '"' works fine.


public VBlock(string name, IList<IVNode> body = null)
{
    Name = name;
    if (body == null)
        body = new List<IVNode>();
}

This constructor won't work, you never actually assign to Body.


You can make members of internal classes public, instead of internal. It will work exactly the same, but it will make it much simpler to make the class public, if you ever decide to do that.


IList<IVNode> nBody = new List<IVNode>();
int depth = 0;
var wasDeep = false;

The name nBody is not very good. What does the n mean? You shouldn't use abbreviations like that to make your code clearer.

Also, there is no need to specify the type for nBody, you could use var here. On the other hand, I would be explicit with wasDeep, I think saving that one character is not worth it.

\$\endgroup\$
  • \$\begingroup\$ Thank you, I hadn't actually tested the manual creation of Properties and Blocks yet as it takes a lot more effort than loading up a file. nBody meant newBody, but I see your point. I didn't use var because that would evaluate nBody to be a List<T> vs an IList<T> while this probably would be fine here, I'm just in the habit of using IList<T>. \$\endgroup\$ – BenVlodgi Mar 21 '14 at 13:02
  • \$\begingroup\$ Using interfaces makes sense for public parts of your code (like method parameters and return values). But for local variables, there is no need to do that. \$\endgroup\$ – svick Mar 21 '14 at 16:14
5
\$\begingroup\$

Instead of writing your own parser by hand, I think you should use a parser generator. If you've never used anything like that before, it requires some learning, but it's not actually that hard and the grammar is actually pretty understandable and readable.

I don't personally have much experience with parser generators, so I took this as an excuse to try out ANTLR, mostly following this tutorial. With that, you would have a grammar that looks something like this:

grammar VMF;

/*
 * Parser Rules
 */

file : node+;

node : block
     | property;

block : NAME '{' node* '}';

property : STRING STRING;

/*
 * Lexer Rules
 */

NAME   : [a-zA-Z_]+;

STRING : '"'  (~["\r\n])* '"';

WS     : [ \r\n] -> channel(HIDDEN);

And then write some simple code to map from the generated ANTLR classes to your classes:

class VMFFileVisitor : VMFBaseVisitor<VMF>
{
    public override VMF VisitFile(VMFParser.FileContext context)
    {
        var nodeVisitor = new VMFNodeVisitor();

        return new VMF(context.node().Select(n => n.Accept(nodeVisitor)).ToList());
    }
}

class VMFNodeVisitor : VMFBaseVisitor<IVNode>
{
    public override IVNode VisitBlock(VMFParser.BlockContext context)
    {
        return new VBlock(
            context.NAME().GetText(),
            context.node().Select(n => Visit(n)).ToList());
    }

    public override IVNode VisitProperty(VMFParser.PropertyContext context)
    {
        return new VProperty(
            context.STRING(0).GetText().Trim('"'),
            context.STRING(1).GetText().Trim('"'));
    }
}

And finally use it like this:

var input = new AntlrInputStream(text);
var lexer = new VMFLexer(input);
var tokens = new CommonTokenStream(lexer);
var parser = new VMFParser(tokens);
var visitor = new VMFFileVisitor();
VMF result = visitor.Visit(parser.file());
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.