--!strict

local RunService = game:GetService("RunService") :: RunService

local Timeout = {}

local function getStudioTimeoutLength(default: number)
	local duration = default
	if RunService:IsStudio() then
		pcall(function()
			local timeoutLength = settings().Studio.ScriptTimeoutLength - 0.1
			if timeoutLength > 0 then
				duration = timeoutLength
			end
		end)
	end
	return duration
end

function Timeout.benchmark<T..., U...>(callback: (T...) -> U..., ...: T...): (number, U...)
	local t = os.clock()
	local packed = table.pack(callback(...))
	local dt = os.clock() - t
	return dt, table.unpack(packed :: { any }, 1, packed.n)
end

function Timeout.scriptExhaustionStepper(default: number)
	local prevTick = os.clock()
	local duration = getStudioTimeoutLength(default)
	return function(reset: boolean?)
		if reset then
			prevTick = os.clock()
		end

		if os.clock() - prevTick >= duration then
			task.wait()
			prevTick = os.clock()
		end
	end
end

Timeout.tryYield = Timeout.scriptExhaustionStepper(10)

return Timeout


local Promise = require(script.Parent.Promise)

local RunService = game:GetService("RunService")

local OUTPUT_PREFIX = "roblox-ts: "
local NODE_MODULES = "node_modules"
local DEFAULT_SCOPE = "@rbxts"

local TS = {}

TS.Promise = Promise

local function isPlugin(context)
	return RunService:IsStudio() and context:FindFirstAncestorWhichIsA("Plugin") ~= nil
end

function TS.getModule(context, scope, moduleName)
	-- legacy call signature
	if moduleName == nil then
		moduleName = scope
		scope = DEFAULT_SCOPE
	end

	-- ensure modules have fully replicated
	if RunService:IsRunning() and RunService:IsClient() and not isPlugin(context) and not game:IsLoaded() then
		game.Loaded:Wait()
	end

	local object = context
	repeat
		local nodeModulesFolder = object:FindFirstChild(NODE_MODULES)
		if nodeModulesFolder then
			local scopeFolder = nodeModulesFolder:FindFirstChild(scope)
			if scopeFolder then
				local module = scopeFolder:FindFirstChild(moduleName)
				if module then
					return module
				end
			end
		end
		object = object.Parent
	until object == nil

	error(OUTPUT_PREFIX .. "Could not find module: " .. moduleName, 2)
end

-- This is a hash which TS.import uses as a kind of linked-list-like history of [Script who Loaded] -> Library
local currentlyLoading = {}
local registeredLibraries = {}

function TS.import(context, module, ...)
	for i = 1, select("#", ...) do
		module = module:WaitForChild((select(i, ...)))
	end

	if module.ClassName ~= "ModuleScript" then
		error(OUTPUT_PREFIX .. "Failed to import! Expected ModuleScript, got " .. module.ClassName, 2)
	end

	currentlyLoading[context] = module

	-- Check to see if a case like this occurs:
	-- module -> Module1 -> Module2 -> module

	-- WHERE currentlyLoading[module] is Module1
	-- and currentlyLoading[Module1] is Module2
	-- and currentlyLoading[Module2] is module

	local currentModule = module
	local depth = 0

	while currentModule do
		depth = depth + 1
		currentModule = currentlyLoading[currentModule]

		if currentModule == module then
			local str = currentModule.Name -- Get the string traceback

			for _ = 1, depth do
				currentModule = currentlyLoading[currentModule]
				str = str .. "  ⇒ " .. currentModule.Name
			end

			error(OUTPUT_PREFIX .. "Failed to import! Detected a circular dependency chain: " .. str, 2)
		end
	end

	if not registeredLibraries[module] then
		if _G[module] then
			error(
				OUTPUT_PREFIX
				.. "Invalid module access! Do you have multiple TS runtimes trying to import this? "
				.. module:GetFullName(),
				2
			)
		end

		_G[module] = TS
		registeredLibraries[module] = true -- register as already loaded for subsequent calls
	end

	local data = require(module)

	if currentlyLoading[context] == module then -- Thread-safe cleanup!
		currentlyLoading[context] = nil
	end

	return data
end

function TS.instanceof(obj, class)
	-- custom Class.instanceof() check
	if type(class) == "table" and type(class.instanceof) == "function" then
		return class.instanceof(obj)
	end

	-- metatable check
	if type(obj) == "table" then
		obj = getmetatable(obj)
		while obj ~= nil do
			if obj == class then
				return true
			end
			local mt = getmetatable(obj)
			if mt then
				obj = mt.__index
			else
				obj = nil
			end
		end
	end

	return false
end

function TS.async(callback)
	return function(...)
		local n = select("#", ...)
		local args = { ... }
		return Promise.new(function(resolve, reject)
			local ok, result = pcall(callback, unpack(args, 1, n))
			if ok then
				resolve(result)
			else
				reject(result)
			end
		end)
	end
end

function TS.await(promise)
	if not Promise.is(promise) then
		return promise
	end

	local status, value = promise:awaitStatus()
	if status == Promise.Status.Resolved then
		return value
	elseif status == Promise.Status.Rejected then
		error(value, 2)
	else
		error("The awaited Promise was cancelled", 2)
	end
end

local SIGN = 2 ^ 31
local COMPLEMENT = 2 ^ 32
local function bit_sign(num)
	-- Restores the sign after an unsigned conversion according to 2s complement.
	if bit32.btest(num, SIGN) then
		return num - COMPLEMENT
	else
		return num
	end
end

function TS.bit_lrsh(a, b)
	return bit_sign(bit32.arshift(a, b))
end

TS.TRY_RETURN = 1
TS.TRY_BREAK = 2
TS.TRY_CONTINUE = 3

function TS.try(try, catch, finally)
	-- execute try
	local trySuccess, exitTypeOrTryError, returns = pcall(try)
	local exitType, tryError
	if trySuccess then
		exitType = exitTypeOrTryError
	else
		tryError = exitTypeOrTryError
	end

	local catchSuccess = true
	local catchError

	-- if try block failed, and catch block exists, execute catch
	if not trySuccess and catch then
		local newExitTypeOrCatchError, newReturns
		catchSuccess, newExitTypeOrCatchError, newReturns = pcall(catch, tryError)
		local newExitType
		if catchSuccess then
			newExitType = newExitTypeOrCatchError
		else
			catchError = newExitTypeOrCatchError
		end

		if newExitType then
			exitType, returns = newExitType, newReturns
		end
	end

	-- execute finally
	if finally then
		local newExitType, newReturns = finally()
		if newExitType then
			exitType, returns = newExitType, newReturns
		end
	end

	-- if exit type is a control flow, do not rethrow errors
	if exitType ~= TS.TRY_RETURN and exitType ~= TS.TRY_BREAK and exitType ~= TS.TRY_CONTINUE then
		-- if catch block threw an error, rethrow it
		if not catchSuccess then
			error(catchError, 2)
		end

		-- if try block threw an error and there was no catch block, rethrow it
		if not trySuccess and not catch then
			error(tryError, 2)
		end
	end

	return exitType, returns
end

function TS.generator(callback)
	local co = coroutine.create(callback)
	return {
		next = function(...)
			if coroutine.status(co) == "dead" then
				return { done = true }
			else
				local success, value = coroutine.resume(co, ...)
				if success == false then
					error(value, 2)
				end
				return {
					value = value,
					done = coroutine.status(co) == "dead",
				}
			end
		end,
	}
end

return TS


import ts from "typescript";

const SCOPE = "@rbxts/";
const PACKAGE = "types";

export function transformTypeReferenceDirectives() {
	return (sourceFile: ts.SourceFile | ts.Bundle) => {
		if (ts.isSourceFile(sourceFile)) {
			for (const typeReferenceDirective of sourceFile.typeReferenceDirectives) {
				if (typeReferenceDirective.fileName === PACKAGE) {
					typeReferenceDirective.fileName = SCOPE + PACKAGE;
					typeReferenceDirective.end += SCOPE.length;
				}
			}
		}
		return sourceFile;
	};
}


import { ProjectError } from "Shared/errors/ProjectError";
import { assert } from "Shared/util/assert";
import { CALL_MACROS } from "TSTransformer/macros/callMacros";
import { CONSTRUCTOR_MACROS } from "TSTransformer/macros/constructorMacros";
import { IDENTIFIER_MACROS } from "TSTransformer/macros/identifierMacros";
import { PROPERTY_CALL_MACROS } from "TSTransformer/macros/propertyCallMacros";
import { CallMacro, ConstructorMacro, IdentifierMacro, PropertyCallMacro } from "TSTransformer/macros/types";
import { skipUpwards } from "TSTransformer/util/traversal";
import ts from "typescript";

function getType(typeChecker: ts.TypeChecker, node: ts.Node) {
	return typeChecker.getTypeAtLocation(skipUpwards(node));
}

const TYPES_NOTICE = "\nYou may need to update your @rbxts/compiler-types!";

export const SYMBOL_NAMES = {
	globalThis: "globalThis",

	ArrayConstructor: "ArrayConstructor",
	SetConstructor: "SetConstructor",
	MapConstructor: "MapConstructor",
	WeakSetConstructor: "WeakSetConstructor",
	WeakMapConstructor: "WeakMapConstructor",
	ReadonlyMapConstructor: "ReadonlyMapConstructor",
	ReadonlySetConstructor: "ReadonlySetConstructor",

	Array: "Array",
	Generator: "Generator",
	IterableFunction: "IterableFunction",
	LuaTuple: "LuaTuple",
	Map: "Map",
	Object: "Object",
	ReadonlyArray: "ReadonlyArray",
	ReadonlyMap: "ReadonlyMap",
	ReadonlySet: "ReadonlySet",
	ReadVoxelsArray: "ReadVoxelsArray",
	Set: "Set",
	SharedTable: "SharedTable",
	String: "String",
	TemplateStringsArray: "TemplateStringsArray",
	WeakMap: "WeakMap",
	WeakSet: "WeakSet",

	Iterable: "Iterable",

	$range: "$range",
	$tuple: "$tuple",
} as const;

export const NOMINAL_LUA_TUPLE_NAME = "_nominal_LuaTuple";

const MACRO_ONLY_CLASSES = new Set<string>([
	SYMBOL_NAMES.ReadonlyArray,
	SYMBOL_NAMES.Array,
	SYMBOL_NAMES.ReadonlyMap,
	SYMBOL_NAMES.WeakMap,
	SYMBOL_NAMES.Map,
	SYMBOL_NAMES.ReadonlySet,
	SYMBOL_NAMES.WeakSet,
	SYMBOL_NAMES.Set,
	SYMBOL_NAMES.String,
]);

function getFirstDeclarationOrThrow<T extends ts.Node>(symbol: ts.Symbol, check: (value: ts.Node) => value is T): T {
	for (const declaration of symbol.declarations ?? []) {
		if (check(declaration)) {
			return declaration;
		}
	}
	throw new ProjectError("");
}

function getGlobalSymbolByNameOrThrow(typeChecker: ts.TypeChecker, name: string, meaning: ts.SymbolFlags) {
	const symbol = typeChecker.resolveName(name, undefined, meaning, false);
	if (symbol) {
		return symbol;
	}
	throw new ProjectError(`MacroManager could not find symbol for ${name}` + TYPES_NOTICE);
}

function getConstructorSymbol(node: ts.InterfaceDeclaration) {
	for (const member of node.members) {
		if (ts.isConstructSignatureDeclaration(member)) {
			assert(member.symbol);
			return member.symbol;
		}
	}
	throw new ProjectError(`MacroManager could not find constructor for ${node.name.text}` + TYPES_NOTICE);
}

/**
 * Manages the macros of the ts.
 */
export class MacroManager {
	private symbols = new Map<string, ts.Symbol>();
	private identifierMacros = new Map<ts.Symbol, IdentifierMacro>();
	private callMacros = new Map<ts.Symbol, CallMacro>();
	private constructorMacros = new Map<ts.Symbol, ConstructorMacro>();
	private propertyCallMacros = new Map<ts.Symbol, PropertyCallMacro>();

