Advanced Optimisations and Hacks

From The Battle for Wesnoth Wiki
Revision as of 21:07, 19 December 2013 by Dugi (talk | contribs) (Hacking events into other campaigns)

From time to time, especially if working with some advanced WML, you will come to need things that WML does not support. And you might be in a bad need to do that. This isn't a tricky of using WML, it is actually doing what the developers didn't plan to support. I have mostly used it to write much more effective codes at the cost of low understandability.

Note: This article was written supposing that you are skilled with WML. It is expected that you know most WML tags, comprehend the contents of save files and have good understanding of variables and variable arrays in WML (not all of them are required by each of these). It is also assuming that you've read Dunno's article about Wml_optimisation, and did these things before trying these optimisations.


Many common events in scenarios

This trick is useful if your (multiple) scenarios contain a lot of common WML. This happens if your campaign adds some additional functionality, like inventories. It might be also useful if you are using repetitive macros that fire the same events. Or when you need to add event-based weapon specials with [object]s and therefore need to insert these events into all scenarios and not into the unit via macros using unbalanced WML (as it is usually done). It might be even useful if you have a lot of death message events.

The problem that comes from using the same WML events in many scenario is that they are preprocessed and loaded into RAM once for each scenario, and it can be dozens of times, resulting in huge RAM consumption and annoying loading times. This tricks inserts in through a unit so that it is loaded only once. Its downside is that prestart events can't be loaded this way, but start events can set up pretty much anything, they are fired before you see the scenario.

First, create a dummy unit like this:

[unit_type]
    id=Event Loader   #It can be different, but it is assumed in the following code that it will be 'Event Loader',
                      #if you name it differently, you will have to rename it in the rest of the code!
    alignment=neutral
    advances_to=null
    cost=1
    hide_help=true
    do_not_list=yes
    {GLOBAL_EVENTS_LIST}
[/unit_type]

The {GLOBAL_EVENTS_LIST} macro contains all the events you want to insert into all scenarios. This thick is only useful if the list is very long. Example (too short to be worth using this hack, but it is a working example):

#define GLOBAL_EVENTS_LIST
[event]
    # This events gives traits to all ally/enemy leaders (single player is expected)
    name=start
    [store_unit]
          canrecruit=yes
          [not]
                side=1
          [/not]
          variable=leaders
          kill=yes
    [/store_unit]
    {FOREACH leaders i}
          [unit]
                id=$leaders[$i].id
                name=$leaders[$i].name
                gender=$leaders[$i].gender
                x=$leaders[$i].x
                y=$leaders[$i].y
                side=$leaders[$i].side
                random_traits=yes
                canrecruit=yes
           [/unit]
     {NEXT i}
     {CLEAR_VARIABLE leaders}
[/event]
[event]
      # This makes petrification last only for a single turn
      name=turn refresh
      first_time_only=no
      [modify_unit]
             [filter]
                    side=$side_number
                    [filter_wml]
                            [status]
                                    petrified=yes
                            [/status]
                    [/filter_wml]
             [/filter]
             [status]
                     petrified=no
             [/status]
      [/modify_unit]
[/event]
#enddef

Now, we need to add the events in the unit into the scenario. It is trivial. Keep on mind that it means that only start events can be loaded this way, prestart events will be loaded, but will never be fired. It is useful to wrap it in a macro so that only one line makes it load. Because it disappears in prestart, it does not clear shroud for the player.

#define GLOBAL_EVENTS
[event]
     name=prestart
     [unit]
          type=Event Loader
          side=1
          x,y=1,1
     [/unit]
     [kill]
          type=Event Loader
          animate=no
     [/kill]
[/event]
#enddef

This should be placed in all events. If you need to control your events with some variables it will check, it might be useful to use them as arguments for this macro and set them in the event.

There is also a way to do it without touching the scenarios at all, but its use in multiplayer is severely limited. It is based on lua, which is always executed when the scenario is loaded (even if it is in the middle from a save file, because lua environment isn't saved), and can be later accessed only by defining new WML tags or event hooks. Placing this into your _main (inside the campaign's #ifdef wraps, to avoid hacking other campaigns!).

#ifdef CAMPAIGN_YOUR_AWESOME_CAMPAIGN
[lua]
   code = <<
     local helper = wesnoth.require "lua/helper.lua"
     -- We need to check whether the events were loaded in this scenario
     local events_loaded = wesnoth.get_variable( "events_loaded" )
     if events_loaded ~= true then
            wesnoth.set_variable("events_loaded", true)
            -- We don't want this code to believe that the events were loaded because
            -- the variable recording that they are saved remains from previous scenario
            wesnoth.wml_actions.event{ 
                    name="victory" ,
                    { "clear_variable", {
                            name="events_loaded"
                    } }
            }
            -- And now we do WML action itself
            wesnoth.wml_actions.unit{ id = "Event Loader" , side = 1, x = 1, y = 1}
            wesnoth.wml_actions.kill{ type = "Event Loader", animate = false}
     end
>>
[/lua]
#endif

Getting rid of AMLA clutter

This is almost required when a unit has too many AMLA options. Because of AMLA, the unit's can become extremely long, and the game will keep reading through it again and again and again and again, resulting in visible FPS drops when the unit is on the screen. If there are more units like this, it will get even more painful, because the save files will become excessively large, needing seconds to be created.

The trick is based on the fact that units' AMLA is irrelevant all the time, and is used only when it advances. Because of it, you can somehow remove all the [advancement] tags from the unit for most of the time. This is easier said than done, though. The number 1 problem is that if you classically store the unit, remove the [advancement] tags simply with {CLEAR_VARIABLE stored_unit.advancement} and unstore it, it will do nothing at all. When a unit is unstored, [advancement]s are read from the unit_type, and not from the actual variable! There are alternatives to [unstore_unit], simplest of them is [insert_tag] with name=unit (which does almost the same, but for example doesn't check if the unit has enough experience to advance). But here comes another bummer - many WML tags' lua implementations use unstore_unit (harm_unit, transform_unit and modify_unit), and even if we simply avoided using them, unstore_unit is too useful to avoid using it (and refactoring the whole code).

