Adopt
Point shot at an existing Roblox game whose scripts are mirrored to disk as plain Luau, and read and change it as it is, with no .shot files.
What it is
shot adopt <folder> takes a game whose scripts are mirrored to disk as plain Luau and reads it as a project, converting nothing. In such a mirror every folder is one instance, with a metadata file of its own, and a script's code is its source file. Beside each root folder a <Root>.tree file lists every instance of that root, one line each, with its class. shot reads the scripts, the .tree files and the .md files, and changes only a script's source file.
shot adopt Game | index the tree and say what it found |
shot overview --project Game | any command below, on the tree |
shot adopt says what it found, where the index is and how to go on; its first line:
adopted Game: 10 scripts (115 lines) in 7 systems, 1 .md docs, 1 script that copies another's text (1 text)The tree is found by its layout, never registered: a folder holding a <Name>.tree file beside a folder <Name> with the instance's metadata file in it. shot adopt only builds the index ahead of time; any command given the folder as --project (or SHOT_PROJECT) works on it and builds the index itself when there is none.
The commands
Read (start with overview):
overview the systems (folders under the roots, or one level down in a folder that only
groups many), with scripts, lines, .md files and entry scripts; every .md file
with its first heading; the requires between systems; the duplicates
map [path] the systems, or every script under a path with its kind and lines
show <script|file> a script by instance path (ReplicatedStorage/Shop/Cart), a unique end of one
(Shop/Cart) or the path of its source file, whole or a range (Shop/Cart:6-9); any
file of the tree (an .md file, ReplicatedStorage.tree:2-40); a member (Cart.Add)
find <name> where a name is declared (functions, module members, scripts, instances in the
.tree files) and every line of code using it, comments and strings left out
(Module.Member: declared in that module)
search <text> every line holding the text, any case, in the scripts and the .md files; a line
inside a --[[ ]] comment is marked so. --string: only lines where the text is inside
a string literal in a script (no .md files); --word: whole words; A\|B: either part;
--limit N. When --string or --word leaves lines out, the last line counts them
refs <Module.Member> every use through the requires shot follows, then the lines that match only by text;
its first line counts both, and the :Member( calls in scripts that don't use the module;
refs <Module>: the module's members, each with its uses
context <Module.Member> its source, each call in it with where it goes, and its callers
trace <Module.Member|script|script:line> [B]
what it calls across scripts, remotes and bindables; --up: what leads to it; with
B: the route from it to B (--depth N, 4 by default)
wiring every remote and bindable some code fires or handles, with who sends it and who
handles it, then the requires between systems
tree [path] the instances from the .tree files with their classes (--depth N, 2 by default;
--find <text>: instances whose name holds it)
docs, help the tool's docs; the commands that work hereChange a script's source: edit and write (WRITES below).
Every other command refuses in one line, with exit code 2, since it needs .shot files (check, run, build, rename, move, delete, brief, outline, api and the rest):
shot check isn't available on an adopted Luau tree (Game is plain Luau, no .shot files): shot help lists what works hereThe refs of a module's function, copied:
REFS Cart.Add ReplicatedStorage/Shop/Cart:6 (function) 5 uses shot follows, 1 only by text
5 uses through requires shot follows (4 scripts require it or hold its objects, plus its own file):
ReplicatedStorage/Client/Buyer:6 local buy Cart.Add(item)
ReplicatedStorage/Shop/Cart:12 Cart.Clear return Cart.Add("none")
ReplicatedStorage/Shop/Checkout:6 Checkout.Run return Cart.Add("apple")
ServerScriptService/Stats:12 local onChanged Cart.Add("stats")
ServerScriptService/ShopServer:7 local onBuy local price = Cart.Add(item)
1 more line matches only by text, so check it: .Add or :Add on a name holding "Cart", or :Add( on a name shot can't bind (a parameter, a field set elsewhere) in a script that uses Cart:
ServerScriptService/ShopServer:13 local relay myCart.Add("pear") (by text only)A line that matches only by text may be a use shot can't prove, or another module's member with the same name: check it. When :Add( calls sit in scripts that neither require Cart nor hold its objects, the first line ends with ", N more :Add( calls in other scripts" and the last line says so; shot search :Add( lists them.
Writes
edit and write change an existing script's source file in place:
shot edit <script> | stdin is JSON {"old": "...", "new": "..."}, or {"edits": [{"path": ..., "old": ..., "new": ...}, ...]} for several (if one doesn't apply, nothing is changed) |
shot write <script> | stdin is the script's whole new source |
The file keeps its line ends (a CRLF source stays CRLF) and its indentation (old and new may be indented with spaces; the file's tabs are put back). Then the changed script's new text is parsed, syntax only (nothing checks types or runs the code here), and the change ends with one of these lines:
syntax ok (only parsed: types and behaviour aren't checked here)
syntax error from this change, line 4: Expected identifier when parsing expression, got ')' (the change is saved: fix it)
syntax not judged: the script didn't parse before this change either (now line 4: Expected identifier when parsing expression, got ')')An error is the change's only when the script parsed before it; a script that didn't parse before isn't judged. An edit of several scripts names each one that doesn't parse (syntax error from this change in <script>, ...), or says syntax ok in the 2 changed scripts. const is read as local, the way shot run reads it.
Refused:
- the sync's own files, the sync's key file and the instance's metadata file:
the sync's key file of ServerScriptService/Stats belongs to the sync (changing it would break the next sync); only a script's source file can be changed- a new script (its instance is made by the sync tool or in Studio first):
no script Shop/Coupon: only an existing script's source can be changed here (a new script needs its instance made by the sync tool or in Studio)- a short name that copies share (give the full path of the one to change):
Format matches 2 scripts with the same text; give the full path of the one to change (shot show ReplicatedStorage/Util/Format lists them)Nothing else is ever written inside the tree: the index lives outside it.
The index
shot keeps the tree's texts and what it read from them in the temp folder, never inside the tree (a stray file in a sync mirror would reach the place or stop the next upload):
%TEMP%\shot-cache_adopt\<16 hex digits from the tree's path>\
index.json every file's path, modification time, hash, kind and line count
texts.bin every script's source and every .md file's text, in one file
scan.json what shot read from each distinct text (functions, calls, requires), once a text
stamp.txt the index's versionA game of a few thousand scripts takes a few tens of MB. The first command on a tree builds it in full. After that every command walks the tree's file names and modification times and rereads only the files whose time changed, so a change to one script costs one file, never a rebuild. Deleting the folder is safe: the next command builds it again.
Requires shot follows
refs, context, trace, wiring and the overview's dependencies follow a require whose argument names a fixed instance:
require(script.Parent.Cart), require(script.Cart)
require(game.ReplicatedStorage.Shop.Cart), require(game.ReplicatedStorage["Shop"].Cart)
require(game:GetService("ReplicatedStorage"):WaitForChild("Shop"):WaitForChild("Cart"))
workspace.X, .Parent, :FindFirstChild("X") and :FindFirstAncestor("X") in the chain
a local holding such a path: local Shop = game.ReplicatedStorage.Shop, then require(Shop.Cart)A module that only returns require(...) forwards to what it returns (refs Prices.Get finds the uses of PriceTable.Get), and a module whose file returns an object its class made (local self = Bank.new() then return self) has the class's members. The overview counts them:
(9 of 10 require calls reach a script of the tree; the others: 1, forms in shot docs Adopt)Nearly every require call in a game reaches a script. The others are:
require(<number or expression>): an asset id, a call, anything that isn't a path;require(<local>)where the local holds no path: a parameter or a loop variable;- chains from a local that holds no path (
require(folder.Child)where folder is a parameter or a field); - paths outside the mirrored roots, or made at run time.
Uses of such a module aren't followed; refs lists a use on a name holding the module's name as matching by text only, and so is a :Member( call in a script that uses the module on a name shot can't bind (a parameter, a field set elsewhere).
Calls and remotes
A call is placed when its receiver is a required module (Cart.Add), the file's own module or class (Cart.Clear inside Cart, self), a local function, or an object of a module. refs and trace take these as objects of a module:
- what a call into it returns:
local wallet = Wallet.new()orWallet.Get(player), thenwallet:Add(); - what a call into another module returns, when that function declares its type: function Checkout.Account(player) : Account? in a module with export type Account = Bank.Bank, then local account = Checkout.Account(player);
- a parameter or local declared as such a type: local function refund(account : Checkout.Account);
- a required file that returns such a call:
return Bank.new(); - a field set to one in the same file:
self.Account = Checkout.Account(player), thenself.Account:Deposit(2).
A send is joined to the handlers of the instance it names: FireServer to .OnServerEvent:Connect(f), FireClient and FireAllClients to .OnClientEvent, InvokeServer to .OnServerInvoke = f, InvokeClient to .OnClientInvoke, Fire to .Event, Invoke to .OnInvoke. Nothing is type-checked: a call through a field set elsewhere, a parameter with no declared type or an engine value can't be placed, and trace lists those as "can't follow" lines.
The route from a client script to a server handler and on through a bindable, copied:
route from Buyer (file code) to Stats: local onChanged:
Buyer (file code) ReplicatedStorage/Client/Buyer:1
calls Buyer: local buy ReplicatedStorage/Client/Buyer:5
sends remote ReplicatedStorage/Shop/Buy to server ReplicatedStorage/Client/Buyer:7
handled by ShopServer: local onBuy ServerScriptService/ShopServer:16
sends bindable ReplicatedStorage/Lib/Changed ServerScriptService/ShopServer:9
handled by Stats: local onChanged ServerScriptService/Stats:15Duplicates
Scripts with exactly the same text (a module copied into two places, a script cloned into many parts) count once. map, find, search, refs, context and trace show the first by path with "(+N copies with the same text)"; show of one names the others ("The same text is in 1 other script:"); the overview's DUPLICATES section lists every group:
ReplicatedStorage/Util/Format (+1 copy with the same text)