Monday, 28 April 2014

C# Source Code Post #2

Given how late last post was, all of my scheduled posts are being pushed to one day later. So this post was originally due yesterday (Sunday) but instead I'm posting it today (Monday), and the post after this will be Wednesday instead of Tuesday.. so on so forth.

So, as promised, this is InitLoad.cs. This class reads whatever gamedata (items, classes, quests etc etc) when the game first runs, and fills in any missing gamedata by creating stock data files.

This is probably one of my favourite classes to make in the game, aside from the combat class.

##File Start##
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
//Default using statements for a base class file.
using System.IO;
using System.Xml.Serialization;
//2 extra system using statements I added.

using Game.Items;
using Game.Entities;
//2 using statements pointing to other classes I have made

namespace Game
//Same old namespace
{
    public static class InitLoad
    //Class is static
    {
        static List<ItemEquipmentWeapon> weapons;
        static List<ItemEquipmentArmour> armour;
        static List<Quest> quests;
        static List<Hostile> enemies;
        //Declaring 4 variables of type "list" within the class

        public static Dictionary<int, Item> ItemIDList = new Dictionary<int, Item>();
        public static Dictionary<int, Quest> QuestIDList = new Dictionary<int, Quest>();
        public static Dictionary<int, Entity> EntityIDList = new Dictionary<int, Entity>();
        //Declaring 3 variables of type "Dictionary" within the class.
        //These are used throughout the game to reference items, quests and entities.

        public static void InitInstall()
        //This is the method that runs to fill in missing gamedata
        {
            string path, filePath,
                weaponFile = @"\WeaponsItem.agd",
                armourFile = @"\ArmourItem.agd",
                questFile = @"\Quests.agd",
                enemyFile = @"\Enemies.agd";

            XmlSerializer Serializer;
            FileStream WriteFileStream;

            path = string.Format(@"{0}\AwesomeRPG",
                Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData));

            List<ItemEquipmentWeapon> WeaponList = new List<ItemEquipmentWeapon>();
            WeaponList.Add(new ItemEquipmentWeapon("Fists", 1, 100, true, 1000, Stats.Dex));
            WeaponList.Add(new ItemEquipmentWeapon("Fists", 1, 80, false, 1001, Stats.Dex));
            WeaponList.Add(new ItemEquipmentWeapon("Stick", 3, 110, false, 1002, Stats.Str));
            WeaponList.Add(new ItemEquipmentWeapon("Big Stick", 5, 80, true, 1003, Stats.Str));

            List<ItemEquipmentArmour> ArmourList = new List<ItemEquipmentArmour>();
            ArmourList.Add(new ItemEquipmentArmour("Rags", 0.05, ArmourType.Body, 2001));
            ArmourList.Add(new ItemEquipmentArmour("Sandals", 0.01, ArmourType.Boots, 2002));
            ArmourList.Add(new ItemEquipmentArmour("Hand Wraps", 0.01, ArmourType.Gloves, 2003));
            ArmourList.Add(new ItemEquipmentArmour("Hood", 0.02, ArmourType.Helmet, 2004));

            List<Hostile> EnemyList = new List<Hostile>();
            EnemyList.Add(new Hostile("TestEnemy", true, 20, 1, 100, 1001));
            EntityIDList.Add(EnemyList[0].ID, EnemyList[0]);

            List<Quest> QuestList = new List<Quest>();
            QuestList.Add(new Quest("TestQuest", new List<QuestActions>() { new QuestActions(1001) }, new List<int>() { 1003 }, 250, 0));

            if (!Directory.Exists(path)) Directory.CreateDirectory(path);
            path += "\\Data";
            if (!Directory.Exists(path)) Directory.CreateDirectory(path);
            if (!Directory.Exists(path + "\\Items")) Directory.CreateDirectory(path + "\\Items");

            filePath = path + "\\Items" + weaponFile;
            WriteFileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
            Serializer = new XmlSerializer(typeof(List<ItemEquipmentWeapon>));
            Serializer.Serialize(WriteFileStream, WeaponList);
            WriteFileStream.Close();

            filePath = path + "\\Items" + armourFile;
            WriteFileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
            Serializer = new XmlSerializer(typeof(List<ItemEquipmentArmour>));
            Serializer.Serialize(WriteFileStream, ArmourList);
            WriteFileStream.Close();

            filePath = path + enemyFile;
            WriteFileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
            Serializer = new XmlSerializer(typeof(List<Hostile>));
            Serializer.Serialize(WriteFileStream, EnemyList);
            WriteFileStream.Close();

            filePath = path + questFile;
            WriteFileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
            Serializer = new XmlSerializer(typeof(List<Quest>));
            Serializer.Serialize(WriteFileStream, QuestList);
            WriteFileStream.Close();
            //Lot of crazy ass code I'll explain further down
        }

        public static bool PreLoading()
        //This is the method that runs every time the game starts
        //If it functions correctly, it returns true. If it fails somehow, it returns false.
        //If it returns false, the game exits.
        {
            string path, filePath,
                weaponFile = @"\WeaponsItem.agd",
                armourFile = @"\ArmourItem.agd",
                questFile = @"\Quests.agd",
                enemyFile = @"\Enemies.agd";

            XmlSerializer SerializerObj;
            FileStream ReadFileStream;

            path = string.Format(@"{0}\AwesomeRPG\Data",
                Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData));

            ItemIDList.Add(-1, null);
            QuestIDList.Add(-1, null);

            filePath = string.Format(@"{0}\Items{1}", path, weaponFile);
            SerializerObj = new XmlSerializer(typeof(List<ItemEquipmentWeapon>));
            ReadFileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
            weapons = (List<ItemEquipmentWeapon>)SerializerObj.Deserialize(ReadFileStream);
            foreach (Item item in weapons)
            {
                ItemIDList.Add(item.ID, item);
            }

            filePath = string.Format(@"{0}\Items{1}", path, armourFile);
            SerializerObj = new XmlSerializer(typeof(List<ItemEquipmentArmour>));
            ReadFileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
            armour = (List<ItemEquipmentArmour>)SerializerObj.Deserialize(ReadFileStream);
            foreach (Item item in armour)
            {
                ItemIDList.Add(item.ID, item);
            }

            filePath = string.Format(@"{0}{1}", path, questFile);
            SerializerObj = new XmlSerializer(typeof(List<Quest>));
            ReadFileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
            quests = (List<Quest>)SerializerObj.Deserialize(ReadFileStream);
            foreach (Quest item in quests)
            {
                QuestIDList.Add(item.ID, item);
            }

            filePath = string.Format(@"{0}{1}", path, enemyFile);
            SerializerObj = new XmlSerializer(typeof(List<Hostile>));
            ReadFileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
            enemies = (List<Hostile>)SerializerObj.Deserialize(ReadFileStream);
            foreach (Hostile item in enemies)
            {
                EntityIDList.Add(item.ID, item);
            }
            //more crazy ass code
            return true;
        }
    }
}
##File End##

