Step 9 of 14, updated

Events

Declare an event once, as one line in a system's Types.shot, and shot check checks every publish and every listener against it.

You'll learn event lines, publishing, hearing, the wiring, and what is checked.

One line, declared once

Today a topic is a string on both sides, and nothing checks its spelling or the values. In ShotScript:

ShotScriptWallet/Types.shot (part)
-- Coins went into a player's wallet
event CoinsGiven(player : Player, amount : number, reason : Reason)
  • An event line goes in a system's Types.shot, like a type. Every parameter has a name and a type.
  • The event is that system's: only its files publish it. Any system may hear it.
  • Its name is the topic on the bus, so it is declared once.

Watch out Describe an event with a -- comment: a doc { } above an event line is read as the file's rootdoc today, so a second one is "a file can only have one rootdoc".

Events ride a module named EventBus, with Publish and Subscribe. Here is a small one, in a system of its own (EventBus/EventBus.shot and an empty EventBus/Types.shot):

A small EventBus to copyEventBus/EventBus.shot
types script.Parent.Types

local listeners : {[string] : {(...any) -> ()}} = {}

rootdoc { Topics with listeners, on one side }
module EventBus = {
	doc { Calls every listener of the topic }
	function Publish(topic : string, ... : any)
		local list = listeners[topic]
		if not list then
			return
		end
		for _, listener in list do
			listener(...)
		end
	end

	doc { Calls the listener on every publish }
	function Subscribe(topic : string,
		listener : (...any) -> ())
		local list = listeners[topic] or {}
		listeners[topic] = list
		table.insert(list, listener)
	end
}

Publishing

Every file of the system names the event like a type, with no import and no use:

struck ShotScript writes it for youhighlighted new in ShotScript
Luau todayBank (a ModuleScript, part)
function Bank.Give(player: Player,	amount: number)	-- ...	local coins = Whole(amount)	wallet:Add(coins, "Pickup")	EventBus.Publish("CoinsGiven",		player, coins, "Pickup")end
ShotScriptWallet/Bank.shot (part)
function Give(player : Player,	amount : number)	local wallet = module.Wallets[player]	if not wallet then		return	end	local coins = Whole(amount)	wallet:Add(coins, "Pickup")	CoinsGiven.Publish(player, coins,		"Pickup")end
What it compiles toWallet/Bank.luau (part)
local CoinsGiven = require(script.Parent.Events).CoinsGiven

...

function Bank.Give(player : Player,
		amount : number)
	local wallet = module.Wallets[player]
	if not wallet then
		return
	end
	local coins = Whole(amount)
	wallet:Add(coins, "Pickup")
	CoinsGiven.Publish(player, coins,
		"Pickup")
end

Hearing it

Another system imports it in its Types.shot, like a type. Stats adds up every coin given:

ShotScriptStats/Types.shot
import CoinsGiven, Reason from Wallet

Tip from Wallet is a bare name, found the way use finds one: it means from @shot.Wallet.

Luau todayStats (a ModuleScript)
--!strictlocal EventBus = require(path.EventBus)local Stats = {}Stats.Total = 0function Stats.Start()	EventBus.Subscribe("CoinsGiven",		function(player, amount, reason)			Stats.Total += amount		end)endreturn Stats
ShotScriptStats/Stats.shot
types script.Parent.Typesrootdoc { Numbers about the whole game }server module Stats = {	doc { Coins given to every player so far }	Total : number = 0	function Start()		CoinsGiven.Subscribe(function(			player : Player,			amount : number,			reason : Reason)			module.Total += amount		end)	end}
What it compiles toStats/Stats.luau (part)
local CoinsGiven = require(script.Parent.Parent.Wallet.Events).CoinsGiven

--[[$rootdoc
	Numbers about the whole game
]]
--[[$server]]
local Stats : Stats = {} :: any
local module = Stats
--[[$doc
	Coins given to every player so far
]]
Stats.Total = 0

function Stats.Start()
	CoinsGiven.Subscribe(function(
		player : Player,
		amount : number,
		reason : Reason)
		module.Total += amount
	end)