	constructor(typeChecker: ts.TypeChecker) {
		for (const [name, macro] of Object.entries(IDENTIFIER_MACROS)) {
			const symbol = getGlobalSymbolByNameOrThrow(typeChecker, name, ts.SymbolFlags.Variable);
			this.identifierMacros.set(symbol, macro);
		}

		for (const [name, macro] of Object.entries(CALL_MACROS)) {
			const symbol = getGlobalSymbolByNameOrThrow(typeChecker, name, ts.SymbolFlags.Function);
			this.callMacros.set(symbol, macro);
		}

		for (const [className, macro] of Object.entries(CONSTRUCTOR_MACROS)) {
			const symbol = getGlobalSymbolByNameOrThrow(typeChecker, className, ts.SymbolFlags.Interface);
			const interfaceDec = getFirstDeclarationOrThrow(symbol, ts.isInterfaceDeclaration);
			const constructSymbol = getConstructorSymbol(interfaceDec);
			this.constructorMacros.set(constructSymbol, macro);
		}

		for (const [className, methods] of Object.entries(PROPERTY_CALL_MACROS)) {
			const symbol = getGlobalSymbolByNameOrThrow(typeChecker, className, ts.SymbolFlags.Interface);

			const methodMap = new Map<string, ts.Symbol>();
			for (const declaration of symbol.declarations ?? []) {
				if (ts.isInterfaceDeclaration(declaration)) {
					for (const member of declaration.members) {
						if (ts.isMethodSignature(member) && ts.isIdentifier(member.name)) {
							const symbol = getType(typeChecker, member).symbol;
							assert(symbol);
							methodMap.set(member.name.text, symbol);
						}
					}
				}
			}

			for (const [methodName, macro] of Object.entries(methods)) {
				const methodSymbol = methodMap.get(methodName);
				if (!methodSymbol) {
					throw new ProjectError(
						`MacroManager could not find method for ${className}.${methodName}` + TYPES_NOTICE,
					);
				}
				this.propertyCallMacros.set(methodSymbol, macro);
			}
		}

		for (const symbolName of Object.values(SYMBOL_NAMES)) {
			const symbol = typeChecker.resolveName(symbolName, undefined, ts.SymbolFlags.All, false);
			if (symbol) {
				this.symbols.set(symbolName, symbol);
			} else {
				throw new ProjectError(`MacroManager could not find symbol for ${symbolName}` + TYPES_NOTICE);
			}
		}

		const luaTupleTypeDec = this.symbols
			.get(SYMBOL_NAMES.LuaTuple)
			?.declarations?.find(v => ts.isTypeAliasDeclaration(v));
		if (luaTupleTypeDec) {
			const nominalLuaTupleSymbol = typeChecker
				.getTypeAtLocation(luaTupleTypeDec)
				.getProperty(NOMINAL_LUA_TUPLE_NAME);
			if (nominalLuaTupleSymbol) {
				this.symbols.set(NOMINAL_LUA_TUPLE_NAME, nominalLuaTupleSymbol);
			}
		}
	}

	public getSymbolOrThrow(name: string) {
		const symbol = this.symbols.get(name);
		assert(symbol);
		return symbol;
	}

	public isMacroOnlyClass(symbol: ts.Symbol) {
		return this.symbols.get(symbol.name) === symbol && MACRO_ONLY_CLASSES.has(symbol.name);
	}

	public getIdentifierMacro(symbol: ts.Symbol) {
		return this.identifierMacros.get(symbol);
	}

	public getCallMacro(symbol: ts.Symbol) {
		return this.callMacros.get(symbol);
	}

	public getConstructorMacro(symbol: ts.Symbol) {
		return this.constructorMacros.get(symbol);
	}

	public getPropertyCallMacro(symbol: ts.Symbol) {
		const macro = this.propertyCallMacros.get(symbol);
		if (
			!macro &&
			symbol.parent &&
			this.symbols.get(symbol.parent.name) === symbol.parent &&
			this.isMacroOnlyClass(symbol.parent)
		) {
			assert(false, `Macro ${symbol.parent.name}.${symbol.name}() is not implemented!`);
		}
		return macro;
	}
}


local roblox = require("@lune/roblox")
local fs = require("@lune/fs")
local luau = require("@lune/luau")
local process = require("@lune/process")
local stdio = require("@lune/stdio")
local task = require("@lune/task")

local testPlacePath = process.args[1]

local game = roblox.deserializePlace(fs.readFile(testPlacePath))

local function tableJoin(...)
	local result = {}
	for i = 1, select("#", ...) do
		for k, v in select(i, ...) do
			result[k] = v
		end
	end
	return result
end

-- not 100% accurate to tick() functionality, but good enough for TestEZ usage
local function tick()
	return os.clock()
end

-- roblox.spec.ts assumes Workspace already exists
game:GetService("Workspace")

-- RuntimeLib uses :WaitForChild(), but tests don't need networking so :FindFirstChild() should be fine
roblox.implementMethod("Instance", "WaitForChild", function(self, ...)
	return self:FindFirstChild(...)
end)

-- TestEZ uses TestService:Error() when tests fail
roblox.implementMethod("TestService", "Error", function(self, description: string, source: Instance?, line: number?)
	stdio.ewrite(`{description}\n`)
end)

-- Promise.lua indexes RunService.Heartbeat, but only uses it in Promise.defer and Promise.delay
roblox.implementProperty("RunService", "Heartbeat", function()
	return {}
end, function() end)

local SharedTable = {}

function SharedTable.new()
	return setmetatable({
		items = {},
	}, {
		__index = function(self, key)
			return self.items[key]
		end,
		__newindex = function(self, key, value)
			self.items[key] = value
		end,
		__iter = function(self)
			return next, self.items
		end,
	})
end

local robloxRequire

local function runRobloxScript(script: LuaSourceContainer)
	local callableFn = luau.load(luau.compile(script.Source), {
		debugName = script:GetFullName(),
		environment = tableJoin(roblox, {
			game = game,
			script = script,
			require = robloxRequire,
			tick = tick,
			task = task,
			SharedTable = SharedTable,
		}),
	})

	return callableFn()
end

local requireCache = {}

function robloxRequire(moduleScript: ModuleScript)
	-- the same script instance sometimes gives a different ref
	-- unsure why, but using :GetFullName() fixes this for now
	local scriptPath = moduleScript:GetFullName()
	local cached = requireCache[scriptPath]
	if cached then
		return table.unpack(cached)
	end

	local result = table.pack(runRobloxScript(moduleScript))
	requireCache[scriptPath] = result
	return table.unpack(result)
end

runRobloxScript(game.ServerScriptService.main)


import Roact from "@rbxts/roact";

interface ComponentProps extends Array<number> {}

function Component(props: ComponentProps) {
	return <frame />;
}

const e = <Component {...[123]} />;


/// <reference types="@rbxts/testez/globals" />

import { ServerScriptService } from "@rbxts/services";
import TestEZ from "@rbxts/testez";

const results = TestEZ.TestBootstrap.run([ServerScriptService.tests]);
if (results.errors.size() > 0 || results.failureCount > 0) {
	error("Tests failed!");
}


/**
 * This file contains preludes for the transformer.
 *
 * This is primarily necessary due to a bug with the `@rbxts/t` package.
 */

import { t } from "@rbxts/t";

export { t };


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")

local TestEZ = require(ReplicatedStorage.rbxts_include.node_modules["@rbxts"].testez.src)

TestEZ.TestBootstrap:run({
	ServerScriptService.TS.test,
	-- ReplicatedStorage.TS.test,
}, TestEZ.Reporters.TextReporter)


import { Controller, OnStart } from "@flamework/core";
import React from "@rbxts/react";
import { ReflexProvider } from "@rbxts/react-reflex";
import { createPortal, createRoot } from "@rbxts/react-roblox";
import { Players } from "@rbxts/services";
import { store } from "client/store";
import HolderApp from "client/ui/apps/holder/holder-app";
import ScreenGui from "client/ui/components/scaledGui";

@Controller({})
export class GuiController implements OnStart {
	private playerGui = Players.LocalPlayer.WaitForChild("PlayerGui");

	onStart () {
		const root = createRoot( new Instance( "Folder" ) )
		root.render( createPortal( <ReflexProvider producer={store}>
			<ScreenGui>
				<HolderApp />
			</ScreenGui>
		</ReflexProvider>
			, this.playerGui ) )
	}
}


import { InferState, combineProducers } from "@rbxts/reflex";
import { slices } from "shared/store";
import { receiverMiddleware } from "./middleware/receiver";

export type RootStore = typeof store;

export type RootState = InferState<RootStore>;

export function createStore() {
	const store = combineProducers({
		...slices,
	});

	store.applyMiddleware(receiverMiddleware());

	return store;
}

export const store = createStore();


import { ProducerMiddleware, createBroadcastReceiver } from "@rbxts/reflex";
import { Events } from "client/network";

export function receiverMiddleware(): ProducerMiddleware {
	const receiver = createBroadcastReceiver({
		start: () => {
			Events.reflex.start.fire();
		},
	});

	Events.reflex.dispatch.connect((actions) => receiver.dispatch(actions));

	return receiver.middleware;
}


import { HolderPage, ImageName } from "shared/configs/Gui";
import React from "@rbxts/react";
import Frame from "client/ui/components/frame";
import Object from "@rbxts/object-utils";
import Button from "./components/button";
import { store } from "client/store";
import { selectHolderPage } from "shared/store/selectors/client";

const BUTTONS: Partial<Record<HolderPage, ImageName>> = {
    Settings: "Settings"
}

export default function ButtonsApp () {

    const buttons = Object.keys( BUTTONS ).map( ( name ) => {
        return <Button button={name} image={BUTTONS[name]!} click={() => {
            const isCurrentPage = store.getState( selectHolderPage ) === name;
            store.setHolderPage( isCurrentPage ? undefined : name )
        }} />
    } )

    return (
        <Frame
            key="Buttons"
            layoutOrder={1}
            backgroundTransparency={1}
            size={new UDim2( 0, 240, 0, 160 )}
            automaticSize={Enum.AutomaticSize.Y}
        >
            <uigridlayout
                CellPadding={new UDim2( 0, 15, 0, 15 )}
                CellSize={new UDim2( 0, 70, 0, 70 )}
                SortOrder={Enum.SortOrder.LayoutOrder}
                VerticalAlignment={Enum.VerticalAlignment.Top}
                HorizontalAlignment={Enum.HorizontalAlignment.Right}
            />

            {
                buttons
            }
        </Frame >
    );
}


import React from "@rbxts/react"
import ImageLabel from "client/ui/components/imageLabel"
import TextButton from "client/ui/components/textButton"
import TextLabel from "client/ui/components/textLabel"
import { COLORS, ImageName } from "shared/configs/Gui"

interface Props {
    button: string,
    image: ImageName,
    click: () => void
}

export default function Button ( props: Props ) {
    return (
        <TextButton
            key={props.button}
            backgroundColor3={COLORS.PrimaryBackground}
            onClick={props.click}
            uiCornerSize={new UDim( 0, 35 )}
            uiStrokeSize={4}
        >
            <ImageLabel
                image={props.image}
                position={new UDim2( 0.5, 0, .5, 0 )}
                anchorPoint={new Vector2( 0.5, .5 )}
                size={new UDim2( 0.85, 0, 0.85, 0 )}
            />
            <TextLabel
                anchorPoint={new Vector2( 0.5, 1 )}
                position={new UDim2( 0.5, 0, 1, 10 )}
                size={new UDim2( 1, 0, 0, 20 )}
                text={props.button}
                textSize={20}
                uiStrokeSize={2}
            />
        </TextButton >
    )
}


import { useSelectorCreator } from "@rbxts/react-reflex";
import React from "@rbxts/react";
import { Players } from "@rbxts/services";
import Frame from "client/ui/components/frame";
import ImageLabel from "client/ui/components/imageLabel";
import TextLabel from "client/ui/components/textLabel";
import { TextLabelButton } from "client/ui/components/textLabelButton";
import { Currency } from "shared/configs/Currency";
import { selectPlayerBalance } from "shared/store/selectors/players";
import { COLORS } from "shared/configs/Gui";
import { GetStatePlayerId } from "client/ui/utils/GetStatePlayerId";

