Freitag, 22. August 2014

C#: Object Factory with XML Data and Library

I am rebuilding a trading card game (TCG) to make it available on PC.
(NetRunner / Android: Netrunner)

For that I need several card types (classes) and some data associated with them.
First I wanted to make a class for EVERY new card. But then I thought, that there can be the same card class wich just has some other values. So, I made an XML-File with the cards values.

The XML values will be read into a card library. Because a card can have any card-class, the class name is included in the XML-Data. The "right" instance will be created and added to the library.

(I use the short "NM" for the (working title) word "NetMage".)

You can finally use: NMCardFactory.Clone("My_Card_Id") to copy a card from the library.

Now to the code:
First, here is a sample XML file.
<CardList>
<Card id="MyBaseCard">
    <Class>Base_Default</Class>
    <Title>My Base Card</Title>
    <StoryText>Some story text.</StoryText>
    <Cost>2</Cost>
</Card>
 <Card id="MyExtendedCard">
   <Class>Extended_Default</Class>
   <Title>Extended Card</Title>
   <StoryText>Some other story text.</StoryText>
   <Cost>3</Cost>
   <MyValue>Value</MyValue>
   ....
</Card>
</CardList>

I everytime have a class called "Globals" with global methods and a class called "Values" with important values to gather them together.

So, first the NMGlobals class, there are three static methods:
namespace CardGame.Source
{
    public class NMGlobals
   {
        // find the first xml node with a given name in a parent node.
        public static XmlNode FindFirstXMLNode(XmlNode parent, string name)
        {
            foreach (XmlNode node in parent)
            {
                if (node.Name.ToLower().Equals(name.ToLower()))
                    return node;
            }
            return null;
        }

        // find the first innertext from node NodeName in parent.
        public static string FirstXMLInnerText(XmlNode parent, string NodeName)
        {
            // get the text
            string t = "";
            XmlNode xmlText = NMGlobals.FindFirstXMLNode(parent, NodeName);
            if (xmlText != null)
            {
                t = xmlText.InnerText;
                Log.Write(NodeName+": " + t);
            }
            else
            {
                Log.Write("XML Warning: No <"+NodeName+"> found.");
            }
            return t;
        }

        // find first inner int from node NodeName in parent.
        public static int FirstXMLInnerInt(XmlNode parent, string NodeName)
        {
            // get the int
            int i = 0;
            XmlNode xmlText = NMGlobals.FindFirstXMLNode(parent, NodeName);
            if (xmlText != null)
            {
                i = Convert.ToInt32(xmlText.InnerText);
                Log.Write(NodeName + ": " + i);
            }
            else
            {
                Log.Write("XML Warning: No <" + NodeName + "> found.");
            }
            return i;
        }
   }
}

These three methods are used for processing XML Data.

Now to the Values:
namespace CardGame.Source
{
   public class NMValues
   {
      public const string CardClassNamespace = "NetRunner.Cards";
   }
}

This is the namespace for all your card classes, wich you want to use with XML.

Now we need the base card. It has an ID, a Title and a StoryText. (Only the ID will be needed but I want to explain the loading stuff...) This base card class can be used for ANY game. I will derive a base card class from that for "the game itself" in the NetRunner.CardType namespace - and from that the card-type-classes - and then I will derive the actual cards in the NetRunner.Cards namespace. This is to show you how I approached it. Remember the namespaces: For the base stuff, it is "CardGame", for the (my) game, it is "NetRunner".

namespace CardGame.Source
{
     public class NMCard
     {
          protected string _ID; // the ID is an UNIQUE identifier for the card-class-value-pair.
          public string ID {get{return _ID;}}
          public void __setID(string id) {_ID=id;}  // we set the ID outside, in the card factory.

          protected string _Title; // Title of the card, set with CopyValuesFrom or LoadFromXML
          public string Title {get{return _Title;}}

          protected string _StoryText; // some story text for the setting of the game.
          public string StoryText {get{return _StoryText;}}

          public NMCard()
          {
               // Initialize the (new) values.
                __setID("# No ID #");
                _Title= "# No Title #";
                _StoryText="";
          }

          // This is a main feature and MUST be overriden if there are new values.
          // It is used while CLONING a card.
           public virtual void CopyValuesFrom(NMCard card)
          {
              __setID(card.ID);
               _Title=card.Title;
               _StoryText=card.StoryText;
          }

          // This also is a main feature and MUST be overriden if there are new values.
          // It is used while creating the card library.
          public virtual void LoadFromXML(XmlNode xmlCard)
          {
                _Title=NMGlobals.FirstXMLInnerText(xmlCard,"Title");
                _StoryText=NMGlobals.FirstXMLInnerText(xmlCard,"StoryText");
          }
     }
}
As you can see, there are two methods, "CopyValuesFrom" and "LoadFromXML". LoadFromXML will be called while creating the card library. It gets the XML node containing all card values. From that, we can extract our values. CopyValuesFrom makes the same - but it copies the values from a card. So, every new value needs to be "registered" in these two methods.

