Creating a MasterPlan Add-On (Part #2)

Posted on November 1, 2010

4


A MasterPlan Add-In in two parts.

Catch Up

In the last post, I created a basic project for a MasterPlan Add-In.  The “AddInManager” class we created is the controller for the Add-In framework.

Now lets look at the parts that do the heavy lifting: Commands.

ICommand You!

Open the solution that we created in the first post.  I called mine “EncounterTrackingSheet”.

Now add a new class called “SaveEncounterCommand”.  This new class will be the object that reads the MasterPlan application project settings and then writes the encounter tracking document.

Add the “ICommand” interface to the class declaration and right click on it to auto-generate the interface members, just as we did before.

Your class should will have a “Name” member and a “Description” member, as did the AddInManager class.  In addition to these, you also have “Active” and “Available” members that return Boolean values.

“Active” is whether the command is currently running.  It will give a check box next to the name in the menu.

“Available” is whether the command can be executed.  A false value will cause the menu item to be grayed out and disabled on the menu. This is useful when want to turn the Add-In on & off based on the current state of the project.

Fill out these members like this:

/// <summary>
 /// Gets a value indicating whether the command is currently running.
 /// </summary>
 public bool Active
 {
   get { return false; }
 }

 /// <summary>
 /// Gets a value indicating whether the command is currently available to be executed.
 /// </summary>
 /// <value></value>
 public bool Available
 {
   get
   {
     //Check to see that there is a Plot Point selected.
     bool enabled = (_MPApp.SelectedPoint != null);

     //Check to see that the currently selected plot point is an Encounter
     enabled = enabled && (_MPApp.SelectedPoint.Element is Encounter);

     return enabled;
   }
 }

 /// <summary>
 /// Gets the name of the command.
 /// </summary>
 public string Name
 {
   get { return "Print Tracking Sheet"; }
 }

 /// <summary>
 /// Gets the description of the command.
 /// </summary>
 public string Description
 {
   get { return "Save an Encounter Tracking Sheet"; }
 }

Now lets look at the constructor.  We’ll need a way to read from the MasterPlan application, which means we’ll need to pass the reference that we saved in the AddInManager class to this class.

Create a private variable to hold the reference, and add the constructor with IApplication in the parameters:


private IApplication _MPApp;
 /// <summary>
 /// Initializes a new instance of the  class.
 /// </summary>
 public SaveEncounterCommand(IApplication MP_Application)
 {
 this._MPApp = MP_Application;
 }

Adding it to the mix

In the AddInManager.cs, now we need to add a list to hold the commands that we’re adding, then we need to add the commands to that list. Remember, there are two lists of commands that can be executed by MasterPlan, “CombatCommands” happens when running a combat encounter, “Commands” happens outside of combat.

Add the private variable and return it from the “Commands” member, and then add a new instantiation of the SaveEncounterCommand object to the list inside the Initialise() function:

 List encounterCommands = new List();

 /// <summary>
 /// Gets the list of commands supplied by the add-in.
 /// </summary>
 public List<ICommand> Commands
 {
   get { return encounterCommands; }
 }

 /// <summary>
 /// Method which is called when the add-in is loaded into the editor.
 /// </summary>
 ///
<param name="app" />The Masterplan application interface
 /// <returns>
 /// Returns true if the add-in initialised correctly; false otherwise.
 /// </returns>
 public bool Initialise(IApplication app)
 {
   //Set bool to return whether this Add-In has initialized correctly
   bool initializeSuccessful = true;

   try
   {
     this._MPApp = app;
     this.encounterCommands.Add(new SaveEncounterCommand(this._MPApp));
   }
   catch (Exception e)
   {
     initializeSuccessful = false;
   }

   return initializeSuccessful;
 }

Now when we compile this, save it to the “masterplan\addins\” directory and run MasterPlan, we should see it appear on the menu next to the AddInManager “Name”.  It should be disabled until we click on a plot point with an encounter.

The command is enabled when we select a plot point with an encounter.

 

Now, on to the Meat!

So now that we’ve got the “AddInManager” adding a “SaveEncounterCommand” in the Initialise() function, We can see that it turns on as expected when the user selects a plot point with an encounter, we need to add the actual work code to the Execute() function in the command class.

I got to this point this afternoon and realized that I had no idea how I was going to make this work for a simple tutorial.  Sure I could go into creating a print document object, add rectangles, draw text to them and plot it all out on a page, but that’s a lot of work (looks good, but a lot of work).  So I decided to make it simple(r).

The way I’m going to print this out is to write the player and monster variables to an XML string, then run the string through a xslt transformation, write the output to a WebBrowser control embedded within the class, and call the print() function on the WebBrowser control.  Simple, right?

Lame Code Disclaimer

Yeah, I guess there are simpler ways, however, this gives me a way to possible extend this Add-In, allowing users to write their own xslt files or format the output differently later.

I also realize that the code that I wrote for this is far from my best code, so, while I appreciate  feedback on my code and blog posts, just note that this code will probably change in the future.

On to the code!

The XML format that I settled on is a simple one.  I looked at the values I needed for each encounter, and the values that I needed for each creature and came up with a format that holds all of these values:

<Encounter>
 <Name>Thug Attack</Name>
 <XP>750</XP>
 <Players>5</Players>
 <Creature>
<Name>* Deva Swordmage</Name>
<HP>38</HP>
<AC>19</AC>
<InitMod>0</InitMod>
 </Creature>
 <Creature>
* Githzerai Ranger
<HP>42</HP>
<AC>13</AC>
<InitMod>3</InitMod>
 </Creature>
</Encounter>

Each Encounter has a Name, XP and number of Player (PCs), then one child node for each PC, and one for each NPC (monster, combatant, etc).  Each creature has a Name (“*” is for PCs), a HP value, AC value and an Initiative Modifier.

The first part of executing the command is fairly simple.  Loop through the PCs and NPCs in an encounter, and write the data to an XML string.

StringBuilder sb = new StringBuilder();

 int EncounterXP = _MPApp.SelectedPoint.GetXP();

 Encounter currentEncounter = _MPApp.SelectedPoint.Element as Encounter;

 List<Hero> heroes = _MPApp.Project.Heroes;

 //Sort the Heroes by Name
 heroes.Sort();

 foreach (Hero hero in heroes)
 {
   // Get name, initiative Mod, AC & HP for each hero
    string h_name = "* " + hero.Name;
    int h_init = hero.InitBonus;
    int h_ac = hero.AC;
    int h_hp = hero.HP;
    sb.AppendFormat(
        "<Creature><Name>{0}</Name><HP>{1}</HP><AC>{2}</AC><InitMod>{3}</InitMod></Creature>",
         h_name, h_hp, h_ac, h_init);
 }

 //Get each Creature type
 //Creatures are set in "Slots", so 3 spiders and 2 Skeletons will give you 2 slots total.  1 for Spider, 1 for Skeleton.
 foreach (EncounterSlot slot in currentEncounter.Slots)
 {
     //CombatData is seperate for each creature (monster) in the encounter.
     // From the above example, we'll have 3 combatdata objects for Spider, and 2 for Skeleton
     foreach (CombatData combat in slot.CombatData)
     {
       //Get name, initiative mod, AC and HO and add to the XML
      string c_name = combat.DisplayName;
      int c_init = slot.Card.Initiative;
      int c_ac = slot.Card.AC;
      int c_hp = slot.Card.HP;
      sb.AppendFormat(
          "<Creature><Name>{0}</Name><HP>{1}</HP><AC>{2}</AC><InitMod>{3}</InitMod></Creature>",
           c_name, c_hp, c_ac, c_init);
     }
 }

 string encounterName = _MPApp.SelectedPoint.Name;

 //Add Encounter Name, Encounter XP, # of Players and the creatures XML together.
 string FullXMLString = string.Format(
            "<Encounter><Name>{0}</Name><XP>{1}</XP><Players>{2}</Players>{3}</Encounter>",
             encounterName, currentEncounter.GetXP(), _MPApp.Project.Heroes.Count, sb.ToString());

//write it to and XmlDocument
 XmlDocument xDoc = new XmlDocument();
 xDoc.LoadXml(FullXMLString);

This gives us our XML string to transform.

Next! The Transformation!

Now that we had the XML string, we need to transform it.  After some searching, I found that I was able to use a StringReader to load my XSL code into a XslCompiledTransform object from a string.  Using a handy utility I built, I ripped my xslt file into a function that returns a string called “Get_EncounterTransform_xslt_AsString()”.   You may notice the

Ugly, but it works:

private string Get_EncounterTransform_xslt_AsString()
 {

 System.Text.StringBuilder sb = new StringBuilder(2893);
 sb.AppendLine("<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>");
 sb.AppendLine("<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">");
 sb.AppendLine("<xsl:output method=\"html\"/>");
 sb.AppendLine("  <xsl:template match=\"/Encounter\">");
 sb.AppendLine("    <html>");
 sb.AppendLine("      <head>");
 sb.AppendLine("        <title></title>");
 sb.AppendLine("        <style type=\"text/css\">");
 sb.AppendLine("          <!--[<span class="hiddenSpellError" pre="">CDATA</span>[");-->
 sb.AppendLine(".TableHeader {");
 sb.AppendLine("    font-size: large;");
 sb.AppendLine("    font-weight: bold;");
 sb.AppendLine("}");
 sb.AppendLine(".ColumnHeader {");
 sb.AppendLine("    font-size: medium;");
 sb.AppendLine("    font-weight: bold;");
 sb.AppendLine("    border: thin solid #000000;");
 sb.AppendLine("    text-align:center;");
 sb.AppendLine("}");
 sb.AppendLine("table {");
 sb.AppendLine("    margin: 0px;");
 sb.AppendLine("    padding: 0px;");
 sb.AppendLine("}");
 sb.AppendLine(".CharacterRow td {");
 sb.AppendLine("    border-top-width: thin;");
 sb.AppendLine("    border-right-width: thin;");
 sb.AppendLine("    border-bottom-width: thin;");
 sb.AppendLine("    border-left-width: thin;");
 sb.AppendLine("    border-top-style: none;");
 sb.AppendLine("    border-right-style: solid;");
 sb.AppendLine("    border-bottom-style: none;");
 sb.AppendLine("    border-left-style: solid;");
 sb.AppendLine("    border-top-color: #333333;");
 sb.AppendLine("    border-right-color: #333333;");
 sb.AppendLine("    border-bottom-color: #333333;");
 sb.AppendLine("    border-left-color: #333333;");
 sb.AppendLine("}");
 sb.AppendLine(".Shaded {");
 sb.AppendLine("    background-color: #CCCCCC;");
 sb.AppendLine("}");
 sb.AppendLine(".BaseRow td{");
 sb.AppendLine("    border-top-width: thin;");
 sb.AppendLine("    border-right-width: thin;");
 sb.AppendLine("    border-bottom-width: thin;");
 sb.AppendLine("    border-left-width: thin;");
 sb.AppendLine("    border-top-style: solid;");
 sb.AppendLine("    border-top-color: #333333;");
 sb.AppendLine("    border-right-color: #333333;");
 sb.AppendLine("    border-bottom-color: #333333;");
 sb.AppendLine("    border-left-color: #333333;");
 sb.AppendLine("    padding: 3px 0px 3px 0px;");
 sb.AppendLine("}");
 sb.AppendLine("]]>");
 sb.AppendLine("        </style>");
 sb.AppendLine("      </head>");
 sb.AppendLine("      <body>");
 sb.AppendLine("        <table style=\"width: 100%;\">");
 sb.AppendLine("          <tr>");
 sb.AppendLine("            <td colspan=\"5\" class=\"TableHeader\">");
 sb.AppendLine("              Encounter: <xsl:value-of select=\"Name\"/>");
 sb.AppendLine("            </td>");
 sb.AppendLine("            <td width=\"12%\"> </td>");
 sb.AppendLine("          </tr>");
 sb.AppendLine("          <tr>");
 sb.AppendLine("            <td width=\"21%\" class=\"ColumnHeader\">");
 sb.AppendLine("              Creature Name");
 sb.AppendLine("");
 sb.AppendLine("            </td>");
 sb.AppendLine("            <td width=\"28%\" class=\"ColumnHeader\">Status</td>");
 sb.AppendLine("            <td width=\"10%\" class=\"ColumnHeader\">Ends When? </td>");
 sb.AppendLine("            <td width=\"14%\" class=\"ColumnHeader\">Marked/Marking</td>");
 sb.AppendLine("            <td width=\"15%\" class=\"ColumnHeader\">");
 sb.AppendLine("              HP");
 sb.AppendLine("");
 sb.AppendLine("            </td>");
 sb.AppendLine("            <td class=\"ColumnHeader\">");
 sb.AppendLine("              Initiative Order");
 sb.AppendLine("");
 sb.AppendLine("            </td>");
 sb.AppendLine("          </tr>");
 sb.AppendLine("");
 sb.AppendLine("          <xsl:for-each select=\"Creature\">");
 sb.AppendLine("            <tr>");
 sb.AppendLine("              <td>");
 sb.AppendLine("                <xsl:value-of select=\"Name\"/>");
 sb.AppendLine("              </td>");
 sb.AppendLine("              <td> </td>");
 sb.AppendLine("              <td> </td>");
 sb.AppendLine("              <td> </td>");
 sb.AppendLine("              <td>");
 sb.AppendLine("                <xsl:value-of select=\"HP\"/>");
 sb.AppendLine("              </td>");
 sb.AppendLine("              <td>");
 sb.AppendLine("                (+<xsl:value-of select=\"InitMod\"/>)");
 sb.AppendLine("              </td>");
 sb.AppendLine("            </tr>");
 sb.AppendLine("          <!--<span class="hiddenSpellError" pre=""-->xsl:for-each>");
 sb.AppendLine("          <tr class=\"BaseRow\">");
 sb.AppendLine("            <td colspan=\"6\">Rounds: ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( )</td>");
 sb.AppendLine("          </tr>");
 sb.AppendLine("          <tr>");
 sb.AppendLine("            <td colspan=\"6\">");
 sb.AppendLine("              <table width=\"100%\"  border=\"0\" cellspacing=\"0\" cellpadding=\"0\">");
 sb.AppendLine("                <tr class=\"BaseRow\">");
 sb.AppendLine("                  <td>");
 sb.AppendLine("                    Encounter XP: <xsl:value-of select=\"XP\"/>");
 sb.AppendLine("                  </td>");
 sb.AppendLine("                  <td>");
 sb.AppendLine("                    XP/<xsl:value-of select=\"Players\"/>: ");
 sb.AppendLine("                  </td>");
 sb.AppendLine("                  <td>");
 sb.AppendLine("                    XP/(<xsl:value-of select=\"Players\"/> - 1): ");
 sb.AppendLine("                  </td>");
 sb.AppendLine("                </tr>");
 sb.AppendLine("              </table>");
 sb.AppendLine("            </td>");
 sb.AppendLine("          </tr>");
 sb.AppendLine("        </table>");
 sb.AppendLine("      </body>");
 sb.AppendLine("    </html>");
 sb.AppendLine("  <!--<span class="hiddenSpellError" pre=""-->xsl:template>");
 sb.AppendLine("<!--<span class="hiddenSpellError" pre=""-->xsl:stylesheet>");

 return sb.ToString();
 }

So back in the Execute() function….

We take the transform string and shove it together with the XmlDocument that contains our XML output.


//write it to and XmlDocument
 XmlDocument xDoc = new XmlDocument();
 xDoc.LoadXml(FullXMLString);

 //Create the Transfor object
 // and read in the xsl as a string
 XslCompiledTransform xslt = new XslCompiledTransform();
 xslt.Load(XmlReader.Create(new StringReader(Get_EncounterTransform_xslt_AsString())));

 //create a StringWriter for output
 StringWriter sw = new StringWriter();

 //transform
 xslt.Transform(xDoc.CreateNavigator(), null, sw);
 string output = sw.ToString();

Finally, take the transformed output, add a WebBrowser control, get the HtmlDocument of the WebBrowser control and write to it.

then… call Print().

string output = sw.ToString();

//Add a WebBrowser, then navigate to an empty page.
WebBrowser wb = new WebBrowser();
wb.Navigate("about:blank");
HtmlDocument doc = wb.Document;

//write our output to the HtmlDocument
doc.Write(string.Empty);
doc.Write(output);

//and Print it!
wb.Print();

TAA-DAAAA!!!

That’s it.  You now have a MasterPlan Add-In that will print an encounter tracker sheet for each combat encounter in your project.

Here’s a link to the full downloadable source code.

Please lat me know your thoughts on this post.  I have just begun to start messing with MasterPlan and look forward to learning much more about how to make it an integral tool in my DM career.

Advertisements
Posted in: D&D 4e, Gaming, Software