interface Props extends React.PropsWithChildren {
	currency: Currency;
}

export default function CurrencyFrame ( props: Props ) {
	const balance: number = useSelectorCreator( selectPlayerBalance, GetStatePlayerId(), props.currency )!;

	return (
		<Frame
			key={props.currency}
			size={new UDim2( 0, 265, 0, 65 )}
			uiCornerSize={new UDim( 0, 25 )}
		>
			<frame
				key="Holder"
				Size={new UDim2( 1, 0, 1, 0 )}
				BackgroundTransparency={1}
			>
				<uilistlayout
					FillDirection={Enum.FillDirection.Horizontal}
					HorizontalAlignment={Enum.HorizontalAlignment.Right}
					VerticalAlignment={Enum.VerticalAlignment.Center}
					Padding={new UDim( 0, 10 )}
					SortOrder={Enum.SortOrder.LayoutOrder}
				/>
				<uipadding
					PaddingRight={new UDim( 0, 5 )}
				/>
				<ImageLabel
					key="CurrencyIcon"
					image={props.currency}
					position={new UDim2( 0, 206, 0, 8 )}
					size={new UDim2( 0, 50, 0, 50 )}
					layoutOrder={1}
				/>
				<TextLabel
					key={`CurrencyAmount`}
					text={tostring( balance )}
					textSize={30}
					textXAlignment={Enum.TextXAlignment.Right}
					automaticSize={Enum.AutomaticSize.X}
				/>
			</frame>

			<TextLabelButton
				key="BuyCurrency"
				text="+"
				textSize={50}
				backgroundColor3={COLORS.Buttons.On}
				position={new UDim2( 0, 0, 0.5, 0 )}
				textPosition={new UDim2( 0.5, -1, 0.5, -4 )}
				size={new UDim2( 0, 40, 0, 40 )}
				buttonUiStrokeSize={4}
				uiCornerSize={new UDim( 0, 25 )}
				labelUiStrokeSize={3}
			/>
		</Frame>
	);
}


import ScreenGui from "client/ui/components/scaledGui";
import CurrencyFrame from "./components/currency-frame";
import { CURRENCIES } from "shared/configs/Currency";
import { store } from "client/store";
import React from "@rbxts/react";
import { ReflexProvider } from "@rbxts/react-reflex";
import Frame from "client/ui/components/frame";

export default function CurrencyApp () {

	const currencyFrames = CURRENCIES.map( ( currency ) => {
		return <CurrencyFrame currency={currency} />;
	})

	return (
		<Frame
			key="Currency"
			backgroundTransparency={1}
			automaticSize={Enum.AutomaticSize.XY}
			layoutOrder={0}
		>

			{
				currencyFrames
			}

			<uilistlayout Padding={new UDim( 0, 15 )} />
		</Frame>
	);
}


import React, { useEffect, useState } from "@rbxts/react";
import Frame from "client/ui/components/frame";
import SettingsApp from "../settings/settings-app";
import { useSelector } from "@rbxts/react-reflex";
import CurrencyApp from "../currency/currency-app";
import { HOLDER_PAGES } from "shared/configs/Gui";
import { store } from "client/store";
import { RunService, StarterGui } from "@rbxts/services";
import { Events } from "client/network";
import { selectHolderPage } from "shared/store/selectors/client";
import ButtonsApp from "../buttons/buttons-app";
import TextButton from "client/ui/components/textButton";

export default function HolderApp () {
	const page = useSelector( selectHolderPage )

	function getApp () {
        if ( page === "Settings" ) {
            return <SettingsApp />
		}
	}

	function cyclePage ( direction: "next" | "previous" ) {
		if ( !page ) return

		const index = HOLDER_PAGES.indexOf( page )
		const nextIndex = ( index + 1 ) % HOLDER_PAGES.size()
		const previousIndex = ( index - 1 + HOLDER_PAGES.size() ) % HOLDER_PAGES.size()

		if ( direction === "next" ) {
			store.setHolderPage( HOLDER_PAGES[nextIndex] )
		} else {
			store.setHolderPage( HOLDER_PAGES[previousIndex] )
		}
	}

	function getCyclePage ( direction: "next" | "previous" ) {
		if ( !page ) return HOLDER_PAGES[0]

        const index = HOLDER_PAGES.indexOf( page )
        const nextIndex = ( index + 1 ) % HOLDER_PAGES.size()
        const previousIndex = ( index - 1 + HOLDER_PAGES.size() ) % HOLDER_PAGES.size()

        if ( direction === "next" ) {
            return HOLDER_PAGES[nextIndex]
        } else {
            return HOLDER_PAGES[previousIndex]
        }
	}
	return (
		<frame
			AnchorPoint={new Vector2( 0.5, 0.5 )}
			Position={new UDim2( 0.5, 0, 0.5, 0 )}
			Size={new UDim2( 1, 0, 1, 0 )}
			BackgroundTransparency={1}
		>
			{getApp()}

			<Frame
				anchorPoint={new Vector2( 1, .5 )}
				position={new UDim2( 1, -10, .5, 0 )}
				size={new UDim2( 0, 265, 0, 65 )}
				backgroundTransparency={1}
				automaticSize={Enum.AutomaticSize.XY}
				uiStrokeSize={0}
			>
				<uilistlayout
					Padding={new UDim( 0, 25 )}
					HorizontalAlignment={Enum.HorizontalAlignment.Center}
					SortOrder={Enum.SortOrder.LayoutOrder}
				/>

				<CurrencyApp />
				<ButtonsApp />

			</Frame>

			{RunService.IsStudio() && !RunService.IsRunMode() && (
				<TextButton
					text={`Next: ${getCyclePage( "next" )}`}
					size={new UDim2( 0, 200, 0, 65 )}
					textSize={24}
					position={new UDim2( 0, 0, 0, 70 )}
					anchorPoint={new Vector2( 0, 0 )}
					onClick={() => cyclePage( "next" )}
				/>
			)}
			{RunService.IsStudio() && !RunService.IsRunMode() && (
				<TextButton
					text={`Previous: ${getCyclePage( "previous" )}`}
					size={new UDim2( 0, 200, 0, 65 )}
					textSize={24}
					position={new UDim2( 0, 0, 0, 0 )}
					anchorPoint={new Vector2( 0, 0 )}
					onClick={() => cyclePage( "previous" )}
				/>
			)}

		</frame>
	)
}


import { useSelector } from "@rbxts/react-reflex";
import React from "@rbxts/react";
import { Events } from "client/network";
import TextLabel from "client/ui/components/textLabel";
import { Setting } from "shared/configs/Settings";
import { Players, RunService } from "@rbxts/services";
import { selectPlayerSetting } from "shared/store/selectors/players";
import { GetStatePlayerId } from "client/ui/utils/GetStatePlayerId";
import { store } from "client/store";
import { COLORS } from "shared/configs/Gui";
import TextButton from "client/ui/components/textButton";

interface Props {
    setting: Setting
}

export default function SettingButton ( props: Props ) {
    const value = useSelector( selectPlayerSetting( GetStatePlayerId(), props.setting ) )

    const click = () => {
        const isInGame = RunService.IsRunning();
        if ( isInGame )
            Events.toggleSetting( props.setting );
        else
            store.toggleSetting( GetStatePlayerId(), props.setting );
    }

    return (
        <TextButton
            key={props.setting}
            backgroundColor3={Color3.fromRGB( 0, 137, 215 )}
            onClick={click}
            uiStrokeSize={0}
            uiCornerSize={new UDim( 0, 10 )}
        >
            <TextButton
                key="Toggle"
                backgroundColor3={value ? COLORS.Buttons.On : COLORS.Buttons.Off}
                position={new UDim2( 0, 480, 0, 18 )}
                size={new UDim2( 0, 30, 0, 30 )}
                layoutOrder={-1}
                onClick={click}
                uiCornerSize={new UDim( 0, 10 )}
                uiStrokeSize={3}
            />

            <TextLabel
                key={props.setting}
                text={props.setting}
                textSize={32}
                automaticSize={Enum.AutomaticSize.XY}
                textXAlignment={Enum.TextXAlignment.Left}
                uiStrokeSize={3}
            />

            <uistroke
                ApplyStrokeMode={Enum.ApplyStrokeMode.Border}
                Color={Color3.fromRGB( 0, 52, 82 )}
                Thickness={3}
            />
            <uilistlayout
                FillDirection={Enum.FillDirection.Horizontal}
                HorizontalAlignment={Enum.HorizontalAlignment.Left}
                SortOrder={Enum.SortOrder.LayoutOrder}
                VerticalAlignment={Enum.VerticalAlignment.Center}
                Padding={new UDim( 0, 10 )}
            />
            <uipadding
                PaddingLeft={new UDim( 0, 10 )}
            />

        </TextButton>
    );
}


import React from "@rbxts/react";
import ContainerFrame from "client/ui/components/containerFrame";
import Exit from "client/ui/components/exit";
import Frame from "client/ui/components/frame";
import Title from "client/ui/components/title";
import { SETTINGS } from "shared/configs/Settings";
import SettingButton from "./components/setting-button";

interface Props {

}

export default function SettingsApp ( props: Props ) {

    const settingButtons = SETTINGS.map( setting => {
        return <SettingButton setting={setting} />;
    } )

    return (
        <Frame
            key={"Settings"}
            anchorPoint={new Vector2( 0.5, 0.5 )}
            position={new UDim2( 0.5, 0, 0.5, 0 )}
            size={new UDim2( 0, 600, 0, 600 )}
        >
            <Title
                text="Settings"
                anchorPoint={new Vector2( 0.5, 0 )}
                position={new UDim2( 0.5, 0, 0, 20 )}
                size={new UDim2( 0, 366, 0, 73 )}
            />

            <Exit
                size={new UDim2( 0, 65, 0, 65 )}
                position={new UDim2( 1, 30, 0, -30 )}
                anchorPoint={new Vector2( 1, 0 )}
            />

            <ContainerFrame
                anchorPoint={new Vector2( 0.5, 0 )}
                position={new UDim2( 0.5, 0, 0, 120 )}
                size={new UDim2( 0, 561, 0, 441 )}

                cellPadding={new UDim2( 0, 0, 0, 15 )}
                cellSize={new UDim2( 0.95, 0, 0, 65 )}
                horizontalAlignment={Enum.HorizontalAlignment.Center}

                paddingTop={new UDim( 0, 10 )}
                paddingBottom={new UDim( 0, 10 )}
            >

                {
                    settingButtons
                }

            </ContainerFrame>
        </Frame>
    );
}


import React from "@rbxts/react";

interface Props extends React.PropsWithChildren {
    anchorPoint?: Vector2;
    position?: UDim2;
    size: UDim2;
    scrollBarImageColor3?: Color3;
    scrollBarThickness?: number;
    automaticCanvasSize?: Enum.AutomaticSize;
    backgroundColor3?: Color3;
    backgroundTransparency?: number;
    layoutOrder?: number;

    cellPadding?: UDim2;
    cellSize?: UDim2;
    horizontalAlignment?: Enum.HorizontalAlignment;
    verticalAlignment?: Enum.VerticalAlignment;

    paddingTop?: UDim;
    paddingBottom?: UDim;
    paddingLeft?: UDim;
    paddingRight?: UDim;
    allPadding?: UDim;
}