Jeez, if you thought TitleScreen.cs was complex, this one probably blew you away.
This one primarily focuses around XML Serialization, which is one of 2 very basic methods of saving data in C#, and the one I chose to use in AwesomeRPG. The other method is simply writing to plain text.

The reason I chose XML over TXT is because I needed to save instances of classes. For example, every weapon in the game is an instance of the class "ItemEquipmentWeapon" with it's own stats etc. I needed to save those stats, and be able to load them back in and remake them as instances.

While this is possible in TXT, it's incredibly difficult, as you would have to loop through every item, save each individual stat on a new line, then on loading read each line and hardcode where to put each stat on the instance.

With XML, tags are assigned to each stat that the XML serializer can read and figure out where everything belongs. For example, saved weapons in XML format look like this:

##File Start##
<?xml version="1.0"?>  This is a typical XML header
<ArrayOfItemEquipmentWeapon xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> This specifies what the file is.
  <ItemEquipmentWeapon> This marks the start of 1 instance
    <IsUsable>false</IsUsable>
    <ID>1000</ID>
    <Name>Fists</Name>
    <Def>0</Def>                                  All the stats are in here for this weapon (Name = Fists)
    <Dmg>1</Dmg>
    <AtkSpeed>100</AtkSpeed>
    <THand>true</THand>
    <StatMod>Dex</StatMod>
  </ItemEquipmentWeapon> This marks the end of that instance
  <ItemEquipmentWeapon>
    <IsUsable>false</IsUsable>
    <ID>1001</ID>
    <Name>Fists</Name>
    <Def>0</Def>
    <Dmg>1</Dmg>
    <AtkSpeed>80</AtkSpeed>
    <THand>false</THand>
    <StatMod>Dex</StatMod>
  </ItemEquipmentWeapon>
  <ItemEquipmentWeapon>
    <IsUsable>false</IsUsable>
    <ID>1002</ID>
    <Name>Stick</Name>
    <Def>0</Def>
    <Dmg>3</Dmg>
    <AtkSpeed>110</AtkSpeed>
    <THand>false</THand>
    <StatMod>Str</StatMod>
  </ItemEquipmentWeapon>
  <ItemEquipmentWeapon>
    <IsUsable>false</IsUsable>
    <ID>1003</ID>
    <Name>Big Stick</Name>
    <Def>0</Def>
    <Dmg>5</Dmg>
    <AtkSpeed>80</AtkSpeed>
    <THand>true</THand>
    <StatMod>Str</StatMod>
  </ItemEquipmentWeapon>
