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 CoinsGiven on 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 Wallet

Prizes/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 use line, 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 a uses line, 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) and Name.Once(...) take a listener with that payload and return what EventBus.Subscribe returns (keep it to disconnect).
  • It is still EventBus underneath: CoinsGiven.Publish(...) is EventBus.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).CoinsGiven

Event 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, and PlayerEvent, the events whose first parameter is player : 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 : Player is no PlayerEvent, and naming it there is an error.
  • With events on several buses (ON ANOTHER BUS), EventName and PlayerEvent are 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 ShopBus

Shop/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 Shop

Ledger/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 ShopBus names a module declared once in the project, with Publish and Subscribe. Without on, 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 Shop takes 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) is ShopBus.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, Shop

Shop/Types.shot

import CoinsGiven from Wallet

Shop/Till.shot

types script.Parent.Types

server module Till = {
    function Refund(player : Player, price : number)
        CoinsGiven.Publish(player, price)
    end
}
  • published by names 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 Wallet in 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:1

A 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 Wallet

A 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 it

What 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:3

A 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 it

An 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.shot

A 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 once

A 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:3note: event CoinsGiven is never heard; published at Wallet/Bank.shot:8
Wallet/Types.shot:3note: event CoinsGiven is never published; heard at Prizes/Prizes.shot:7
Wallet/Types.shot:3note: 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).