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.
CoinsGiven.Publish(...)"CoinsGiven"CoinsGiven.Subscribe(...)One line, declared once
Today a topic is a string on both sides, and nothing checks its spelling or the values. In ShotScript:
-- Coins went into a player's wallet event CoinsGiven(player : Player, amount : number, reason : Reason)
- An
eventline goes in a system'sTypes.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:
function Bank.Give(player: Player, amount: number) -- ... local coins = Whole(amount) wallet:Add(coins, "Pickup") EventBus.Publish("CoinsGiven", player, coins, "Pickup")end
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:
import CoinsGiven, Reason from Wallet
Tip from Wallet is a bare name, found the way use finds one: it means from @shot.Wallet.
--!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
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
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 -> WalletChecked
A payload that doesn't fit, CoinsGiven.Publish(player, "coins", ...):
./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:
./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:
./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
-- 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
==== 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
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.