</ArrayOfItemEquipmentWeapon>
##File End##

The "ID" stat is used in the Item Dictionary.
Note that while the file's format is XML, it is actually saved with the extension ".agd" (So this file is WeaponsItem.agd). That is an extension I made up, and it stands for Awesome Game Data. That doesn't change the contents of the file in any way though, and it can still be opened and edited in notepad.

So, at the top of InitLoad, we declared 7 variables:

static List<ItemEquipmentWeapon> weapons;
static List<ItemEquipmentArmour> armour;
static List<Quest> quests;
static List<Hostile> enemies;

public static Dictionary<int, Item> ItemIDList = new Dictionary<int, Item>();
public static Dictionary<int, Quest> QuestIDList = new Dictionary<int, Quest>();
public static Dictionary<int, Entity> EntityIDList = new Dictionary<int, Entity>();

List type variables are very self explanatory. They are a list of variables.
In this case, each list is a list of variables containing instances of each gamedata type.
So the variable "enemies" is a list of instances of the class "Hostile".

Lists are similar to arrays, however they are dynamic, and have no set length.

Dictionaries are a bit more complex. They are a list of a certain type of variables, much like lists. However each "Entry" in the dictionary also has an ID in int32 form attached to it.
This is where the ID field on items comes in.

If you remember, Fists are ID 1000. So to get the stats for fists, for example if you were equipping them, you would check ItemIDList, which is a dictionary (Actually outside of the InitLoad class you would check InitLoad.ItemIDList) for ID 1000, and it would find that an instance of ItemEquipmentWeapon called "Fists" is associated with that ID in the Dictionary.

As far as I can tell, dictionaries are by far the best way to handle Item databases with ID's.

Moving on.
The InitInstall Method, as stated, is the method that creates the Gamedata the first time the game runs.

public static void InitInstall()
{
    string path, filePath,
        weaponFile = @"\WeaponsItem.agd",
        armourFile = @"\ArmourItem.agd",
        questFile = @"\Quests.agd",
        enemyFile = @"\Enemies.agd";

    XmlSerializer Serializer;
    FileStream WriteFileStream;

    path = string.Format(@"{0}\AwesomeRPG",
        Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData));

    List<ItemEquipmentWeapon> WeaponList = new List<ItemEquipmentWeapon>();
    WeaponList.Add(new ItemEquipmentWeapon("Fists", 1, 100, true, 1000, Stats.Dex));
    WeaponList.Add(new ItemEquipmentWeapon("Fists", 1, 80, false, 1001, Stats.Dex));
    WeaponList.Add(new ItemEquipmentWeapon("Stick", 3, 110, false, 1002, Stats.Str));
    WeaponList.Add(new ItemEquipmentWeapon("Big Stick", 5, 80, true, 1003, Stats.Str));

    List<ItemEquipmentArmour> ArmourList = new List<ItemEquipmentArmour>();
    ArmourList.Add(new ItemEquipmentArmour("Rags", 0.05, ArmourType.Body, 2001));
    ArmourList.Add(new ItemEquipmentArmour("Sandals", 0.01, ArmourType.Boots, 2002));
    ArmourList.Add(new ItemEquipmentArmour("Hand Wraps", 0.01, ArmourType.Gloves, 2003));
    ArmourList.Add(new ItemEquipmentArmour("Hood", 0.02, ArmourType.Helmet, 2004));

    List<Hostile> EnemyList = new List<Hostile>();
    EnemyList.Add(new Hostile("TestEnemy", true, 20, 1, 100, 1001));
    EntityIDList.Add(EnemyList[0].ID, EnemyList[0]);

    List<Quest> QuestList = new List<Quest>();
    QuestList.Add(new Quest("TestQuest", new List<QuestActions>() { new QuestActions(1001) }, new List<int>() { 1003 }, 250, 0));

    if (!Directory.Exists(path)) Directory.CreateDirectory(path);
    path += "\\Data";
    if (!Directory.Exists(path)) Directory.CreateDirectory(path);
    if (!Directory.Exists(path + "\\Items")) Directory.CreateDirectory(path + "\\Items");

    filePath = path + "\\Items" + weaponFile;
    WriteFileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
    Serializer = new XmlSerializer(typeof(List<ItemEquipmentWeapon>));
    Serializer.Serialize(WriteFileStream, WeaponList);
    WriteFileStream.Close();

    filePath = path + "\\Items" + armourFile;
    WriteFileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
    Serializer = new XmlSerializer(typeof(List<ItemEquipmentArmour>));
    Serializer.Serialize(WriteFileStream, ArmourList);
    WriteFileStream.Close();

    filePath = path + enemyFile;
    WriteFileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
    Serializer = new XmlSerializer(typeof(List<Hostile>));
    Serializer.Serialize(WriteFileStream, EnemyList);
    WriteFileStream.Close();

    filePath = path + questFile;
    WriteFileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
    Serializer = new XmlSerializer(typeof(List<Quest>));
    Serializer.Serialize(WriteFileStream, QuestList);
    WriteFileStream.Close();
}

