Lua scripts

In this section we'll talk about components, component renderers, controllers and observers. Also about saved variables and the lifetime cycle of these scripts.

Remember that in radiant/modules/entities.lua there are many utility functions that you can use in your scripts.

Lifecycle functions

Both components and controllers are classes and share the same similar properties and functions.

The special functions that can be implemented on each are:

  • __init() -- This is the default Lua constructor for a class. This will be called first ALWAYS. There will be no special variables or anything available during this constructor. It's advised to just not include it and instead put initialization information in the special initialize() function.

  • initialize() -- This function is always called after __init. In Lua components, the special self._entity field will be set and you can get the component's JSON data by calling radiant.entities.get_json(self). You CANNOT access services or public fields (like stonehearth.constants) in initialize() because during restore, those might not exist yet.

    icon Important: you must declare all saved variable fields in the initialize() function, including nil variables. You can ask the entity if it has other components in the initialize, but note that those other components might not have had initialize() called on them yet.

  • create() -- This is called ONCE when the component is first created, either on entity creation or on dynamically added. For controllers, the create function can take arguments passed in from the controller creator.

    For controllers, create() is called immediately after initialize(). For components specified via JSON, the create() is called after all other components on the entity have been initialized.

  • restore() -- This is called when the component/controller is loaded from a save game. It is called after initialize() and fixup_post_load().

  • activate() -- This is called every time. It is called after create() / restore(). For controllers on creation, this is called immediately after create(). For components on creation, this is called after all components have had create() called on them. For everything on load, this is called after all components/controllers have had restore() called on them.

  • post_activate() -- This is called after all components/controllers have had activate() called on them. On an entity that has just been created, it will have all components specified in the entity's JSON get activated before post_activate() is called on it. On a restore, EVERY component/controller in the world will get activate() called on them first before every component/controller in the world gets post_activate() called on them.

  • destroy() -- This is called when the entity gets destroyed. Here we usually add code to destroy listeners and any other variable that needs to be cleaned up safely.

To sum up, the call order for these functions goes:

  __init() -> initialize() -> {create(optional_controller_variables) OR restore()} -> activate() -> post_activate()

Examples

