Freeciv supports Event Scripting in rulesets and scenarios since version 2.1, using the Lua language. The tutorial is implemented as a scripted scenario.

Hardcoded game events which are candidates for scripting[]

  • Entering village huts. (implemented)
  • Partisan generation on conquering city. (implemented)
  • Darwin free techs on build, and Philosophy free tech to first player who discovers it.
  • Marco Polo gives embassies.
  • Building a spaceship "building" gives no building but rather a spaceship part.
  • The following text is transcluded from an older Events Tutorial:

So you want to add some events, but you have no idea how? Well you've come to the right place, because so do I. This tutorial documents my journey into the world of events (it may later be refined to be a more straightforward tutorial).

Please read the Lua reference manual for a reference to all available events, actions and datatypes with their methods that are available in the Event Scripting API. The following examples work with Freeciv 2.1 (Example 1) and Freeciv 2.2 (All examples)

The basic premise[]

An event includes a trigger (signal) and an action. In the script code you will write, you create a callback function using the Lua scripting language. This callback function provides the actions and is linked up to a trigger that is invoked from the freeciv code itself.

An example is signal.connect('unit_moved', 'unit_moved_callback') which registers the script to listen for unit_moved events. unit_moved_callback should be a function in the Lua script.

Getting started[]

You may add events either to a ruleset or to a scenario. Changing the ruleset will affect anyone who plays with that ruleset; if you add an event to the default ruleset it will affect almost everyone. Adding an event to the scenario means only someone who plays with your scenario will get this event. As an example, the civ2 ruleset is a ruleset, while a Europe or World-War-Two scenario would probably be scenarios.

To add events to the ruleset you must edit the script.lua file in the ruleset directory. This is pretty straightforward.

To add events to the scenario you must add some script code directly inside the .sav file itself. This means you add a section


to the .sav file. The code you will write goes in place of the ... bits between the $ signs.

If we insert the shortest possible event handler in such a code block, it looks like this:

-- This callback reacts when a unit is moved
function unit_moved_callback(unit, src_tile, dst_tile)
  notify.all('A/An %s moved!', unit.utype:name_translation())

signal.connect('unit_moved', 'unit_moved_callback')

To see how this works in practice, look in the tutorial at …/data/scenarios/tutorial.sav.

One important thing to note is the use of "." (as in "unit.utype" above) vs ":" (as in "utype:name_translation()" above). The "." is required before a variable name, and the ":" is required before a function name. Otherwise, the code will fail to load, and the position of the syntax error is not reported.

Example 1: Scenario narrative[]

An example that could be used in the tutorial: Tell the user that a found spot is good for a city. We make a trigger for the unit_moved event and look if a Setter moves to a Plains or Grassland tile:

-- This line is a comment since it is marked with two dashes
function has_unit_type_name(unit, utype_name)
  return ( == find.unit_type(utype_name).id)
function has_tile_terrain_name(tile, terrain_name)
  return ( == find.terrain(terrain_name).id)

-- This callback reacts when a unit is moved
function unit_moved_callback(unit, src_tile, dst_tile)
  if unit.owner:is_human() then
    if has_unit_type_name(unit, 'Settlers')
       and (has_tile_terrain_name(dst_tile, 'Grassland')
       or has_tile_terrain_name(dst_tile, 'Plains')) then
       notify.event(unit.owner, nil, E.TUTORIAL or E.SCRIPT, [[
This looks like a good place to build a city.  The next time this
unit gets a chance to move, press (b) to found a city.

In general you want to build cities on open ground near water.  Food
is the most important resource for any city.  Grassland and plains
provide plenty of food.]])
signal.connect('unit_moved', 'unit_moved_callback')

In this example, the special [[ … ]] syntax denotes a multiline string. This is so we can show a long message to the user. A short string may also be written with quotes in only one line: notify.player(unit.owner, "This is a good spot for a city!")

notify.event takes four parameters here: Player to receive the message, Tile where the event happened, message category and the message itself. As you can see, we can say nil (which means "empty value") to say that there is no specific tile to this event. If you want though, you extend this example by using unit.tile as the event location.

Example 2: Creating Units[]

In this example, we change the way we create mercenaries from huts. Instead of defining a new event handler, we override an event handler in the default ruleset (Freeciv 2.2).

In our version, we always create Archers as mercenaries, and we tell the user "Archers join your side!".

-- Define a function by the same name as in the
-- default rulset. Our Scenario version supercedes
-- the ruleset version.
function default_hut_get_mercenaries(unit)
  local owner = unit.owner
  local utype = find.unit_type("Archers")

  if utype then
    notify.event(owner, unit.tile, E.HUT_MERC,
                 "%s join your side!", utype:name_translation())
    -- create_unit takes arguments:
    -- Player, Tile, Unit Type, Veteran level, Homecity, Moves Left
    create_unit(owner, unit.tile, utype, 0, unit:get_homecity(), -1)
    return true
    return false

-- In early versions, the function used this name instead
hut_get_mercenaries = default_hut_get_mercenaries

-- NOTE: We don't use signal.connect when overriding

Example 3: Scenario Objective[]

We may use events to give scenario victory to a player according to any custom objectives. Let's create a mini-scenario with two opposing sides with each side with its own capital (here, Berlin and Paris). The scenario shall be won whenever one side conquers the capital of the other.

-- I used the Map editor to look up the identifiers of these cities,
-- they stay constant through the game even if the cities change name
berlin_city_id = 105
paris_city_id = 106

-- React to the city_lost event
function city_was_lost(city, loser, winner)
  local napoleon = find.player(0)
  local friedrich = find.player(1)

  if == paris_city_id then
    notify.all("%s is lost! %s wins the war.",,
  elseif == berlin_city_id then
    notify.all("%s is lost! %s wins the war.",,

function city_was_destroyed(city, loser, destroyer)
  -- in this case, we can handle it the same way
  city_was_lost(city, loser, destroyer)

signal.connect('city_lost', 'city_was_lost')
signal.connect('city_destroyed', 'city_was_destroyed')

See also[]

Game Anatomy & Modding
Event ScriptingEditing RulesetsEditing TilesetsMore RulesetsMore Tilesets
Event Scripting
Lua Reference ManualTutorial Scenario
Update from 2.5 to 2.62.6 to 3.03.0 to 3.13.1 to 3.2