Straight away this looks confusing. Let's go through it.
Firstly, we're creating 6 String variables.

string path, filePath,
    weaponFile = @"\WeaponsItem.agd",
    armourFile = @"\ArmourItem.agd",
    questFile = @"\Quests.agd",
    enemyFile = @"\Enemies.agd";

The first 2, "path" and "filePath" have no value.
The other 4 "weaponFile", "armourFile", "questFile" and "enemyFile" all have a backslash followed by the name of the file where that data is stored. (The @ simply stops it counting backslashes as in-text commands, for example, \n in a string signifies a new line if there is no @. You can also use \\ to signify an actual backslash)

Next we declare 2 more variables.

XmlSerializer Serializer;
FileStream WriteFileStream;

A variable called Serializer of type XmlSerializer, and a variable called WriteFileStream of type FileStream.

Then we give the string variable "path" a value.

path = string.Format(@"{0}\AwesomeRPG\Data",
    Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData));

Don't be confused by the fact this is on 2 lines, it's just to make is simpler, it could easily be done as one line, however it would be awkward to read.

Anyway, in here, we're saying path equals the output of a method called string.Format and we're giving that method 2 args:

@"{0}\AwesomeRPG\Data"
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData));

So what does this mean?
String.Format allows some extra functionality. The first arg you give it is the actual string, any you give it after will be put in place of the {X} notations.

So you could do:

string.Format("{0} + {1} = 5", 2, 3);

And the method would return a string that says "2 + 3 = 5"

In this case, we're putting the result of ANOTHER method before a piece of plain text.
So what does the other method do?

Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)

Long story short, it returns the location of the AppData folder on the users computer. You can find this yourself by going to run (Windows Key + R on Win7) and typing "%appdata%". Minecraft players may be familiar with this folder.

It does this because AwesomeRPG's gamedata is stored in "%appdata%\AwesomeRPG\Data"
Sadly C# doesn't recognize "%appdata%" as a valid location.

Next up we do a bunch of weird stuff with lists:

List<ItemEquipmentWeapon> WeaponList = new List<ItemEquipmentWeapon>();
WeaponList.Add(new ItemEquipmentWeapon("Fists", 1, 100, true, 1000, Stats.Dex));
WeaponList.Add(new ItemEquipmentWeapon("Fists", 1, 80, false, 1001, Stats.Dex));
WeaponList.Add(new ItemEquipmentWeapon("Stick", 3, 110, false, 1002, Stats.Str));
WeaponList.Add(new ItemEquipmentWeapon("Big Stick", 5, 80, true, 1003, Stats.Str));

This is actually fairly simple. We're creating the stock data for the game and putting it in the list, ready to be saved to a file.

WeaponList.Add() simply adds an entry to the list. The arg is the instance to be added.

We then do the same for Armour, Quests and Enemies.

if (!Directory.Exists(path)) Directory.CreateDirectory(path);
path += "\\Data";
if (!Directory.Exists(path)) Directory.CreateDirectory(path);
if (!Directory.Exists(path + "\\Items")) Directory.CreateDirectory(path + "\\Items");

This is just making sure the folders and such exist, otherwise the code will error trying to make files inside them.

filePath = path + "\\Items" + weaponFile;
WriteFileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
Serializer = new XmlSerializer(typeof(List<ItemEquipmentWeapon>));
Serializer.Serialize(WriteFileStream, WeaponList);
WriteFileStream.Close();

Now we get to start making files.
The first line here is quite self explanatory, so I'll skip to the second.

WriteFileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);

So, we're giving WriteFileStream a value. We're creating a new instance of the class FileStream, whose constructor requires 4 args, the file path as a string, a FileMode value, a FileAccess value and a FileShare value.

For the latter 3 we simply use existing constants.
FileMode.Create means the file stream will forcefully create a new file, this will error if the file already exists.
FileAccess.Write means the file stream will only output data to the file, it will not input data from the file (Read)
FileShare.None means the file stream will fully restrict access to the file.

Serializer = new XmlSerializer(typeof(List<ItemEquipmentWeapon>));

Now we're giving Serializer a value, an instance of the XmlSerializer class, whose constructor requires only 1 arg, a "Type".

Types are rather strange. Variable types, such as List, String, Int, any non-static class you make are not types, however they HAVE types, which must be obtained using the special typeof method.

Well, that's not strictly true, they are types, but also have types which comes under System.Type.
Some common types include:

typeof(int) returns System.Int32
typeof(char) returns System.Char

So on so forth.
However it is often easier to use typeof than to actually find the exact System type constant, especially with specific lists.

After giving Serializer a value, we run a method from it.

Serializer.Serialize(WriteFileStream, WeaponList);

The Serialize method. A non-static method in the XmlSerializer class.
This takes 2 args, a FileStream and a variable of the same type given to the constructor of the XmlSerializer, in this case a variable of type List<ItemEquipmentWeapon>

Serialize is effectively the "write to file" method. It uses the FileStream to access the file, and uses the second arg as a source for the data to be serialized. It then runs through the list (in this case) and runs through all fields etc required for each instance in the list, and sends them through the FileStream, which works out where to put them (What file).

After serializing, we close the FileStream. This is for multiple reasons. Not only is it good practice to always close a filestream, but we're about to change one of the parameters and redefine the value of WriteFileStream, as we do what we just did for weapons, but with armour, quests and entities.

The next method, PreLoading() is very similar to InitInstall() in most respects, I'll briefly cover the main differences.

Firstly, Serializer from InitInstall() is called SerializerObj in PreLoading(). I'm not sure why, that's just how I did it. WriteFileStream is also now called ReadFileStream, this is because we're only reading from a file, not writing to it, so calling it WriteFileStream wouldn't make much sense.

Finally, these parts of code:

ItemIDList.Add(-1, null);
QuestIDList.Add(-1, null);

and

ReadFileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
weapons = (List<ItemEquipmentWeapon>)SerializerObj.Deserialize(ReadFileStream);
foreach (Item item in weapons)
{
    ItemIDList.Add(item.ID, item);
}

So, the first part simply adds null entries to the ItemIDList and the QuestIDList with the ID -1.

The second part is a little different.
Firstly, we're giving slightly different args to the FileStream constructor.
FileMode.Open means the FileStream will attempt to open the file, and error if it doesn't exist.
FileAccess.Read means the FileStream can only input data from the file, not vice versa.
FileShare.Read means the FileStream will allow other programs to read the file while it is open.

As for what happens after that... well.

weapons = (List<ItemEquipmentWeapon>)SerializerObj.Deserialize(ReadFileStream);

Here, rather than serializing from a list, we're deserializing to a list.
Deserialization is effectively reading a file through a file stream, therefore we must give it a filestream to use, and catch it's output in a variable. By default the return type of Deserialize is a little odd, so we cast it as a List<ItemEquipmentWeapon>.

Finally, we loop through all the items in the list after deserialization, and add them to the dictionary.

foreach (Item item in weapons)
{
    ItemIDList.Add(item.ID, item);
}

You may notice we didn't close the FileStream, that's because I'm stupid, and forgot to do it.

Anyway, we then run through this same deserialization process for Armour, Quests and Entities.

And then we return true, to say it loaded successfully. Because all the error checking I had in there that returned false I temporarily took out and forgot to put back in, because again, I'm stupid :P

And that's it for today's post.

Next C# lesson will be Friday, because on Wednesday I have a special announcement post for you all.

2 comments:

  1. This is just a quick reminder somebody appreciates your hard work on this project and definitely not only me reading your blog. ;)
    I'm hardly used to c++ (currently learning haha) but when you need to have someone copy pasta some files (item ids f.e.) or just balancing game feel free to get back to me

    ReplyDelete
    Replies
    1. Hi, thanks for the comment!

      I'd also like to apologize for the recent lack of posts, My internet's been pretty bad lately, however I am getting it fixed in about an hour, so I'll definitely post that announcement today!

      Delete