Advanced Optimisations and Hacks

From The Battle for Wesnoth Wiki
Revision as of 18:48, 29 October 2013 by Dugi (talk | contribs)

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: It is supposed 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. It is also assuming that you've read Dunno's article about Wml_optimisation, and do these things before.


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.

#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]
#GLOBAL_EVENTS

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!).

[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]


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.

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]