export default function ContainerFrame ( props: Props ) {
    if ( props.allPadding ) {
        props.paddingTop = props.allPadding;
        props.paddingBottom = props.allPadding;
        props.paddingLeft = props.allPadding;
        props.paddingRight = props.allPadding;
    }

    if ( props.backgroundTransparency === undefined ) {
        props.backgroundTransparency = props.backgroundColor3 ? 0 : 1;
    }

    return (
        <scrollingframe
            key="Container"
            AnchorPoint={props.anchorPoint}
            AutomaticCanvasSize={props.automaticCanvasSize ?? Enum.AutomaticSize.Y}
            BackgroundTransparency={props.backgroundTransparency}
            BackgroundColor3={props.backgroundColor3}
            BorderSizePixel={0}
            CanvasSize={new UDim2( 0, 0, 0, 0 )}
            Position={props.position}
            ScrollBarImageColor3={props.scrollBarImageColor3 ?? Color3.fromRGB( 0, 44, 68 )}
            ScrollBarThickness={props.scrollBarThickness ?? 6}
            Size={props.size}
            VerticalScrollBarInset={Enum.ScrollBarInset.ScrollBar}
            LayoutOrder={props.layoutOrder}
        >
            {( props.cellPadding || props.cellSize ) &&
                <uigridlayout
                    CellPadding={props.cellPadding}
                    CellSize={props.cellSize}
                    HorizontalAlignment={props.horizontalAlignment}
                    VerticalAlignment={props.verticalAlignment}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                />
            }
            <uipadding
                PaddingBottom={props.paddingBottom}
                PaddingLeft={props.paddingLeft}
                PaddingRight={props.paddingRight}
                PaddingTop={props.paddingTop}
            />

            {props["children"]}

        </scrollingframe>
    );
}


import React from "@rbxts/react";
import { store } from "client/store";
import { COLORS } from "shared/configs/Gui";

interface Props extends React.PropsWithChildren {
	uiStrokeSize?: number;
	position?: UDim2;
	anchorPoint?: Vector2;
	size: UDim2;
}

export default function Exit ( props: Props ) {
	return (
		<textbutton
			key="Exit"
			BackgroundColor3={COLORS.Buttons.Off}
			BorderSizePixel={0}
			AnchorPoint={props.anchorPoint}
			Position={props.position}
			Size={props.size}
			Text={""}
			Event={{
				MouseButton1Click: () => {
					store.setHolderPage();
				}
			}}
		>
			<textlabel
				key="X"
				BackgroundTransparency={1}
				Font={Enum.Font.FredokaOne}
				AnchorPoint={new Vector2( 0.5, 0.5 )}
				Position={new UDim2( 0.5, 0, 0.5, 0 )}
				Size={new UDim2( 1, 0, 1, 0 )}
				Text="X"
				TextColor3={COLORS.White}
				TextSize={38}
			>
				<uistroke Thickness={5} />
			</textlabel>
			<uistroke
				ApplyStrokeMode={Enum.ApplyStrokeMode.Border}
				Thickness={props.uiStrokeSize ?? 5}
			/>
			<uicorner CornerRadius={new UDim( 0, 18 )} />
		</textbutton>
	);
}


import React, { useEffect, useState } from "@rbxts/react";
import { TweenService } from "@rbxts/services";

interface Props extends React.PropsWithChildren {
    position?: UDim2 | React.Binding<UDim2>;
    anchorPoint?: Vector2;
    size?: UDim2;
    backgroundTransparency?: number;
    backgroundColor3?: Color3;
    automaticSize?: Enum.AutomaticSize;
    layoutOrder?: number;

    uiStrokeSize?: number;
    uiCornerSize?: UDim;

    zIndex?: number;
}

export default function Frame ( props: Props ) {
    props.backgroundTransparency = props.backgroundTransparency ?? 0; // Do this to make Stroke condition simpler

    return (
        <frame
            BackgroundColor3={props.backgroundColor3 ?? Color3.fromRGB( 0, 163, 255 )}
            BackgroundTransparency={props.backgroundTransparency ?? 0}
            BorderSizePixel={0}
            AnchorPoint={props.anchorPoint ?? new Vector2( 0.5, 0.5 )}
            Position={props.position ?? new UDim2( 0.5, 0, 0.5, 0 )}
            Size={props.size ?? new UDim2( 0,0,0,0 )}
            AutomaticSize={props.automaticSize ?? Enum.AutomaticSize.None}
            ZIndex={props.zIndex}
            LayoutOrder={props.layoutOrder}
        >

            {props.children}

            { props.backgroundTransparency === 0 && <uistroke Thickness={props.uiStrokeSize ?? 5} />}
            <uicorner CornerRadius={props.uiCornerSize ?? new UDim( 0, 22 )} />
        </frame>
    );
}


import React, { useEffect, useState } from "@rbxts/react";
import { IMAGES, ImageName } from "shared/configs/Gui";

interface Props extends React.PropsWithChildren {
    image?: ImageName;
    position?: UDim2 | React.Binding<UDim2>;
    anchorPoint?: Vector2;
    size?: UDim2;
    backgroundTransparency?: number;
    backgroundColor3?: Color3;
    layoutOrder?: number;
    zIndex?: number;
    visible?: boolean;
}

export default function ImageLabel ( props: Props ) {
    return (
        <imagelabel
            ScaleType={Enum.ScaleType.Fit}
            Position={props.position}
            AnchorPoint={props.anchorPoint}
            Size={props.size ?? new UDim2( 1, 0, 1, 0 )}
            BackgroundTransparency={props.backgroundTransparency ?? 1}
            BackgroundColor3={props.backgroundColor3}
            LayoutOrder={props.layoutOrder}
            ZIndex={props.zIndex ?? 1}
            Image={`rbxassetid://${IMAGES[props.image ?? "Settings"]}`}
            Visible={props.visible ?? true}
        >
            {props["children"]}
        </imagelabel>
    );
}


import React, { createRef, useEffect } from "@rbxts/react";
import { CollectionService } from "@rbxts/services";
interface Props extends React.PropsWithChildren {

}

export default function ScreenGui(props: Props) {
	const uiScaleRef = createRef<UIScale>()

	useEffect(() => {
		const uiScale = uiScaleRef.current
		if (!uiScale) return;

		CollectionService.AddTag(uiScale, "scale_component");
		uiScale.SetAttribute("base_resolution", new Vector2(1920, 1080));
	}, []);

	return (
		<screengui
			Enabled={true}
		>
			<uiscale
				Scale={1}
				ref={uiScaleRef}
			/>

			{props["children"]}

		</screengui>
	);
}


import React, { useEffect, useState } from "@rbxts/react";
import { COLORS } from "shared/configs/Gui";

interface Props extends React.PropsWithChildren {
    key?: string;
    text?: string;
    textSize?: number;
    size?: UDim2;
    position?: UDim2;
    anchorPoint?: Vector2;
    automaticSize?: Enum.AutomaticSize;
    textColor3?: Color3;
    textXAlignment?: Enum.TextXAlignment;
    textYAlignment?: Enum.TextYAlignment;
    backgroundColor3?: Color3;
    backgroundTransparency?: number;
    layoutOrder?: number;

    uiStrokeSize?: number;
    uiCornerSize?: UDim;

    onClick?: () => void;
}

export default function TextButton ( props: Props ) {
    let automaticSize = props.automaticSize
    if ( !automaticSize ) {
        automaticSize = props.size ? Enum.AutomaticSize.None : Enum.AutomaticSize.XY;
    }

    return (
        <textbutton
            key={props.key ?? props.text}
            AutomaticSize={props.automaticSize}
            Position={props.position ?? new UDim2( 0.5, 0, 0.5, 0 )}
            AnchorPoint={props.anchorPoint ?? new Vector2( 0.5, 0.5 )}
            Size={props.size}
            Font={Enum.Font.FredokaOne}
            TextColor3={props.textColor3 ?? new Color3( 1, 1, 1 )}
            TextXAlignment={props.textXAlignment}
            TextYAlignment={props.textYAlignment}
            Text={props.text ?? ""}
            TextSize={props.textSize}
            BackgroundTransparency={props.backgroundTransparency ?? 0}
            BackgroundColor3={props.backgroundColor3 ?? COLORS.PrimaryBackground}
            LayoutOrder={props.layoutOrder}
            Event={{
                MouseButton1Click: props.onClick,
            }}
        >

            {props["children"]}

            <uistroke Thickness={props.uiStrokeSize ?? 5} ApplyStrokeMode={Enum.ApplyStrokeMode.Border} />
            <uistroke Thickness={props.uiStrokeSize ?? 5} ApplyStrokeMode={Enum.ApplyStrokeMode.Contextual} />
            <uicorner CornerRadius={props.uiCornerSize ?? new UDim( 0, 10 )} />
        </textbutton>
    );
}


import React from "@rbxts/react";

interface Props extends React.PropsWithChildren {
	key?: string;
	text: string;
	textSize: number;
	textScaled?: boolean;
	size?: UDim2;
	position?: UDim2;
	anchorPoint?: Vector2;
	automaticSize?: Enum.AutomaticSize;
	textColor3?: Color3;
	textXAlignment?: Enum.TextXAlignment;
	textYAlignment?: Enum.TextYAlignment;
	textWrap?: boolean;
	richText?: boolean;

	uiStrokeSize?: number;
	zIndex?: number;
}

export default function TextLabel ( props: Props ) {
	if ( !props.automaticSize || !props.size ) {
		props.automaticSize = props.size ? Enum.AutomaticSize.None : Enum.AutomaticSize.XY;
	}

	return (
		<textlabel
			key={props.key ?? props.text}
			AutomaticSize={props.automaticSize}
			Position={props.position ?? new UDim2( 0.5, 0, 0.5, 0 )}
			AnchorPoint={props.anchorPoint ?? new Vector2( 0.5, 0.5 )}
			Size={props.size}
			TextScaled={props.textScaled}
			Font={Enum.Font.FredokaOne}
			TextColor3={props.textColor3 ?? new Color3( 1, 1, 1 )}
			TextXAlignment={props.textXAlignment}
			TextYAlignment={props.textYAlignment}
			Text={props.text}
			TextSize={props.textSize}
			BackgroundTransparency={1}
			TextWrap={props.textWrap}
			RichText={props.richText}
			ZIndex={props.zIndex}
		>
			<uistroke Thickness={props.uiStrokeSize ?? 5} />
		</textlabel>
	);
}


import React from "@rbxts/react";
import TextButton from "./textButton";
import TextLabel from "./textLabel";

interface Props extends React.PropsWithChildren {
    key?: string;
    text?: string;
    textSize?: number;
    size?: UDim2;
    position?: UDim2;
    textPosition?: UDim2;
    textAnchorPoint?: Vector2;
    anchorPoint?: Vector2;
    automaticSize?: Enum.AutomaticSize;
    textColor3?: Color3;
    textXAlignment?: Enum.TextXAlignment;
    textYAlignment?: Enum.TextYAlignment;
    backgroundColor3?: Color3;
    backgroundTransparency?: number;
    layoutOrder?: number;

    buttonUiStrokeSize?: number;
    labelUiStrokeSize?: number;
    uiCornerSize?: UDim;

    onClick?: () => void;
}

export function TextLabelButton ( props: Props ) {
    return (
        <TextButton
            key={props.key}
            size={props.size ?? UDim2.fromScale( 1, 1 )}
            position={props.position ?? new UDim2( 0.5, 0, 0.5, 0 )}
            anchorPoint={props.anchorPoint ?? new Vector2( 0.5, 0.5 )}
            automaticSize={props.automaticSize}
            backgroundColor3={props.backgroundColor3}
            backgroundTransparency={props.backgroundTransparency}
            uiStrokeSize={props.buttonUiStrokeSize}
            uiCornerSize={props.uiCornerSize}
            onClick={props.onClick}
            layoutOrder={props.layoutOrder}
        >
            {props["children"]}

            {props.text !== undefined && <TextLabel
                text={props.text ?? ""}
                textSize={props.textSize ?? 24}
                textColor3={props.textColor3}
                textXAlignment={props.textXAlignment}
                textYAlignment={props.textYAlignment}
                uiStrokeSize={props.labelUiStrokeSize}
                position={props.textPosition}
                anchorPoint={props.textAnchorPoint}
            />
            }
        </TextButton>
    );
}


import React from "@rbxts/react";
import Frame from "./frame";
import TextLabel from "./textLabel";

interface Props extends React.PropsWithChildren {
	text: string;
	textSize?: number;
	key?: string;
	uiStrokeSize?: number;
	position?: UDim2;
	anchorPoint?: Vector2;
	size?: UDim2;
	uiCornerSize?: UDim;
	backgroundColor3?: Color3;
	layoutOrder?: number;
	zIndex?: number;
	textXAlignment?: Enum.TextXAlignment;

