Step 8 of 14, updated

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:

Serverserver module@server scriptClientclient module@client scriptSharedclass, module, functhe defaultmay requiremay requirenever
Shared code requires only shared code. Everything still compiles into ReplicatedStorage.Shot.

Tip Each side may also require its own side's code. Nothing is hidden or moved.

A shop on both sides

FileWallet/BuyRequest.instance
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.

struck ShotScript writes it for youhighlighted new in ShotScript
Luau todayShopClient (a ModuleScript)
--!strictlocal ReplicatedStorage =	game:GetService("ReplicatedStorage")local buy = ReplicatedStorage	:WaitForChild("BuyRequest") :: RemoteEventlocal ShopClient = {}function ShopClient.Buy(item: string)	buy:FireServer(item)endreturn ShopClient
ShotScript109 linesWallet/ShopClient.shot
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:

ShotScriptWallet/ShopServer.shot
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.

Luau todayShop (a ModuleScript)
--!strictlocal RunService =	game:GetService("RunService")local Shop = {}if RunService:IsServer() then	Shop.Server = require(script.ShopServer)else	Shop.Client = require(script.ShopClient)endreturn Shop
ShotScript105 linesWallet/Shop.shot
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
ShotScriptInit/Server.shot
@server enabled

use @shot.Wallet.Bank
local Shop = require(@shot.Wallet.Shop)

Bank.Start()
Shop.Server.Start()
ShotScriptInit/Client.shot
@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:

$ shot check
./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:

$ shot check
./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:

LuauScenarios/Buy.luau
-- 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
$ shot run Buy
==== 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
Try it

Drop server from server module ShopServer and run shot check. It answers both code uses server Bank (Wallet/Bank.shot).