How to Debug and Fix WML Errors in Battle for Wesnoth Add-ons

How to Debug and Fix WML Errors in Battle for Wesnoth Add-ons

# gamedev# opensource# debugging# tutorial
How to Debug and Fix WML Errors in Battle for Wesnoth Add-onsAlan West

A 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.

What Is WML and Why Does It Break So Quietly?

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]
Enter fullscreen mode Exit fullscreen mode

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.

Step 1: Enable the WML Log Output

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
Enter fullscreen mode Exit fullscreen mode

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.

Step 2: Use the Built-in Debug Console

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 position

The :inspect command alone saved me hours. Instead of guessing what value a variable holds at a certain point, you can just look.

Step 3: Validate Tag Nesting Before Anything Else

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Run this before loading your scenario and you'll catch 80% of structural bugs instantly.

Step 4: Watch Out for Macro Expansion Issues

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}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Step 5: Test Events in Isolation

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]
Enter fullscreen mode Exit fullscreen mode

This is the WML equivalent of writing a unit test. Isolate the behavior, verify it works, then integrate it back.

Prevention: Habits That Save Hours

  • Write closing tags immediately. Type [event] and [/event] as a pair, then fill in the middle. Every time.
  • Use consistent indentation. WML doesn't care about whitespace, but you will when you're hunting for a missing tag at midnight.
  • Run the tag checker script before every playtest. Make it part of your workflow, not an afterthought.
  • Keep macros small and focused. A macro that generates 50 lines of WML is a macro that will eventually hide a bug from you.
  • Use the --preprocess flag liberally. If something works without macros but breaks with them, the expanded output will show you why.
  • Version control your add-on. Git makes it trivial to bisect when something breaks — git bisect will find the exact commit that introduced a WML regression.

The Bigger Lesson

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.