Events
Declare an event once in a system's Types.shot, and shot check checks every publish and every listener against it.
An event is one line: its name and the values it carries. The system that declares it publishes it with CoinsGiven.Publish(...), any system can hear it with CoinsGiven.Subscribe(...), and underneath it is still your project's EventBus.
The line
Wallet/Types.shot
public type Reason = "Prize" | "Sale"
event CoinsGiven(player : Player, amount : number, reason : Reason)event Name(param : Type, ...)is a line of a system's Types.shot, like a type. Every parameter has a name and a type; the types are anything that Types.shot can name (its own types, its imports, Roblox types like Player).- The event belongs to that system (the folder of its Types.shot and the folders under it). Only its files publish it, unless the line lists who does (PUBLISHED BY); any system may hear it.
- The name is the EventBus topic, so it is unique on its bus: a second
event CoinsGivenon EventBus is an error. - Events need a module named EventBus in the project, with Publish and Subscribe (and Once, used when it has one), or name another bus module with
on(ON ANOTHER BUS).
Publishing and hearing
Wallet/Bank.shot
types script.Parent.Types
server module Bank = {
Coins : {[Player] : number} = {}
function Give(player : Player, amount : number, reason : Reason)
module.Coins[player] = (module.Coins[player] or 0) + amount
CoinsGiven.Publish(player, amount, reason)
end
}Prizes/Types.shot
import CoinsGiven, Reason from WalletPrizes/Prizes.shot
types script.Parent.Types
module Prizes = {
Total : {[Player] : number} = {}
function Start()
CoinsGiven.Subscribe(function(player : Player, amount : number, reason : Reason)
if reason == "Prize" then
module.Total[player] = (module.Total[player] or 0) + amount
end
end)
end
}- Every file of the owning system names the event with no import and no
useline, like its types. - Another system imports it in its Types.shot, exactly like a type:
import CoinsGiven from Wallet(import *brings the system's events too). That import is the declared dependency: with ausesline, Prizes must list Wallet (Uses, WHAT COUNTS). The payload's types are separate imports (Reason above). Name.Publish(...)sends the declared payload.Name.Subscribe(function(...) ... end)andName.Once(...)take a listener with that payload and return what EventBus.Subscribe returns (keep it to disconnect).- It is still EventBus underneath:
CoinsGiven.Publish(...)isEventBus.Publish("CoinsGiven", ...). Topics kept in data (for _, topic in Prizes.Topics() do EventBus.Subscribe(topic, ...)) hear typed publishes, and shot wiring, shot trace and the CONNECTS lines of shot show treat typed calls like EventBus calls (see The shot command). - Compiled, a system with events gets a module named Events beside its Types (Wallet/Events.luau: one table per event with Publish, Subscribe and Once), and a file that names an event gets the line below in its header. So no other file of a system with events may be called Events.
local CoinsGiven = require(script.Parent.Events).CoinsGivenEvent names in data
Prizes/Types.shot
import CoinsGiven, Reason from Wallet
public type Goal = {
Topic : PlayerEvent,
Count : number,
}- In a project that declares events, every file and Types.shot can name two generated types without importing them:
EventName, the union of every declared event's name, andPlayerEvent, the events whose first parameter isplayer : Player. Use them for topic names kept in data, so a misspelt or wrong name is a type error. - A listener that takes the player first should only be given PlayerEvent topics: an event that doesn't start with
player : Playeris no PlayerEvent, and naming it there is an error. - With events on several buses (ON ANOTHER BUS),
EventNameandPlayerEventare per bus: in a system they hold the names on the buses that system goes through (the buses of the events it declares or imports, and any bus module its files name), so a wrong name lists only those. A system that goes through no bus gets every bus's names.
On another bus
A project with a second bus module (here Utils/ShopBus/ShopBus.shot, module ShopBus, with Publish and Subscribe like EventBus) puts events on it with on:
Shop/Types.shot
event ItemSold(player : Player, item : string, price : number) on ShopBus
event CoinsGiven(player : Player, amount : number) on ShopBusShop/Till.shot
types script.Parent.Types
server module Till = {
function Sell(player : Player, item : string, price : number)
ItemSold.Publish(player, item, price)
CoinsGiven.Publish(player, price)
end
}Ledger/Types.shot
import ItemSold, CoinsGiven from ShopLedger/Ledger.shot
types script.Parent.Types
module Ledger = {
Sales : {string} = {}
function Start()
ItemSold.Subscribe(function(player : Player, item : string, price : number)
table.insert(module.Sales, player.Name .. " bought " .. item .. " for " .. price)
end)
CoinsGiven.Subscribe(function(player : Player, amount : number)
print(player.Name, amount)
end)
end
}on ShopBusnames a module declared once in the project, with Publish and Subscribe. Withouton, the bus is the module named EventBus, as above.- An event is its bus and its name. Shop's CoinsGiven on ShopBus and Wallet's CoinsGiven on EventBus are two events, so a name may repeat on another bus; twice on one bus is the already-declared error.
import CoinsGiven from Shoptakes the CoinsGiven that Shop declares. A system's files name an event by its name alone, so one system can't reach two events of one name (Prizes, which imports Wallet's, can't also import Shop's).- Everything follows the bus: Shop's Events module requires ShopBus,
CoinsGiven.Publish(player, price)isShopBus.Publish("CoinsGiven", player, price), and shot wiring and shot trace show it on ShopBus. A raw call is an error only for a topic declared on that bus:ShopBus.Subscribe("ItemSold", ...)is,EventBus.Subscribe("ItemSold", ...)names no declared event and is fine. - When a name is on two buses, shot wiring, shot trace, notes and messages say which: "CoinsGiven" on ShopBus.
What shot check reports:
Shop/Types.shot:1:1 event ItemSold on ShopBuss: no module is named ShopBuss; the modules with Publish and Subscribe are EventBus, ShopBus
Shop/Types.shot:1:1 event ItemSold on ShopBus: 2 modules are named ShopBus (Shop/Old/ShopBus.shot, Utils/ShopBus/ShopBus.shot): the bus is one module, so give the others other names
Shop/Types.shot:1:1 event ItemSold on Bank: Bank (Wallet/Bank.shot) has no Publish and Subscribe, so it is no event bus: a bus module has Publish(topic : string, ...: any) and Subscribe(topic : string, callback)
Prizes/Types.shot:2:1 import CoinsGiven from Shop: Prizes/Types.shot already reaches event CoinsGiven on EventBus (Wallet/Types.shot:3), and its files name an event by its name alone, so one system can't reach two events CoinsGiven
Ledger/Ledger.shot:9:3 ShopBus.Subscribe("ItemSold", ...) skips the declared payload of ItemSold(player : Player, item : string, price : number) (Shop/Types.shot:1): write ItemSold.Subscribe(function(player : Player, item : string, price : number) ... end)Anything else after the parameters is a parse error (here event ItemSold(...) via ShopBus):
Shop/Types.shot:1 event ItemSold: after the parameters comes nothing or on and a bus module, like event ItemSold(player : Player) on EventBus; got `via ShopBus`With the two buses, a name kept in data lists only its own bus's names. Ledger hears ShopBus's ItemSold and StockLow, and Wallet's CoinsGiven and BankOpened are on EventBus, so in
Ledger/Types.shot
import ItemSold, StockLow from Shop
public type Goal = {
Topic : PlayerEvent,
Count : number,
}a goal with Topic = "StockLo" gives:
Ledger/Ledger.shot:6:3 TypeError: Expected this to be exactly 'Goal', but got '{| Count: number, Topic: "StockLo" |}' caused by: Property 'Topic' is not compatible. Expected this to be exactly '"ItemSold" | "StockLow"', but got '"StockLo"'; none of the union options are compatible; no event is declared as StockLo (nearest: StockLow)Published by: an event several systems publish
Some events are published from more than one system: coins come from the bank and from shop refunds. The event line lists them after the parameters (and after on, if any):
Wallet/Types.shot
event CoinsGiven(player : Player, amount : number) published by Wallet, ShopShop/Types.shot
import CoinsGiven from WalletShop/Till.shot
types script.Parent.Types
server module Till = {
function Refund(player : Player, price : number)
CoinsGiven.Publish(player, price)
end
}published bynames systems: the project's top folders (Wallet, Shop), separated by commas.- Exactly the listed systems publish it. The declaring system is one of them only when listed (Wallet above). Without
published by, only the declaring system publishes, as before. - A listed system reaches the event the usual way:
import CoinsGiven from Walletin its Types.shot. - Everything else stays: the payload is checked at every publish and listener, raw EventBus calls with the name are errors, and the notes below still count every publish.
- Listing generates no code:
CoinsGiven.Publish(...)compiles exactly as without it.
A publish from a system that isn't listed (here Prizes, which imports CoinsGiven to hear it, also publishes it):
Prizes/Prizes.shot:13:3 CoinsGiven is published only by Wallet, Shop (Wallet/Types.shot:1), and this publish is from Prizes: its listeners count on those publishers alone, so publish through one of them (call the function of theirs that publishes it), or, if Prizes is meant to publish it too, add it to the list: published by Wallet, Shop, Prizes at Wallet/Types.shot:1A name that is no top folder (here published by Wallet, Shpo; Shop's publish is then unlisted too):
Wallet/Types.shot:1:1 event CoinsGiven: published by names Shpo, which is no system of this project (nearest: Shop); published by lists systems, the project's top folders, like published by WalletA list that doesn't end in a system (here published by Wallet,) is a parse error:
Wallet/Types.shot:1 event CoinsGiven: after the parameters comes nothing or on and a bus module, then published by and the systems that publish it, like event CoinsGiven(player : Player) on EventBus published by Wallet, Shop; got `published by Wallet,`A listed system none of whose files publishes the event gets a note (not an error), so the list stays true (here published by Wallet, Shop, Prizes):
Wallet/Types.shot:1 note: event CoinsGiven lists Prizes in published by, but nothing in Prizes publishes itWhat shot check reports
A payload that doesn't fit, at the publish and at the listener (Luau's own error, with the declaration added):
Wallet/Bank.shot:8:30 TypeError: Expected this to be 'number', but got 'string'; CoinsGiven is declared (player : Player, amount : number, reason : Reason) at Wallet/Types.shot:3
Prizes/Prizes.shot:7:24 TypeError: Expected this to be '(Player, number, "Prize" | "Sale") -> ()' but got '(number, Player) -> ()' caused by: Argument #1 type is not compatible. Expected this to be 'number', but got 'Player'; CoinsGiven is declared (player : Player, amount : number, reason : Reason) at Wallet/Types.shot:3A publish from another system (an event with published by names its list instead, PUBLISHED BY):
Prizes/Prizes.shot:7:3 CoinsGiven is Wallet's event (Wallet/Types.shot:3): only files in Wallet publish itAn EventBus call whose topic is a declared event. It is an error, so every use of a declared event is checked against its payload; topics from data, which aren't literals, are fine:
Prizes/Prizes.shot:9:3 EventBus.Subscribe("CoinsGiven", ...) skips the declared payload of CoinsGiven(player : Player, amount : number, reason : Reason) (Wallet/Types.shot:3): write CoinsGiven.Subscribe(function(player : Player, amount : number, reason : Reason) ... end)Hearing an event without importing it:
Prizes/Prizes.shot:7:3 TypeError: Unknown global 'CoinsGiven'; CoinsGiven is an event of Wallet (Wallet/Types.shot:3): only Wallet publishes it; to hear it, add import CoinsGiven from Wallet to Prizes/Types.shotA wrong name in data typed PlayerEvent (here the only player event is CoinsGiven):
Prizes/Prizes.shot:8:3 TypeError: Expected this to be exactly 'Goal', but got '{| Count: number, Topic: "CoinGiven" |}' caused by: Property 'Topic' is not compatible. Expected this to be exactly '"CoinsGiven"' but got '"CoinGiven"'; no event is declared as CoinGiven (nearest: CoinsGiven)The same name twice:
Wallet/Types.shot:3:1 event CoinsGiven is already declared at Prizes/Types.shot:3: an event's name is its EventBus topic, so each name is declared onceA parameter without a type (here event CoinsGiven(player : Player, amount : number, reason)):
Wallet/Types.shot:3:1 event CoinsGiven: every parameter needs a name and a type, like event CoinsGiven(player : Player, amount : number); got `reason`Also errors: an event line in a file other than Types.shot ("event goes in the system's Types.shot ..."), a project with no module named EventBus ("event CoinsGiven: events compile to calls on the project's EventBus module (Publish, Subscribe, Once), and this project has no module named EventBus"), and a file named Events in a folder that declares events.
Unused events
shot check prints a note under the errors for each declared event that no code publishes, or that nothing hears (typed listeners, EventBus calls with its name, and topics from data all count). Notes are not errors: they are not in "N errors" and don't change the exit code.
Wallet/Types.shot:3 | note: event CoinsGiven is never heard; published at Wallet/Bank.shot:8 |
Wallet/Types.shot:3 | note: event CoinsGiven is never published; heard at Prizes/Prizes.shot:7 |
Wallet/Types.shot:3 | note: event CoinsGiven is never published and nothing hears it |
Seeing them
shot wiring's EVENTBUS TOPICS shows each declared event with its owner, payload and declaration, then every publish and listener. For the example above:
"CoinsGiven" event of Wallet (player : Player, amount : number, reason : Reason) Wallet/Types.shot:3
publish Wallet/Bank.shot:8 Bank.Give (player, amount, reason)
listen Prizes/Prizes.shot:7 Prizes.Start function(player : Player, amount : number, reason : Reason)With --baseline-dir, a shot write or shot edit that changes an event line prints RIPPLE: the new and the old payload (or bus, for a changed on), with where it is published and heard (The shot command).
