In this section we'll talk about what are call handlers and provide examples of usage. Existing call handlers can be found under stonehearth/call_handlers
.
- What are call handlers?
- Overview of the Lua client and server
- Example 1: Client renderer through user interaction
- Example 2: Zones
What are call handlers?
Most operations in the game are done through the hearthling AI or by manipulating values on an object, which results in a cascade of actions that result in changes to the game state. Sometimes, though, operations in the game belong to no single hearthling or object, and these are managed by services like farming or town.
A call handler helps the UI call a function on one of these high-level services, triggering a sequence of actions. For examples, see the farming or the unit control services.
Overview of the Lua client and server
Lua is run in 2 places -- on the Lua server, where the bulk of the game is processed and run, and in a smaller Lua client, which handles rendering entities to the screen in user-interactable ways.
For example, when the user selects an object to put in the world, the temporary item that is attached to the user's mouse is rendered in the Lua client, and the click to put it down in the world is caught by that client before the data itself (where the object will be put down, at what rotation, etc.) is sent to the server for processing. The ghost item that exists between when the user puts down the object and when a worker actually instantiates the object also exists only on the Lua client; as a result, the people in the world cannot see it and cannot interact with it.
In some ways, you can think of the Lua client renderer as the realm of all the things that the player can see and interact with to play the game, but that do not actually exist within the simulation of the world.
As a general rule, you can assume that the Lua client has all the same data as the server, but is read-only: new components cannot be created and data cannot be saved to existing data structures. Some things, like effects, cannot be created on the client at all.
Here are some examples of communication between client and server using call handlers:
Example 1: Client renderer through user interaction
Let's say you want to draw something in the lua client due to some user interaction with the UI (for example, the user has clicked that they want to place a new item into the world).
Start by adding a call handler with a "client" endpoint to the "functions" section of your mod's manifest (same level than "aliases" or "mixintos"):
"functions": {
"choose_place_item_location": {
"controller": "file(call_handlers/place_item_call_handler.lua)",
"endpoint": "client"
}
}
This means that if the user calls to the 'stonehearth:choose_place_item_location'
address from the UI, the choose_place_item_location
function from call_handlers/place_item_call_handler.lua
will be called. And this function will exist in the context of the Lua client, instead of the Lua server.
The UI call might look like this:
radiant.call('stonehearth:choose_place_item_location', data_function_is_expecting)
.done(function(o){
// Do something
});
And the function declaration itself inside place_item_call_handler.lua
might look like this:
function PlaceItemCallHandler:choose_place_item_location(session, response, item_uri)
self._cursor_entity = radiant.entities.create_entity(item_uri)
local re = _radiant.client.create_render_entity(RenderRootNode, self._cursor_entity)
-- ...calculate where the thing should appear...
self._cursor_entity:add_component('mob'):set_location_grid_aligned(pt)
end
This bit of code will create an entity in the renderer and put it in the world at the associated point. Later, the entity can be destroyed with:
_radiant.client.destroy_authoring_entity(self._cursor_entity:get_id())
For more information about capturing the mouse and mouse clicks, see move_unit_call_handler.lua
.
For client rendering on entities see component renderers.
Example 2: Zones
A zone is an area of the game that the player (or DM) has earmarked for a particular purpose. Zones may appear only in zone view, or they may also have an in-game appearance. For example: farms, stockpiles, trapping zones, etc.
Zones are created by the player from the UI. Usually, once they are created, their associated components fire off tasks that instruct any available people in a task group to tend to the zone (or whatever is in it).
Adding a zone to the UI
These are instructions on how would it have been to add the pasture zone via a mod.
The start menu is defined in stonehearth/data/ui/start_menu.json
. First we'll need to create a mixinto to add a new entry to the zones section (it's the most suitable place for consistency):
"mixintos" : {
"/stonehearth/data/ui/start_menu.json" : "file(data/ui/start_menu.json)"
}
Our mixinto would look like this:
{
"zone_menu": {
"items": {
"create_pasture": {
"name": "i18n(stonehearth:ui.game.menu.zone_menu.items.create_pasture.display_name)",
"description": "i18n(stonehearth:ui.game.menu.zone_menu.items.create_pasture.description)",
"required_job": "stonehearth:jobs:shepherd",
"required_job_text": "i18n(stonehearth:ui.game.menu.zone_menu.items.create_pasture.required_job_text)",
"class": "button",
"icon": "/stonehearth/ui/game/start_menu/images/zone_pasture.png",
"sticky": true,
"hotkey_action": "zone:pasture"
}
}
}
}
Notice that we can mixinto the start\_menu.json
file to add any new option to the start menu. There are some optional fields, like "required_job" and "required_job_text" which are only used in zones to disable them until the player has promoted someone to that job. For the crafting menu, they'll work right away, but for the rest of options it's tricky since it involves UI files.
The name of the entry (create_pasture
in the example above) will become the action that is called when that item is chosen (alternatively, some menus have a "menu_action" field with the action to be called). The handlers for these actions are in stonehearth/ui/game/start_menu/start_menu.js
:
menuActions: {
// ...
create_pasture : function () {
App.stonehearthClient.createPasture();
},
// ...
In this case, the create_pasture
function calls another function defined in stonehearth/ui/root/js/stonehearth/stonehearth_client.js
:
createPasture: function() {
var self = this;
App.setGameMode('zones');
var tip = self.showTip('stonehearth:ui.game.menu.zone_menu.items.create_pasture.tip_title',
'stonehearth:ui.game.menu.zone_menu.items.create_pasture.tip_description', { i18n: true });
// We call a client-side operation that allows the player to draw out the zone
return this._callTool('createPasture', function() {
// We call a client function to create the zone and return it
return radiant.call('stonehearth:choose_pasture_location')
.done(function(response) {
radiant.call('radiant:play_sound', {'track' : 'stonehearth:sounds:place_structure'} );
// The UI will then select it
radiant.call('stonehearth:select_entity', response.pasture);
// Call the tool again in case the player wants to keep drawing pastures
self.createPasture();
})
.fail(function(response) {
self.hideTip(tip);
});
});
},
Now you'll be wondering, if the functions are read from start_menu.js
, how can we add a custom function for our new menu to it, from our mod? See this page to solve your doubts.
Designating the zone in the client
The UI called a client side Lua function to draw the zone. This function is declared in the "functions" section of the manifest:
"functions": {
"choose_pasture_location": {
"controller" : "file(call_handlers/shepherd_call_handler.lua)",
"endpoint" : "client"
}
}
The call handler function looks like this:
function ShepherdCallHandler:choose_pasture_location(session, response)
stonehearth.selection:select_designation_region(stonehearth.constants.xz_region_reasons.PASTURE)
:set_min_size(10)
:set_max_size(50)
:require_unblocked(false)
:use_designation_marquee(Color4(56, 80, 0, 255))
:set_find_support_filter(stonehearth.selection.valid_terrain_blocks_only_xz_region_support_filter({
grass = true,
dirt = true
}))
:set_can_contain_entity_filter(function(entity)
-- avoid other designations.
if radiant.entities.get_entity_data(entity, 'stonehearth:designation') then
return false
end
if entity:get_component('terrain') then
return false
end
return true
end)
:set_cursor('stonehearth:cursors:zone_pasture')
:done(
function(selector, box)
local size = {
x = box.max.x - box.min.x,
z = box.max.z - box.min.z,
}
_radiant.call('stonehearth:create_pasture', box.min, size)
:done(
function(r)
response:resolve({ pasture = r.pasture })
end
)
:always(
function()
selector:destroy()
end
)
end
)
:fail(
function(selector)
selector:destroy()
response:reject('no region')
end
)
:go()
end
Note that it runs on the client (client endpoint), and will only have access to client-available Lua items.
The function allows the player to click in the world and designate a zone. It will draw the zone according to the Color4(r, g, b, a) you pass in.
Once the player releases the mouse, this function calls a server function to actually create the zone. Since we're communicating between client and server, it'll need to be defined in a call handler too.
Note that for these examples we're using the stonehearth namespace, you'd be using your mod namespace when writing this in your code. Make sure to change any references that should point to your mod, if you're making a custom type of zone.
Creating the zone
The function to create the zone lives on the server:
_radiant.call('stonehearth:create_pasture', box.min, size)
.
It is also declared under "functions" in the manifest, but has a server endpoint:
"create_pasture": {
"controller" : "file(call_handlers/shepherd_call_handler.lua)",
"endpoint" : "server"
},
And it looks like this:
function ShepherdCallHandler:create_pasture(session, response, location, size)
validator.expect_argument_types({'Point3', 'table'}, location, size)
validator.expect.num.range(size.x, 10, 50)
validator.expect.num.range(size.z, 10, 50)
local entity = stonehearth.shepherd:create_new_pasture(session, location, size)
return { pasture = entity }
end
Usually, the actual zone is created by an appropriate service (initialized in stonehearth/stonehearth_server.lua
). Since it's a zone, it will need region components for collision and size. In this case the function is in the shepherd service, and looks like this:
function ShepherdService:create_new_pasture(session, location, size)
-- A little sanitization: what we get from the client isn't exactly a Point3
location = Point3(location.x, location.y, location.z)
local entity = radiant.entities.create_entity('stonehearth:shepherd:shepherd_pasture', { owner = session.player_id })
self:_add_region_components(entity, size)
local pasture_component = entity:get_component('stonehearth:shepherd_pasture')
pasture_component:set_size(size.x, size.z)
radiant.terrain.place_entity_at_exact_location(entity, location)
return entity
end
(You can check how to create services here).
The entity itself (stonehearth:shepherd:shepherd_pasture
) is defined as an alias in the manifest.
Its JSON looks like this:
{
"type": "entity",
"components": {
"destination": {
"adjacency_flags": [
"center"
]
},
"stonehearth:shepherd_pasture": {
"check_for_strays_interval": "6h",
"default_type": "stonehearth:sheep",
"pasture_data": {
"stonehearth:sheep": {
"name": "i18n(stonehearth:ui.game.zones_mode.pasture.sheep_pasture_name)",
"description": "i18n(stonehearth:ui.game.zones_mode.pasture.sheep_pasture_description)",
"icon": "/stonehearth/entities/critters/sheep/sheep.png",
"min_population": 2,
"reproduction_uri": "stonehearth:sheep:young",
"max_num_per_10_square_unit": 0.48,
"base_reproduction_period": "36h",
"min_reproduction_period": "12h",
"feed_uri": "stonehearth:food:sheep_feed"
},
"stonehearth:rabbit": {
"name": "i18n(stonehearth:ui.game.zones_mode.pasture.rabbit_pasture_name)",
"description": "i18n(stonehearth:ui.game.zones_mode.pasture.rabbit_pasture_description)",
"icon": "/stonehearth/entities/critters/rabbit/rabbit.png",
"min_population": 2,
"max_num_per_10_square_unit": 1,
"base_reproduction_period": "18h",
"min_reproduction_period": "8h",
"feed_uri": "stonehearth:food:rabbit_feed"
},
"stonehearth:poyo": {
"name": "i18n(stonehearth:ui.game.zones_mode.pasture.poyo_pasture_name)",
"description": "i18n(stonehearth:ui.game.zones_mode.pasture.poyo_pasture_description)",
"icon": "/stonehearth/entities/critters/poyo/poyo.png",
"reproduction_uri": "stonehearth:poyo:egg",
"min_population": 2,
"max_num_per_10_square_unit": 1,
"base_reproduction_period": "24h",
"min_reproduction_period": "10h",
"feed_uri": "stonehearth:food:poyo_feed"
}
}
}
},
"entity_data": {
"stonehearth:designation": {
"allow_placed_items": true
},
"stonehearth:territory": {
"marks_territory": true
},
"stonehearth:catalog": {
"display_name": "i18n(stonehearth:jobs.shepherd.shepherd_pasture.display_name)",
"description": "i18n(stonehearth:jobs.shepherd.shepherd_pasture.description)"
}
}
}
You'll notice that the pasture has a component. This component is necessary to give the zone functionality. The zone will also need a renderer in order to appear properly in the world.
Adding behavior to the zone
First, let's look at the zone's component. This is a pretty standard component. It saves important data into it's _sv variable. Usually, it also spits out tasks that are given to the task group that is responsible for the zone.
The zone's component is declared in the manifest.json under "components":
"shepherd_pasture" : "file(components/shepherd_pasture/shepherd_pasture_component.lua)"
Rendering the zone
If there is a renderer that is named identically to a component, the renderer will be associated with that component. In this case, since the zone has no Qubicle model, it relies on a renderer to draw itself.
In the example above, the component is named shepherd_pasture
, so we can declare a renderer in the manifest's component_renderer's section that looks like this:
"component_renderers" : {
"shepherd_pasture" : "file(renderers/shepherd_pasture/shepherd_pasture_renderer.lua)"
}
The renderer has access to any data saved in the component's _sv variable. See stonehearth/renderers/shepherd_pasture/shepherd_pasture_renderer.lua
for examples.
Selecting the zone
When the UI gets the zone back from the creation process, it should then select the zone so that any zone related config can happen. To do this, make sure the UI calls:
radiant.call('stonehearth:select_entity', response.zone_name);
In stonehearth/ui/game/modes/zones_mode/zones_mode.js
there's the _examineEntity
function. You'll need to add another if
to it so that it shows your custom UI view when selecting your new zone. Make sure that your UI view extends StonehearthBaseZonesModeView
so that it has the common functionality for zones.
Also, make sure the zone type is registered as a zone in stonehearth/ui/game/modes/mode_manager.js
's _getModeForEntity
function.
At the moment this guide was written, the ACE mod overrides mode_manager.js
to make it easier to change the UI mode based on selected entities. If you base your code in that, players will need to have the ACE mod installed and enabled in order to play your mod.