There is a solution, of course. You can create an additional unit_type. One unit_type that is played all the time and has only one dummy AMLA (because we want it to show the experience bar and advance to nothing, I will call it dummy_amla), usable really a lot of times, and then another unit_type, let's call it advancing_unit_type, that uses the first unit_type as base unit, but contains all the AMLA options we want. Then we create events to transform between the unit types when advancing. This has some odd consequences, but they can be exploited.

Unfortunately, when I discovered the secrets behind this and wrote about them, the developers decided that it wasn't the intended behaviour and changed it in wesnoth 1.11.1 (and it is changed in all later versions of the 1.11 branch). There are four more problems to face - first is that when a unit advances, it shows the advancement window before the advance event is fired, second is that all changes done to the unit in an advance event are discarded, third is that there is no way to make the player choose an advancement if it isn't his turn ([unstore_unit] chooses a random one even in singleplayer and I think this wasn't fixed yet) and the fourth one is that if the advancing is activated by [unstore_unit], it may call the advance and post advance events again (it somehow doesn't loop, fortunately).

I am not writing the exact code because it would be too long and chaotic, but writing a pseudocode describes the steps.

Note: recreate unit means using the [unit] tag with all relevant properties of the unit that is sort of unstored by this, like name, id, location, canrecruit, gender, experience, unrenamable etc, as well as modifications, variables and status via insert_tag - it is similar to unstore, but will recalculate its other properties like attacks, damage, maximum hitpoints, resistances, abilities and so on.

The advance event:

if unit.advancement.id=dummy_amla then
  # now we know if the unit has to be transformed or not
  if unit.side=$side_number
    # if it isn't the units' turn, we will deal with it later
    kill unit fire_event=no animate=no
    full_heal, clear_statuses
    set_variable amla_processing yes
    set_variables advancing_store unit
  else
    clear_statuses
    # edit the unit so that it is created with the new unit_type and advancements and thrown right into a variable
    set_variable unit.type advancing_$unit_type
    set_variable unit.to_variable advancing_store
    # recreate it with a new unit_type
    recreate_unit variable=unit to_variable=advancing_store
    unstore_unit advancing_store find_vacant=no
    store_unit x,y=$x1,$y1 variable=advancing_store
    recreate_unit variable=advancing_store to_variable=advancing_store
    # clean up some stuff
    set_variables advancing_store.modifications unit.modifications
    set_variable advancing_store.experience unit.experience
    set_variable advancing_store.achieved_amla yes
  end
#ifver WESNOTH_VERSION >= 1.11.1
  fire_event post advance on the unit
#endif
end

The post advance event basically just unstores the unit from the advancing_store variable. Expanding it with more things you might need should be done here (it may cause problems that might have different solutions before and after 1.11.1).

If implemented correctly, it does what is it supposed to do, but more problems come - we haven't solved advancing if it is not the unit's turn. It will have to be delayed until the start of the unit's turn. These units were marked by setting their variable named amla_processing to yes. At turn refresh, fire the following event on all that side's units that have that variable set to yes. Pseudocode again:

[event]
  name=respecialisation
  first_time_only=no
  # count how many times it has taken the dummy_amla
  set_variable times_advanced 0
  foreach unit.modifications.advance
    if unit.modifications.advance.id=dummy_amla
      variable_op times_advanced add 1
    end
  repeat this as many times as the number in times_advanced is
    variable_op unit.experience $unit.max_experience
    clear_variable unit.variables.amla_processing
    set_variable unit.type 
    set_variable unit.type advancing_$unit_type
    recreate_unit variable=unit to_variable=advancing_store
    unstore_unit advancing_store find_vacant=no
    store_unit x,y=$x1,$y1 variable=advancing_store
    recreate_unit variable=advancing_store to_variable=advancing_store
  end
  # you might want to fire the post advance event here
[/event]


Handling many non-disjunctive possibilities

Sometimes, especially when working with inventories, you will come to need this. You have 100 possible items (or they might be something else if not used to create an inventory). You want the game to choose one when they are dropping and you want to show exactly those the unit currently has in a message's options (to let the player manipulate them). Some manipulation would require a huge [switch] and another one would require countless [show_if] tags. This can be done very comfortably if you use a variable as a database.

