Client, server and split
Mark a file server or client, and shot check keeps each side out of the other's code. A split module gives one name a module on each side.
You'll learn what may require what, a typed remote, and a split module.
Sides
Today nothing stops a LocalScript requiring a server module until it fails at run time. In ShotScript the side is written on the declaration:
class,module,func: shared (the default).server module,@serverscripts: server only.client module,@clientscripts: client only.
ReplicatedStorage.Shot.Tip Each side may also require its own side's code. Nothing is hidden or moved.
A shop on both sides
Buy("Sword")(item : string)Buy(player, item)Add(-price, "Shop")RemoteEvent (item : string)
Tip A remote is an .instance file: its class, and the signature the client sends after the Player. It compiles to BuyRequest.model.json; the signature is only checked.
--!strictlocal ReplicatedStorage = game:GetService("ReplicatedStorage")local buy = ReplicatedStorage :WaitForChild("BuyRequest") :: RemoteEventlocal ShopClient = {}function ShopClient.Buy(item: string) buy:FireServer(item)endreturn ShopClient
types script.Parent.Typeslocal here = script.Parentlocal buy = here.BuyRequest :: RemoteEventclient module ShopClient = { doc { Asks the server to buy an item } function Buy(item : string) buy:FireServer(item) end}
What it compiles toWallet/ShopClient.luau (part)
local here = script.Parent local buy = here.BuyRequest :: RemoteEvent --[[$client]] local ShopClient : ShopClient = {} :: any local module = ShopClient --[[$doc Asks the server to buy an item ]] function ShopClient.Buy(item : string) buy:FireServer(item) end return ShopClient
The server side:
types script.Parent.Types local here = script.Parent local buy = here.BuyRequest :: RemoteEvent use Bank server module ShopServer = { doc { The price of each item } Prices : {[string] : number} = { Sword = 10, } doc { Sells the item if the player can pay } function Buy(player : Player, item : any) local price = module.Prices[item] local wallet = Bank.Wallets[player] if not price or not wallet then return end if wallet.Balance >= price then wallet:Add(-price, "Shop") end end function Start() buy.OnServerEvent:Connect(module.Buy) end }
Watch out item : any: a client can send anything, so check it before use (here, by looking it up in Prices).
Split module
Both Init scripts want "the shop": one name for the two.
split module Shopone name, a module on each sideShop.Server= ShopServerShop.Client= nilShop.Client= ShopClientShop.Server= nil--!strictlocal RunService = game:GetService("RunService")local Shop = {}if RunService:IsServer() then Shop.Server = require(script.ShopServer)else Shop.Client = require(script.ShopClient)endreturn Shop
types script.Parent.Typessplit module Shop = { Client -> script.Parent.ShopClient, Server -> script.Parent.ShopServer}
What it compiles toWallet/Shop.luau (part)
local RunService = game:GetService("RunService") --[[$split]] local Shop : Shop = {} :: any if RunService:IsServer() then Shop.Server = require(script.Parent.ShopServer) else Shop.Client = require(script.Parent.ShopClient) end return Shop
@server enabled use @shot.Wallet.Bank local Shop = require(@shot.Wallet.Shop) Bank.Start() Shop.Server.Start()
@client enabled local Shop = require(@shot.Wallet.Shop) print("client started")
Checked
Add use @shot.Wallet.Bank and Shop.Server.Start() to Init/Client.shot:
./Init/Client.shot:4 client code uses server Bank (Wallet/Bank.shot) ./Init/Client.shot:7:6 client code uses Shop.Server 2 errors in 1 file (12 files checked, 2.3 s)
Watch out Luau thinks both of a split's fields are always there. ShotScript catches the nil side when the split is a local made by local Shop = require(...), not through a use line today.
One value too many, buy:FireServer(item, 2), is caught where a local like local buy = here.BuyRequest :: RemoteEvent fires it:
./Wallet/ShopClient.shot:9 BuyRequest:FireServer sends 2 values, but its signature (item : string) takes 1: the handler never reads the extra ones; send what the signature says, or add the parameter in Wallet/BuyRequest.instance 1 errors in 1 file (12 files checked, 2.3 s)
Run it
world:AddPlayer also returns the player's client; client.Require reaches its modules:
-- Buy: a client asks for a Sword, and the server takes its price. return function(world) local Bank = world.Server.Require("Wallet.Bank") world:Boot() local ana, client = world:AddPlayer("Ana") world:Step(0.1) Bank.Give(ana, 25) client.Require("Wallet.Shop").Client.Buy("Sword") world:Step(0.1) world.Logf("Ana has %s", Bank.Wallets[ana]:Describe()) assert(Bank.Wallets[ana].Balance == 15, "25 - 10") end
==== Buy [client:Ana print] client started Ana has 15 coins ==== summary Buy OK (2 lines, 0 errors, 0.2s virtual) 1 of 1 scenario(s) passed
Drop server from server module ShopServer and run shot check. It answers both code uses server Bank (Wallet/Bank.shot).