	uiTextStrokeSize?: number;
}

export default function Title ( props: Props ) {
	const zIndex = props.zIndex ?? 1;

	return (
		<Frame
			key="Title"
			backgroundColor3={props.backgroundColor3 ?? Color3.fromRGB( 0, 137, 215 )}
			anchorPoint={props.anchorPoint}
			position={props.position}
			size={props.size}
			layoutOrder={props.layoutOrder}
			zIndex={zIndex}
			uiCornerSize={props.uiCornerSize ?? new UDim( 0, 31 )}
			uiStrokeSize={props.uiStrokeSize ?? 0}
		>
			{props["children"]}

			<TextLabel
				key="Title"
				anchorPoint={new Vector2( 0.5, 0.5 )}
				position={new UDim2( 0.5, 0, 0.5, 0 )}
				size={new UDim2( 1, 0, 1, 0 )}
				text={props.text}
				textSize={props.textSize ?? 60}
				textXAlignment={props.textXAlignment ?? Enum.TextXAlignment.Center}
				uiStrokeSize={props.uiTextStrokeSize ?? 5}
			>
			</TextLabel>
		</Frame>
	);
}


import React from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { store } from "client/store";
import { ReflexProvider } from "@rbxts/react-reflex";
import HolderApp from "./apps/holder/holder-app";
import { HOLDER_PAGES } from "shared/configs/Gui";
import { defaultPlayerData } from "shared/store/slices/players/utils";
import { GetStatePlayerId } from "./utils/GetStatePlayerId";

const controls = {}

const Story = {
    summary: "Holder",
    react: React,
    reactRoblox: ReactRoblox,
    story: () => {
        const latestPage = HOLDER_PAGES[HOLDER_PAGES.size() - 1]
        store.loadPlayerData( GetStatePlayerId(), defaultPlayerData )
        store.setHolderPage( latestPage )
        store.toggleSetting( GetStatePlayerId(), "PvP" )

        // store.setHolderPage( "Settings" )

        return (
            <ReflexProvider producer={store}>
                <HolderApp />
            </ReflexProvider>
        )
    },
}

export = Story


import { Players } from "@rbxts/services";

export function GetStatePlayerId () {
    const player = Players.LocalPlayer;
    return player ? tostring( player.UserId ) : "0";
}


import { CommandDefinition } from "@rbxts/cmdr";

export = identity<CommandDefinition>({
	Name: "giveCurrency",
	Aliases: ["gC"],
	Description: "Increase the Players Currency",
	Group: "Admin",
	Args: [
		{
			Type: "player",
			Name: "player",
			Description: "Player",
		},
		{
			Type: "currency",
			Name: "Currency",
			Description: "Currency",
		},
		{
			Type: "number",
			Name: "Amount",
			Description: "Amount",
		},
	],
});


import { CommandContext } from "@rbxts/cmdr";
import { store } from "server/store";
import { Currency } from "shared/configs/Currency";

export = function (context: CommandContext, player: Player, currency: Currency, amount: number) {
	store.changeBalance(tostring(player.UserId), currency, amount);
};


import { Cmdr } from "@rbxts/cmdr";

const parent = <Folder>script.Parent

Cmdr.RegisterDefaultCommands()
Cmdr.RegisterCommandsIn( <Folder>parent.FindFirstChild( "commands" ) )
Cmdr.RegisterTypesIn( <Folder>parent.FindFirstChild( "types" ) )


import { Registry } from "@rbxts/cmdr";
import { CURRENCIES } from "shared/configs/Currency";

export = function (registry: Registry) {
	registry.RegisterType("currency", registry.Cmdr.Util.MakeEnumType("currency", [...CURRENCIES]));
};


/* eslint-disable roblox-ts/no-private-identifier */
import { OnInit, Service } from "@flamework/core";
import ProfileService from "@rbxts/profileservice";
import { Profile } from "@rbxts/profileservice/globals";
import { Players, RunService } from "@rbxts/services";
import { store } from "server/store";
import { selectPlayerBalances, selectPlayerData } from "shared/store/selectors/players";
import { PlayerData } from "shared/store/slices/players/types";
import { defaultPlayerData } from "shared/store/slices/players/utils";
import { forEveryPlayer } from "shared/utils/functions/forEveryPlayer";

let DataStoreName = "Production";
const KEY_TEMPLATE = "%d_Data";

if ( RunService.IsStudio() ) DataStoreName = "Testing";

@Service()
export class PlayerDataService implements OnInit {
	private profileStore = ProfileService.GetProfileStore(DataStoreName, defaultPlayerData);
	private profiles = new Map<Player, Profile<PlayerData>>();

	onInit() {
		forEveryPlayer(
			(player) => this.createProfile(player),
			(player) => this.removeProfile(player),
		);

		task.spawn(() => {
			while (true) {
				Players.GetPlayers().forEach((player) => store.changeBalance(tostring(player.UserId), "Coins", 1));
				task.wait(1);
			}
		});
	}

	private createProfile(player: Player) {
		const userId = player.UserId;
		const profileKey = KEY_TEMPLATE.format(userId);
		const profile = this.profileStore.LoadProfileAsync(profileKey);

		if (!profile) return player.Kick();

		profile.ListenToRelease(() => {
			this.profiles.delete(player);
			store.closePlayerData(tostring(player.UserId));
			player.Kick();
		});

		profile.AddUserId(userId);
		profile.Reconcile();

		this.profiles.set(player, profile);
		store.loadPlayerData(tostring(player.UserId), profile.Data);
		this.createLeaderstats(player);

		const unsubscribe = store.subscribe(selectPlayerData(tostring(player.UserId)), (save) => {
			if (save) profile.Data = save;
		});
		Players.PlayerRemoving.Connect((player) => {
			if (player === player) unsubscribe();
		});
	}

	private createLeaderstats(player: Player) {
		const leaderstats = new Instance("Folder", player);
		leaderstats.Name = "leaderstats";

		const coins = new Instance("NumberValue", leaderstats);
		coins.Name = "Coins";

		const gems = new Instance("NumberValue", leaderstats);
		gems.Name = "Gems";

		const unsubscribe = store.subscribe(selectPlayerBalances(tostring(player.UserId)), (save) => {
			coins.Value = save?.Coins ?? 0;
			gems.Value = save?.Gems ?? 0;
		});
		Players.PlayerRemoving.Connect((player) => {
			if (player === player) unsubscribe();
		});
	}

	private removeProfile(player: Player) {
		const profile = this.profiles.get(player);
		profile?.Release();
	}

	public getProfile(player: Player) {
		return this.profiles.get(player);
	}
}


import { InferState, combineProducers } from "@rbxts/reflex";
import { slices } from "shared/store";
import { broadcasterMiddleware } from "./middleware/broadcaster";

export type RootState = InferState<typeof store>;

export function createStore() {
	const store = combineProducers({
		...slices,
	});

	// Apply middleware
	store.applyMiddleware(broadcasterMiddleware());

	return store;
}

export const store = createStore();


import { ProducerMiddleware, createBroadcaster } from "@rbxts/reflex";
import { Events } from "server/network";
import { slices } from "shared/store";

export function broadcasterMiddleware(): ProducerMiddleware {
	const broadcaster = createBroadcaster({
		producers: slices,
		dispatch: (player, actions) => {
			Events.reflex.dispatch(player, actions);
		},
	});

	Events.reflex.start.connect((player) => broadcaster.start(player));

	return broadcaster.middleware;
}


/* eslint-disable no-unused-vars */
import { Networking } from "@flamework/networking";
import { BroadcastAction } from "@rbxts/reflex";
import { PlayerData } from "./store/slices/players/types";
import { Setting } from "./configs/Settings";

interface ServerEvents {
	reflex: {
		start: () => void;
	}

	toggleSetting: (setting: Setting) => void;
}

interface ServerFunctions {}

interface ClientEvents {
	reflex: {
		dispatch: (actions: Array<BroadcastAction>) => void;
		hydrate: (actions: PlayerData) => void;
		start: () => void;
	}
}

interface ClientFunctions {}

export const GlobalEvents = Networking.createEvent<ServerEvents, ClientEvents>();
export const GlobalFunctions = Networking.createFunction<ServerFunctions, ClientFunctions>();


import { CombineStates } from "@rbxts/reflex";
import { playersSlice } from "./slices/players";
import { clientSlice } from "./slices/client";

export type SharedState = CombineStates<typeof slices>;

export const slices = {
  players: playersSlice,
  client: clientSlice,
};


import { Currency } from "shared/configs/Currency";
import { SharedState } from "..";
import { createSelector } from "@rbxts/reflex";
import { defaultPlayerData } from "../slices/players/utils";
import { PlayerData } from "../slices/players/types";
import { Setting } from "shared/configs/Settings";

export const selectPlayerBalances = ( playerId: string ) => {
    return ( state: SharedState ) => {
        return state.players.balance[playerId];
    };
}

export const selectPlayerBalance = ( playerId: string, currency: Currency ) => {
    return createSelector(
        selectPlayerBalances( playerId ),
        ( balances ) => {
            return balances && balances[currency];
        },
    );
};

export const selectPlayerSettings = ( playerId: string ) => {
    return ( state: SharedState ) => {
        return state.players.settings[playerId];
    };
}

export const selectPlayerSetting = ( playerId: string, setting: Setting ) => {
    return createSelector(
        selectPlayerSettings( playerId ),
        ( settings ) => {
            return settings && settings[setting];
        },
    );
}

export const selectPlayerData = ( playerId: string ) => {
    return createSelector(
        selectPlayerBalances( playerId ),
        selectPlayerSettings( playerId ),
        ( balances, settings ) => {
            return {
                balance: balances || defaultPlayerData.balance,
                settings: settings || defaultPlayerData.settings,
            } as PlayerData;
        },
    )
}


import { createProducer } from "@rbxts/reflex";
import { HolderPage } from "shared/configs/Gui";

export interface GuiState {
    holderPage?: HolderPage
}

const initalState: GuiState = {

};

export const guiSlice = createProducer( initalState, {
    setHolderPage: ( state, page?: HolderPage ) => ( {
        ...state,
        holderPage: page,
    })
})


import { combineProducers } from "@rbxts/reflex";
import { guiSlice } from "./gui";

export const clientSlice = combineProducers( {
    gui: guiSlice
})


import { createProducer } from "@rbxts/reflex";
import { PlayerBalance, PlayerData } from "./types";
import { Currency } from "shared/configs/Currency";

export interface BalanceState {
    readonly [player: string]: PlayerBalance | undefined;
}

const initialState: BalanceState = {};

export const balanceSlice = createProducer( initialState, {
    loadPlayerData: ( state, playerId: string, data: PlayerData ) => ( {
        ...state,
        [playerId]: data.balance,
    } ),

    closePlayerData: ( state, playerId: string ) => ( {
        ...state,
        [playerId]: undefined,
    } ),

    changeBalance: ( state, playerId: string, currency: Currency, amount: number ) => {
        const balance = state[playerId];

        return {
            ...state,
            [playerId]: balance && {
                ...balance,
                [currency]: balance[currency] + amount,
            },
        };
    },
} );


import { combineProducers } from "@rbxts/reflex";
import { balanceSlice } from "./balance";
import { settingsSlice } from "./settings";

export const playersSlice = combineProducers( {
    balance: balanceSlice,
    settings: settingsSlice
})


import { createProducer } from "@rbxts/reflex";
import { PlayerData, PlayerSettings } from "./types";

export interface SettingsState {
    readonly [player: string]: PlayerSettings | undefined;
}

const initialState: SettingsState = {};

export const settingsSlice = createProducer( initialState, {
    loadPlayerData: ( state, player: string, data: PlayerData ) => ( {
        ...state,
        [player]: data.settings,
    } ),

    closePlayerData: ( state, player: string ) => ( {
        ...state,
        [player]: undefined,
    } ),

    toggleSetting: ( state, player: string, setting: keyof PlayerSettings ) => ( {
        ...state,
        [player]: {
            ...state[player]!,
            [setting]: !state[player]![setting],
        },
    } )
} );


import { Players } from "@rbxts/services";