Example of the lifecycle functions in a component:

  local MyComponent = class()

  function MyComponent:initialize()
     local json = radiant.entities.get_json(self)
     self._sv.first_saved_variable = nil
     self._sv.second_saved_variable = 'test string'
     self._sv._unremoted_save_variable = json.my_json_data_variable
     self._sv._created_linked_entity = nil

     -- We don't know if this is a load or a new creation yet,
     -- so self._sv will have the values set in initialize.
     -- If this is a load, the self._sv variables will have
     -- their loaded values by the restore function
  end

  function MyComponent:create()
     self._sv._created_linked_entity = radiant.entities.create_entity('my_mod:sub_entity')
  end

  function MyComponent:restore()
     -- If this is loaded from a save game, variables in self._sv
     -- will now have their saved values
  end

  function MyComponent:activate()
     self._log = radiant.log.create_logger(self._sv.second_saved_variable)
  end

  function MyComponent:post_activate()
     -- Can access other components here and tell them to do stuff
     local component2 = self._entity:get_component('stonehearth:my_component_2)
     component2:some_setter_function(self._sv._unremoted_save_variable)
  end

  return MyComponent

Controllers and services can also make use of these functions.

About saved variables

saved_variables (actually the field __saved_variables) is an object that is present on every controller, component and service in Stonehearth's Lua code.

It is actually a C++ datastore, and serves two purposes: to be the place where persistent data can be stored/loaded, and as the means by which data is transferred from the server to the client.

Generally speaking, the only time you should ever need to access it directly is when you've changed some piece of data on the server, and now you need the client (or UI) to know you've made a change. In this case, you call self.__saved_variables.mark_changed(), and the changed data will be serialized over the wire to the client, and potentially trigger any event handlers you have installed there.

Fields of a datastore are 'diffed' before transferring to the client, so if you only change one field, you only need to pay for the transfer of that field.

In order to add a field that you want to save to a controller, component or service simply add it to the ._sv field in your initialize Lua routine. As an illustrative example, consider the AI component:

  function AIComponent:initialize()
     self._log = radiant.log.create_logger('ai.component')
                                :set_entity(self._entity)

     self._sv.spin_count = 0
     self._sv.status_text_key = nil
     self._sv.status_text_data = nil
     self._num_unyielded_spins = 0
  end

Here, we see a couple of fields added, some are to self, and the others are to ._sv. Those directly on self will NOT be saved, and will NOT be transferred to the client. They are effectively private to the server's component. The fields added to ._sv will be saved, as well as transferred to the client.

One more important feature is that if you name an ._sv field starting with an underscore (self._sv._foo), while that field will be saved, it will NOT be transferred to the client. This is extremely useful when you have important persistable data that might also be very prohibitive over a network.

Maintaining save compatibility

By default, _sv's have a version on them that specifies the version of the data store that was saved. The version value is 0 by default. If you need to update the controller to a different version because you renamed a variable, added a variable that needs to be set in the create(), or anything else where older versions of the controller need to be fixed up, you can implement the version function on the controller/component to tell the persistence system that the version of the code has changed.

First add a versions variable to the controller/component so you can document what was added each version. Example from the job component:

  local VERSIONS = {
     ZERO = 0,
     CLEAN_UP_SAVED_FIELDS = 1,
     JOB_CONTROLLER_JSON_PATHS = 2,
     LEVEL_0_REMOVED = 3
  }

Then implement the get_version function and have it point to the latest version:

  function JobComponent:get_version()
     return VERSIONS.LEVEL_0_REMOVED
  end

And finally implement the fixup_post_load function. This function will only be called on load if the saved version does not match the current version, after initialize and before restore, (the variables in _sv will already have been populated). Then you can modify anything in the saved data.

For example, in the CLEAN_UP_SAVED_FIELDS version we renamed the the _sv.equipment variable to _sv._job_equipment. Notice that we can retrieve the saved data from the old_save_data parameter if we need to:

  function JobComponent:fixup_post_load(old_save_data)
     if old_save_data.version < VERSIONS.CLEAN_UP_SAVED_FIELDS then
        self._sv._job_equipment = old_save_data.equipment
     end

     if old_save_data.version < VERSIONS.JOB_CONTROLLER_JSON_PATHS then
        self._fix_up_job_controller_json_paths = true
     end

     if old_save_data.version < VERSIONS.LEVEL_0_REMOVED then
        self._needs_level_0_update = true
     end
  end

In the later versions, we set up some flags so that we can use them in restore/activate/post_activate to patch the controller appropiately, so that players can keep playing their old savefiles with the new version of the mod.

About observers

An observer watches a variable on an entity and does stuff if that variable changes. It is sort of like a component, but it is passive instead of active. For example, see the sleep observer in stonehearth\ai\observers, which watches for sleepiness scores to decrease, and then triggers a bunch of actions.

If you want a controller to hold publicly accessible state, you usually will want a component. Observers are more useful when you have an item that needs to inject behavior onto an entity that can't be encapsulated by an action.

Generally, what you can do with an observer, you can do with a component. Components are preferred over observers because this is what we use across the codebase and their behavior is more predictable.

About validators

In radiant/modules/validator.lua we can find functions to validate parameters in our Lua code.

You'll see many places in the code where we get the validator at the top of the file:

  local validator = radiant.validator

And then right at the beginning of a function, call one of its functions to validate the parameters that were passed:

  function CatalogCallHandler:get_catalog_data(session, response, uri)
     validator.expect_argument_types({'string'}, uri)
     -- ...
  end

This is useful to prevent bad data on multiplayer.

There's also some validation functions for the UI, in stonehearth/ui/common/validator/validator.js:

  var townName = App.stonehearth.validator.enforceStringLength(self.$('#name'), 28);

About state machines

We can create state machines in our code too, using the radiant/controllers/state_machine.lua controller. The only example in the game that uses this is the incapacitation component, so check it to see how it's done.

Requiring files in Lua

If we need to require a file in a Lua script to use functions or constants from it, we'd do it at the top of the script, before beginning our code:

  local constants = require 'constants'

However, make sure to prefix this by mods.stonehearth when requiring stonehearth files from your mod:

  local constants = require 'mods.stonehearth.constants'

Otherwise the game won't know where to find that file. Normally the path is the directory names separated by dots.

For requiring C++ stuff we omit the require and the quotes:

  local Point2 = _radiant.csg.Point2

We can also create shortcuts for services or other:

  local calendar = stonehearth.calendar