
Alan WestA practical guide to debugging WML errors in Battle for Wesnoth add-ons, from enabling log output to fixing silent failures in custom scenarios.
If you've ever tried writing a custom campaign or add-on for Battle for Wesnoth using WML (Wesnoth Markup Language), you've probably hit a wall where the game silently fails, your units don't spawn, or your event triggers fire at completely the wrong time.
I spent a weekend building a custom scenario for Wesnoth recently and burned hours on errors that, in hindsight, had obvious causes. Here's what I learned about debugging WML so you don't repeat my mistakes.
WML is Wesnoth's domain-specific markup language for defining campaigns, scenarios, units, and game events. It looks deceptively simple — tag-based, no compilation step, just plain text files. But that simplicity hides a lot of sharp edges.
The core problem: WML doesn't throw errors the way a compiled language does. When something goes wrong, you often get no error at all — just unexpected behavior in-game. A misspelled attribute? Silently ignored. A missing closing tag three levels deep? Good luck finding it.
# This looks fine but has a subtle bug
[event]
name=moveto
[filter]
side=1
x,y=10,10
[/filter]
[message]
speaker=narrator
message="You found the hidden cave!"
[/message]
[/event]
This event fires when any unit on side 1 moves to (10,10). But what if you only wanted your leader? Without an id or canrecruit=yes in the filter, every single unit triggers it. Not an error — just not what you wanted.
The first thing you need to do is actually see what Wesnoth is telling you. By default, log output is minimal. Launch the game with verbose logging for WML:
# Linux/macOS — launch with WML debug logging enabled
wesnoth --log-debug=wml
# You can also combine multiple log domains
wesnoth --log-debug=wml,engine,display
# Send output to a file so you can search through it
wesnoth --log-debug=wml 2>&1 | tee wesnoth_debug.log
This changes everything. Suddenly those silent failures become actual messages telling you which tags were ignored, which attributes didn't match, and where the parser got confused.
Wesnoth ships with an in-game debug mode that most people never discover. While running a scenario, you can enable it by typing :debug in the chat input (press enter/return to open chat first).
Once debug mode is active, you get access to several powerful commands:
:inspect — opens a dialog showing the current game state, all WML variables, and unit data:set_var variable_name=value — modify WML variables on the fly:unit attribute=value — modify the currently selected unit:create — spawn units at the cursor positionThe :inspect command alone saved me hours. Instead of guessing what value a variable holds at a certain point, you can just look.
The single most common WML bug is mismatched tags. WML uses [tag] and [/tag] pairs, and when you're nesting events inside conditional blocks inside scenarios, it's easy to lose track.
Here's a pattern that breaks silently:
[event]
name=start
[if]
[variable]
name=difficulty
equals=hard
[/variable]
[then]
[message]
speaker=narrator
message="Prepare for a challenge."
[/message]
[/then]
# Missing [/if] tag!
[/event]
The parser might not crash — it might just interpret everything after the [if] block differently than you intended. The fix is mechanical but important: always write both tags before filling in the content.
You can catch these with a quick script:
#!/bin/bash
# wml_check.sh — quick tag balance checker for WML files
# Usage: ./wml_check.sh path/to/your_scenario.cfg
file="$1"
if [ -z "$file" ]; then
echo "Usage: $0 <wml_file>"
exit 1
fi
# Count opening and closing tags
open_tags=$(grep -oP '\[(?!/)\w+\]' "$file" | sort | uniq -c | sort -rn)
closing_tags=$(grep -oP '\[/\w+\]' "$file" | sed 's/\[\//[/' | sort | uniq -c | sort -rn)
echo "=== Opening tags ==="
echo "$open_tags"
echo ""
echo "=== Closing tags ==="
echo "$closing_tags"
echo ""
# Find mismatches
echo "=== Checking for mismatches ==="
for tag in $(grep -oP '\[(?!/)\K\w+(?=\])' "$file" | sort -u); do
open_count=$(grep -c "\[$tag\]" "$file")
close_count=$(grep -c "\[/$tag\]" "$file")
if [ "$open_count" -ne "$close_count" ]; then
echo "MISMATCH: [$tag] opened $open_count times, closed $close_count times"
fi
done
Run this before loading your scenario and you'll catch 80% of structural bugs instantly.
WML supports macros via #define and curly-brace inclusion ({MACRO_NAME}). These are powerful but can introduce bugs that are invisible in the source file because the error is in the expanded output.
# Defining a reusable spawner macro
#define SPAWN_ENEMY TYPE X_POS Y_POS
[unit]
type={TYPE}
side=2
x={X_POS}
y={Y_POS}
ai_special=guardian
[/unit]
#enddef
# Using it — but watch the argument order
{SPAWN_ENEMY "Orcish Grunt" 15 7}
The gotcha: macro arguments are positional and space-separated. If your unit type name has a space and you forget the quotes, Orcish becomes TYPE, Grunt becomes X_POS, and everything goes sideways. The error message (if you get one) will reference the expanded code, not your macro call.
When debugging macro issues, use the --preprocess flag:
# Expand all macros and write the result to a directory
wesnoth --preprocess ~/wesnoth_userdata/data/add-ons/my_addon /tmp/preprocessed/
# Now you can inspect the fully-expanded WML
less /tmp/preprocessed/_main.cfg
This shows you exactly what the engine sees after all macros are expanded. It's the WML equivalent of running gcc -E to see preprocessor output.
Don't try to debug a full campaign. Create a minimal test scenario that only contains the event you're working on. Wesnoth's add-on structure makes this straightforward:
# test_scenario.cfg — minimal scenario for testing a single event
[scenario]
id=test_event
name="Event Test"
map_data="{test_map.map}"
turns=20
next_scenario=null
[side]
side=1
controller=human
type=Elvish Archer
id=test_leader
[/side]
# Paste ONLY the event you're debugging here
[event]
name=start
[message]
speaker=test_leader
message="Event fired successfully."
[/message]
[/event]
[/scenario]
This is the WML equivalent of writing a unit test. Isolate the behavior, verify it works, then integrate it back.
[event] and [/event] as a pair, then fill in the middle. Every time.--preprocess flag liberally. If something works without macros but breaks with them, the expanded output will show you why.git bisect will find the exact commit that introduced a WML regression.WML debugging is really a lesson in working with any DSL that lacks strong tooling. The techniques here — enabling verbose logging, isolating test cases, expanding macros, checking structural validity with scripts — apply to any domain-specific language.
Battle for Wesnoth's content creation ecosystem is one of the more approachable entry points into open-source game development. The WML documentation on the Wesnoth wiki is extensive, and the community on the Wesnoth forums is genuinely helpful when you get stuck.
Just remember to check your closing tags first. It's always the closing tags.