WML is the primary source code language for all add-on and mainline content for Wesnoth. Creators like me write large amounts of WML to instill life into their user-made projects. Our wiki has a rich reference section for the various dialects of WML understood by the game in different contexts. Tools like wmllint exist to make sanity-checking and porting tasks easier for the few people who can tolerate its obtuseness us. There is a whole forum section in Wesnoth.org dedicated to assisting people with their WML endeavors.

With so many resources available, you’d think WML is the most fun thing to work with, right?

1. The problem

One of the most annoying aspects of add-on development is that whenever something goes horribly wrong with the WML document’s well-formedness, you may get a barrage of file paths thrown at you in a superdense text-wall format, particularly so if the error site has been subject to file or macro substitutions, which is the case with pretty much every add-on that has more files than its top-level _main.cfg. Trying to find the relevant portions of the substitution trace to debug the issue can be frustrating at best.

The report is displayed on the screen by calling a generic GUI function that creates a new instance of a generic message dialog with the specified contents, which are also printed in stderr. Since the only formatting option available for the dialog is to rearrange the text with a combination of whitespace and newlines, the same thing winds up in stderr in the same format:

20140216 10:50:12 error config: error reading usermade add-on '/home/shadowm/.wesnoth-1.11/data/add-ons/After_the_Storm/_main.cfg'
20140216 10:50:12 error general: The following add-on had errors and could not be loaded:
/home/shadowm/.wesnoth-1.11/data/add-ons/After_the_Storm/_main.cfg
ERROR DETAILS:
Found invalid closing tag [/unit] for tag [side] (opened at ~add-ons/After_the_Storm/episode3/scenarios/13_Epilogue.cfg:98 included from ~add-ons/After_the_Storm/base-loader.cfg:20 included from ~add-ons/After_the_Storm/_main.cfg:174 included from ~add-ons/After_the_Storm/base-loader.cfg:108 included from ~add-ons/After_the_Storm/_main.cfg:174 included from ~add-ons/After_the_Storm/episode3/scenarios/13_Epilogue.cfg:101 included from ~add-ons/After_the_Storm/base-loader.cfg:20 included from ~add-ons/After_the_Storm/_main.cfg:174 included from ~add-ons/After_the_Storm/base-loader.cfg:108 included from ~add-ons/After_the_Storm/_main.cfg:174), value ']' at ~add-ons/After_the_Storm/episode3/scenarios/13_Epilogue.cfg:94 included from ~add-ons/After_the_Storm/base-loader.cfg:20 included from ~add-ons/After_the_Storm/_main.cfg:174 included from ~add-ons/After_the_Storm/base-loader.cfg:108 included from ~add-ons/After_the_Storm/_main.cfg:174 included from ~add-ons/After_the_Storm/episode3/scenarios/13_Epilogue.cfg:112 included from ~add-ons/After_the_Storm/base-loader.cfg:20 included from ~add-ons/After_the_Storm/_main.cfg:174 included from ~add-ons/After_the_Storm/base-loader.cfg:108 included from ~add-ons/After_the_Storm/_main.cfg:174

This issue affects mainline and UMC developers alike, and yet, it has gone unsolved for a whole decade of development, even while other usability issues have been and continue to be addressed. Perhaps the very fact that the problem has existed for so long has silently perpetuated a misguided notion of impossibility — because, surely, if fixing the relevant code was that easy, somebody would have already done it? Or maybe, just maybe, everyone is afraid of altering code that lies deep within the core of Wesnoth’s WML processing pipeline; because it is an indisputable truth that the WML preprocessor and parser aren’t the most accessible modules for inexperienced contributors, and changing functionality that constitutes the circulatory system of the game engine is obviously a risky thing to do.

But wait! Wouldn’t it be possible to just sort of... make the error report presentation nicer without meddling too much with the implementation details?

2. The solution

While the basic proposal sounds simple and the results of my two-day-long coding effort made it into Wesnoth 1.11.10 (a.k.a. 1.12 beta 1), it turns out that there are many more factors to take into account, and in the end I did have to mess with parser and preprocessor implementation details to a greater degree than I feel comfortable with.

Against what one would expect, there is only one exception object class in existence for parser errors, and it is extremely generic. Callers have no way to tell what just happened when WML parsing has failed, other than asking the exception object for an error message... which is both stored and delivered in plain string form. The error type, the error site, and the WML preprocessor substitutions that led to it; these are all provided to the caller in an atomic string that may contain portions translated to the user’s language beforehand. This means that we (the parser’s callers) can’t reliably scan the error message to choose the format in which we are going to present the error to the user. All we can do is grab that big-ass message string and throw it into a dialog.

The dialog used to display the report in versions up to and including 1.11.9 is part of the GUI2 framework, but the particular dialog class in use is a plain generic message box. Naturally, it is not optimized for presenting WML error messages to the user — or displaying overlong strings in general, really. It is for this reason that the first part of this little project of mine consisted of designing a new dialog class (gui2::twml_error) for presenting this information to the player in a more useful fashion.

3. The dialog

Because the Wesnoth config manager will try to load all add-ons at once, the first thing that the dialog needs to do is to display a list of add-ons that caused a parser or preprocessor error when attempting to load them. The 1.11.9 approach is to hastily concatenate a newline-delimited list of paths to the files Wesnoth attempted to load, which are nearly always the _main.cfg files. This can be quite misleading in most cases, and newbies might try to look for the faulty code in the listed _main.cfg since it is the very first thing displayed in the error message.

gui2::twml_error instead displays the user-friendly name (1) of the add-on that was stored in a special file (_info.cfg) when it was first downloaded from the server — failing that, it makes its best guess by replacing underscores in the directory name with blanks. If multiple add-ons failed to load, they are shown in a nice bullet list.

Where there used to be more plain dialog text, there is now a pretty scrollable box with the report contents (2). The report itself contains the list of errors found when loading the affected add-ons, but it is no longer all thrown at your face at once — the error log for each add-on is followed by an additional empty line, so telling them apart should be far easier than before. Finally, right above the report there is a button (3) to copy it to clipboard so players don’t need to take a screenshot of the whole thing to give to the add-on maintainer(s).

To be perfectly honest, the only reason that copy-to-clipboard button even exists is that I find error message screenshots irritatingly cumbersome to work with at both ends. It doesn’t help that I’m usually on slow Internet connections where downloading a screenshot of a Wesnoth instance surrounded by some fancy window decoration and taskbar and (occasionally) desktop wallpaper can be excruciatingly painful. Furthermore, as a forum administrator who has to keep an eye on how people use attachments, this preposterous waste of disk space can be quite annoying.

⁂

But replacing the error report dialog and the presentation of reports as a whole is not enough to improve the WML coder’s experience. gui2::twml_error would still display the text wall-like errors if it were asked to do so by the config manager. Even though I hinted at the essentials above, the muddy implementation details screened by gui2::twml_error would still fill up a whole blog post on their own. Thus, I am going to leave that story for another time.