User:Maven/entitybot: Difference between revisions

From Valve Developer Community
Jump to navigation Jump to search
(version 2)
Line 6: Line 6:
It uses C# 2.0, and comes without support. Please excuse its ugly kuldgeness. The correct way would have been to use Flex/Bison.
It uses C# 2.0, and comes without support. Please excuse its ugly kuldgeness. The correct way would have been to use Flex/Bison.


To use this, copy an entity entry from the FGD into the top box, click the convert button, and paste the resulting bottom-box text into the appropriate VDC edit page. You will need to fill in some of the Availability section.
To use this, pass an entity entry from the FGD as a parameter to <code>Conversion.ConvertFGD2Wiki</code> and returned string into the appropriate VDC edit page. You will need to fill in some of the Availability section and make sure that there are no missing templates.
 
This code does not include a UI.


(Feel free to change anything in this article, including the code.)
(Feel free to change anything in this article, including the code.)


==Form1.cs==
<pre>
<pre>
using System;
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Text;
using System.Text;
using System.Windows.Forms;


namespace FGD2Wiki
namespace FGD2Wiki
{
{
     public partial class Form1 : Form
     public class Conversion
     {
     {
        public Form1()
        {
            InitializeComponent();
        }
        private void fgd2wikiButton_Click( object sender, EventArgs e )
        {
            this.ConvertFGD2Wiki();
        }


         private enum EntityType
         private enum EntityType
Line 66: Line 53:
         };
         };


         private class EntityOutput  
         private class EntityOutput
         {
         {
             public string name;
             public string name;
Line 90: Line 77:
         };
         };


         private struct Pair<A,B>
         private struct Pair<A, B>
         {
         {
             public Pair( A a, B b ) { this.a = a; this.b = b; }
             public Pair( A a, B b ) { this.a = a; this.b = b; }
Line 188: Line 175:
         };
         };


         private string StripTextUntil( ref string s, char c )
         private static string StripTextUntil( ref string s, char c )
         {
         {
             int i = s.IndexOf( c );
             int i = s.IndexOf( c );
Line 202: Line 189:
         }
         }


         private void ConvertFGD2Wiki()
         public static string ConvertFGD2Wiki( string input )
         {
         {
            // Get the (trimmed) input string
            string input = this.fgdTextbox.Text.Trim();
             string token;
             string token;
            // Trim input string
            input = input.Trim();


             //------------------
             //------------------
Line 218: Line 206:
             {
             {
                 case "@PointClass":
                 case "@PointClass":
                case "@FilterClass":
                case "@NPCClass":
                     eEntityType = EntityType.PointBased;
                     eEntityType = EntityType.PointBased;
                     break;
                     break;
Line 224: Line 214:
                     break;
                     break;
                 case "@BaseClass":
                 case "@BaseClass":
                     this.wikiTextbox.Text = "@BaseClass not supported";
                     return "@BaseClass not supported";
                    return;
                 default:
                 default:
                     eEntityType = EntityType.Error;
                     eEntityType = EntityType.Error;
Line 243: Line 232:
                 {
                 {
                     case "base":
                     case "base":
                    {
                        {
                        // Split into individual items
                            // Split into individual items
                        basetypes = sParamData.Split( ',' );
                            basetypes = sParamData.Split( ',' );
                        // Remove whitespace
                            // Remove whitespace
                        for ( int j = 0; j < basetypes.Length; ++j )
                            for ( int j = 0; j < basetypes.Length; ++j )
                            basetypes[j] = basetypes[j].Trim().ToLower();
                                basetypes[j] = basetypes[j].Trim().ToLower();
 
                            // Place targetname and parentname go first, if they exist, followed by the other templates
                            List<string> llBaseTypes = new List<string>( basetypes );  // temporary linked list
                            // TODO: this way is cheesy and inefficient; fix it
                            if ( llBaseTypes.Contains( "parentname" ) )
                            {
                                // Move parentname to the front
                                llBaseTypes.Remove( "parentname" );
                                llBaseTypes.Insert( 0, "parentname" );
                            }
                            if ( llBaseTypes.Contains( "targetname" ) )
                            {
                                // Move targetname to the front
                                llBaseTypes.Remove( "targetname" );
                                llBaseTypes.Insert( 0, "targetname" );
                            }
                            basetypes = llBaseTypes.ToArray();  // convert back to array


                        // Place targetname and parentname go first, if they exist, followed by the other templates
                             break;
                        List<string> llBaseTypes = new List<string>( basetypes );  // temporary linked list
                        // TODO: this way is cheesy and inefficient; fix it
                        if ( llBaseTypes.Contains( "parentname" ) )
                        {
                             // Move parentname to the front
                            llBaseTypes.Remove( "parentname" );
                            llBaseTypes.Insert( 0, "parentname" );
                        }
                        if ( llBaseTypes.Contains( "targetname" ) )
                        {
                            // Move targetname to the front
                            llBaseTypes.Remove( "targetname" );
                            llBaseTypes.Insert( 0, "targetname" );
                         }
                         }
                        basetypes = llBaseTypes.ToArray();  // convert back to array
                        break;
                    }
                     default:
                     default:
                         /* ignore it */
                         /* ignore it */
Line 361: Line 350:
                         while ( sFlags.Length > 0 )
                         while ( sFlags.Length > 0 )
                         {
                         {
                            // Check for line comment
                            while ( sFlags.StartsWith( "//" ) )
                            {
                                // Eat everything until the next newline
                                StripTextUntil( ref sFlags, '\n' );
                            }
                             // Create a new spawnflag
                             // Create a new spawnflag
                             EntitySpawnflag fl = new EntitySpawnflag();
                             EntitySpawnflag fl = new EntitySpawnflag();
                             // Read its data
                             // Read its data
                             fl.value = int.Parse( StripTextUntil( ref sFlags, ':' ) );
                            string sFlag = StripTextUntil( ref sFlags, '\n' );
                             StripTextUntil( ref sFlags, '"' );
                             fl.value = int.Parse( StripTextUntil( ref sFlag, ':' ) );
                             fl.desc = StripTextUntil( ref sFlags, '"' );
                             StripTextUntil( ref sFlag, '"' );
                             StripTextUntil( ref sFlags, ':' );
                             fl.desc = StripTextUntil( ref sFlag, '"' );
                             fl.def = int.Parse( StripTextUntil( ref sFlags, '\n' ) );
                             StripTextUntil( ref sFlag, ':' );
                             fl.def = int.Parse( StripTextUntil( ref sFlag, '/' ) );
 
                            // Check for a flag comment
                            if ( sFlag.StartsWith( "/" ) )
                                fl.desc += " (" + sFlag.Substring(1).Trim() + ")";
 
                             // Add it to the list of spawnflags
                             // Add it to the list of spawnflags
                             llSpawnflags.Add( fl );
                             llSpawnflags.Add( fl );
Line 377: Line 380:
                     else
                     else
                     {
                     {
                        // TODO: handle multi-line strings in desc
                         // Create a new keyvalue
                         // Create a new keyvalue
                         EntityKeyvalue kv = new EntityKeyvalue();
                         EntityKeyvalue kv = new EntityKeyvalue();
Line 399: Line 404:
                             {
                             {
                                 // Create a new int,string pair
                                 // Create a new int,string pair
                                 Pair<int,string> pair = new Pair<int,string>();
                                 Pair<int, string> pair = new Pair<int, string>();
                                 // Parse the choice info
                                 // Parse the choice info
                                 pair.a = int.Parse( StripTextUntil( ref sChoices, ':' ) );
                                 pair.a = int.Parse( StripTextUntil( ref sChoices, ':' ) );
Line 412: Line 417:
                         }
                         }
                         // Not a 'choices' keyvalue
                         // Not a 'choices' keyvalue
                         else  
                         else
                         {
                         {
                             // Read the rest of the line
                             // Read the rest of the line
Line 470: Line 475:
                     break;
                     break;
                 default:
                 default:
                     this.wikiTextbox.Text = "Unexpected entity type: " + eEntityType;
                     return "Unexpected entity type: " + eEntityType;
                    return;
             }
             }
             sb.AppendLine( "}} ???" );
             sb.AppendLine( "}} ???" );
Line 563: Line 567:
             sb.AppendLine( "[[Category:Entities]]" );
             sb.AppendLine( "[[Category:Entities]]" );
             sb.AppendLine();
             sb.AppendLine();
           
 
             /*
             /*
             // TEMP: dump the tokens to the output box
             // TEMP: dump the tokens to the output box
Line 571: Line 575:


             // Output the results
             // Output the results
             this.wikiTextbox.Text = sb.ToString();
             return sb.ToString();
         }
         }


         // TODO: IsEmptyKeyvalues
         // TODO: check FGD instead of using the hard-coding below


         private bool IsEmptyInput( string s )
         private static bool IsEmptyFlag( string s )
         {
         {
             // TODO: efficiency
             // TODO: efficiency
Line 582: Line 586:
             {
             {
                 case "angles":
                 case "angles":
                case "parentname":
                case "targetname":
                 case "renderfxchoices":
                 case "renderfxchoices":
                case "basevehicle":
                case "renderfields":
                case "basefilter":
                case "origin":
                case "shadow":
                case "global":
                case "enabledisable":
                case "baseplat":
                case "button":
                     return true;
                     return true;
                 default:
                 default:
Line 589: Line 604:
         }
         }


         private bool IsEmptyOutput( string s )
         private static bool IsEmptyInput( string s )
         {
         {
             // TODO: efficiency
             // TODO: efficiency
Line 595: Line 610:
             {
             {
                 case "angles":
                 case "angles":
                case "parentname":
                 case "renderfxchoices":
                 case "renderfxchoices":
                case "renderfields":
                case "origin":
                case "global":
                     return true;
                     return true;
                 default:
                 default:
Line 603: Line 620:
         }
         }


         private bool IsEmptyFlag( string s )
         private static bool IsEmptyOutput( string s )
         {
         {
             // TODO: efficiency
             // TODO: efficiency
Line 610: Line 627:
                 case "angles":
                 case "angles":
                 case "parentname":
                 case "parentname":
                case "targetname":
                 case "renderfxchoices":
                 case "renderfxchoices":
                 case "basevehicle":
                 case "renderfields":
                case "origin":
                case "shadow":
                case "global":
                case "enabledisable":
                case "baseplat":
                     return true;
                     return true;
                 default:
                 default:
Line 618: Line 639:
             }
             }
         }
         }
    }
}
</pre>


==Form1.Designer.cs==
     } // class Conversion
<pre>
namespace FGD2Wiki
{
     partial class Form1
    {
        /// <summary>
        /// Required designer variable.
        /// </summary>
        private System.ComponentModel.IContainer components = null;


        /// <summary>
} // namespace FGD2Wiki
        /// Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose( bool disposing )
        {
            if ( disposing && (components != null) )
            {
                components.Dispose();
            }
            base.Dispose( disposing );
        }
 
        #region Windows Form Designer generated code
 
        /// <summary>
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// </summary>
        private void InitializeComponent()
        {
            this.splitContainer1 = new System.Windows.Forms.SplitContainer();
            this.fgdTextbox = new System.Windows.Forms.TextBox();
            this.wikiTextbox = new System.Windows.Forms.TextBox();
            this.panel1 = new System.Windows.Forms.Panel();
            this.fgd2wikiButton = new System.Windows.Forms.Button();
            this.splitContainer1.Panel1.SuspendLayout();
            this.splitContainer1.Panel2.SuspendLayout();
            this.splitContainer1.SuspendLayout();
            this.panel1.SuspendLayout();
            this.SuspendLayout();
            //
            // splitContainer1
            //  
            this.splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill;
            this.splitContainer1.Location = new System.Drawing.Point( 0, 0 );
            this.splitContainer1.Name = "splitContainer1";
            this.splitContainer1.Orientation = System.Windows.Forms.Orientation.Horizontal;
            //
            // splitContainer1.Panel1
            //
            this.splitContainer1.Panel1.Controls.Add( this.fgdTextbox );
            //
            // splitContainer1.Panel2
            //
            this.splitContainer1.Panel2.Controls.Add( this.wikiTextbox );
            this.splitContainer1.Panel2.Controls.Add( this.panel1 );
            this.splitContainer1.Size = new System.Drawing.Size( 522, 449 );
            this.splitContainer1.SplitterDistance = 205;
            this.splitContainer1.TabIndex = 0;
            this.splitContainer1.Text = "splitContainer1";
            //
            // fgdTextbox
            //
            this.fgdTextbox.Dock = System.Windows.Forms.DockStyle.Fill;
            this.fgdTextbox.Font = new System.Drawing.Font( "Courier New", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte) (0)) );
            this.fgdTextbox.Location = new System.Drawing.Point( 0, 0 );
            this.fgdTextbox.Multiline = true;
            this.fgdTextbox.Name = "fgdTextbox";
            this.fgdTextbox.ScrollBars = System.Windows.Forms.ScrollBars.Both;
            this.fgdTextbox.Size = new System.Drawing.Size( 522, 205 );
            this.fgdTextbox.TabIndex = 0;
            //
            // wikiTextbox
            //
            this.wikiTextbox.Dock = System.Windows.Forms.DockStyle.Fill;
            this.wikiTextbox.Font = new System.Drawing.Font( "Courier New", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte) (0)) );
            this.wikiTextbox.Location = new System.Drawing.Point( 0, 25 );
            this.wikiTextbox.Multiline = true;
            this.wikiTextbox.Name = "wikiTextbox";
            this.wikiTextbox.ReadOnly = true;
            this.wikiTextbox.ScrollBars = System.Windows.Forms.ScrollBars.Both;
            this.wikiTextbox.Size = new System.Drawing.Size( 522, 215 );
            this.wikiTextbox.TabIndex = 1;
            //
            // panel1
            //
            this.panel1.Controls.Add( this.fgd2wikiButton );
            this.panel1.Dock = System.Windows.Forms.DockStyle.Top;
            this.panel1.Location = new System.Drawing.Point( 0, 0 );
            this.panel1.Name = "panel1";
            this.panel1.Size = new System.Drawing.Size( 522, 25 );
            this.panel1.TabIndex = 0;
            //
            // fgd2wikiButton
            //
            this.fgd2wikiButton.Dock = System.Windows.Forms.DockStyle.Left;
            this.fgd2wikiButton.Location = new System.Drawing.Point( 0, 0 );
            this.fgd2wikiButton.Name = "fgd2wikiButton";
            this.fgd2wikiButton.Size = new System.Drawing.Size( 101, 25 );
            this.fgd2wikiButton.TabIndex = 0;
            this.fgd2wikiButton.Text = "&Convert";
            this.fgd2wikiButton.Click += new System.EventHandler( this.fgd2wikiButton_Click );
            //
            // Form1
            //
            this.AutoScaleDimensions = new System.Drawing.SizeF( 6F, 13F );
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size( 522, 449 );
            this.Controls.Add( this.splitContainer1 );
            this.Name = "Form1";
            this.Text = "FGD to Wiki";
            this.splitContainer1.Panel1.ResumeLayout( false );
            this.splitContainer1.Panel1.PerformLayout();
            this.splitContainer1.Panel2.ResumeLayout( false );
            this.splitContainer1.Panel2.PerformLayout();
            this.splitContainer1.ResumeLayout( false );
            this.panel1.ResumeLayout( false );
            this.ResumeLayout( false );
 
        }
 
        #endregion
 
        private System.Windows.Forms.SplitContainer splitContainer1;
        private System.Windows.Forms.Panel panel1;
        private System.Windows.Forms.Button fgd2wikiButton;
        private System.Windows.Forms.TextBox fgdTextbox;
        private System.Windows.Forms.TextBox wikiTextbox;
    }
}
</pre>
 
==Program.cs==
If it's not auto-generated for you, you'll need this too.
<pre>
using System;
using System.Collections.Generic;
using System.Windows.Forms;
 
namespace FGD2Wiki
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.Run( new Form1() );
        }
    }
}
</pre>
</pre>

Revision as of 21:52, 18 October 2005

Here is code for a small app to convert FGD entries into markup for an entity entry in this wiki.

Notes

This code is in the public domain.

It uses C# 2.0, and comes without support. Please excuse its ugly kuldgeness. The correct way would have been to use Flex/Bison.

To use this, pass an entity entry from the FGD as a parameter to Conversion.ConvertFGD2Wiki and returned string into the appropriate VDC edit page. You will need to fill in some of the Availability section and make sure that there are no missing templates.

This code does not include a UI.

(Feel free to change anything in this article, including the code.)

using System;
using System.Collections.Generic;
using System.Text;

namespace FGD2Wiki
{
    public class Conversion
    {

        private enum EntityType
        {
            Error = 0,
            PointBased,
            BrushBased,
        };

        private class EntityInput
        {
            public string name;
            public string type;
            public string desc;

            public string ToWiki()
            {
                StringBuilder sb = new StringBuilder();
                sb.Append( "*'''" );
                sb.Append( this.name );
                if ( this.type != "void" )
                {
                    sb.Append( " <" );
                    sb.Append( this.type );
                    sb.Append( ">" );
                }
                sb.AppendLine( "'''" );
                sb.Append( ":" );
                sb.AppendLine( this.desc );
                return sb.ToString();
            }
        };

        private class EntityOutput
        {
            public string name;
            public string type;
            public string desc;

            public string ToWiki()
            {
                StringBuilder sb = new StringBuilder();
                sb.Append( "*'''" );
                sb.Append( this.name );
                if ( this.type != "void" )
                {
                    sb.Append( " <" );
                    sb.Append( this.type );
                    sb.Append( ">" );
                }
                sb.AppendLine( "'''" );
                sb.Append( ":" );
                sb.AppendLine( this.desc );
                return sb.ToString();
            }
        };

        private struct Pair<A, B>
        {
            public Pair( A a, B b ) { this.a = a; this.b = b; }
            public A a;
            public B b;
        }

        private class EntityKeyvalue
        {
            public string name;
            public string type;
            public string name2;    // the SmartEdit name
            public string desc;
            public string def;
            public List<Pair<int, string>> choices = new List<Pair<int, string>>();

            public string ToWiki()
            {
                StringBuilder sb = new StringBuilder();

                // Add the name
                sb.Append( "*'''" );
                sb.Append( this.name );
                sb.AppendLine( "'''" );

                // Add the type
                sb.Append( ":<" );
                switch ( this.type )
                {
                    case "angle":
                    case "angles":
                    case "vecline":
                    case "byte":
                    case "color255":
                        sb.Append( "[[" );
                        sb.Append( this.type );
                        sb.Append( "]]" );
                        break;
                    case "boolean":
                    case "bool":
                        sb.Append( "[[Wikipedia:boolean|boolean]]" );
                        break;
                    default:
                        sb.Append( this.type );
                        break;
                }
                sb.Append( "> " );

                // Add the SmartEdit name to the desc if the desc is shortish
                if ( this.desc.Length < 12 )
                {
                    sb.Append( this.name2 );
                    sb.Append( ". " );
                }

                // Add the description
                sb.Append( this.desc );
                // Add a period if there isn't one aleady
                if ( this.desc.Length > 0 && !this.desc.EndsWith( "." ) )
                    sb.Append( "." );

                /*
                // Give the default value
                if ( this.def != "" )
                {
                    sb.Append( " Default: " );
                    sb.Append( this.def );
                }
                */
                sb.AppendLine();

                // If this is a 'choices' variable, create the choice table
                if ( this.type == "choices" )
                {
                    sb.AppendLine( ":{|" );
                    sb.AppendLine( "! Literal value || Description" );
                    foreach ( Pair<int, string> pair in choices )
                    {
                        sb.AppendLine( "|-" );
                        sb.Append( "| " );
                        sb.Append( pair.a );
                        sb.Append( " || " );
                        sb.AppendLine( pair.b );
                    }
                    sb.AppendLine( "|}" );
                }

                return sb.ToString();
            }
        };

        private class EntitySpawnflag
        {
            public int value;
            public string desc;
            public int def;
        };

        private static string StripTextUntil( ref string s, char c )
        {
            int i = s.IndexOf( c );
            if ( i < 0 )
            {
                string m = s.Trim();
                s = "";
                return m;
            }
            string n = s.Substring( 0, i );
            s = s.Substring( i + 1 ).TrimStart();
            return n.Trim();
        }

        public static string ConvertFGD2Wiki( string input )
        {
            string token;

            // Trim input string
            input = input.Trim();

            //------------------
            // Parse the header
            //------------------

            // Read the entity type: point-based or brush-based
            EntityType eEntityType = EntityType.Error;
            token = StripTextUntil( ref input, ' ' );
            switch ( token )
            {
                case "@PointClass":
                case "@FilterClass":
                case "@NPCClass":
                    eEntityType = EntityType.PointBased;
                    break;
                case "@SolidClass":
                    eEntityType = EntityType.BrushBased;
                    break;
                case "@BaseClass":
                    return "@BaseClass not supported";
                default:
                    eEntityType = EntityType.Error;
                    break;
            }

            // Read everything until the equal sign
            string[] basetypes = new string[0];
            string sEntityParams = StripTextUntil( ref input, '=' );
            while ( sEntityParams.Length > 0 )
            {
                // Get the param name
                string sParamName = StripTextUntil( ref sEntityParams, '(' );
                string sParamData = StripTextUntil( ref sEntityParams, ')' );

                switch ( sParamName )
                {
                    case "base":
                        {
                            // Split into individual items
                            basetypes = sParamData.Split( ',' );
                            // Remove whitespace
                            for ( int j = 0; j < basetypes.Length; ++j )
                                basetypes[j] = basetypes[j].Trim().ToLower();

                            // Place targetname and parentname go first, if they exist, followed by the other templates
                            List<string> llBaseTypes = new List<string>( basetypes );   // temporary linked list
                            // TODO: this way is cheesy and inefficient; fix it
                            if ( llBaseTypes.Contains( "parentname" ) )
                            {
                                // Move parentname to the front
                                llBaseTypes.Remove( "parentname" );
                                llBaseTypes.Insert( 0, "parentname" );
                            }
                            if ( llBaseTypes.Contains( "targetname" ) )
                            {
                                // Move targetname to the front
                                llBaseTypes.Remove( "targetname" );
                                llBaseTypes.Insert( 0, "targetname" );
                            }
                            basetypes = llBaseTypes.ToArray();  // convert back to array

                            break;
                        }
                    default:
                        /* ignore it */
                        break;
                }
            }

            // TODO: not all entities have a description; handle that case

            // Read the entity name
            string sEntityName = StripTextUntil( ref input, ':' );

            // Read the entity description
            string sEntityDescriptionUnparsed = StripTextUntil( ref input, '[' );
            string sEntityDescription = "";
            while ( sEntityDescriptionUnparsed.Length > 0 )
            {
                // Ignore everything until a quote
                StripTextUntil( ref sEntityDescriptionUnparsed, '"' );
                // Take everything until the next quote
                sEntityDescription = sEntityDescription + StripTextUntil( ref sEntityDescriptionUnparsed, '"' ) + " ";

                // TODO: this will choke on escaped quotes
            }

            //---------------
            // Read the body
            //---------------

            string sBody = StripTextUntil( ref input, '@' );
            List<EntityInput> llInputs = new List<EntityInput>();
            List<EntityOutput> llOutputs = new List<EntityOutput>();
            List<EntityKeyvalue> llKeyvalues = new List<EntityKeyvalue>();
            List<EntitySpawnflag> llSpawnflags = new List<EntitySpawnflag>();
            while ( sBody.Length > 0 && !sBody.StartsWith( "]" ) )
            {
                // Check for line comment
                while ( sBody.StartsWith( "//" ) )
                {
                    // Eat everything until the next newline
                    StripTextUntil( ref sBody, '\n' );
                }

                // Get the item part of the next item:value pair
                string sItem = StripTextUntil( ref sBody, ')' );

                // Input
                if ( sItem.StartsWith( "input " ) )
                {
                    // Create a new input
                    EntityInput ei = new EntityInput();
                    // Discard the "input" part
                    StripTextUntil( ref sItem, ' ' );
                    // Read its information
                    ei.name = StripTextUntil( ref sItem, '(' );
                    ei.type = sItem;
                    // Read the desc
                    StripTextUntil( ref sBody, '"' );
                    ei.desc = StripTextUntil( ref sBody, '"' );
                    // Add it to the list of inputs
                    llInputs.Add( ei );
                }

                // Output
                else if ( sItem.StartsWith( "output " ) )
                {
                    // Create a new output
                    EntityOutput eo = new EntityOutput();
                    // Discard the "output" part
                    StripTextUntil( ref sItem, ' ' );
                    // Read its information
                    eo.name = StripTextUntil( ref sItem, '(' );
                    eo.type = StripTextUntil( ref sItem, ')' );
                    // Read the desc
                    StripTextUntil( ref sBody, '"' );
                    eo.desc = StripTextUntil( ref sBody, '"' );
                    // Add it to the list of outputs
                    llOutputs.Add( eo );
                }

                // Keyvalue
                else
                {
                    // Read the name
                    string sName = StripTextUntil( ref sItem, '(' );

                    // Is it the spawnflags field?
                    if ( sName == "spawnflags" )
                    {
                        StripTextUntil( ref sBody, '[' );
                        string sFlags = StripTextUntil( ref sBody, ']' );
                        while ( sFlags.Length > 0 )
                        {
                            // Check for line comment
                            while ( sFlags.StartsWith( "//" ) )
                            {
                                // Eat everything until the next newline
                                StripTextUntil( ref sFlags, '\n' );
                            }

                            // Create a new spawnflag
                            EntitySpawnflag fl = new EntitySpawnflag();

                            // Read its data
                            string sFlag = StripTextUntil( ref sFlags, '\n' );
                            fl.value = int.Parse( StripTextUntil( ref sFlag, ':' ) );
                            StripTextUntil( ref sFlag, '"' );
                            fl.desc = StripTextUntil( ref sFlag, '"' );
                            StripTextUntil( ref sFlag, ':' );
                            fl.def = int.Parse( StripTextUntil( ref sFlag, '/' ) );

                            // Check for a flag comment
                            if ( sFlag.StartsWith( "/" ) )
                                fl.desc += " (" + sFlag.Substring(1).Trim() + ")";

                            // Add it to the list of spawnflags
                            llSpawnflags.Add( fl );
                        }
                    }

                    // Not spawnflag
                    else
                    {
                        // TODO: handle multi-line strings in desc

                        // Create a new keyvalue
                        EntityKeyvalue kv = new EntityKeyvalue();
                        // Read its information
                        kv.name = sName;
                        kv.type = StripTextUntil( ref sItem, ')' );
                        // Read the SmartEdit name
                        StripTextUntil( ref sBody, '"' );
                        kv.name2 = StripTextUntil( ref sBody, '"' );
                        StripTextUntil( ref sBody, ':' );

                        // Is this a 'choices' keyvalue?
                        if ( kv.type == "choices" )
                        {
                            // Read the default value
                            kv.def = StripTextUntil( ref sBody, '=' );

                            // Parse the choices
                            StripTextUntil( ref sBody, '[' );
                            string sChoices = StripTextUntil( ref sBody, ']' );
                            while ( sChoices.Length > 0 )
                            {
                                // Create a new int,string pair
                                Pair<int, string> pair = new Pair<int, string>();
                                // Parse the choice info
                                pair.a = int.Parse( StripTextUntil( ref sChoices, ':' ) );
                                StripTextUntil( ref sChoices, '"' );
                                pair.b = StripTextUntil( ref sChoices, '"' );
                                // Add the pair
                                kv.choices.Add( pair );
                            }

                            // No desc (the SmartEdit name will be used)
                            kv.desc = "";
                        }
                        // Not a 'choices' keyvalue
                        else
                        {
                            // Read the rest of the line
                            String sLine = StripTextUntil( ref sBody, '\n' );

                            // Read the default value
                            if ( sLine.StartsWith( "\"" ) )
                            {
                                // This is the case where it's in quotes
                                StripTextUntil( ref sLine, '"' );
                                kv.def = StripTextUntil( ref sLine, '"' );
                            }
                            else
                            {
                                // This is the case where it isn't (it's an integer or blank)
                                kv.def = StripTextUntil( ref sLine, ':' );
                            }

                            // Read the desc
                            StripTextUntil( ref sLine, '"' );
                            kv.desc = StripTextUntil( ref sLine, '"' );
                        }

                        // Add it to the list of keyvalues
                        llKeyvalues.Add( kv );
                    }
                }
            }

            //------------------------
            // Create the output text
            //------------------------

            StringBuilder sb = new StringBuilder();

            // Create the wrongtitle line
            sb.Append( "{{wrongtitle|title=" );
            sb.Append( sEntityName );
            sb.AppendLine( "}}" );
            sb.AppendLine();

            // Create the Entity Description section
            sb.AppendLine( "==Entity Description==" );
            sb.AppendLine( sEntityDescription );
            sb.AppendLine();

            // Create the Availability section
            sb.AppendLine( "==Availability==" );
            sb.Append( "{{in game|" );
            switch ( eEntityType )
            {
                case EntityType.PointBased:
                    sb.Append( "point" );
                    break;
                case EntityType.BrushBased:
                    sb.Append( "brush" );
                    break;
                default:
                    return "Unexpected entity type: " + eEntityType;
            }
            sb.AppendLine( "}} ???" );
            sb.AppendLine( "{{in code|class=???|file=???}}" );
            sb.AppendLine();

            // Create the Keyvalues section
            sb.AppendLine( "==Keyvalues==" );
            foreach ( string template in basetypes )
            {
                sb.Append( "*{{kv " );
                sb.Append( template );
                sb.AppendLine( "}}" );
            }
            foreach ( EntityKeyvalue kv in llKeyvalues )
            {
                sb.Append( kv.ToWiki() );
            }
            sb.AppendLine();

            // Create the Flags section
            StringBuilder sbFlags = new StringBuilder();
            foreach ( string template in basetypes )
            {
                if ( IsEmptyFlag( template ) )
                    continue;
                sbFlags.Append( "*{{fl " );
                sbFlags.Append( template );
                sbFlags.AppendLine( "}}" );
            }
            foreach ( EntitySpawnflag fl in llSpawnflags )
            {
                sbFlags.Append( "*" );
                sbFlags.Append( fl.value );
                sbFlags.Append( " : " );
                sbFlags.AppendLine( fl.desc );
            }
            // If flag data is not empty, add the Flags section
            if ( sbFlags.ToString().Length > 0 )
            {
                sb.AppendLine( "==Flags==" );
                sb.Append( sbFlags.ToString() );
                sb.AppendLine();
            }

            // Create the Inputs section
            StringBuilder sbInputs = new StringBuilder();
            foreach ( string template in basetypes )
            {
                if ( IsEmptyInput( template ) )
                    continue;
                sbInputs.Append( "*{{i " );
                sbInputs.Append( template );
                sbInputs.AppendLine( "}}" );
            }
            foreach ( EntityInput ei in llInputs )
            {
                sbInputs.Append( ei.ToWiki() );
            }
            // If input data is not empty, add the Inputs section
            if ( sbInputs.ToString().Length > 0 )
            {
                sb.AppendLine( "==Inputs==" );
                sb.Append( sbInputs.ToString() );
                sb.AppendLine();
            }

            // Create the Outputs sections
            StringBuilder sbOutputs = new StringBuilder();
            foreach ( string template in basetypes )
            {
                if ( IsEmptyOutput( template ) )
                    continue;
                sbOutputs.Append( "*{{o " );
                sbOutputs.Append( template );
                sbOutputs.AppendLine( "}}" );
            }
            foreach ( EntityOutput eo in llOutputs )
            {
                sbOutputs.Append( eo.ToWiki() );
            }
            // If output data is not empty, add the Outputs section
            if ( sbOutputs.ToString().Length > 0 )
            {
                sb.AppendLine( "==Outputs==" );
                sb.Append( sbOutputs.ToString() );
                sb.AppendLine();
            }

            // Add the Category:Entities line
            sb.AppendLine( "[[Category:Entities]]" );
            sb.AppendLine();

            /*
            // TEMP: dump the tokens to the output box
            foreach ( string token in tokens )
                sb.AppendLine( token );
            */

            // Output the results
            return sb.ToString();
        }

        // TODO: check FGD instead of using the hard-coding below

        private static bool IsEmptyFlag( string s )
        {
            // TODO: efficiency
            switch ( s.ToLower() )
            {
                case "angles":
                case "parentname":
                case "targetname":
                case "renderfxchoices":
                case "basevehicle":
                case "renderfields":
                case "basefilter":
                case "origin":
                case "shadow":
                case "global":
                case "enabledisable":
                case "baseplat":
                case "button":
                    return true;
                default:
                    return false;
            }
        }

        private static bool IsEmptyInput( string s )
        {
            // TODO: efficiency
            switch ( s.ToLower() )
            {
                case "angles":
                case "renderfxchoices":
                case "renderfields":
                case "origin":
                case "global":
                    return true;
                default:
                    return false;
            }
        }

        private static bool IsEmptyOutput( string s )
        {
            // TODO: efficiency
            switch ( s.ToLower() )
            {
                case "angles":
                case "parentname":
                case "renderfxchoices":
                case "renderfields":
                case "origin":
                case "shadow":
                case "global":
                case "enabledisable":
                case "baseplat":
                    return true;
                default:
                    return false;
            }
        }

    } // class Conversion

} // namespace FGD2Wiki