type PlayerReceivingFunction = (player: Player) => unknown;

export function forEveryPlayer(
	joinFunc: PlayerReceivingFunction,
	leaveFunc?: PlayerReceivingFunction,
): Array<RBXScriptConnection> {
	const events: Array<RBXScriptConnection> = [];

	Players.GetPlayers().forEach(joinFunc);
	events.push(Players.PlayerAdded.Connect(joinFunc));
	if (leaveFunc) events.push(Players.PlayerRemoving.Connect(leaveFunc));

	return events;
}


--!strict
--[[
    Automatic deployment and artefact builder for Net
]]

local process = require("@lune/process")
local path = require("modules/path")
local node = require("modules/node")
local fs = require("@lune/fs")

local luauDir = path.join(process.cwd, "luau")

local luauDistDir = path.join(luauDir, "dist")
local robloxArtefactsDir = path.join(luauDir, "artefacts")

local outDir = path.join(process.cwd, "out")

local function compileLuau()
	return process.spawn("node", {
		"node_modules/roblox-ts/out/CLI/cli.js",
		"--verbose",
		"--type",
		"model",
		'--rojo="luau/build.project.json"',
	}, {
		env = {
			TYPE = "Luau",
			NODE_ENV = "production",
		},
	})
end

local function compileTypeScript()
	return process.spawn("node", {
		"node_modules/roblox-ts/out/CLI/cli.js",
		"--verbose",
	}, {
		env = {
			TYPE = "TS",
			NODE_ENV = "production",
		},
	})
end

local function buildTypeScript() end

type File = { filePath: string, fileName: string }
type FileIter = (file: File) -> ()
local function eachFile(path: string, fn: FileIter, recursive: boolean?): ()
	for _, child in fs.readDir(path) do
		local fullPath = `{path}/{child}`

		if fs.isFile(fullPath) then
			fn({
				filePath = fullPath,
				fileName = child,
			})
		elseif fs.isDir(fullPath) and recursive == true then
			eachFile(fullPath, fn, true)
		end
	end
end

local function copyFiles(fromPath: string, toPath: string, recursive: boolean)
	eachFile(fromPath, function(file)
        local parent = path.dirname(file.filePath)
        print(parent)

        local fromPath = path.join(fromPath, file.fileName)
        local toPath = path.join(toPath, file.fileName)

        print(fromPath, toPath)
    end, recursive)
end

local function buildLuau()
	if fs.isDir(outDir) then
		fs.removeDir(outDir)
	end

	local compilation = compileLuau()
	if not compilation.ok then
		print(compilation.stdout)
		process.exit(1)
	else
		print("Compiled Luau Dist... OK")
	end

	-- # mkdir -p out/dist/TS
	fs.writeDir(path.join(outDir, "dist", "TS"))

	-- # cp -r ../include/* out/dist/TS
	copyFiles(path.join(process.cwd, "include"), path.join(outDir, "dist", "TS"), true)

	-- cp -r ../out/* out/dist
	copyFiles(outDir, path.join(outDir, "dist"), true)
end

buildLuau()


local process = require("@lune/process")

local node = {}

function node.publish()
	local params = { "publish" }

	if process.os == "windows" then
		return process.spawn("npm.cmd", params)
	else
		return process.spawn("npm", params)
	end
end

return node


--!strict
local process = require("@lune/process")
local path = {}
local SEP = if process.os == "windows" then "\\" else "/"

--- Join multiple path parts into a combined path
--- @return A joined path
function path.join(...: string): string
	return path.ospath(table.concat({ ... }, SEP))
end

--- Gets the extension of the given path (if applicable) - otherwise returns nil
--- @param path The path to get the extension of
--- @return The path extension or nil
function path.extname(path: string): string?
	return string.match(path, ".(%.[A-z0-9_-]+)$")
end

--- Gets the full directory path of the given path
--- @param value The path
--- @return The full directory path without the end item
function path.dirname(value: string): string
	local parts = string.split(path.ospath(value), SEP)
	table.remove(parts, #parts)
	return table.concat(parts, SEP)
end

--- Fixes the path so that it's properly formatted to the local operating system
--- @param path The path to format
--- @return A fixed path matching the OS format
function path.ospath(path: string): string
	local newPath = ""

	if process.os == "windows" then
		local i = 1
		while i <= #path do
			local char = string.sub(path, i, i)
			if char == "/" then
				char = "\\"
			end

			newPath ..= char
			i += 1
		end

		newPath = newPath:gsub("\\+", "\\")
	else
		local i = 1
		while i <= #path do
			local char = string.sub(path, i, i)
			if char == "\\" then
				char = "/"
			end

			newPath ..= char
			i += 1
		end

		newPath = newPath:gsub("/+", "/")
	end

	return newPath
end

--- The OS-specific path separator
path.sep = SEP
return path


local function isMixed(t)
	assert(type(t) == "table")
	local mixed = false
	local _idxType

	for index, value in next, t do
		if _idxType and _idxType ~= type(index) then
			return true
		end

		_idxType = type(index)
		if type(value) == "table" then
			mixed = mixed and isMixed(value) and not (not getmetatable(value))
		end
	end

	return mixed
end

return {
	mixed = isMixed
}


import { isMixed } from "./tables";

export interface ValidatorLike {
	readonly errorMessage: string;
	readonly check: (value: unknown) => boolean;
}

export interface Validator<T> extends ValidatorLike {
	readonly check: (value: unknown) => value is T;
}

type NetworkValue = string | number | boolean | Instance | CFrame | Vector3 | Vector2 | Region3;

const Workspace = game.GetService("Workspace");
const ServerStorage = game.GetService("ServerStorage");
const ServerScriptService = game.GetService("ServerScriptService");

/** @internal */
export function validateArguments(...args: unknown[]) {
	for (const [index, value] of ipairs(args)) {
		if (!isSerializable.check(value)) {
			error(isSerializable.errorMessage.format(index), 2);
		}
		if (typeIs(value, "Instance")) {
			if (value.IsDescendantOf(ServerStorage) || value.IsDescendantOf(ServerScriptService)) {
				error(
					`[rbx-net] Instance at argument #${index} is inside a server-only container and cannot be sent via remotes.`,
				);
			}
			if (!value.IsDescendantOf(game)) {
				error(`[rbx-net] Instance at argument #${index} is not a valid descendant of game, and wont replicate`);
			}
		}
	}
}

/** @internal */
export const isSerializable: Validator<NetworkValue> = {
	errorMessage: `Argument #%d is not serializable. - see http://docs.vorlias.com/rbx-net/docs/2.0/serialization`,
	check: (value): value is NetworkValue => {
		// Can't allow functions or threads
		if (typeIs(value, "function") || typeIs(value, "thread")) {
			return false;
		}

		// Can't allow metatabled objects
		if (typeIs(value, "table") && getmetatable(value) !== undefined) {
			return false;
		}

		// Ensure not a mixed table type
		if (typeIs(value, "table")) {
			return !isMixed(value);
		}

		return true;
	},
};

/** @internal */
export function oneOf<T extends string>(...values: T[]): Validator<T> {
	return {
		errorMessage: `Expected one of: ${values.join(", ")}` as string,
		check: (value: unknown): value is T => {
			if (!typeIs(value, "string")) return false;

			for (const cmpValue of values) {
				if (value === cmpValue) {
					return true;
				}
			}

			return false;
		},
	} as const;
}


local RunService = game:GetService("RunService")
local MessagingService = game:GetService("MessagingService")

local MockMessagingService = {}
local topics = {}

function MockMessagingService:PublishAsync(topicName, message)
	local topic = topics[topicName]
	if topic then
		topic:Fire(
			{
				Sent = tick(),
				Data = message
			}
		)
	end
end

function MockMessagingService:SubscribeAsync(topicName, callback)
	local topic = topics[topicName]
	if not topic then
		topic = Instance.new("BindableEvent")
		topic.Parent = script
		topic.Name = topicName
		topics[topicName] = topic
	end

	return topic.Event:Connect(callback)
end

local service
if RunService:IsStudio() then
    service = MockMessagingService
else
    service = MessagingService
end

return service


local RunService = game:GetService("RunService")
local HttpService = game:GetService("HttpService")
local IS_STUDIO = RunService:IsStudio()

local function defaultLogger(name, args)
    if IS_STUDIO then
        local str = {};
        for _, arg in ipairs(args) do
            table.insert(str, HttpService:JSONEncode(arg))
        end
        print("[rbx-net] Called", name, "with arguments", "[ " .. table.concat(str, ", ") .. " ]")
    end
end

return function(options)
    options = options or {}
    local logger = options.Logger or defaultLogger

    return function (next, event)
        local name = options.Name or event:GetInstance().Name
        return function(player, ...)
            logger(name, {...})
            return next(player, ...)
        end
    end
end


local Throttle = {
	counters = {}
}
local RequestCounter = {}
RequestCounter.__index = RequestCounter

function RequestCounter.new()
	local self = {
		counter = {}
	}

	return setmetatable(self, RequestCounter)
end

function RequestCounter:Get(player)
	local counter = self.counter
	local playerQueue = counter[player.UserId]
	return playerQueue or 0
end

function RequestCounter:Increment(player)
	local counter = self.counter
	local playerQueue = counter[player.UserId]

	if not counter[player.UserId] then
		counter[player.UserId] = 1
	else
		counter[player.UserId] = playerQueue + 1
	end
end

function RequestCounter:__tostring()
	return "RequestCounter"
end

function RequestCounter:ClearAll()
	self.counter = {}
end

function Throttle:Get(name)
	local existing = self.counters[name]
	if (existing) then
		return existing
	else
		local newCounter = RequestCounter.new()
		self.counters[name] = newCounter
		return newCounter
	end
end

function Throttle:Clear()
	for _, counter in pairs(self.counters) do
		counter:ClearAll()
	end
end

return Throttle


local RunService = game:GetService("RunService")
local HttpService = game:GetService("HttpService")
local IS_STUDIO = RunService:IsStudio();

local function defaultErrorHandler(event, args, index)
    local name = event:GetInstance().Name
    if IS_STUDIO then
        warn("[TypeCheckMiddleware] Call to " .. name .. " failed")
        warn("\tRecieved: " .. HttpService:JSONEncode(args))
        warn("\tInvalid argument at index " .. tostring(index))
    end
end

local MiddlewareGlobal = {}
MiddlewareGlobal.__index = MiddlewareGlobal
MiddlewareGlobal.defaultErrorHandler = defaultErrorHandler

local function typeCheckMiddleware(...)
    local checks = {...}
    local MiddlewareInstance = {}
    MiddlewareInstance.__index = MiddlewareInstance;

    function MiddlewareInstance:next(next, event)
        local errorHandler = self.errorHandler or MiddlewareGlobal.defaultErrorHandler
        --  what's returned as callbackFn
        return function(player, ...)
            local args = {...}
            for index, check in ipairs(checks) do
                if not check(args[index]) then
                    pcall(errorHandler, event, args, index)
                    return false
                end
            end

            -- Invoke the next middleware OR the callback (if none left)
            return next(player, ...)
        end
    end

    function MiddlewareInstance.__tostring()
        return "TypeCheckMiddleware"
    end

    function MiddlewareInstance:WithErrorHandler(fn)
        self.errorHandler = fn
        return self
    end

    function MiddlewareInstance:__call(...)
        return self:next(...)
    end

    return setmetatable({}, MiddlewareInstance)
    -- ^ The middleware
end

function MiddlewareGlobal.__call(_, ...)
    return typeCheckMiddleware(...)
end

function MiddlewareGlobal.SetDefaultErrorHandler(_, fn)
    MiddlewareGlobal.defaultErrorHandler = fn
end

return setmetatable({}, MiddlewareGlobal)


import { $ifEnv } from "rbxts-transform-env";
import { NetGlobalMiddleware, NetMiddleware, NextCaller } from "../middleware";

/** @internal */
export type MutableMiddlewareList = Array<NetMiddleware<Array<unknown>>>;
export type MiddlewareList = ReadonlyArray<NetMiddleware<ReadonlyArray<unknown>>>;
abstract class MiddlewareEvent {
	protected constructor(private readonly middlewares: MiddlewareList = []) {}