Now we derive the base NetRunner card class from that card in NetRunner.CardTypes:

namespace NetRunner.CardTypes
{
   public class NetRunnerBaseCard : NMCard
   {
      // Every Netrunner card has a cost.
      protected int _Cost;
      public int Cost  {get{return _Cost;}}

      // Overriden functions.
      public override void CopyValuesFrom(NMCard card)
      {
             base.CopyValuesFrom(card); // DO NOT FORGET THAT!
            if(card is NetRunnerBaseCard)
                  _Cost=((NetRunnerBaseCard)card).Cost;
      }

       public override void LoadFromXML(XmlNode xmlCard)
       {
            base.LoadFromXML(xmlCard); // DO NOT FORGET THAT!
            _Cost=NMGlobals.FirstXMLInnerInt(xmlCard,"Cost");
       }
    }
}

Now you can derive your card types from that NetRunnerBaseCard in the namespace NetRunner.CardTypes. Everytime you have new values wich need to be loaded or copied, overwrite the two methods like in the example above. (Cost)

Finally you create actual cards in the namespace NetRunner.Cards. That is to prevent the XML-creator from using the base class/es wich are also in the CardTypes namespace but cannot directly be played.. 
namespace NetRunner.Cards
{
    public class Base_Default : NetRunnerBaseCard {}
    public class Base_Special : NetRunnerBaseCard
    {
       ///  ..add values like strength or something like above ..
     }
    public class Extended_Default : NetRunnerEventCard {} // That's an example.
}

We have now an XML file with some cards and some card classes. Let's finally get on to the actual card factory class wich we are all waiting for. It has a list (_cardClasses) wich contains the names of all registered classes (all classes in the given namespace) and a dictionary (_cardLibrary) wich contains all the different cards with their values.

When you use the Clone-Method, it searches the library for the given ID and clones that object.

The classes-list is only needed for security reasons while loading from XML.

namespace CardGame.Source
{
    public class NMCardFactory
    {
        // all registered card classes in the "Cards" namespace
        // (see NMValues for the namespace)
        protected static IEnumerable<Type> _cardClasses=null;

        // all the cards there are available
        // This cards will be loaded per XML and then created by the classname.
        // Then card.LoadFromXML will be called.
        protected static Dictionary<string,NMCard> _cardLibrary = null;

        // library stuff
        public static Dictionary<string,NMCard> Library { get { return _cardLibrary; } }
        public static int LibrarySize { get { return _cardLibrary.Count; } }
        public static void ClearLibrary() {_cardLibrary.Clear();}

        // initialize the card factory
        // You must call that once in the game.
        // Load all card classes into the _cardClasses list.
        public static void Initialize()
        {
            if(_cardClasses==null)
                _cardClasses = GetTypesFromNamespace(Assembly.GetExecutingAssembly(), NMValues.CardNamespaceName);
            if (_cardLibrary == null)
                _cardLibrary = new Dictionary<string,NMCard>();
        }

        // Load a card library from a given XML file.
        public static bool LoadLibraryFromXML(string filename)
        {
            Log.Write("Loading card library from " + filename+"...");

            if (File.Exists(filename))
            {
                XmlDocument xmlDoc = new XmlDocument();
                xmlDoc.Load(filename);

                string root=xmlDoc.DocumentElement.Name.ToLower();
                if(root.Equals("cardlist"))
                {
                    foreach (XmlNode xmlCard in xmlDoc.DocumentElement.ChildNodes)
                    {
                        if (xmlCard.Name.ToLower().Equals("card"))
                        {
                            Log.Write("--- Card --------------------------------------------------------------------------------------------------");
                            // Load the ID
                            XmlAttribute ID = xmlCard.Attributes["id"];
                            // Load it again, with "ID"
                            if (ID == null)
                                ID = xmlCard.Attributes["ID"];
                            // Load it again, with "Id"
                            if (ID == null)
                                ID = xmlCard.Attributes["Id"];

                            if (ID!=null)
                            {
                                Log.Write("ID: " + ID.Value);
                                // check if id already exists.
                                if (!_cardLibrary.ContainsKey(ID.Value))
                                {
                                    // get the class of the card
                                    XmlNode xmlClass = NMGlobals.FindFirstXMLNode(xmlCard, "class");
                                    if (xmlClass != null)
                                    {
                                        // does the class name exist in the namespace?
                                        if (ClassExists(xmlClass.InnerText))
                                        {
                                            // create the card and load values.
                                            NMCard card = CreateByClass(xmlClass.InnerText);
                                            card.__setID(ID.Value);
                                            card.LoadFromXML(xmlCard);
                                            _cardLibrary.Add(ID.Value, card);
                                        }else{
                                            Log.Write("XML Error: Card class \"" + xmlClass.InnerText + "\" for card (ID) \"" + ID.Value + "\" does not exist. Ignoring.");
                                        }
                                    }else{
                                        Log.Write("XML ERROR: No <Class> found! Ignoring.");
                                    }
                                }else{
                                    Log.Write("XML ERROR: ID \"" + ID.Value + "\" does already exist. Ignoring.");
                                }
                            }else{
                                Log.Write("XML ERROR: No ID on the <card> found. Ignoring.");
                            }
                        }else{
                            Log.Write("--- WARNING -----------------------------------------------------------------------------------------------");
                            Log.Write("XML Warning: First sub-node of root must be  <Card>. It's \""+xmlCard.Name+"\". Ignoring.");
                        }
                    }
                    Log.Write("-----------------------------------------------------------------------------------------------------------");
                    return true;
                }else{
                    Log.Write("XML Error: Root node must be named <CardList> in "+filename+". Aborting.");
                }
            }
            return false;
        }