end

return Stats

Underneath it is still your EventBus: a module named Events beside the Types, one typed table per event, and each file that names an event gets a local CoinsGiven = require(...).CoinsGiven line. Init/Server.shot now also starts Stats, with use @shot.Stats and Stats.Start().

Underneath: the Events moduleWallet/Events.luau
--!strict
-- The events Wallet declares in Wallet/Types.shot, typed over EventBus (each topic is the event's name).
local Types = require(script.Parent.Types)
type Reason = Types.Reason
local EventBus = require(script.Parent.Parent.EventBus)

return {
	CoinsGiven = {
		Publish = function(player : Player, amount : number, reason : Reason)
			EventBus.Publish("CoinsGiven", player, amount, reason)
		end,
		Subscribe = function(callback : (player : Player, amount : number, reason : Reason) -> ())
			return EventBus.Subscribe("CoinsGiven", callback)
		end,
	},
}

Seeing the wiring

$ shot wiring
EVENTBUS TOPICS (who publishes, who listens, with which arguments)
  "CoinsGiven"   event of Wallet (player : Player, amount : number, reason : Reason)   Wallet/Types.shot:11
    publish  Wallet/Bank.shot:28  Bank.Give    (player, coins, "Pickup")
    listen   Stats/Stats.shot:9   Stats.Start  function(player : Player, amount : number, reason : Reason)

REMOTES (instance files and who fires or handles them)
  Wallet/BuyRequest (RemoteEvent)
    fire to server  Wallet/ShopClient.shot:9   ShopClient.Buy
    server handles  Wallet/ShopServer.shot:27  ShopServer.Start

SAVED DATA SECTIONS
  (none)

SYSTEM DEPENDENCIES (requires, use and imports)
  Init   -> Stats, Wallet
  Stats  -> Wallet

Checked

A payload that doesn't fit, CoinsGiven.Publish(player, "coins", ...):

$ shot check
./Wallet/Bank.shot:29:30   TypeError: Expected this to be 'number', but got 'string'
1 errors in 1 file (16 files checked, 2.5 s)

A publish from Stats, which doesn't own the event:

$ shot check
./Stats/Stats.shot:14:4   CoinsGiven is Wallet's event (Wallet/Types.shot:11): only files in Wallet publish it
1 errors in 1 file (16 files checked, 2.8 s)

The old way for a declared event, EventBus.Subscribe("CoinsGiven", function(...) ... end) in Stats.Start with use EventBus above the declaration, is refused, so no listener skips the check:

$ shot check
./Stats/Stats.shot:11:3   EventBus.Subscribe("CoinsGiven", ...) skips the declared payload of CoinsGiven(player : Player, amount : number, reason : Reason) (Wallet/Types.shot:11): write CoinsGiven.Subscribe(function(player : Player, amount : number, reason : Reason) ... end)
1 errors in 1 file (16 files checked, 2.6 s)

Run it

LuauScenarios/Given.luau
-- Given: every pickup publishes CoinsGiven, and Stats adds it up.
return function(world)
	local Bank = world.Server.Require("Wallet.Bank")
	local Stats = world.Server.Require("Stats")
	world:Boot()
	local ana = world:AddPlayer("Ana")
	local bo = world:AddPlayer("Bo")
	world:Step(0.1)
	Bank.Give(ana, 25)
	Bank.Give(bo, 10)
	world.Logf("Stats.Total is %s", world.Fmt(Stats.Total))
	assert(Stats.Total == 35, "25 + 10")
end
$ shot run Given
==== Given
[client:Ana print] client started
[client:Bo print] client started
Stats.Total is 35

==== summary
  Given          OK  (3 lines, 0 errors, 0.1s virtual)
1 of 1 scenario(s) passed
Try it

Take the CoinsGiven.Subscribe(...) call out of Stats.Start and run shot check. It still passes, with a note under it: note: event CoinsGiven is never heard; published at Wallet/Bank.shot:28.