	/** @internal */
	abstract GetInstance(): RemoteEvent;

	/** @internal */
	public _use(middleware: NetGlobalMiddleware) {
		(this.middlewares as MutableMiddlewareList).push(middleware);
	}

	protected _processMiddleware<A extends ReadonlyArray<unknown>, R = void>(
		callback: (player: Player, ...args: A) => R,
	) {
		const { middlewares } = this;
		try {
			assert(
				typeIs(middlewares, "table"),
				"The middleware argument should be an array of middlewares not a " + typeOf(middlewares),
			);

			if (middlewares.size() > 0) {
				let callbackFn = callback as NextCaller<R>;

				// Run through each middleware
				for (const middleware of middlewares) {
					callbackFn = middleware(callbackFn, this) as NextCaller<R>;
				}

				return callbackFn;
			} else {
				return callback;
			}
		} catch (e) {
			warn("[rbx-net] " + tostring(e));
		}
	}
}

export default MiddlewareEvent;


import { NetMiddleware, NextCaller } from "../middleware";
import { findOrCreateRemote, IS_CLIENT, IS_RUNNING, NetManagedInstance } from "../internal";
import MiddlewareEvent, { MiddlewareList } from "./MiddlewareEvent";
import { MiddlewareOverload } from "../middleware";
import { NetServerScriptSignal, NetServerSignalConnection } from "./NetServerScriptSignal";
import { DefinitionConfiguration } from "../definitions";
import { $dbg } from "rbxts-transform-debug";

/**
 * Interface for server listening events
 */
export interface ServerListenerEvent<CallArguments extends ReadonlyArray<unknown>> {
	/**
	 * Connects a callback function to this event, in which if any events are recieved by the client will be called.
	 * @param callback The callback function
	 */
	Connect(callback: (player: Player, ...args: CallArguments) => void): Readonly<NetServerSignalConnection>;
}

/**
 * Interface for server sender events
 */
export interface ServerSenderEvent<CallArguments extends ReadonlyArray<unknown>> {
	/**
	 * Sends an event to all players on the server
	 * @param args The arguments to send to the players
	 */
	SendToAllPlayers(...args: CallArguments): void;

	/**
	 * Sends an event to all players on the server except the specified player
	 * @param blacklist The blacklist
	 * @param args The arguments
	 */
	SendToAllPlayersExcept(blacklist: Player | Array<Player>, ...args: CallArguments): void;

	/**
	 * Sends an event to the specified player
	 * @param player The player
	 * @param args The arguments to send to the player
	 */
	SendToPlayer(player: Player, ...args: CallArguments): void;

	/**
	 * Sends an event to the specified players on the server
	 * @param players The players
	 * @param args The arugments to send to these players
	 */
	SendToPlayers(players: Array<Player>, ...args: CallArguments): void;
}

export default class ServerEvent<
		ConnectArgs extends ReadonlyArray<unknown> = Array<unknown>,
		CallArgs extends ReadonlyArray<unknown> = Array<unknown>
	>
	extends MiddlewareEvent
	implements NetManagedInstance, ServerListenerEvent<ConnectArgs>, ServerSenderEvent<CallArgs> {
	private instance: RemoteEvent;
	private connection;

	public constructor(
		name: string,
		middlewares: MiddlewareOverload<ConnectArgs> = [],
		private configuration: DefinitionConfiguration,
	) {
		super([...middlewares, ...(configuration.ServerGlobalMiddleware ?? [])]);
		assert(!IS_CLIENT, "Cannot create a NetServerEvent on the client!");
		this.instance = findOrCreateRemote("RemoteEvent", name);
		this.connection = new NetServerScriptSignal(this.instance.OnServerEvent, this.instance);
	}

	/** @deprecated */
	public GetInstance() {
		return this.instance;
	}

	/**
	 * Connect a fucntion to fire when the event is invoked by the client
	 * @param callback The function fired when the event is invoked by the client
	 */
	public Connect(callback: (player: Player, ...args: ConnectArgs) => void): Readonly<NetServerSignalConnection> {
		const remoteId = this.instance.Name;
		const microprofile = this.configuration.MicroprofileCallbacks;

		return this.connection.Connect((player, ...args) => {
			if (microprofile) debug.profilebegin(`Net: ${remoteId}`);
			this._processMiddleware(callback)?.(player, ...((args as unknown) as ConnectArgs));
		});
	}

	/**
	 * Sends the specified arguments to all players
	 * @param args The arguments to send to the players
	 */
	public SendToAllPlayers(...args: CallArgs) {
		if (!IS_RUNNING) return;

		this.instance.FireAllClients(...args);
	}

	/**
	 * Will send this message to all players except specified players
	 * @param blacklist The blacklist
	 * @param args The arguments
	 */
	public SendToAllPlayersExcept(blacklist: Player | Array<Player>, ...args: CallArgs) {
		if (!IS_RUNNING) return;
		const Players = game.GetService("Players");

		if (typeIs(blacklist, "Instance")) {
			const otherPlayers = Players.GetPlayers().filter(p => p !== blacklist);
			for (const player of otherPlayers) {
				this.instance.FireClient(player, ...(args as CallArgs));
			}
		} else if (typeIs(blacklist, "table")) {
			for (const player of Players.GetPlayers()) {
				if (blacklist.indexOf(player) === -1) {
					this.instance.FireClient(player, ...(args as CallArgs));
				}
			}
		}
	}

	/**
	 * Sends the specified arguments to a specified player
	 * @param player The player
	 * @param args The arguments to send to the player
	 */
	public SendToPlayer(player: Player, ...args: CallArgs) {
		if (!IS_RUNNING) return;

		this.instance.FireClient(player, ...(args as CallArgs));
	}

	/**
	 * Sends the specified argumetns to the specified list of players
	 * @param players The players
	 * @param args The arugments to send to these players
	 */
	public SendToPlayers(players: Array<Player>, ...args: CallArgs) {
		if (!IS_RUNNING) return;

		for (const player of players) {
			this.SendToPlayer(player, ...args);
		}
	}
}


import ExperienceBroadcastEvent, {
	isSubscriptionMessage,
	ISubscriptionMessage,
} from "../messaging/ExperienceBroadcastEvent";
import { getGlobalRemote, IS_CLIENT, isLuaTable } from "../internal";
import ServerEvent from "./ServerEvent";
import { DefinitionConfiguration } from "../definitions";
const Players = game.GetService("Players");

export interface IMessage<TArgs> {
	InnerData: TArgs;
	TargetId?: number;
	TargetIds?: Array<number>;
}

export interface ISubscriptionTargetedMessage extends ISubscriptionMessage {
	Data: IMessage<any>;
}

function isTargetedSubscriptionMessage(value: unknown): value is ISubscriptionTargetedMessage {
	if (isSubscriptionMessage(value)) {
		if (isLuaTable(value.Data)) {
			return value.Data.has("InnerData");
		}
	}

	return false;
}

/**
 * Similar to a ServerEvent, but works across all servers.
 */
export default class ServerMessagingEvent<TArgs extends readonly unknown[] = unknown[]> {
	private readonly instance: ServerEvent<[], TArgs>;
	private readonly event: ExperienceBroadcastEvent<IMessage<TArgs>>;
	private readonly eventHandler: RBXScriptConnection;

	constructor(name: string, config: DefinitionConfiguration) {
		this.instance = new ServerEvent(getGlobalRemote(name), undefined, config);
		this.event = new ExperienceBroadcastEvent(name);
		assert(!IS_CLIENT, "Cannot create a Net.GlobalServerEvent on the Client!");

		this.eventHandler = this.event.Connect(message => {
			if (isTargetedSubscriptionMessage(message)) {
				this.recievedMessage(message.Data);
			} else {
				warn(`[rbx-net] Recieved malformed message for ServerGameEvent: ${name}`);
			}
		});
	}

	private getPlayersMatchingId(matching: Array<number> | number) {
		if (typeIs(matching, "number")) {
			return Players.GetPlayerByUserId(matching);
		} else {
			const players = new Array<Player>();
			for (const id of matching) {
				const player = Players.GetPlayerByUserId(id);
				if (player) {
					players.push(player);
				}
			}

			return players;
		}
	}

	private recievedMessage(message: IMessage<TArgs>) {
		if (message.TargetIds) {
			const players = this.getPlayersMatchingId(message.TargetIds);
			if (players) {
				this.instance.SendToPlayers(players as Array<Player>, ...message.InnerData);
			}
		} else if (message.TargetId !== undefined) {
			const player = this.getPlayersMatchingId(message.TargetId);
			if (player) {
				this.instance.SendToPlayer(player as Player, ...message.InnerData);
			}
		} else {
			this.instance.SendToAllPlayers(...message.InnerData);
		}
	}

	/**
	 * Connects to the event on the server
	 * @returns
	 */
	public Connect(serverListener: (receivedData: IMessage<TArgs>, timestampSent: number) => void) {
		return this.event.Connect((data, sent) => {
			serverListener(data, sent);
		});
	}

	/**
	 * SEnds an event to all servers in the game
	 * @param args The args of the message
	 */
	public SendToAllServers(...args: TArgs) {
		this.event.SendToAllServers({ InnerData: args });
	}

	/**
	 * Sends an event to the specified server
	 * @param jobId The game.JobId of the target server
	 * @param args The args of the message
	 */
	public SendToServer(jobId: string, ...args: TArgs) {
		this.event.SendToServer(jobId, { InnerData: args });
	}

	/**
	 * Sends an event to the specified player (if they're in any of the game's servers)
	 * @param userId The userId of the target player
	 * @param args The args
	 */
	public SendToUserId(userId: number, ...args: TArgs) {
		const player = Players.GetPlayerByUserId(userId);
		// If the player exists in this instance, just send it straight to them.
		if (player) {
			this.instance.SendToPlayer(player, ...args);
		} else {
			this.event.SendToAllServers({ InnerData: args, TargetId: userId });
		}
	}

	/**
	 * Sends an event to all the specified players (if they're in any of the game's servers)
	 * @param userIds The list of user ids to send this message to
	 * @param args The args of the message
	 */
	public SendToUserIds(userIds: Array<number>, ...args: TArgs) {
		// Check to see if any of these users are in this server first, and handle accordingly.
		for (const targetId of userIds) {
			const player = Players.GetPlayerByUserId(targetId);
			if (player) {
				this.instance.SendToPlayer(player, ...args);
				userIds.remove(targetId);
			}
		}

		if (userIds.size() > 0) {
			this.event.SendToAllServers({ InnerData: args, TargetIds: userIds });
		}
	}
}


import type { ExtensionContext } from "vscode";
import { workspace } from "vscode";
import { CONFIG_ROOT } from "~/constants";
import { updateThemes } from "./update";
import { isDefaultConfig } from "./utils/config";
import { isFreshInstall, promptReload } from "./utils/interactions";

export async function activate(context: ExtensionContext) {
	if (await isFreshInstall(context) && !isDefaultConfig()) {
		await updateThemes(context);
		await promptReload();
	}

	context.subscriptions.push(
		workspace.onDidChangeConfiguration(async (event) => {
			if (event.affectsConfiguration(CONFIG_ROOT)) {
				await updateThemes(context);
				await promptReload();
			}
		}),
	);
}


import type { Config } from "~/types";
import { defu } from "defu";
import { workspace } from "vscode";
import { CONFIG_ROOT, ConfigKey } from "~/constants";
import { defaultConfig } from "~/defaults";

export function getConfig(): Partial<Config> {
	const config = workspace.getConfiguration(CONFIG_ROOT);

	return {
		hidesExplorerArrows: config.get(ConfigKey.HidesExplorerArrows),
		outlineFolders: config.get(ConfigKey.OutlineFolders),
		associations: {
			languageIds: config.get(ConfigKey.AssociationsLanguages, {}),
			fileExtensions: config.get(ConfigKey.AssociationsExtensions, {}),
			fileNames: config.get(ConfigKey.AssociationsFiles, {}),
			folderNames: config.get(ConfigKey.AssociationsFolders, {}),
		},
	};
}