        // checks if a card class exists in the
        public static bool ClassExists(string name)
        {
            foreach (Type t in ClassTypes)
            {
                if (t.Name.Equals(name))
                    return true;
            }
            return false;
        }

        // get the count of registered card classes.
        public static int ClassCount
        {
            get
            {
                if (_cardClasses == null)
                    return 0;
                else
                    return _cardClasses.Count();
            }
        }

        // return the card class list.
        public static IEnumerable<Type> ClassTypes { get { return _cardClasses; } }

        /* Create a card from its class name.
         * The card class must be in the namespace wich is defined in
         * the NMValues class.
         * Returns the raw card with default values (mostly 0 ;) )
         * TODO: protected
         */
        protected static NMCard CreateByClass(string classname)
        {
            NMCard c = null;

            // Factory generating card with string from classname.
            Type hai = Type.GetType(NMValues.CardClassNamespace + "." + classname, true);
            c = (NMCard)(Activator.CreateInstance(hai));

            return c;
        }

        /* This is the main function of the factory.
         * It clones a card wich is in the library and returns it.
         * You can clone by card ID.
         */
        public static NMCard Clone(string ID)
        {
            NMCard card = GetCardByID(ID);
            NMCard clone = null;
            if (card != null)
            {
                clone = CreateByClass(card.GetType().Name);
                clone.CopyValuesFrom(card);
            }
            return clone;
        }

        /* Get a card from the library by its ID */
        protected static NMCard GetCardByID(string ID)
        {
            NMCard card = null;
            if (_cardLibrary.ContainsKey(ID))
            {
                card = _cardLibrary[ID];
            }else{
                Log.Write("Library Error: (id)"+ID+" does not exist.");
            }
            return card;
        }

        // get all types (classes) in a specific namespace and assembly.
        // used to get all card types (classes) into the _cardTypes list.
        // You can then create cards by index. (!)
        protected static IEnumerable<Type> GetTypesFromNamespace(Assembly assembly, String desiredNamespace)
        {
            return assembly.GetTypes().Where(type => type.Namespace == desiredNamespace);
        }
    }
}

That's it. You can now use the following commands:
NMCardFactory.Initialize() // Initialize the card factory.
NMCardFactory.LoadFromXML("c:\\myFile.xml"); // load a card set.

NMCard card = NMCardFactory.Clone("My_Card_ID");


Here are the missing classes:

namespace CardGame.Source
{
    public class Log
    {
        public static void Write(string text) {Console.WriteLine(text);} // you can also save it to file here...
    }
}

namespace NetRunner.CardTypes
{
   public class NetRunnerEventCard : NetRunnerBaseCard
   {
      // A value like in the example xml.
      protected string _MyValue;
      public string MyValue  {get{return _MyValue;}}

      // Overriden functions.
      public override void CopyValuesFrom(NMCard card)
      {
             base.CopyValuesFrom(card); // DO NOT FORGET THAT!
            if(card is NetRunnerEventCard)
                  _MyValue=((NetRunnerEventCard)card).MyValue;
      }

       public override void LoadFromXML(XmlNode xmlCard)
       {
            base.LoadFromXML(xmlCard); // DO NOT FORGET THAT!
            _MyValue=NMGlobals.FirstXMLInnerString(xmlCard,"MyValue");
       }
   }
}


Hope that helps. Have Fun!

Keine Kommentare:

Kommentar veröffentlichen