Owned fields

Give a field to its system with owns Purse.Coins via WalletServer.Grant, and shot check names that function at every write from anywhere else.

What it is

Some fields must only change through their system's API, because the API does more than set the value: it tells the player, fires a signal, levels up, saves. A write from another system compiles and runs, and silently skips all of that. A line

owns Purse.Coins via WalletServer.Grant

in a system's Types.shot gives the field to that system. To Luau it is nothing (no code is generated, nothing runs). What it adds is a check: an assignment to the field (=, +=, -= and the other compound forms), or to one of its entries when it is a table (purse.Bank[currency] = ...), in a file outside the system's folder is an error at the write, naming the API to call instead.

The whole syntax

Wallet/Types.shot

doc { Grant also tells the player how many coins they have; a write to Coins does not }
owns Purse.Coins via WalletServer.Grant
  • owns Class.Field via Module.Function goes on its own line in a Types.shot, with an optional doc above it. The doc is the why: the error quotes it, so say what the API does that a write skips.
  • Class is a class or module of the system's own folder (subfolders included), and Field one of its fields (a module's field works too). Module.Function is any class or module and one of its functions or methods; write a method as Class:Method (owns Experience.Rested via Experience:AddRested).
  • One line per field. Give each field you want to guard its own line.

A whole example

Wallet/Purse.shot

types script.Parent.Types

rootdoc {
    The coins of one player.
}
class Purse = {
    doc {Coins the player holds}
    public Coins : number

    doc {Creates an empty purse}
    constructor new()
        local self : Purse = init()
        self.Coins = 0
        return self
    end
}

Wallet/WalletServer.shot

types script.Parent.Types

use Purse

local purses : {[Player] : Purse} = {}

rootdoc {
    Every player's purse.
}
server module WalletServer = {
    doc {The purse of a player, nil before their first coins}
    function Get(player : Player) : Purse?
        return purses[player]
    end

    doc {Gives a player coins and tells them their total}
    function Grant(player : Player, amount : number)
        local purse = purses[player] or Purse.new()
        purses[player] = purse
        purse.Coins += amount
        print(player.Name, "has", purse.Coins, "coins")
    end
}

Grant writes purse.Coins itself: writes inside Wallet are never errors.

Prizes/Types.shot

import Purse from @shot.Wallet.Purse

Prizes/Prizes.shot

types script.Parent.Types

use @shot.Wallet.WalletServer

rootdoc {
    The daily prize.
}
server module Prizes = {
    doc {Pays the daily prize}
    function Pay(player : Player)
        WalletServer.Grant(player, 100)
    end
}

Write the prize as a direct write instead:

    function Pay(player : Player)
        local purse = WalletServer.Get(player)
        if purse then
            purse.Coins += 100
        end
    end

and shot check says:

Prizes/Prizes.shot:13:10   Purse.Coins belongs to Wallet: change it with WalletServer.Grant instead (Grant also tells the player how many coins they have; a write to Coins does not)

Without a doc (owns alone on line 1) the error points at the owns line instead:

Purse.Coins belongs to Wallet: change it with WalletServer.Grant instead (Wallet/Types.shot:1 gives it to WalletServer.Grant; a direct write skips what Grant does around it)

Which writes are seen

A write is matched to the field by the type of the object before it, read the way the other checks read it:

  • a typed parameter or local: function Fine(purse : Purse) ... purse.Coins -= 1;
  • a local set from a call whose return type is known: local purse = WalletServer.Get(player) (Purse? counts as Purse), a constructor (local purse = Purse.new()), a typed field (local purse = shop.Purse);
  • chains of typed fields: progression.Experience.Xp, where progression is a PlayerProgression and its Experience field an Experience;
  • self, module and a use name;
  • a field inherited from the class that owns it (a subclass's object writing the owner's field).

Another class's field of the same name is never confused with the owned one.

What it doesn't check

  • An object whose type can't be told: an untyped parameter or local, a loop variable (for _, purse in purses), an element of a list or map (purses[player].Coins = 0), a call in the chain (WalletServer.Get(player).Coins = 0), a record type from a Types.shot (owns takes classes and modules only), and a .luau file.
  • A write to what the field holds rather than to the field: table.insert(bag.Items, item), bag.Items[1] = nil.
  • A multiple assignment (a.Coins, b = 1, 2).
  • A name typed by an earlier function in the same file and reused untyped later keeps its last known type: the check reads declarations in file order, not scopes.
  • When the write happens. owns says who writes, not when: a write through the API that runs too early (before the player's data loads, say) passes. The order between systems is a different contract (see Start order).

Errors

Purse.Coins belongs to Wallet: change it with WalletServer.Grant instead (Grant also tells the player how many coins they have; a write to Coins does not)
owns Purse.Coin: Purse has no field Coin (nearest: Coins)
owns Purse.Gems via WalletServer.Grnt: WalletServer has no function or method Grnt (nearest: Grant)
owns Chest.Gold: Chest is Prizes's (Prizes/Chest.shot), and a system owns only fields of its own classes and modules: move this line to Prizes/Types.shot
owns Purse.Add: Add is a method of Purse, not a field; owns guards a field (Purse has Coins and Gems), and the method goes after via
owns goes in the system's Types.shot, the system whose class or module has the field: move this line there
owns needs: owns Class.Field via Module.Function, like owns Wallet.Coins via WalletServer.Grant; got `owns Purse.Coins to WalletServer.Grant`