export function isDefaultConfig() {
	const config = defu(getConfig(), defaultConfig);

	return JSON.stringify(config) === JSON.stringify(defaultConfig);
}


import type { ExtensionContext } from "vscode";
import type { IconVariant } from "~/constants";
import type { IconDefinitions } from "~/types";
import { Uri, workspace } from "vscode";

export async function getIconDefinitions(context: ExtensionContext, variant: IconVariant) {
	const path = Uri.joinPath(context.extensionUri, "dist", "themes", variant, "iconDefinitions.json");

	return workspace.fs.readFile(path).then((data) => {
		return JSON.parse(data.toString()) as IconDefinitions;
	});
}


import type { ExtensionContext } from "vscode";
import { Buffer } from "node:buffer";
import { commands, Uri, window, workspace } from "vscode";

export async function promptReload() {
	const message = `Charmed Icons: Theme changed - Reload required.`;
	const action = "Reload window";
	return window.showInformationMessage(message, action).then(async (selectedAction) => {
		if (selectedAction === action) {
			commands.executeCommand("workbench.action.reloadWindow");
		}
	});
};

export async function isFreshInstall(context: ExtensionContext) {
	const flag = Uri.joinPath(context.extensionUri, "dist", ".flag");
	return await workspace.fs.stat(flag).then(
		() => false,
		() => workspace.fs.writeFile(flag, Buffer.from("")).then(
			() => true,
			() => true,
		),
	);
}

export async function readFile(uri: Uri) {
	return workspace.fs.readFile(uri).then(b => b.toString());
}

export async function writeFile(uri: Uri, content: string) {
	return workspace.fs.writeFile(uri, Buffer.from(content));
}

export async function writeJsonFile(uri: Uri, json: Record<string, any>) {
	return await writeFile(uri, JSON.stringify(json, null, 2));
}


import { $package } from "rbxts-transform-debug";
import { BaseRegistry } from "../../../shared";

export = (registry: BaseRegistry) => {
	registry.registerGroup({
		name: "centurion",
		description: "Commands relating to Centurion",
	});

	const version = $package.version;
	registry.registerCommand(
		{
			name: "version",
			description: "Display the current version of Centurion",
		},
		(ctx) => {
			ctx.reply(`Centurion v${version}`);
		},
		["centurion"],
	);
};


import { RunService } from "@rbxts/services";
import { DEFAULT_CONFIG } from "../shared/config";
import { getRemotes } from "../shared/network";
import { ReadonlyDeep } from "../shared/util/data";
import { ClientDispatcher } from "./dispatcher";
import { ClientRegistry } from "./registry";
import { ClientConfig } from "./types";

export class CenturionClient {
	private started = false;
	readonly registry: ClientRegistry;
	readonly dispatcher: ClientDispatcher;
	readonly config: ReadonlyDeep<ClientConfig>;

	constructor(config: Partial<ClientConfig> = {}) {
		assert(
			RunService.IsClient(),
			"CenturionClient can only be created from the client",
		);

		let networkConfig = config.network;
		if (networkConfig === undefined) {
			const remotes = getRemotes();
			networkConfig = {
				syncStart: {
					Fire: () => remotes.syncStart.FireServer(),
				},
				syncDispatch: {
					Connect: (callback) => {
						remotes.syncDispatch.OnClientEvent.Connect(callback);
					},
				},
				execute: {
					Invoke: (path, args) => remotes.execute.InvokeServer(path, args),
				},
			};
		}

		this.config = {
			...DEFAULT_CONFIG,
			historyLength: 1000,
			registerBuiltInCommands: true,
			shortcutsEnabled: true,
			syncTimeout: 10,
			network: networkConfig,
			...config,
		};
		this.registry = new ClientRegistry(this.config);
		this.dispatcher = new ClientDispatcher(this.config, this.registry);
	}

	/**
	 * Starts {@link CenturionClient}.
	 *
	 * @returns A promise that resolves when the registry has been synchronized.
	 */
	async start() {
		assert(!this.started, "Centurion has already been started");

		this.registry.init();
		this.dispatcher.init();
		this.registry.register();
		return this.registry.sync().then(() => {
			this.started = true;
		});
	}
}


import { t } from "@rbxts/t";
import { CommandShortcut, ShortcutContext } from "../shared";

const key = Enum.KeyCode;
const modifierKeys = new Map<Enum.KeyCode, string>([
	[key.LeftAlt, "Alt"],
	[key.RightAlt, "Alt"],
	[key.LeftControl, "Ctrl"],
	[key.RightControl, "Ctrl"],
	[key.LeftShift, "Shift"],
	[key.RightShift, "Shift"],
	[key.LeftMeta, "Meta"],
	[key.RightMeta, "Meta"],
]);

export function isModifierKey(keyCode: Enum.KeyCode) {
	return modifierKeys.has(keyCode);
}

export function getModifierKeyName(keyCode: Enum.KeyCode) {
	return modifierKeys.get(keyCode);
}

export function getShortcutKeycodes(shortcut: CommandShortcut): Enum.KeyCode[] {
	if (isKeyCode(shortcut)) {
		return [shortcut];
	}

	if (isKeyCodeArray(shortcut)) {
		return shortcut;
	}

	if (isShortcutContext(shortcut)) {
		return shortcut.keys;
	}

	return [];
}

export const isKeyCode = t.enum(Enum.KeyCode);
export const isKeyCodeArray = t.array(isKeyCode);
export const isShortcutContext: t.check<ShortcutContext> = t.strictInterface({
	keys: isKeyCodeArray,
	arguments: t.optional(t.array(t.string)),
});


import { RunService } from "@rbxts/services";
import { DEFAULT_CONFIG } from "../shared/config";
import { getRemotes } from "../shared/network";
import { ReadonlyDeep } from "../shared/util/data";
import { ServerDispatcher } from "./dispatcher";
import { ServerRegistry } from "./registry";
import { ServerConfig } from "./types";

export class CenturionServer {
	private started = false;
	readonly registry: ServerRegistry;
	readonly dispatcher: ServerDispatcher;
	readonly config: ReadonlyDeep<ServerConfig>;

	constructor(config: Partial<ServerConfig> = {}) {
		assert(
			RunService.IsServer(),
			"CenturionServer can only be created from the server",
		);

		let networkConfig = config.network;
		if (networkConfig === undefined) {
			const remotes = getRemotes();
			networkConfig = {
				syncStart: {
					Connect: (player) => {
						return remotes.syncStart.OnServerEvent.Connect(player);
					},
				},
				syncDispatch: {
					Fire: (player, data) => remotes.syncDispatch.FireClient(player, data),
				},
				execute: {
					SetCallback: (callback) => {
						remotes.execute.OnServerInvoke = callback;
					},
				},
			};
		}

		this.config = {
			...DEFAULT_CONFIG,
			network: networkConfig,
			syncFilter: () => true,
			...config,
		};
		this.registry = new ServerRegistry(this.config);
		this.dispatcher = new ServerDispatcher(this.config, this.registry);
	}

	/**
	 * Starts {@link CenturionServer}.
	 */
	start() {
		assert(!this.started, "Centurion has already been started");

		this.registry.init();
		this.dispatcher.init();
		this.registry.register();
		this.started = true;
	}
}


import { Players } from "@rbxts/services";
import {
	CenturionLogLevel,
	CommandOptions,
	GroupOptions,
	RegistryPath,
} from "../shared";
import {
	BaseCommand,
	CommandGroup,
	ExecutableCommand,
} from "../shared/core/command";
import { BaseRegistry } from "../shared/core/registry";
import { SyncData } from "../shared/network";
import { ArrayUtil, ReadonlyDeep } from "../shared/util/data";
import { inspect } from "../shared/util/inspect";
import { ServerConfig } from "./types";

export class ServerRegistry extends BaseRegistry<ReadonlyDeep<ServerConfig>> {
	private syncedCommands = new Map<Player, Set<string>>();
	private initialized = false;

	/**
	 * Initializes the server registry.
	 *
	 * @param options Server options
	 * @internal
	 * @ignore
	 */
	init() {
		super.init();

		Players.PlayerRemoving.Connect((player) => {
			this.syncedCommands.delete(player);
		});

		this.config.network.syncStart.Connect((player) => {
			this.sync(player);
		});

		this.initialized = true;
	}

	isCommandSynced(player: Player, path: RegistryPath) {
		const syncedCommands = this.syncedCommands.get(player);
		if (syncedCommands === undefined) return false;
		return syncedCommands.has(path.toString());
	}

	sync(player: Player) {
		if (!this.initialized) return;

		const data = {
			commands: new Map(),
			groups: new Map(),
		} satisfies SyncData;

		const syncedCommands = new Set<string>();
		for (const [_, command] of this.commands) {
			const path = command.getPath();
			if (data.commands.has(path.toString())) continue;
			if (!this.config.syncFilter(player, command as ExecutableCommand)) {
				continue;
			}

			syncedCommands.add(path.toString());
			data.commands.set(path.toString(), {
				...(command.options as CommandOptions),
			});
		}
		this.syncedCommands.set(player, syncedCommands);

		for (const [path] of data.commands) {
			const parts = path.split("/");
			if (parts.size() === 1) continue;

			let groupPath = "";
			for (const part of ArrayUtil.slice(parts, 0, parts.size() - 1)) {
				groupPath += part;

				const group = this.getGroupByString(groupPath);
				if (group === undefined) break;
				data.groups.set(groupPath, { ...(group.options as GroupOptions) });
				groupPath += "/";
			}
		}

		this.config.network.syncDispatch.Fire(player, data);
		if (this.logger.level <= CenturionLogLevel.Debug) {
			this.logger.debug(
				`Synced data to ${player.Name}`,
				inspect(data, {
					depth: 2,
				}),
			);
		}
	}

	protected addCommand(command: BaseCommand, group?: CommandGroup | undefined) {
		super.addCommand(command, group);
		for (const [player] of this.syncedCommands) {
			this.sync(player);
		}
	}
}


import { t } from "@rbxts/t";
import { CenturionType } from ".";
import { BaseRegistry, TransformResult, TypeBuilder } from "../../core";

const transformToNumber = (text: string) => {
	const num = tonumber(text);
	if (num === undefined) {
		return TransformResult.err<number>(`Invalid number: ${text}`);
	}

	return TransformResult.ok(num);
};

const stringType = TypeBuilder.create<string>(CenturionType.String)
	.transform((text) => TransformResult.ok(text))
	.build();

const numberType = TypeBuilder.create<number>(CenturionType.Number)
	.transform(transformToNumber)
	.build();

const integerType = TypeBuilder.create<number>(CenturionType.Integer)
	.transform((text) => {
		const numResult = transformToNumber(text);
		if (!numResult.ok) {
			return numResult;
		}

		const num = numResult.value;
		return t.integer(num)
			? TransformResult.ok(num)
			: TransformResult.err(`Invalid integer: ${text}`);
	})
	.build();

const truthyValues = new Set<string>(["true", "yes", "y"]);
const falsyValues = new Set<string>(["false", "no", "n"]);
const booleanType = TypeBuilder.create<boolean>(CenturionType.Boolean)
	.transform((text) => {
		const textLower = text.lower();
		if (truthyValues.has(textLower)) return TransformResult.ok(true);
		if (falsyValues.has(textLower)) return TransformResult.ok(false);

		return TransformResult.err(`Invalid boolean: ${text}`);
	})
	.build();

export = (registry: BaseRegistry) => {
	registry.registerType(stringType, numberType, integerType, booleanType);
};


import { Teams } from "@rbxts/services";
import { CenturionType } from ".";
import { BaseRegistry, TransformResult, TypeBuilder } from "../../core";

const teamType = TypeBuilder.create<Team>(CenturionType.Team)
	.transform((text) => {
		const team = Teams.FindFirstChild(text);
		if (team === undefined || !classIs(team, "Team")) {
			return TransformResult.err(`Team not found: ${text}`);
		}
		return TransformResult.ok(team);
	})
	.suggestions(() => Teams.GetChildren().map((team) => team.Name))
	.build();

export = (registry: BaseRegistry) => {
	registry.registerType(teamType);
};