How not to write an XML file
I’ll be honest. I don’t like XML. I don’t like SOAP (REST is far nicer in my opinion), since it manipulates the HTTP spec to do things it was never meant to do. Raw sockets and bit twiddling seem like a more logical extension, just that port 80 happens to be open on most corporate firewalls, so SOAP and CORBA have taken off. Inasmuch as I may dislike XML, though, it has its uses. Representing a datastream on for sets where CSV doesn’t really make sense, and YAML isn’t available, and it’s not that hard to deal with.
The ArmyBuilder developers seem to have squeezed SGML into an XML doctype somehow, and the roster files are littered with references I can’t quite make out. Yes, ArmyBuilder can export to XML, I guess, but it leads to XSL from hell. In some ways, I would have preferred to rip apart a binary format with a hex editor, as long as the data was formatted logically.
This, for instance:
<link id="dwWarCrew" count="1" actual="1" script="0" sequence="106" pseudo="no" totalcost="0" \ name="Crew" category="Equip" visible="no" sourceid="dwBoltThrw" sourceindex="1"></link>
Or this:
<ruleset context="dwSubtype" ruleset="dwDwarves" contextname="Army Subtype" rulesetname="Dwarf Army"/>Is not formatted logically. The second record, as you can see, uses XML attributes rather than nodes for everything, which kinda defeats the point. XSL to parse ArmyBuilder’s XML output? Ahh…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | <?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/> <xsl:variable name="newlinefeed"><![CDATA[ ]]></xsl:variable> <xsl:variable name="statCountGlobal" select="count(document/definition/stat_def)"/> <xsl:template match="/"> <xsl:variable name="namedModelCount" select="document/composition/@model_count"/> <xsl:variable name="actualModelCount" select="sum(//regiment/@model_count)"/> <xsl:variable name="actualPoints" select="sum(//regiment/@cost[not(contains(.,'['))])"/> <xsl:value-of select="concat(/document/summary/@race_name,': ',$actualPoints, /document/definition/@points_abbrev,' - ',$actualModelCount,' ')"/> Models<xsl:value-of select="$newlinefeed"/> <xsl:for-each select="/document/composition/comp_entry"> <xsl:variable name="groupName" select="@group_name"/> <xsl:if test="/document/roster/top_level/regiment[@composition = $groupName]"> <xsl:variable name="unit" select="@group_name"/> <xsl:for-each select="/document/roster/top_level/regiment[@composition = $unit]"> <xsl:apply-templates select="." mode="top_level"> <xsl:with-param name="regDepth"> <xsl:choose> <xsl:when test="position()=1"><xsl:value-of select="count(/document/roster/top_level[regiment/@composition = $unit]//regiment)"/></xsl:when> <xsl:otherwise>0</xsl:otherwise> </xsl:choose> </xsl:with-param> </xsl:apply-templates> </xsl:for-each> </xsl:if> </xsl:for-each> </xsl:template> <xsl:template match="regiment" mode="top_level"> <xsl:param name="regDepth"> <xsl:value-of select="count(..//regiment)"/> </xsl:param> <xsl:variable name="statCountLocal" select="count(stat)"/> <xsl:variable name="fsib" select="preceding-sibling::node()"/> <xsl:variable name="composition" select="@composition"/> <xsl:if test="$regDepth > 0"> <xsl:choose> <xsl:when test="not($composition = $fsib/@composition)"> <xsl:value-of select="$composition"/>(<xsl:value-of select="/document/composition/comp_entry[@group_name = $composition]/@percentage"/>)<xsl:value-of select="$newlinefeed"/> </xsl:when> </xsl:choose> </xsl:if> <xsl:value-of select="concat('[',@model_count,'] ')"/> <xsl:variable name="itemcost"> <xsl:if test="@cost"><xsl:value-of select="@cost"/></xsl:if> </xsl:variable> <xsl:variable name="retinuecost"> <xsl:call-template name="getRetinueCost"> <xsl:with-param name="retinuecostSum" select="0"/> <xsl:with-param name="current" select="regiment[position()=1]"/> <xsl:with-param name="rest" select="regiment[position()!=1]"/> </xsl:call-template> </xsl:variable> <xsl:variable name="transportcost"> <xsl:if test="regiment[@stat_set = 0]/@cost"><xsl:value-of select="substring-after(substring-before(regiment[@stat_set = 0]/@cost,']'),'[')"/></xsl:if> </xsl:variable> <xsl:choose> <xsl:when test="$retinuecost and @model_count=1 and @composition='HQ'"> <xsl:value-of select="concat('[',$itemcost - $retinuecost,'] ')"/> </xsl:when> <xsl:when test="$transportcost != ''"> <xsl:value-of select="concat('[',$itemcost - $transportcost,'] ')"/> </xsl:when> <xsl:otherwise> <xsl:value-of select="concat('[',$itemcost,'] ')"/> </xsl:otherwise> </xsl:choose> <xsl:value-of select="substring-before(name,' (')"/> <xsl:if test="@model_count=1 and @composition='HQ'"> <xsl:text disable-output-escaping="yes">(IC)</xsl:text> </xsl:if> <xsl:text disable-output-escaping="yes">: </xsl:text> <xsl:for-each select="item"> <xsl:variable name="namedItem" select="name"/> <xsl:choose> <xsl:when test="count(../item[name=$namedItem]) > 1"><xsl:value-of select="concat($namedItem,'(x',count(../item/name[.=$namedItem]),');')"/></xsl:when> <xsl:otherwise><xsl:value-of select="concat(name,';')"/></xsl:otherwise> </xsl:choose> </xsl:for-each> <xsl:for-each select="choice"><xsl:value-of select="concat(name,';')"/></xsl:for-each> <xsl:value-of select="$newlinefeed"/> <xsl:for-each select=".//regiment[not(../@category = 'Wargear Item')] "> <xsl:apply-templates select="." mode="regiment" /> </xsl:for-each> <xsl:value-of select="$newlinefeed"/> </xsl:template> <xsl:template match="regiment" mode="regiment"> <xsl:variable name="statCountLocal" select="count(stat)"/> <xsl:variable name="depth"> <xsl:choose> <xsl:when test="../../@stat_count=1"><xsl:value-of select="number(@depth)-1" /></xsl:when> <xsl:otherwise><xsl:value-of select="@depth" /></xsl:otherwise> </xsl:choose> </xsl:variable> <xsl:value-of select="concat('[',@model_count,'] ')"/> <xsl:variable name="itemcost"> <xsl:if test="@cost"><xsl:value-of select="substring-after(substring-before(@cost,']'),'[')"/></xsl:if> </xsl:variable> <xsl:variable name="transportcost"> <xsl:if test="regiment[@stat_set = 0]/@cost"><xsl:value-of select="substring-after(substring-before(regiment[@stat_set = 0]/@cost,']'),'[')"/></xsl:if> </xsl:variable> <xsl:choose> <xsl:when test="((../@composition='HQ' and @depth = 1) or (@depth = 0)) and (regiment[@stat_set = 0])"> <xsl:value-of select="concat('[',$itemcost - $transportcost,'] ')"/> </xsl:when> <xsl:when test="@stat_set = 0"> <xsl:value-of select="concat('[',$itemcost,'] ')"/> </xsl:when> <xsl:when test="../@composition = 'HQ' and ../@stat_count > 1"> <xsl:value-of select="concat('[',$itemcost,'] ')"/> </xsl:when> </xsl:choose> <xsl:call-template name="formatName"><xsl:with-param name="strName" select="concat(name,': ')"/></xsl:call-template> <xsl:for-each select="item[not(name = preceding-sibling::item/name)]"> <xsl:variable name="namedItem" select="name"/> <xsl:choose> <xsl:when test="count(../item[name=$namedItem]) > 1"><xsl:value-of select="concat($namedItem,'(x',count(../item[name=$namedItem]),');')"/></xsl:when> <xsl:otherwise><xsl:value-of select="concat(name,';')"/></xsl:otherwise> </xsl:choose> </xsl:for-each> <xsl:for-each select="choice"><xsl:value-of select="concat(name,';')"/></xsl:for-each> <xsl:value-of select="$newlinefeed"/> </xsl:template> <xsl:template name="getRetinueCost"> <xsl:param name="retinuecostSum" /> <xsl:param name="current" /> <xsl:param name="rest" /> <xsl:variable name="curCost"> <xsl:choose> <xsl:when test="contains($current/@cost,'[')"><xsl:value-of select="substring-after(substring-before($current/@cost,']'),'[')" /></xsl:when> <xsl:otherwise><xsl:value-of select="$current" /></xsl:otherwise> </xsl:choose> </xsl:variable> <xsl:choose> <xsl:when test="$current"> <xsl:call-template name="getRetinueCost"> <xsl:with-param name="retinuecostSum" select="$retinuecostSum + $curCost"/> <xsl:with-param name="current" select="$rest[position()=1]"/> <xsl:with-param name="rest" select="$rest[position()!=1]"/> </xsl:call-template> </xsl:when> <xsl:otherwise> <xsl:value-of select="$retinuecostSum" /> </xsl:otherwise> </xsl:choose> </xsl:template> <xsl:template name="halfCost"> <xsl:param name="itemCost"/> <xsl:value-of select="round($itemCost div 2)" /> </xsl:template> <xsl:template name="formatName"> <xsl:param name="strName"/> <xsl:choose> <xsl:when test="contains($strName,' (')"><xsl:value-of select="substring-before($strName,' (')"/></xsl:when> <xsl:otherwise><xsl:value-of select="$strName"/></xsl:otherwise> </xsl:choose> </xsl:template> <xsl:template name="doReplaceCar"> <xsl:param name="text"/> <xsl:param name="replace"/> <xsl:param name="by"/> <xsl:choose> <xsl:when test="contains($text, $replace)"> <xsl:value-of select="substring-before($text, $replace)" disable-output-escaping="yes"/> <xsl:value-of select="$by" disable-output-escaping="yes"/> <xsl:call-template name="doReplaceCar"> <xsl:with-param name="text" select="substring-after($text, $replace)"/> <xsl:with-param name="replace" select="$replace"/> <xsl:with-param name="by" select="$by"/> </xsl:call-template> </xsl:when> <xsl:otherwise> <xsl:value-of select="$text" disable-output-escaping="yes"/> </xsl:otherwise> </xsl:choose> </xsl:template> </xsl:stylesheet> |
No, I am not writing a stylesheet like that again, and the one to parse the roster files would be far more complicated.
The problem with the roster file, fundamentally, is that it’s too tightly linked with ArmyBuilder. That makes sense, in a way, but is still irksome. The <link> elements don’t have any nodes under them, just assloads of attributes, and it’s not easy to figure out which ones I am interested in:
<link id="HeavyArmor" count="1" actual="1" script="0" sequence="26" pseudo="no" totalcost="0" name="Heavy Armor" category="Equip" \ abbrev="Hv" description="5+ Armor Save" equipment="yes" footnote="yes" sourceid="dwWarrVet" sourceindex="5"></link>
Versus ones I’m not interested in:
<link id="ItemCost" count="1" actual="1" script="0" sequence="28" pseudo="no" totalcost="0" name="Item Cost Worker" category="Equip"\ visible="no" sourcetype="3" sourceid="Globals" sourceindex="1"></link>
Without passing a long hashlist of element.attribute[$thing] values, or specifically excluding anything with “Helper” or “Worker” or whatever in the name, etc. Not to mention it’s formatted as:
<document> <squad> <!-- unit name and cost is here --> <entity> <!-- unit stats are here, along with composition and whatnot --> <link> <!-- sometimes there's nothing of note in the link tags --> <entity> <!-- this might be a magic item, warmachine crew, magic banner, champion, and probably other stuff, but is not easily \ identified, and there may be more than one --> <link> <!-- might be info for whatever is in entity, might be a helper which I don't want --> </link> <unitstat> <!-- if it's crew, champion, whatever, stats would be here, but this node may not exist --> </unitstat> </entity> </link> </entity> </squad> </document>
The problem with some of these is that by the mantra of whoever wrote ArmyBuilder, champions fall into the “Equip” category. There is, in fact, a “isunit” attribute, but it isn’t set to yes anywhere. Only set to “no” for items, which I can’t figure out (unless there’s some kind of magic item which qualifies as a unit you can add? I don’t know).
I’ve got a parser that works in Ruby written, but I haven’t converted it to C# yet. Also, I’ve not tested it against anything that might have more complicated schema than dwarves: mounted units, chariots, to check if it’s undead/daemon/greenskin and see if special rules apply (since not everything in the army is guaranteed to be), embedded assassins, magic, et al. Sadly, the only roster I’ve got at work is for dwarves, so I’ll have to dump some more output from ArmyBuilder and run the parser against it to see how it handles it.
Any other niche cases either of you can think of that may have specific rules? I’m going to try to stabilize the parser and get it to properly validate every army type, then move it to .NET
Also, thinking about it, I’m utterly convinced that snapping things to some kind of a grid is the only real feasible solution. Querying the object via System.Drawing or GDI might work, but I’m not sure how accurate the pixel mapping is. At any rate, for things like the Lance Formation, line of sight on skirmishers, determining base contact for champions/characters embedded, reforming the unit, and templates, a grid seems like the only way to go without doing occlusion detection (for the templates). Convert inches to millimeters, and make it 1mm x 1mm squares or something.
7 Comments
Other Links to this Post
RSS feed for comments on this post. TrackBack URI
Leave a comment
You must be logged in to post a comment.
By Missy, September 27, 2008 @ 8:21 pm
The sidebar is completely broken in Chrome, but looks fine in Firefox. Also, it’s only broken on the main page.. on the comment page it fixes itself. Also, the text runs off to the right side of the screen and I have to scroll to read it all.
By Missy, September 27, 2008 @ 8:22 pm
Oh yes and — it appears that your favicon got eaten? I swear I can recall you having one, but Dan thinks you never did. In any case, just thought I’d mention it.
Also, this post was really boring :p
By Ryan, September 27, 2008 @ 10:03 pm
I did, in fact, have a favicon. I’ll have to track it down (it’s somewhere on my workstation at work, I’d guess).
I’m not sure what’s happening with the sidebar in Chrome. It looks fine in Safari, and they’re both WebKit based browsers, so I’ve just kind of assumed that a Chrome update will fix the rendering later. It scrolls off the right side? What’s your resolution at? It’s designed for a width of 1280 at the moment, since I didn’t think I knew anybody who ran less than that.
As an aside, I played with ArmyBuilder today. The XML output is identical to the RST output. It also means that I’ve got working parsing rules for chariots, knights, banners, assassins, champions, and monsters with handlers. Also, it does, in fact, spit validation errors in the output (along with the normal validation stuff), though I probably won’t look for it.
Expect the next post to be boring also. Ruby vs. .NET XML parsing for the rosters.
By Missy, September 27, 2008 @ 11:39 pm
Mine is smaller, but it also runs off to the side on Dan’s and his is set so freaking teeny tiny that I can’t read his screen QQ
By Ryan, September 27, 2008 @ 11:43 pm
WTF. I looks fine on Safari here, IE here, Firefox here, Opera here (IE, Firefox, Opera at work, also) at 1280×800 (and 1280×1024). Is this only happening in Chrome?
A horizontal scrollbar shows up in Firefox, I guess, but there’s no content off to the right.
By Missy, September 28, 2008 @ 11:39 am
Wish I knew why.. the sidebar is *only* broken in Chrome on my computer, and only on the main page. Looks absolutely fine here, for example. And yeah, I have to scroll to the right (as does Dan) in both Chrome and Firefox (and yar, not just a scrollbar like you have, I have to do it to read all of the text).
By Missy, September 29, 2008 @ 3:56 pm
Sidebar is fixed. Text still runs off to the right.