The database variable is just created with something like this:

[set_variables]
    name=items
    [value]
         [object]
               name= _ "Leather Armour"
               required_strength=20
               image=items/armour-leather.png
               description= _ "This armour makes the user 20% more resistant to damage."
               [effect]
                   apply_to=resistance
                   [resistances]
                       replace=false
                       impact=-20
                       blade=-20
                       pierce=-20
                   [/resistances]
                [/effect]
         [/object]
         [object]
               name= _ "Chain Armour"
               required_strength=20
               image=items/armour-chain.png
               description= _ "This armour makes the user 30% more resistant to damage, but slows down slightly."
               [effect]
                   apply_to=resistance
                   [resistances]
                       replace=false
                       impact=-30
                       blade=-30
                       pierce=-30
                   [/resistances]
                [/effect]
                [effect]
                   apply_to=movement
                   add=-1
                [/effect]
         [/object]
         [object]
               name= _ "Steel Plate Armour"
               required_strength=20
               image=items/armour-plate.png
               description= _ "This armour makes the user 40% more resistant to damage, but slows down."
               [effect]
                   apply_to=resistance
                   [resistances]
                       replace=false
                       impact=-40
                       blade=-40
                       pierce=-40
                   [/resistances]
                [/effect]
                [effect]
                   apply_to=movement
                   add=-2
                [/effect]
         [/object]
         #... This would be useless if there were only 3 of them
    [/value]
[/set_variables]

To randomly drop one of them, just choose a random number between 0 and 2 like {SET_VARIABLE rand rand 0..2}, then draw the picture $items.object[$rand].image and then create a moveto event (nested event with delayed_variable_substitution=no) on that hex that if a player steps on it, a it sets a variable informing about the item type being picked and fires a pickup event on that unit. The pickup event is fireable more than once, and basically does this:

[insert_tag]
    name=object
    variable=items.object[$item_type_being_picked]
[/insert_tag]

When the inventory is viewed, we can avoid having to use [show_if] for all possible items, we just read what is inside the unit (or look it up in the items variable array). The message can be composed in a variable and then asked via insert_tag.

{VARIABLE message.message "Which item do you want to manipulate?"}
{FOREACH unit.modifications.object i}
  [set_variables]
      name=message.option[$message.option.length]
      [value]
            message="&$unit.modifications.object[$i].image $unit.modifications.object[$i].name|:
$unit.modifications.object[$i].description"
            [command]
                   [set_variables]
                           name=item_chosen
                           to_variable=unit.modifications.object[$i]
                   [/set_variables]
            [/command]
      [/value]
    [/set_variables]
 {NEXT i}
 [insert_tag]
     name=message
     variable=message
 [/insert_tag]

Then the properties of the object chosen will be in a variable named item_chosen.


Hacking events into other campaigns

This is the most dangerous trick, but it lets you make an add-on that mods the game - you can easily give inventories to mainline campaigns (without editing them at all), remove all randomness from campaigns, create a dungeonmaster kit for multiplayer survivals (without editing these survivals), change the general balance... possibilities are unlimited.

It is wholly based on the repetitive scenario events optimisation mentioned above in this article. It allows you to add events into scenarios, without actually editing the scenarios. If you don't protect it by any #ifdef, it will affect all scenarios! This is usually the best way to get kicked in the butt by other add-on writers, but in this case, you'll have to make it carefully and with the knowledge that it will affect all scenarios in all campaigns (or just some, you can wrap it in #ifndef MULTIPLAYER if you want it to be singleplayer-only).

Like before, we place these events into a unit_type, and create and kill at at the beginning of the scenario. If done by lua in _main, it will be inserted in any case.

[unit_type]
    id=Event Loader   #It can be different, but it is assumed in the following code that it will be 'Event Loader',
                      #if you name it differently, you will have to rename it in the rest of the code!
    alignment=neutral
    advances_to=null
    cost=1
    hide_help=true
    do_not_list=yes
    {GLOBAL_EVENTS_LIST}   # This is your magical collection of events. Should be mostly start, prerecruit and turn refresh events.
[/unit_type]

Then insert it using lua.

[lua]
   code = <<
     local helper = wesnoth.require "lua/helper.lua"
     -- We need to check whether the events were loaded in this scenario
     local events_loaded = wesnoth.get_variable( "events_loaded" )
     if events_loaded ~= true then
            wesnoth.set_variable("events_loaded", true)
            -- We don't want this code to believe that the events were loaded because
            -- the variable recording that they are saved remains from previous scenario
            wesnoth.wml_actions.event{ 
                    name="victory" ,
                    { "clear_variable", {
                            name="events_loaded"
                    } }
            }
            -- And now we do WML action itself
            wesnoth.wml_actions.unit{ type = "Event Loader" , side = 1, x = 1, y = 1}
            wesnoth.wml_actions.kill{ type = "Event Loader", animate = false}
     end
>>
[/lua]

An add-on named No Randomness mod uses this, although it is entirely written in lua, you might want to see it to know more possible combinations around customising the mod, but it will require you to know lua.