import type { OnStart } from "@flamework/core";
import { Service } from "@flamework/core";
import type { Logger } from "@rbxts/log";
import { Error } from "@rbxts/luau-polyfill";
import { PhysicsService } from "@rbxts/services";
import { promiseTree } from "@rbxts/validate-tree";

import type { ListenerData } from "shared/util/flamework-util";
import { setupLifecycle } from "shared/util/flamework-util";
import { addToCollisionGroup } from "shared/util/physics-util";
import {
	CHARACTER_LOAD_TIMEOUT,
	type CharacterRig,
	CharacterSchema,
	loadCharacter,
	onCharacterAdded,
} from "shared/util/player-util";
import CollisionGroup from "types/enum/collision-group";
import { Tag } from "types/enum/tag";

import type { PlayerEntity } from "../player-entity";
import type { OnPlayerJoin } from "../player-service";

PhysicsService.RegisterCollisionGroup(CollisionGroup.Character);

export interface OnCharacterAdded {
	/** Fires when a character is added to the game. */
	onCharacterAdded(character: CharacterRig, playerEntity: PlayerEntity): void;
}

/**
 * A service for managing character rigs in the game. This service listens for
 * when a player's character is added to the game and ensures that the character
 * rig is loaded and fully exists according to the schema before allowing any
 * other systems to interact with it. We also handle retries for loading the
 * character rig in case it fails to load.
 */
@Service({})
export class CharacterService implements OnStart, OnPlayerJoin {
	private readonly characterAddedEvents = new Array<ListenerData<OnCharacterAdded>>();
	private readonly characterRigs = new Map<Player, CharacterRig>();

	constructor(private readonly logger: Logger) {}

	/** @ignore */
	public onStart(): void {
		setupLifecycle<OnCharacterAdded>(this.characterAddedEvents);
	}

	/** @ignore */
	public onPlayerJoin(playerEntity: PlayerEntity): void {
		const { janitor, player } = playerEntity;

		janitor.Add(
			onCharacterAdded(player, (character) => {
				janitor.AddPromise(this.characterAdded(playerEntity, character)).catch((err) => {
					this.logger.Fatal(`Could not get character rig because:\n${err}`);
				});
			}),
		);
	}

	/**
	 * Returns the character rig associated with the given player, if it exists.
	 *
	 * @param player - The player whose character rig to retrieve.
	 * @returns The character rig associated with the player, or undefined if it
	 *   does not exist.
	 */
	public getCharacterRig(player: Player): CharacterRig | undefined {
		return this.characterRigs.get(player);
	}

	/**
	 * This method wraps a callback and replaces the first argument (that must
	 * be of type `Player`) with that players `character rig`.
	 *
	 * @param func - The callback to wrap.
	 * @returns A new callback that replaces the first argument with the
	 *   player's character rig.
	 */
	public withPlayerRig<T extends Array<unknown>, R = void>(
		func: (playerRig: CharacterRig, ...args: T) => R,
	) {
		return (player: Player, ...args: T): R | undefined => {
			const playerRig = this.getCharacterRig(player);
			if (!playerRig) {
				this.logger.Info(`Could not get character rig for ${player.UserId}`);
				return;
			}

			return func(playerRig, ...args);
		};
	}

	private async characterAdded(playerEntity: PlayerEntity, model: Model): Promise<void> {
		const promise = promiseTree(model, CharacterSchema);

		const { player } = playerEntity;

		// If our character fails to load, we want to cancel the promise and
		// attempt to load it again.
		const timeout = Promise.delay(CHARACTER_LOAD_TIMEOUT).then(async () => {
			promise.cancel();
			return this.retryCharacterLoad(player);
		});

		// If our character is removed before it loads, we want to cancel.
		const connection = model.AncestryChanged.Connect(() => {
			if (model.IsDescendantOf(game)) {
				return;
			}

			promise.cancel();
		});

		const [success, rig] = promise.await();
		timeout.cancel();
		connection.Disconnect();

		if (!success) {
			throw new Error(`Could not get character rig for ${player.UserId}`);
		}

		this.listenForCharacterRemoving(player, model);
		this.onRigLoaded(playerEntity, rig);
	}

	private listenForCharacterRemoving(player: Player, character: Model): void {
		const connection = character.AncestryChanged.Connect(() => {
			if (character.IsDescendantOf(game)) {
				return;
			}

			this.logger.Verbose(`Character ${character.GetFullName()} has been removed.`);

			connection.Disconnect();
			this.characterRemoving(player);
		});
	}

	private onRigLoaded(playerEntity: PlayerEntity, rig: CharacterRig): void {
		const { name, janitor, player, userId } = playerEntity;

		janitor.Add(addToCollisionGroup(rig, CollisionGroup.Character, true), true);
		rig.AddTag(Tag.PlayerCharacter);
		this.characterRigs.set(player, rig);

		this.logger.Debug(`Loaded character rig for ${name}`);

		debug.profilebegin("Lifecycle_Character_Added");

		for (const { id, event } of this.characterAddedEvents) {
			janitor
				.Add(
					Promise.defer(() => {
						debug.profilebegin(id);
						event.onCharacterAdded(rig, playerEntity);
					}),
				)
				.catch((err) => {
					this.logger.Error(`Error in character lifecycle ${id}: ${err}`);
				});
		}

		debug.profileend();

		janitor.AddPromise(this.characterAppearanceLoaded(player, rig)).catch((err) => {
			this.logger.Info(
				`Character appearance did not load for ${userId}, with reason: ${err}`,
			);
		});
	}

	private async characterAppearanceLoaded(player: Player, rig: CharacterRig): Promise<void> {
		if (!player.HasAppearanceLoaded()) {
			await Promise.fromEvent(player.CharacterAppearanceLoaded).timeout(
				CHARACTER_LOAD_TIMEOUT,
			);
		}

		rig.Head.AddTag(Tag.PlayerHead);
	}

	private characterRemoving(player: Player): void {
		this.characterRigs.delete(player);
	}

	private async retryCharacterLoad(player: Player): Promise<void> {
		this.logger.Warn(`Getting full rig for ${player.UserId} timed out. Retrying...`);
		return loadCharacter(player);
	}
}


import { Service } from "@flamework/core";
import type { Logger } from "@rbxts/log";
import { BadgeService } from "@rbxts/services";

import { store } from "server/store";
import { selectPlayerAchievements } from "shared/store/persistent";
import { Badge } from "types/enum/badge";

import type { PlayerEntity } from "./player-entity";
import type { OnPlayerJoin } from "./player-service";

@Service({})
export class PlayerBadgeService implements OnPlayerJoin {
	constructor(private readonly logger: Logger) {}

	public onPlayerJoin(playerEntity: PlayerEntity): void {
		const { userId } = playerEntity;

		this.awardBadge(playerEntity, Badge.Welcome).catch((err) => {
			this.logger.Error(`Failed to check if ${userId} has badge ${Badge.Welcome}: ${err}`);
		});

		this.awardUnrewardedBadges(playerEntity).catch((err) => {
			this.logger.Error(`Failed to award unrewarded badges to ${userId}: ${err}`);
		});
	}

	/**
	 * Awards a badge to a player if they don't already have it.
	 *
	 * If the badge is unable to be awarded, the error will be logged and the
	 * badge will not be awarded. We will still internally track that the badge
	 * was attempted to be awarded so that any function that relies on a badge
	 * having been awarded will consider it as awarded.
	 *
	 * @param playerEntity - The player entity to award the badge to.
	 * @param badge - The badge to be awarded.
	 * @returns A promise that resolves when the badge has been awarded.
	 */
	public async awardBadge(playerEntity: PlayerEntity, badge: Badge): Promise<void> {
		const hasBadge = await this.checkIfPlayerHasBadge(playerEntity, badge);
		if (hasBadge) {
			return;
		}

		return this.giveBadge(playerEntity, badge);
	}

	public async checkIfPlayerHasBadge(
		{ player, userId }: PlayerEntity,
		badge: Badge,
	): Promise<boolean> {
		const hasBadge = store.getState(selectPlayerAchievements(userId))?.badges.get(badge);
		if (hasBadge !== undefined) {
			return true;
		}

		return Promise.try(() => BadgeService.UserHasBadgeAsync(player.UserId, tonumber(badge)));
	}

	public async getBadgeInfo(badge: Badge): Promise<BadgeInfo> {
		return Promise.try(() => BadgeService.GetBadgeInfoAsync(tonumber(badge)));
	}

	private async giveBadge(playerEntity: PlayerEntity, badge: Badge): Promise<void> {
		const { player, userId } = playerEntity;

		const badgeInfo = await this.getBadgeInfo(badge);
		if (!badgeInfo.IsEnabled) {
			this.logger.Warn(`Badge ${badge} is not enabled.`);
			return;
		}

		const [success, awarded] = pcall(() =>
			BadgeService.AwardBadge(player.UserId, tonumber(badge)),
		);
		if (!success) {
			throw awarded;
		}

		if (!awarded) {
			this.logger.Warn(`Awarded badge ${badge} to ${userId} but it was not successful.`);
		} else {
			this.logger.Info(`Awarded badge ${badge} to ${userId}`);
		}

		store.awardBadge(userId, badge, awarded);
	}

	private async awardUnrewardedBadges(playerEntity: PlayerEntity): Promise<void> {
		const { userId } = playerEntity;

		const badges = store.getState(selectPlayerAchievements(userId))?.badges;
		if (badges === undefined) {
			return;
		}

		for (const [badge, hasBadge] of badges) {
			if (hasBadge) {
				continue;
			}

			this.awardBadge(playerEntity, badge).catch((err) => {
				this.logger.Error(`Failed to check if ${userId} has badge ${badge}: ${err}`);
			});
		}
	}
}


import type { Janitor } from "@rbxts/janitor";
import type { Document } from "@rbxts/lapis";

import type { PlayerData } from "shared/store/persistent";

export class PlayerEntity {
	/** The player's username. */
	public readonly name: string;
	/** A string representation of the player's UserId. */
	public readonly userId: string;

	constructor(
		public readonly player: Player,
		public readonly janitor: Janitor,
		public readonly document: Document<PlayerData>,
	) {
		this.name = player.Name;
		this.userId = tostring(player.UserId);
	}
}


import type { OnStart } from "@flamework/core";
import { Service } from "@flamework/core";
import { Janitor } from "@rbxts/janitor";
import type { Document } from "@rbxts/lapis";
import type { Logger } from "@rbxts/log";
import { Object } from "@rbxts/luau-polyfill";
import Signal from "@rbxts/rbx-better-signal";
import { Players } from "@rbxts/services";

import { $NODE_ENV } from "rbxts-transform-env";
import type { PlayerData } from "shared/store/persistent";
import type { ListenerData } from "shared/util/flamework-util";
import { setupLifecycle } from "shared/util/flamework-util";
import { onPlayerAdded, promisePlayerDisconnected } from "shared/util/player-util";
import KickCode from "types/enum/kick-reason";

import type { PlayerDataService } from "./data/player-data-service";
import { PlayerEntity } from "./player-entity";
import type { PlayerRemovalService } from "./player-removal-service";

export interface OnPlayerJoin {
	/**
	 * Fires when a player joins the game and is fully initialized. The player
	 * is considered "fully initialized" once all of their data is loaded and
	 * their PlayerEntity class is fully setup. At which point, anything that
	 * implements this lifecycle event will be fired with the class.
	 */
	onPlayerJoin(player: PlayerEntity): void;
}

export interface OnPlayerLeave {
	/**
	 * Fires when a player leaves the game. This is called before the player is
	 * removed from the game and their PlayerEntity class is cleaned up. This
	 * means it is still safe to access player data before it is removed.
	 */
	onPlayerLeave(player: PlayerEntity): Promise<void> | void;
}

/** A service that manages player entities in the game. */
@Service({})
export class PlayerService implements OnStart {
	private readonly onEntityJoined = new Signal<(playerEntity: PlayerEntity) => void>();
	private readonly onEntityRemoving = new Signal();
	private readonly playerEntities = new Map<Player, PlayerEntity>();
	private readonly playerJoinEvents = new Array<ListenerData<OnPlayerJoin>>();
	private readonly playerLeaveEvents = new Array<ListenerData<OnPlayerLeave>>();

	constructor(
		private readonly logger: Logger,
		private readonly playerDataService: PlayerDataService,
		private readonly playerRemovalService: PlayerRemovalService,
	) {}

	/** @ignore */
	public onStart(): void {
		setupLifecycle<OnPlayerJoin>(this.playerJoinEvents);
		setupLifecycle<OnPlayerLeave>(this.playerLeaveEvents);

		onPlayerAdded((player) => {
			this.onPlayerAdded(player).catch((err) => {
				this.logger.Error(`Failed to load player ${player.UserId}: ${err}`);
			});
		});

		Players.PlayerRemoving.Connect(
			this.withPlayerEntity((playerEntity) => {
				this.onPlayerRemoving(playerEntity).catch((err) => {
					this.logger.Error(`Failed to close player ${playerEntity.userId}: ${err}`);
				});
			}),
		);

		this.bindHoldServerOpen();
	}

	/**
	 * Returns an array of all `PlayerEntity` instances associated with players
	 * that have joined the game.
	 *
	 * @returns An array of `PlayerEntity` instances.
	 */
	public getPlayerEntities(): Array<PlayerEntity> {
		return Object.values(this.playerEntities);
	}

	/**
	 * Gets the `PlayerEntity` object for a given player.
	 *
	 * @param player - The `Player` instance to get the `PlayerEntity` for.
	 * @returns The `PlayerEntity` instance associated with the given `Player`,
	 *   if any.
	 */
	public getPlayerEntity(player: Player): PlayerEntity | undefined {
		return this.playerEntities.get(player);
	}

	/**
	 * Retrieves the player entity asynchronously. This method will reject if
	 * the player disconnects before the entity is created.
	 *
	 * @param player - The player object.
	 * @returns A promise that resolves to the player entity, or undefined if
	 *   not found.
	 */
	public async getPlayerEntityAsync(player: Player): Promise<PlayerEntity | undefined> {
		const potentialEntity = this.getPlayerEntity(player);
		if (potentialEntity) {
			return potentialEntity;
		}

		const promise = Promise.fromEvent(
			this.onEntityJoined,
			(playerEntity: PlayerEntity) => playerEntity.player === player,
		);

		// eslint-disable-next-line promise/always-return -- This is the last callback
		const disconnect = promisePlayerDisconnected(player).then(() => {
			promise.cancel();
		});

		const [success, playerEntity] = promise.await();
		disconnect.cancel();

		if (!success) {
			this.logger.Debug(`Player ${player.UserId} disconnected before entity was created`);
			return;
		}

		return playerEntity;
	}

	/**
	 * This method wraps a callback and replaces the first argument (that must
	 * be of type `Player`) with that players `PlayerEntity` class.
	 *
	 * @param func - The callback to wrap.
	 * @returns A new callback that replaces the first argument with the
	 *   player's `PlayerEntity` class.
	 */
	public withPlayerEntity<T extends Array<unknown>, R = void>(
		func: (playerEntity: PlayerEntity, ...args: T) => R,
	) {
		return (player: Player, ...args: T): R | undefined => {
			const playerEntity = this.getPlayerEntity(player);
			if (!playerEntity) {
				this.logger.Error(
					`No entity for player ${player.UserId}, cannot continue to callback`,
				);
				return;
			}

			return func(playerEntity, ...args);
		};
	}

	/**
	 * Called internally when a player joins the game.
	 *
	 * @param player - The player that joined the game.
	 */
	private async onPlayerAdded(player: Player): Promise<void> {
		const playerDocument = await this.playerDataService.loadPlayerData(player);
		if (!playerDocument) {
			this.playerRemovalService.removeForBug(player, KickCode.PlayerInstantiationError);
			return;
		}

		const janitor = this.setupPlayerJanitor(player, playerDocument);
		const playerEntity = new PlayerEntity(player, janitor, playerDocument);
		this.playerEntities.set(player, playerEntity);

		// Call all connected lifecycle events
		debug.profilebegin("Lifecycle_Player_Join");

		for (const { id, event } of this.playerJoinEvents) {
			janitor
				.AddPromise(
					Promise.defer(() => {
						debug.profilebegin(id);
						event.onPlayerJoin(playerEntity);
					}),
				)
				.catch((err) => {
					this.logger.Error(`Error in player lifecycle ${id}: ${err}`);
				});
		}

		debug.profileend();

		this.logger.Info(`Player ${player.UserId} joined the game.`);

		this.onEntityJoined.Fire(playerEntity);
	}

	private setupPlayerJanitor(player: Player, playerDocument: Document<PlayerData>): Janitor {
		const janitor = new Janitor();
		janitor.Add(async () => {
			this.logger.Info(`Player ${player.UserId} leaving game, cleaning up Janitor`);

			try {
				await playerDocument.close();
			} catch (err) {
				this.logger.Error(`Failed to close player document for ${player.UserId}: ${err}`);
			}

			this.playerEntities.delete(player);
			this.onEntityRemoving.Fire();
		});

		return janitor;
	}

	/**
	 * Called internally when a player is removed from the game. We hold the
	 * PlayerEntity until all lifecycle events have been called, so that we can
	 * access player data on player leaving if required.
	 *
	 * @param playerEntity - The player entity associated with the player.
	 */
	private async onPlayerRemoving(playerEntity: PlayerEntity): Promise<void> {
		// Call all connected lifecycle events
		const promises = new Array<Promise<void>>();
		debug.profilebegin("Lifecycle_Player_Leave");

		for (const { id, event } of this.playerLeaveEvents) {
			const promiseEvent = Promise.defer<void>((resolve, reject) => {
				debug.profilebegin(id);
				try {
					const leaveEvent = async (): Promise<void> => {
						await event.onPlayerLeave(playerEntity);
					};

					const [success, err] = leaveEvent().await();
					if (!success) {
						reject(err);
						return;
					}

					resolve();
				} catch (err) {
					this.logger.Error(`Error in player lifecycle ${id}: ${err}`);
				}
			});

			promises.push(promiseEvent);
		}

		debug.profileend();

		// We ensure that all lifecycle events are called before we continue
		// since we may need to access player data in these events.
		await Promise.all(promises).finally(() => {
			playerEntity.janitor.Destroy();
		});
	}

	private bindHoldServerOpen(): void {
		// We want to hold the server open until all PlayerEntities are cleaned
		// up and removed.
		game.BindToClose(() => {
			// We don't want to hold the server open in development
			if ($NODE_ENV !== "production") {
				return;
			}

			this.logger.Debug("Game closing, holding open until all player entities are removed.");

			while (!this.playerEntities.isEmpty()) {
				this.onEntityRemoving.Wait();
			}

			this.logger.Debug("All player entities removed, closing game.");
		});
	}
}


import { Flamework, Modding } from "@flamework/core";
import type { Logger } from "@rbxts/log";
import Log from "@rbxts/log";

import { GAME_NAME } from "shared/constants";
import { setupLogger } from "shared/functions/setup-logger";

function start(): void {
	setupLogger();

	Log.Info(`${GAME_NAME} is starting up! Version: ${game.PlaceVersion}`);

	Modding.registerDependency<Logger>((ctor) => Log.ForContext(ctor));

	Flamework.addPaths("src/server");

	Log.Info("Flamework ignite!");
	Flamework.ignite();
}

start();


import type { BroadcastAction, ProducerMiddleware } from "@rbxts/reflex";
import { createBroadcaster } from "@rbxts/reflex";
import { Players } from "@rbxts/services";

import { $NODE_ENV } from "rbxts-transform-env";
import { events } from "server/network";
import { IS_EDIT } from "shared/constants";
import type { SerializedSharedState, SharedState } from "shared/store";
import { slices, stateSerDes } from "shared/store";

export const ONCE_PER_MINUTE = 60;

export function broadcasterMiddleware(): ProducerMiddleware {
	// Storybook support
	if ($NODE_ENV === "development" && IS_EDIT) {
		return () => (innerDispatch) => innerDispatch;
	}

	const hydrated = new Set<number>();

	const broadcaster = createBroadcaster({
		beforeHydrate: beforeHydrate(hydrated),
		dispatch,
		hydrate,
		hydrateRate: ONCE_PER_MINUTE,
		producers: slices,
	});

	events.store.start.connect((player) => {
		broadcaster.start(player);
	});

	Players.PlayerRemoving.Connect((player) => {
		hydrated.delete(player.UserId);
	});

	return broadcaster.middleware;
}

function beforeHydrate(hydrated: Set<number>): (player: Player, state: SharedState) => SharedState {
	return (player: Player, state: SharedState) => {
		// The cast is necessary due to the typings of the reflex library.
		const serialized = stateSerDes.serialize(state) as unknown as SharedState;

		const isInitialHydrate = !hydrated.has(player.UserId);
		if (!isInitialHydrate) {
			return serialized;
		}

		hydrated.add(player.UserId);
		return serialized;
	};
}

function dispatch(player: Player, actions: Array<BroadcastAction>): void {
	events.store.dispatch.fire(player, actions);
}

function hydrate(player: Player, state: SharedState): void {
	// The cast is necessary due to the typings of the reflex library.
	events.store.hydrate.fire(player, state as unknown as SerializedSharedState);
}


return {
	badges = {
		["example-badge"] = "rbxassetid://18110928024",
	},
	marketing = {
		["example-icon"] = "rbxassetid://18110928241",
		googly = "rbxassetid://18110928530",
	},
}


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

export const GAME_NAME = "Template for roblox-ts";

export const IS_EDIT = RunService.IsStudio() && !RunService.IsRunning();


import { $NODE_ENV } from "rbxts-transform-env";

export enum GameId {
	Development = 6031475575,
	Production = 6110424408,
}

function isGameId(value: number): value is GameId {
	return value in GameId;
}

export function getConfigValueForGame<const T>(gameIdToValueTable: Record<GameId, T>): T {
	if ($NODE_ENV === "development" && game.PlaceId === 0) {
		return gameIdToValueTable[GameId.Development];
	}

	assert(isGameId(game.GameId), `Invalid game id for place: ${game.GameId}`);
	return gameIdToValueTable[game.GameId];
}


import Log, { Logger, LogLevel } from "@rbxts/log";
import type { ILogEventSink, LogEvent } from "@rbxts/log/out/Core";
import { MessageTemplateParser, PlainTextMessageTemplateRenderer } from "@rbxts/message-templates";
import { RunService } from "@rbxts/services";

import { $package } from "rbxts-transform-debug";
import { $NODE_ENV } from "rbxts-transform-env";

export const LOG_LEVEL: LogLevel =
	$NODE_ENV === "development" ? LogLevel.Debugging : LogLevel.Information;

const ENVIRONMENT = RunService.IsClient() ? "Client" : "Server";

const STACK_TRACE_LEVEL_MODULE = 5;
const STACK_TRACE_LEVEL_FLAMEWORK = 4;

/**
 * Represents a log event sink that outputs log messages using the Roblox
 * logging functions.
 */
class LogEventOutputSink implements ILogEventSink {
	public Emit(message: LogEvent): void {
		const template = new PlainTextMessageTemplateRenderer(
			MessageTemplateParser.GetTokens(message.Template),
		);

		const tag = this.getLogLevelString(message.Level);
		const context = message.SourceContext ?? "Game";
		const messageResult = template.Render(message);
		const fileInfo = this.getFileInformation(context);

		const formattedMessage = `[${tag}] ${context} (${ENVIRONMENT}) - ${messageResult}${fileInfo}`;

		if (message.Level >= LogLevel.Fatal) {
			error(formattedMessage);
		} else if (message.Level >= LogLevel.Warning) {
			warn(formattedMessage);
		} else {
			print(formattedMessage);
		}
	}

	private getLogLevelString(level: LogLevel): string {
		switch (level) {
			case LogLevel.Debugging: {
				return "DEBUG";
			}
			case LogLevel.Error: {
				return "ERROR";
			}
			case LogLevel.Fatal: {
				return "FATAL";
			}
			case LogLevel.Information: {
				return "INFO";
			}
			case LogLevel.Verbose: {
				return "VERBOSE";
			}
			case LogLevel.Warning: {
				return "WARN";
			}
		}
	}

	private getFileInformation(context: string): string {
		if (LOG_LEVEL > LogLevel.Verbose) {
			return "";
		}

		const [source] =
			context === "Game"
				? debug.info(STACK_TRACE_LEVEL_MODULE, "sl")
				: debug.info(STACK_TRACE_LEVEL_FLAMEWORK, "sl");
		const [file, line] = source;
		return ` (${file}:${line})`;
	}
}

/** Sets up the logger for the application, for both the client and server. */
export function setupLogger(): void {
	Log.SetLogger(
		Logger.configure()
			.SetMinLogLevel(LOG_LEVEL)
			.EnrichWithProperty("Version", $package.version)
			.WriteTo(new LogEventOutputSink())
			.Create(),
	);
}


-- Services
local RunService = game:GetService("RunService")
local SoundService = game:GetService("SoundService")

if not RunService:IsClient() then
	error("Sound System 3D is to be run on the client. Use RemoteEvents to have the client create the sound.")
end

-- Localize maths for optimization
-- local acos, cos, pi = math.acos, math.cos, math.pi
-- local atan2, deg = math.atan2, math.deg
local v3, cf = Vector3.new, CFrame.new
local dot = v3().Dot
local newInst, getType = Instance.new, typeof

-- Camera setup
local Camera = workspace.CurrentCamera

-- Create container object
local SoundContainer = newInst("Part")
SoundContainer.Name = "SoundContainer"
SoundContainer.CFrame = cf()
SoundContainer.Anchored = true
SoundContainer.CanCollide = false
SoundContainer.Transparency = 1
SoundContainer.Parent = Camera

-- Setup system
local SoundSystem = {}
local CurrentObjects = {}

function SoundSystem.Attach(SoundObj)
	--------------------------
	-- Sanity checks
	--------------------------

	assert(SoundObj:IsA("Sound"), "Attempt to attach invalid Sound object.")
	assert(
		SoundObj.Parent
			and (SoundObj.Parent:IsA("Attachment") or SoundObj.Parent:IsA("BasePart"))
			and SoundObj:IsDescendantOf(workspace),
		"Cannot have 3D effect on sound that is not in 3D environment"
	)

	--------------------------
	-- Object creation
	--------------------------

	local Equalizer = newInst("EqualizerSoundEffect") -- Create a separate one to ensure it doesn't mess with any existing effects
	Equalizer.LowGain = 0
	Equalizer.MidGain = 0
	Equalizer.HighGain = 0

	--------------------------
	-- Effect controller
	--------------------------

	local isAttachment = SoundObj.Parent:IsA("Attachment")

	local Emitter =
		{ Sound = SoundObj, Position = isAttachment and SoundObj.Parent.WorldPosition or SoundObj.Parent.Position }

	local PositionTracker = SoundObj.Parent:GetPropertyChangedSignal("Position"):Connect(function()
		Emitter.Position = isAttachment and SoundObj.Parent.WorldPosition or SoundObj.Parent.Position
	end)

	CurrentObjects[Emitter] = true

	--------------------------
	-- Finalization
	--------------------------

	Equalizer.Parent = SoundObj

	SoundObj.AncestryChanged:Connect(function(_, Parent)
		if not Parent then
			--Destroyed
			CurrentObjects[Emitter] = nil
		elseif
			(SoundObj.Parent:IsA("Attachment") or SoundObj.Parent:IsA("BasePart"))
			and SoundObj:IsDescendantOf(workspace)
		then
			--Moved
			CurrentObjects[Emitter] = true

			PositionTracker:Disconnect()
			PositionTracker = SoundObj.Parent:GetPropertyChangedSignal("Position"):Connect(function()
				Emitter.Position = isAttachment and SoundObj.Parent.WorldPosition or SoundObj.Parent.Position
			end)
		else
			--Moved to invalid object
			CurrentObjects[Emitter] = nil
		end
	end)
end

function SoundSystem.Create(ID, Target, Looped)
	local TargetType

	--------------------------
	-- Sanity checks
	--------------------------

	if not ID or getType(ID) ~= "string" or not ID:match("%d+") then -- Must exist, be a string, and have numbers
		error("Invalid ID: " .. tostring(ID))
	end
	if Target then -- Must exist
		TargetType = getType(Target)
		if TargetType ~= "Instance" and TargetType ~= "Vector3" and TargetType ~= "CFrame" then -- Must be valid type
			error("Invalid Target: " .. tostring(Target))
		end
	else
		error("Invalid Target: " .. tostring(Target))
	end
	Looped = Looped or false

	--------------------------
	-- Object creation
	--------------------------

	local Emitter = newInst("Attachment")
	--Emitter.Visible	= true

	if TargetType == "Instance" and Target.Position then
		-- Sound follows object
		RunService.RenderStepped:Connect(function()
			Emitter.WorldPosition = Target.Position
		end)
	elseif TargetType == "Vector3" then
		-- Sound in static position
		Emitter.WorldPosition = Target
	elseif TargetType == "CFrame" then
		-- Sound in static position
		Emitter.WorldPosition = Target.Position
	end

	local Sound = newInst("Sound")
	Sound.Looped = Looped
	Sound.SoundId = "rbxassetid://" .. ID:match("%d+")

	local Equalizer = newInst("EqualizerSoundEffect")
	Equalizer.LowGain = 0
	Equalizer.MidGain = 0
	Equalizer.HighGain = 0

	--------------------------
	-- Effect controller
	--------------------------

	CurrentObjects[Emitter] = true
	if not Looped then
		Sound.Ended:Connect(function()
			CurrentObjects[Emitter] = nil
			Emitter:Destroy()
		end)
	end

	--------------------------
	-- Finalization
	--------------------------

	Equalizer.Parent = Sound
	Sound.Parent = Emitter
	Emitter.Parent = SoundContainer

	Sound:Play()

	return Emitter
end

--------------------------
-- 3D-Effect management
--------------------------

RunService.RenderStepped:Connect(function()
	local _, Listener = SoundService:GetListener()

	if Listener then
		if Listener:IsA("BasePart") then
			Listener = Listener.CFrame
		end
	else
		Listener = Camera.CFrame
	end

	for Emitter, _ in pairs(CurrentObjects) do
		local Facing = Listener.LookVector
		local Vector = (Emitter.Position - Listener.Position).unit

		--Remove Y so up/down doesn't matter
		Facing = v3(Facing.X, 0, Facing.Z)
		Vector = v3(Vector.X, 0, Vector.Z)

		local Angle = math.acos(dot(Facing, Vector) / (Facing.magnitude * Vector.magnitude))

		Emitter.Sound.EqualizerSoundEffect.HighGain = -(25 * ((Angle / math.pi) ^ 2))
	end
end)

return SoundSystem


/* eslint-disable eslint-comments/no-unlimited-disable -- See below. */
/* eslint-disable -- Taken directly from https://www.npmjs.com/package/@rbxts/make due to compiler type mismatch. */
type WritablePropertyNames<T> = {
	readonly [K in keyof T]-?: T[K] extends Callback
		? never
		: (<F>() => F extends { [Q in K]: T[K] } ? 1 : 2) extends <F>() => F extends {
					-readonly [Q in K]: T[K];
			  }
					? 1
					: 2
			? K
			: never;
}[keyof T];

type GetBindableToRBXScriptSignal<T> = {
	[key in {
		[K in keyof T]-?: T[K] extends RBXScriptSignal ? K : never;
	}[keyof T]]: T[key] extends RBXScriptSignal<infer R> ? R : never;
};

type GetPartialObjectWithBindableConnectSlots<T extends Instance> = Partial<
	GetBindableToRBXScriptSignal<T> & Pick<T, WritablePropertyNames<T>>
>;

function Make<
	T extends keyof CreatableInstances,
	Q extends {
		/** The Children to place inside of this Instance. */
		Children?: ReadonlyArray<Instance>;
		Parent?: Instance | undefined;
	} & GetPartialObjectWithBindableConnectSlots<CreatableInstances[T]>,
>(className: T, settings: Q) {
	const { Children: children, Parent: parent } = settings;
	const instance = new Instance(className);

	for (const [setting, value] of pairs(settings as unknown as Map<never, never>)) {
		if (setting !== "Children" && setting !== "Parent") {
			const { [setting]: property } = instance;

			if (typeIs(property, "RBXScriptSignal")) {
				(property as RBXScriptSignal).Connect(value);
			} else {
				instance[setting] = value;
			}
		}
	}

	if (children) {
		for (const child of children) {
			child.Parent = instance;
		}
	}

	instance.Parent = parent;
	return instance as CreatableInstances[T] &
		Reconstruct<
			{
				[G in "ClassName"]: T;
			} & { [O in Extract<"Name", keyof Q>]: Q[O] } & (Q["Children"] extends never
					? never
					: {
								[K in Exclude<
									keyof Q["Children"],
									"length" | keyof ReadonlyArray<any>
								>]: Q["Children"][K] extends infer A
									? A extends { Name: string }
										? string extends A["Name"]
											? never
											: (k: { [P in A["Name"]]: A }) => void
										: never
									: never;
						  }[Exclude<
								keyof Q["Children"],
								"length" | keyof ReadonlyArray<any>
						  >] extends (k: infer U) => void
						? U
						: never)
		>;
}

export = Make;


import { Networking } from "@flamework/networking";
import type { BroadcastAction } from "@rbxts/reflex";

import type { GamePass } from "types/enum/mtx";

import type { SerializedSharedState } from "./store";

/** Fired by client to server. */
interface ClientToServerEvents {
	mtx: {
		/**
		 * Sets the active state of a game pass, which is used to determine if a
		 * game pass is "on" for a player. E.g. If a player has a game pass for
		 * toggling increased walk speed, this would be used to turn that on or
		 * off.
		 *
		 * @param gamePass - The game pass to set the active state of.
		 * @param active - The active state to set the game pass to.
		 */
		setGamePassActive: (gamePass: GamePass, active: boolean) => void;
	};
	store: {
		/**
		 * Called by the client when they are ready to receive data from the
		 * server.
		 */
		start: () => void;
	};
}

/** Fired by server to client. */
interface ServerToClientEvents {
	store: {
		/**
		 * Sends state updates to the client.
		 *
		 * @param actions - The actions to send to the client.
		 */
		dispatch: (actions: Array<BroadcastAction>) => void;
		hydrate: (state: SerializedSharedState) => void;
	};
}

interface ClientToServerFunctions {}

export const GLOBAL_EVENTS = Networking.createEvent<ClientToServerEvents, ServerToClientEvents>();
export const GLOBAL_FUNCTIONS = Networking.createFunction<
	ClientToServerFunctions,
	NonNullable<unknown>
>();


import { createBinarySerializer } from "@rbxts/flamework-binary-serializer";
import type { CombineStates } from "@rbxts/reflex";

import { persistentSlice } from "./persistent/persistent-slice";

export type SharedState = CombineStates<typeof slices>;
export type SerializedSharedState = ReturnType<typeof stateSerDes.serialize>;

export const stateSerDes = createBinarySerializer<SharedState>();

export const slices = {
	persistent: persistentSlice,
};


import type { ProducerMiddleware } from "@rbxts/reflex";

export const profilerMiddleware: ProducerMiddleware = () => {
	return (dispatch, name) => {
		return (...args) => {
			debug.profilebegin(name);
			const result = dispatch(...args);
			debug.profileend();
			return result;
		};
	};
};


import { createSelector } from "@rbxts/reflex";

import type { SharedState } from "..";
import type { PlayerData } from "./persistent-slice";

export function selectPlayerAchievements(playerId: string) {
	return (state: SharedState) => state.persistent.achievements[playerId];
}

export function selectPlayerBalance(playerId: string) {
	return (state: SharedState) => state.persistent.balance[playerId];
}

export function selectPlayerMtx(playerId: string) {
	return (state: SharedState) => state.persistent.mtx[playerId];
}

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

export function selectPlayerData(playerId: string): (state: SharedState) => PlayerData | undefined {
	return createSelector(
		selectPlayerAchievements(playerId),
		selectPlayerBalance(playerId),
		selectPlayerMtx(playerId),
		selectPlayerSettings(playerId),
		(achievements, balance, mtx, settings): PlayerData | undefined => {
			if (!achievements || !balance || !mtx || !settings) {
				return undefined;
			}

			return {
				achievements,
				balance,
				mtx,
				settings,
			};
		},
	);
}


import { createProducer } from "@rbxts/reflex";

import type { Badge } from "types/enum/badge";

import type { PlayerAchievements, PlayerData } from "./default-data";

export type AchievementState = Readonly<Record<string, PlayerAchievements | undefined>>;

const initialState: AchievementState = {};

export const achievementsSlice = createProducer(initialState, {
	/** @ignore */
	closePlayerData: (state, player: string): AchievementState => {
		return {
			...state,
			[player]: undefined,
		};
	},

	/** @ignore */
	loadPlayerData: (state, player: string, data: PlayerData): AchievementState => {
		return {
			...state,
			[player]: data.achievements,
		};
	},

	/**
	 * Stores the badge status for a player.
	 *
	 * @param state - The current state.
	 * @param player - The player to award the badge to.
	 * @param badge - The badge to award.
	 * @param badgeStatus - True if the badge was successfully awarded, false if
	 *   it needs to be retried in the future.
	 * @returns The new state.
	 */
	awardBadge: (state, player: string, badge: Badge, badgeStatus: boolean): AchievementState => {
		const achievements = state[player];
		return {
			...state,
			[player]: achievements && {
				...achievements,
				badges: new Map([...achievements.badges]).set(badge, badgeStatus),
			},
		};
	},
});


import { createProducer } from "@rbxts/reflex";

import type { PlayerBalance, PlayerData } from "./default-data";

export type BalanceState = Readonly<Record<string, PlayerBalance | undefined>>;

const initialState: BalanceState = {};

export const balanceSlice = createProducer(initialState, {
	/** @ignore */
	closePlayerData: (state, player: string): BalanceState => {
		return {
			...state,
			[player]: undefined,
		};
	},

	/** @ignore */
	loadPlayerData: (state, player: string, data: PlayerData): BalanceState => {
		return {
			...state,
			[player]: data.balance,
		};
	},

	giveCurrency: (state, player: string, amount: number): BalanceState => {
		const balance = state[player];
		return {
			...state,
			[player]: balance && {
				...balance,
				currency: balance.currency + amount,
			},
		};
	},
});


import { combineProducers } from "@rbxts/reflex";

import { achievementsSlice } from "./achievements";
import { balanceSlice } from "./balance";
import { mtxSlice } from "./mtx";
import { settingsSlice } from "./settings";

export * from "./achievements";
export * from "./balance";
export * from "./default-data";
export * from "./mtx";
export * from "./settings";

export const persistentSlice = combineProducers({
	achievements: achievementsSlice,
	balance: balanceSlice,
	mtx: mtxSlice,
	settings: settingsSlice,
});


import { createProducer } from "@rbxts/reflex";

import type { PlayerData, PlayerSettings } from "./default-data";

export type SettingsState = Readonly<Record<string, PlayerSettings | undefined>>;

const initialState: SettingsState = {};

export const settingsSlice = createProducer(initialState, {
	/** @ignore */
	closePlayerData: (state, player: string): SettingsState => {
		return {
			...state,
			[player]: undefined,
		};
	},

	/** @ignore */
	loadPlayerData: (state, player: string, data: PlayerData): SettingsState => {
		return {
			...state,
			[player]: data.settings,
		};
	},

	changeSetting: (
		state,
		player: string,
		settingType: keyof PlayerSettings,
		value: PlayerSettings[keyof PlayerSettings],
	): SettingsState => {
		const setting = state[player];

		return {
			...state,
			[player]: setting && {
				...setting,
				[settingType]: value,
			},
		};
	},
});


local coreCall
do
	local MAX_RETRIES = 8

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

	function coreCall(method, ...)
		local result = {}
		for _ = 1, MAX_RETRIES do
			result = { pcall(StarterGui[method], StarterGui, ...) }
			if result[1] then
				break
			end
			RunService.Stepped:Wait()
		end
		return unpack(result)
	end
end

return coreCall


--!strict
--[[
	Calls a function and throws an error if it attempts to yield.

	Pass any number of arguments to the function after the callback.

	This function supports multiple return; all results returned from the
	given function will be returned.
]]
local function resultHandler(co: thread, ok: boolean, ...)
	if not ok then
		local err = (...)
		if typeof(err) == "string" then
			error(debug.traceback(co, err), 2)
		else
			-- If the error is not of type string, just assume it has some
			-- meaningful information and rethrow it with a `tostring` so that
			-- top-level error handlers can process it
			error(tostring(err), 2)
		end
	end

	if coroutine.status(co) ~= "dead" then
		error(debug.traceback(co, "Attempted to yield inside changed event!"), 2)
	end

	return ...
end

local function noYield(callback, ...)
	local co = coroutine.create(callback)

	return resultHandler(co, coroutine.resume(co, ...))
end

return {
	noYield = noYield,
}


import { Players } from "@rbxts/services";
import type { EvaluateInstanceTree } from "@rbxts/validate-tree";

export const CHARACTER_LOAD_TIMEOUT = 10;

export type CharacterRig = EvaluateInstanceTree<typeof CharacterSchema>;

export const CharacterSchema = {
	$className: "Model",
	Head: "MeshPart",
	Humanoid: {
		$className: "Humanoid",
		Animator: "Animator",
	},
	HumanoidRootPart: "BasePart",
} as const;

/**
 * Destroys the player's character model, ensuring that there is no memory leak.
 *
 * @param player - The player whose character model to destroy.
 * @see https://twitter.com/mrchickenrocket/status/1699005062360789405?s=46
 */
export function cleanupCharacter(player: Player): void {
	if (!player.Character) {
		return;
	}

	player.Character.Destroy();
	player.Character = undefined;
}

/**
 * A utility function that gets a player by their username.
 *
 * @param name - The username of the player to get.
 * @returns The player with the given username, if any.
 */
export function getPlayerByName(name: string): Player | undefined {
	const player = Players.FindFirstChild(name);

	if (player?.IsA("Player") === false) {
		return;
	}

	return player;
}

/**
 * Attempts to load the player's character model. If the character model fails
 * to load within 10 seconds, the promise will still resolve.
 *
 * `LoadCharacter` is a Roblox API call that is prone to failure and can end up
 * yielding forever. This function is a workaround to that issue.
 *
 * @param player - The player whose character model to load.
 * @see character-service.ts which handles retries
 */
export async function loadCharacter(player: Player): Promise<void> {
	await Promise.race<unknown>([
		Promise.try(() => {
			cleanupCharacter(player);
			player.LoadCharacter();
		}),
		Promise.delay(CHARACTER_LOAD_TIMEOUT),
	]);
}

/**
 * Registers a callback function to be called when a player's character is
 * added.
 *
 * @param player - The player whose character is being monitored.
 * @param callback - The callback function to be called when the character is
 *   added. It receives the character's rig as a parameter.
 * @returns A function that can be called to disconnect the callback from the
 *   event.
 */
export function onCharacterAdded(player: Player, callback: (rig: Model) => void): () => void {
	if (player.Character) {
		callback(player.Character);
	}

	const connection = player.CharacterAdded.Connect(callback);

	return () => {
		connection.Disconnect();
	};
}

/**
 * A utility function that listens for when a player is added to the game and
 * calls the callback with the player. This function also calls the callback for
 * all players that are already in the game.
 *
 * @param callback - The callback to call when a player is added to the game.
 * @returns A function that, when called, disconnects the connection.
 */
export function onPlayerAdded(callback: (player: Player) => void): () => void {
	const connection = Players.PlayerAdded.Connect(callback);

	for (const player of Players.GetPlayers()) {
		callback(player);
	}

	return () => {
		connection.Disconnect();
	};
}

/**
 * Returns a promise that resolves when the specified player is disconnected. If
 * the player is not a descendant of the Players service, the promise will
 * immediately resolve.
 *
 * @param player - The player to wait for disconnection.
 * @returns A promise that resolves when the player is disconnected.
 */
export async function promisePlayerDisconnected(player: Player): Promise<void> {
	if (!player.IsDescendantOf(Players)) {
		return;
	}

	await Promise.fromEvent(Players.PlayerRemoving, (playerWhoLeft) => playerWhoLeft === player);
}


import { Component } from "@flamework/components";
import { OnStart } from "@flamework/core";
import { Janitor } from "@rbxts/janitor";
import { CharacterController } from "client/controllers/character-controller";
import { PlayerAtom } from "client/controllers/player-controller";
import { Character } from "shared/components/character";
import { InjectType } from "shared/decorators/field/Inject-type";
import { DataCollection } from "shared/singletons/data-collection";
import { DependenciesContainer } from "shared/utilities/dependencies-container";

@Component({})
export class ClientCharacter extends Character implements OnStart {
	private currentTrack?: AnimationTrack;
	private previousTrack?: AnimationTrack;
	private loadedTracks: Map<Animation, AnimationTrack> = new Map();
	private dependencyContainer = new DependenciesContainer(true);
	private janitor = new Janitor();

	private getTrack(animation: Animation) {
		if (!this.loadedTracks.has(animation)) {
			const track = this.instance.Humanoid.Animator.LoadAnimation(animation);
			this.loadedTracks.set(animation, track);
		}

		return this.loadedTracks.get(animation)!;
	}

	public StopAnimation(fade?: number) {
		if (!this.currentTrack) return;
		this.currentTrack.Stop(fade);
		this.currentTrack = undefined;
	}

	public PlayAnimation(
		animation: Animation,
		fade?: number,
		priority?: Enum.AnimationPriority,
		sideTrack: boolean = false,
		fadePreviousAnimation?: number,
	) {
		const track = this.getTrack(animation);

		if (priority) track.Priority = priority;

		if (this.currentTrack === track) return this.currentTrack;
		if (this.currentTrack && !sideTrack) this.currentTrack.Stop(fadePreviousAnimation ?? fade);

		track.Play(fade);

		if (!sideTrack) {
			this.previousTrack = this.currentTrack;
			this.currentTrack = track;
		}

		return track;
	}

	public PlayPreviousAnimation(fade?: number) {
		if (!this.previousTrack || !this.previousTrack.Animation) return;
		this.PlayAnimation(this.previousTrack.Animation, fade, this.previousTrack!.Priority);
	}

	public destroy(): void {
		super.destroy();
		this.janitor.Cleanup();
	}

	public onStart() {
		this.dependencyContainer.Register<ClientCharacter>(() => this);
	}
}


import { Components } from "@flamework/components";
import { Controller, OnStart } from "@flamework/core";
import { promiseR15 } from "@rbxts/character-promise";
import Signal from "@rbxts/rbx-better-signal";
import { ClientCharacter } from "client/components/character";
import { Multiplace } from "shared/decorators/constructor/multiplace";
import { InjectType } from "shared/decorators/field/Inject-type";
import { LocalPlayer } from "shared/utilities/constants";

@Controller({})
export class CharacterController implements OnStart {
	public readonly OnDied = new Signal<() => void>();

	@InjectType
	private components!: Components;

	private processCharacter(character: Model) {
		promiseR15(character).then((validatedCharacter) => {
			this.components.addComponent<ClientCharacter>(validatedCharacter);
		});
	}

	public Get() {
		if (!LocalPlayer.Character) return;
		return this.components.getComponent<ClientCharacter>(LocalPlayer.Character);
	}

	public onStart() {
		LocalPlayer.CharacterAdded.Connect((character) => this.processCharacter(character));
		LocalPlayer.CharacterRemoving.Connect((character) => {
			this.components.removeComponent<ClientCharacter>(character);
		});

		LocalPlayer.Character && this.processCharacter(LocalPlayer.Character);
	}
}


import { Controller } from "@flamework/core";
import { ContentProvider, ReplicatedStorage, Workspace } from "@rbxts/services";
import { PlayerGui } from "shared/utilities/constants";
import { OnDataReplicated } from "./player-controller";

@Controller({})
export class LoadingController implements OnDataReplicated {
	private isReplicated = false;
	private thread?: thread;

	private loadInstance(instance: Instance | string) {
		return new Promise<void>((resolve) => {
			ContentProvider.PreloadAsync([instance]);
			resolve();
		})
			.timeout(10)
			.catch(() => {});
	}
	public async LoadAssets(progressBinding: (value: number) => void) {
		const promises = Workspace.GetChildren().map((instance) => this.loadInstance(instance));
		ReplicatedStorage.GetChildren().forEach((instance) => promises.push(this.loadInstance(instance)));
		PlayerGui.GetDescendants().forEach((instance) => promises.push(this.loadInstance(instance)));

		promises.forEach((promise) => {
			promise.then(() => progressBinding(1 / promises.size()));
		});

		await Promise.allSettled(promises);
	}

	public OnDataReplicated() {
		this.isReplicated = true;
		this.thread && coroutine.resume(this.thread)[0];
	}

	public async WaitForLoadData() {
		if (this.isReplicated) return;
		this.thread = coroutine.running();

		coroutine.yield();
	}
}


import { BaseComponent } from "@flamework/components";
import { Controller, Modding, OnRender, OnStart } from "@flamework/core";
import { UserInputService, Workspace } from "@rbxts/services";
import { OnMouseHoverEnd, OnMouseHoverStart } from "client/event-hooks/on-mouse-hovered";
import { LocalPlayer } from "shared/utilities/constants";
import { FindFirstAncestorOfClassWithPredict, GetDistance } from "shared/utilities/function-utilities";

@Controller({})
export class MouseHoverController implements OnStart, OnRender {
	private instances = new Map<BasePart | Model, Partial<OnMouseHoverStart & OnMouseHoverEnd>>();
	private raycastParams = new RaycastParams();
	private foundInstance?: BasePart | Model;

	onStart() {
		this.raycastParams.FilterType = Enum.RaycastFilterType.Include;
		this.handleMouseHover();
	}

	private handleMouseHover() {
		Modding.onListenerAdded<OnMouseHoverStart>((object) => {
			if (!(object instanceof BaseComponent)) return;

			const component = object as BaseComponent;
			if (!component.instance.IsA("Model") && !component.instance.IsA("BasePart")) return;

			this.raycastParams.AddToFilter(object.instance);
			this.instances.set(object.instance, object);
		});

		Modding.onListenerRemoved<OnMouseHoverStart>((object) => {
			if (!(object instanceof BaseComponent)) return;

			this.instances.delete(object.instance);
		});

		Modding.onListenerAdded<OnMouseHoverEnd>((object) => {
			if (!(object instanceof BaseComponent)) return;

			const component = object as BaseComponent;
			if (!component.instance.IsA("Model") && !component.instance.IsA("BasePart")) return;

			this.raycastParams.AddToFilter(object.instance);
			this.instances.set(object.instance, object);
		});

		Modding.onListenerRemoved<OnMouseHoverEnd>((object) => {
			if (!(object instanceof BaseComponent)) return;

			this.instances.delete(object.instance);
		});
	}

	private activateListener(instance: BasePart | Model) {
		const component = this.instances.get(instance);
		if (!component || !component.onMouseHoverStart) return;

		component.onMouseHoverStart();
	}

	private deactivateListener(instance: BasePart | Model) {
		const component = this.instances.get(instance);
		if (!component || !component.onMouseHoverEnd) return;

		component.onMouseHoverEnd();
	}

	private validateDistance(startPoint: Vector3, endPoint: Vector3, distance: number) {
		return GetDistance(startPoint, endPoint) <= distance;
	}

	private findInstance() {
		const mousePosition = UserInputService.GetMouseLocation();
		const ray = Workspace.CurrentCamera!.ViewportPointToRay(mousePosition.X, mousePosition.Y);
		if (!ray) return;

		const result = Workspace.Raycast(ray.Origin, ray.Direction.mul(50), this.raycastParams);
		if (!result) return;

		if (this.instances.has(result.Instance)) {
			const component = result.Instance;
			return component;
		}

		return (
			FindFirstAncestorOfClassWithPredict(result.Instance, "Model", (instance) => {
				return this.instances.has(instance);
			}) ??
			FindFirstAncestorOfClassWithPredict(result.Instance, "Tool", (instance) => {
				return this.instances.has(instance);
			})
		);
	}

	private getComponent(instance: BasePart | Model) {
		return this.instances.get(instance);
	}

	private getPosition(instance: BasePart | Model) {
		if (instance.IsA("Model")) {
			return instance.GetPivot().Position;
		}

		return instance.Position;
	}

	public onRender() {
		const instance = this.findInstance();
		const character = LocalPlayer.Character;
		if (instance === this.foundInstance || !character || !character.PrimaryPart) return;

		if (this.foundInstance && this.foundInstance !== instance) {
			this.deactivateListener(this.foundInstance);
		}

		if (!instance) {
			this.foundInstance = instance;
			return;
		}

		const component = this.getComponent(instance);
		if (component && component?.validDistance) {
			if (
				!this.validateDistance(
					character.PrimaryPart!.Position,
					this.getPosition(instance),
					component.validDistance,
				)
			)
				return;
		}

		this.foundInstance = instance;
		instance && this.activateListener(instance);
	}
}


/* eslint-disable @typescript-eslint/no-unused-expressions */
import { Controller, Modding, OnInit } from "@flamework/core";
import { subscribe } from "@rbxts/charm";
import { client, SyncPayload } from "@rbxts/charm-sync";
import { Events } from "client/network";
import { InjectType } from "shared/decorators/field/Inject-type";
import { PlayerAtoms } from "shared/network";
import { GameDataSchema } from "shared/schemas/game-data";
import { GameAtom, GameData } from "shared/schemas/game-data-types";
import { PlayerDataSchema } from "shared/schemas/player-data";
import { PlayerData } from "shared/schemas/player-data-types";
import { CreateAtom, WrappedAtom } from "shared/utilities/atom-utility";
import { GetCurrentThread } from "shared/utilities/function-utilities";
import { DeepCloneTable } from "shared/utilities/object-utilities";
import { ReflexDevToolsController } from "./reflex-devtools-controller";

export interface OnDataReplicated {
	OnDataReplicated(): void;
}

interface ClientPlayerData {
	playerData: PlayerData;
	gameData: GameData;
}

export type PlayerAtom = WrappedAtom<PlayerData>;

const gameAtom = CreateAtom(DeepCloneTable(GameDataSchema));
Modding.registerDependency<GameAtom>((ctor) => gameAtom);

const playerAtom = CreateAtom(DeepCloneTable(PlayerDataSchema));
Modding.registerDependency<PlayerAtom>((ctor) => playerAtom);

@Controller({
	loadOrder: -1,
})
export class PlayerController implements OnInit {
	private atoms = {
		playerData: playerAtom,
		gameData: gameAtom,
	} as unknown as PlayerAtoms;

	private syncer = client<PlayerAtoms>({
		atoms: this.atoms,
	});
	private isGotData = false;

	@InjectType
	private reflexDevTools!: ReflexDevToolsController;

	public onInit() {
		const listeners = new Set<OnDataReplicated>();
		Modding.onListenerAdded<OnDataReplicated>((obj) => listeners.add(obj));
		Modding.onListenerRemoved<OnDataReplicated>((obj) => listeners.delete(obj));

		this.expectData().then(() => {
			listeners.forEach((listener) => listener.OnDataReplicated());
		});
		this.StartReplication();
	}

	/**@metadata macro */
	public GetClass<T>(id?: Modding.Generic<T, "id">) {
		assert(id);
		return Modding.getObjectFromId(id) as T;
	}

	public GetPlayerAtom() {
		return this.atoms.playerData;
	}

	public GetData(): ClientPlayerData;
	public GetData<S>(selector: (state: ClientPlayerData) => S): S;
	public GetData(selector?: (state: ClientPlayerData) => unknown) {
		const data = {
			playerData: this.atoms.playerData(),
			gameData: this.atoms.gameData(),
		};

		return selector ? selector(data) : data;
	}

	public GetPlayerData(): PlayerData;
	public GetPlayerData<S>(selector: (state: PlayerData) => S): S;
	public GetPlayerData(selector?: (state: PlayerData) => unknown) {
		return selector ? selector(this.atoms.playerData()) : this.atoms.playerData();
	}

	public GetGameData(): GameData;
	public GetGameData<S>(selector: (state: GameData) => S): S;
	public GetGameData(selector?: (state: GameData) => unknown) {
		return selector ? selector(this.atoms.gameData()) : this.atoms.gameData();
	}

	public GetGameAtom() {
		return this.atoms.gameData;
	}

	public GetAtoms() {
		return this.atoms;
	}

	public Subscribe(listener: (state: ClientPlayerData, previousState: ClientPlayerData) => void): () => void;

	public Subscribe<T>(
		selector: (state: ClientPlayerData) => T,
		listener: (state: T, previousState: T) => void,
	): () => void;

	public Subscribe(...args: unknown[]) {
		if (args.size() === 1) {
			return subscribe(() => this.GetData(), args[0] as never);
		}

		const [selector, listener] = args as [
			(state: ClientPlayerData) => unknown,
			(state: unknown, previousState: unknown) => void,
		];
		return subscribe(() => selector(this.GetData()), listener as never);
	}

	public SubscribeToPlayerData(listener: (state: PlayerData, previousState: PlayerData) => void): () => void;

	public SubscribeToPlayerData<T>(
		selector: (state: PlayerData) => T,
		listener: (state: T, previousState: T) => void,
	): () => void;

	public SubscribeToPlayerData(...args: unknown[]) {
		if (args.size() === 1) {
			return subscribe(this.GetPlayerAtom(), args[0] as never);
		}

		const [selector, listener] = args as [
			(state: PlayerData) => unknown,
			(state: unknown, previousState: unknown) => void,
		];
		return subscribe(() => selector(this.GetPlayerAtom()()), listener as never);
	}

	private async expectData() {
		const thread = GetCurrentThread();

		Events.Dispatch.connect((payload) => {
			this.dispatch(payload as SyncPayload<PlayerAtoms>);

			if (!this.isGotData) {
				this.isGotData = true;
				thread.Resume();
			}
		});

		thread.Yield();
	}

	private dispatch(payload: SyncPayload<PlayerAtoms>) {
		this.syncer.sync(payload);

		payload.data.playerData &&
			this.reflexDevTools.DisplayData(`PlayerData-${payload.type}`, payload.data.playerData);
		payload.data.gameData && this.reflexDevTools.DisplayData(`GameData-${payload.type}`, payload.data.gameData);
	}

	private StartReplication() {
		Events.StartReplication.fire();
	}
}


import { Controller, OnStart } from "@flamework/core";
import { subscribe } from "@rbxts/charm";
import React from "@rbxts/react";
import { createPortal, createRoot } from "@rbxts/react-roblox";
import { StarterGui } from "@rbxts/services";
import { App } from "client/ui/app";
import RootProducer from "client/ui/store";
import { InjectType } from "shared/decorators/field/Inject-type";
import { LocalPlayer } from "shared/utilities/constants";
import { PlayerController } from "./player-controller";

const root = createRoot(new Instance("Folder"));
const PlayerGui = LocalPlayer?.WaitForChild("PlayerGui") as PlayerGui;

@Controller({})
export class UiController implements OnStart {
	@InjectType
	private playerController!: PlayerController;

	public onStart() {
		this.disableRobloxUI();
		this.initSyncRootProducer();

		this.renderApp();
	}

	public renderApp() {
		root.unmount();
		root.render(createPortal(<App />, PlayerGui));
	}

	private disableRobloxUI() {
		StarterGui.SetCoreGuiEnabled(Enum.CoreGuiType.Backpack, false);
	}

	private initSyncRootProducer() {
		const atoms = this.playerController.GetAtoms();

		subscribe(atoms.playerData, (state) =>
			RootProducer.setState({
				...RootProducer.getState(),
				playerData: state as never,
			}),
		);

		subscribe(atoms.gameData, (state) =>
			RootProducer.setState({
				...RootProducer.getState(),
				gameData: state,
			}),
		);
	}
}


import { Flamework, Modding } from "@flamework/core";
import { Start } from "@rbxts/better-refx";
import { Centurion } from "@rbxts/centurion";
import { CenturionUI } from "@rbxts/centurion-ui";
import Log, { Logger } from "@rbxts/log";
import { StartFlameworkUtils } from "shared/flamework-utils";
import { LoadGameDataFromReplicatedStorage } from "shared/singletons/data-collection";
import { SetupLogger } from "shared/utilities/setup-logger";

Flamework.addPaths("src/shared");
Flamework.addPaths("src/client");

Modding.registerDependency<Logger>((ctor) => {
	SetupLogger();
	return Log.ForContext(ctor);
});

LoadGameDataFromReplicatedStorage();
Start();
StartFlameworkUtils();
Flamework.ignite();

Centurion.client()
	.start()
	.then(() =>
		CenturionUI.start(Centurion.client(), {
			activationKeys: [Enum.KeyCode.Backquote],
		}),
	)
	.catch((err) => warn("Failed to start Centurion:", err));


import { createContext, useContext } from "@rbxts/react";
import { ScalerApi } from "@rbxts/ui-scaler";

interface AppContext {
	ScreenGui: ScreenGui;
	PlayerGui: PlayerGui;
	ScalerApi: ScalerApi;
}

export const AppContext = createContext<AppContext>(undefined!);

export const useAppContext = () => {
	const context = useContext(AppContext);
	assert(context, "AppContext not found. Did you call outside of App");

	return context;
};

export const useScreenGui = () => {
	const { ScreenGui } = useAppContext();
	return ScreenGui;
};


import React, { useState } from "@rbxts/react";
import { ReflexProvider } from "@rbxts/react-reflex";
import { withScaler } from "client/wrappers/with-scaler";
import { GetPlaceName } from "shared/places";
import { PlayerGui } from "shared/utilities/constants";
import { useScalerApi } from "../hooks/scaler/context";
import { Apps } from "../settings/apps";
import RootProducer from "../store";
import { AppContext } from "./app-context";
import { Canvas } from "../utility-components/canvas";

export const ScreenGUIName = "react-root";

export const App = withScaler(() => {
	const scalerApi = useScalerApi();
	const [ref, setRef] = useState<ScreenGui>();
	const ComponentData = Apps[GetPlaceName()];

	return (
		<AppContext.Provider value={{ ScreenGui: ref!, PlayerGui: PlayerGui, ScalerApi: scalerApi }}>
			<ReflexProvider producer={RootProducer}>
				<Canvas Key={ScreenGUIName} ref={setRef} ignoreGuiInset={ComponentData?.IgnoreGuiInset}>
					{ref ? (
						<>
							<uiscale Scale={scalerApi.scale} key={"app-scaler"} />
							{ComponentData ? <ComponentData.Component /> : <></>}
						</>
					) : (
						<></>
					)}
				</Canvas>
			</ReflexProvider>
		</AppContext.Provider>
	);
});


import React, { createElement, PropsWithChildren, useEffect, useRef, useState } from "@rbxts/react";
import * as ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import { ReturnControls } from "@rbxts/ui-labs/src/ControlTypings/Typing";
import { InferProps, StoryCreation } from "@rbxts/ui-labs/src/Typing/Typing";
import { withScaler } from "client/wrappers/with-scaler";
import { AppContext } from "../app/app-context";
import { useScalerApi } from "./scaler/context";

function ScalerComponent(props: PropsWithChildren) {
	const scalerApi = useScalerApi();
	const ref = useRef<Frame>();
	const [screenGui, setScreenGui] = useState<ScreenGui>();

	useEffect(() => {
		if (!ref.current) return;
		setScreenGui(ref.current.FindFirstAncestorOfClass("ScreenGui"));
	}, [ref.current]);

	return (
		<>
			<frame BackgroundTransparency={1} key={"Pointer"} ref={ref} />
			{screenGui !== undefined ? (
				<AppContext.Provider value={{ ScreenGui: screenGui!, PlayerGui: undefined!, ScalerApi: scalerApi }}>
					{ReactRoblox.createPortal(
						<uiscale Scale={scalerApi.scale} key={"app-scaler"} />,
						screenGui,
						"app-scaler",
					)}
					{props.children}
				</AppContext.Provider>
			) : (
				<></>
			)}
		</>
	);
}

export function QuickStory<C extends ReturnControls>(
	controls: C,
	render: StoryCreation<InferProps<C>, ReturnType<(typeof React)["createElement"]>>,
) {
	const finalControls = (controls as unknown as Map<unknown, unknown>).isEmpty() ? undefined : controls;
	const Component = withScaler(ScalerComponent as never);

	return CreateReactStory({ controls: finalControls as C, react: React, reactRoblox: ReactRoblox }, (...args) => {
		return <Component>{createElement(render, ...(args as never as never[]))}</Component>;
	});
}


/* eslint-disable prettier/prettier */
import { UseSelectorHook, useSelector, useProducer } from "@rbxts/react-reflex";
import { RootProducer, RootState } from "../store";
import { useBinding, useEffect } from "@rbxts/react";

export const useAppSelector: UseSelectorHook<RootProducer> = useSelector;
export const useAppProducer = useProducer<RootProducer>;

export const useAppSelectorBinding = <R>(selector: (state: RootState) => R) => {
	const rootProducer = useAppProducer();
	const [stateBinding, setStateBinding] = useBinding(rootProducer.getState(selector));

	useEffect(() => {
		return rootProducer.subscribe(selector, setStateBinding);
	}, [selector]);

	return stateBinding;
}


import { createContext, useContext } from "@rbxts/react";
import { ScalerApi } from "@rbxts/ui-scaler";

export const ScalerContext = createContext<ScalerApi>(undefined!);

export function useScalerApi() {
	const context = useContext(ScalerContext);
	assert(context, "ScalerContext not found. Did you call outside of ScalerContext?");
	return context;
}


import { useEventListener, useLatest } from "@rbxts/pretty-react-hooks";
import { useMemo, useState } from "@rbxts/react";
import { UserInputService } from "@rbxts/services";
import { setTimeout } from "@rbxts/set-timeout";

import { useInputDevice } from "./use-input-device";

export interface ButtonEvents {
	onMouseDown: () => void;
	onMouseUp: () => void;
	onMouseEnter: () => void;
	onMouseLeave: () => void;
}

export type ButtonStateApi = [press: boolean, hover: boolean, events: ButtonEvents];

/**
 * Returns the current state of a button based on the events returned.
 * @param enabled Whether the button is enabled or not.
 * @returns The press state, hover state, and a `ButtonEvents` object.
 */
export function useButtonState(enabled = true): LuaTuple<[press: boolean, hover: boolean, events: ButtonEvents]> {
	const [{ press, hover }, setState] = useState({
		press: false,
		hover: false,
	});

	const on = useLatest(enabled);
	const touch = useLatest(useInputDevice() === "touch");

	const events: ButtonEvents = useMemo(() => {
		return {
			onMouseDown: () => setState((state) => ({ ...state, press: on.current })),
			onMouseUp: () => setState((state) => ({ ...state, press: false })),
			onMouseEnter: () => setState((state) => ({ ...state, hover: on.current && !touch.current })),
			onMouseLeave: () => setState({ press: false, hover: false }),
		};
	}, []);

	// Touch devices might not fire mouse leave events, so assume that all
	// releases are a mouse leave.
	useEventListener(UserInputService.InputEnded, (input) => {
		if (input.UserInputType === Enum.UserInputType.Touch) {
			setTimeout(() => {
				setState({ press: false, hover: false });
			}, 0);
		}
	});

	return $tuple(press, hover, events);
}


/* eslint-disable @typescript-eslint/no-explicit-any */
import { useEffect, useState } from "@rbxts/react";

export interface LikeSharedComponent<S> {
	GetState(): S;
	Subscribe(listener: (state: S, previousState: S) => void): () => void;
	Subscribe<T>(selector: (state: S) => T, listener: (state: T, previousState: T) => void): () => void;
}

export function useComponentState<S, R = S>(
	component: LikeSharedComponent<S>,
	selector: (state: S) => R = (state) => state as unknown as R,
) {
	const [state, setState] = useState(selector(component.GetState()));

	useEffect(() => {
		return component.Subscribe(selector, (state) => setState(state));
	}, [component, selector]);

	useEffect(() => {
		setState(selector(component.GetState()));
	}, [component, selector]);

	return state;
}


import { Components } from "@flamework/components";
import { AbstractConstructorRef } from "@flamework/components/out/utility";
import { Janitor } from "@rbxts/janitor";
import { useDeferState } from "@rbxts/pretty-react-hooks";
import { useEffect } from "@rbxts/react";
import { useFlameworkDependency } from "./use-flamework-depedency";

/** @metadata macro */
export function useComponents<T extends object>(componentSpecifier?: AbstractConstructorRef<T>): T[] {
	assert(componentSpecifier, "componentSpecifier must be specified");

	const components = useFlameworkDependency<Components>();
	const [componentInstances, setComponentInstances] = useDeferState<T[]>(
		components.getAllComponents(componentSpecifier),
	);

	useEffect(() => {
		const janitor = new Janitor();

		janitor.Add(
			components.onComponentAdded<T>((component) => {
				setComponentInstances((state) => [...state, component]);
			}, componentSpecifier),
		);

		janitor.Add(
			components.onComponentRemoved<T>((component) => {
				setComponentInstances((state) => state.filter((b) => b !== component));
			}, componentSpecifier),
		);

		return () => janitor.Destroy();
	}, []);

	return componentInstances;
}


import { Dependency, Flamework } from "@flamework/core";
import { IntrinsicSymbolId } from "@flamework/core/out/utility";
import { useMemo } from "@rbxts/react";

/** @metadata macro {@link Flamework.resolveDependency intrinsic-flamework-rewrite} */
export function useFlameworkDependency<T>(id?: IntrinsicSymbolId<T>): T {
	assert(id, "id must be defined");
	return useMemo(() => Dependency(id), []);
}


import { useLatest, useMountEffect } from "@rbxts/pretty-react-hooks";
import { useBinding, useRef } from "@rbxts/react";

export function useInputBox(filter?: (text: string) => string) {
	const [content, setContent] = useBinding("");
	const onFilter = useLatest(filter);
	const ref = useRef<TextBox>();

	useMountEffect(() => {
		if (!ref.current) return;

		setContent(ref.current.Text);
		const connection = ref.current.GetPropertyChangedSignal("Text").Connect(() => {
			if (onFilter.current) {
				ref.current!.Text = onFilter.current(ref.current!.Text);
			}

			setContent(ref.current!.Text);
		});

		return () => connection.Disconnect();
	});

	return [
		content,
		ref,
		(text: string) => {
			if (!ref.current) return;
			setContent(text);
			ref.current.Text = text;
		},
	] as const;
}


import { useEventListener } from "@rbxts/pretty-react-hooks";
import { useState } from "@rbxts/react";
import { UserInputService } from "@rbxts/services";

export type InputDevice = "keyboard" | "gamepad" | "touch";

const getInputType = (inputType = UserInputService.GetLastInputType()): InputDevice | undefined => {
	if (inputType === Enum.UserInputType.Keyboard || inputType === Enum.UserInputType.MouseMovement) {
		return "keyboard";
	} else if (inputType === Enum.UserInputType.Gamepad1) {
		return "gamepad";
	} else if (inputType === Enum.UserInputType.Touch) {
		return "touch";
	}
};

/**
 * Returns the current input device being used by the player.
 * @returns An InputDevice string.
 */
export function useInputDevice() {
	const [device, setDevice] = useState<InputDevice>(() => {
		return getInputType() ?? "keyboard";
	});

	useEventListener(UserInputService.LastInputTypeChanged, (inputType) => {
		const newDevice = getInputType(inputType);

		if (newDevice !== undefined) {
			setDevice(newDevice);
		}
	});

	return device;
}


import { useLatest, useMountEffect } from "@rbxts/pretty-react-hooks";
import { Binding, useBinding, useMemo } from "@rbxts/react";
import { Motion, MotionGoal, createMotion } from "@rbxts/ripple";

export function useMotion(
	initialValue: number,
	onComplete?: (lastValue: number) => void,
): LuaTuple<[Binding<number>, Motion, number]>;

export function useMotion<T extends MotionGoal>(
	initialValue: T,
	onComplete?: (lastValue: T) => void,
): LuaTuple<[Binding<T>, Motion<T>, T]>;

export function useMotion<T extends MotionGoal>(initialValue: T, onComplete?: (lastValue: T) => void) {
	const motion = useMemo(() => {
		return createMotion(initialValue);
	}, []);
	const initValue = useMemo(() => initialValue, []);
	const onCompleteRef = useLatest(onComplete);

	const [binding, setValue] = useBinding(initialValue);

	useMountEffect(() => {
		motion.onStep((v) => setValue(v));
		motion.onComplete((val) => {
			if (!onCompleteRef.current) return;
			onCompleteRef.current(val);
		});

		motion.start();
		return () => motion.destroy();
	});

	return $tuple(binding, motion, initValue);
}


import { useBindingListener } from "@rbxts/pretty-react-hooks";
import React, { useBinding, useEffect } from "@rbxts/react";
import { setTimeout } from "@rbxts/set-timeout";
import { Pages } from "../settings/pages";
import { SelectPage } from "../store/slices/page-slice";
import { useAppSelector, useAppSelectorBinding } from "./reflex-hooks";

export const usePage = (page: Pages, timeToClose?: number, onChangedState?: (isOpen: boolean) => void) => {
	const previosPage = useAppSelector((state) => state.page.PreviosPage);
	const currentPage = useAppSelector(SelectPage);
	const isOpen = page === currentPage;

	const [visible, setVisible] = React.useState(isOpen);

	useEffect(() => {
		onChangedState?.(isOpen);

		if (timeToClose !== undefined && !isOpen && previosPage === page) {
			return setTimeout(() => {
				setVisible(false);
			}, timeToClose);
		}

		setVisible(isOpen);
	}, [isOpen]);

	return visible;
};

export const usePageBinding = (page: Pages, timeToClose?: number, onChangedState?: (isOpen: boolean) => void) => {
	const previosPage = useAppSelectorBinding((state) => state.page.PreviosPage);
	const currentPage = useAppSelectorBinding(SelectPage);
	const [visible, setVisible] = useBinding(currentPage.getValue() === page);

	useBindingListener(currentPage, (currentPage) => {
		const isOpen = page === currentPage;
		onChangedState?.(isOpen);

		if (timeToClose !== undefined && !isOpen && previosPage.getValue() === page) {
			return setTimeout(() => {
				setVisible(false);
			}, timeToClose);
		}

		setVisible(isOpen);
	});

	return visible;
};


import { useEventListener, useMountEffect } from "@rbxts/pretty-react-hooks";
import { useEffect, useState } from "@rbxts/react";
import { Players } from "@rbxts/services";
import { LocalPlayer } from "shared/utilities/constants";

export function usePlayers() {
	const [players, setPlayers] = useState(Players.GetPlayers());

	useMountEffect(() => {
		setPlayers(players.filter((v) => v !== LocalPlayer));
	});

	useEventListener(Players.PlayerAdded, (p) => setPlayers([...players, p]));
	useEventListener(Players.PlayerRemoving, (p) => setPlayers(players.filter((v) => v !== p)));

	return players;
}


import { useMotion } from "@rbxts/pretty-react-hooks";
import { useEffect } from "@rbxts/react";
import { Springs } from "shared/utilities/constants";

export function useProgressBar(
	value: number,
	maxValue: number,
	style: keyof typeof Springs = "default",
	onComplete?: (v: number) => void,
	onStep?: (v: number) => void,
) {
	const target = math.clamp(value / maxValue, 0, 1);
	const [percent, percentMotion] = useMotion(target);

	useEffect(() => {
		percentMotion.spring(target, Springs[style]);
		const con1 = percentMotion.onStep((v) => onStep?.(v));
		const con2 = percentMotion.onComplete(onComplete ?? (() => {}));

		return () => {
			con1();
			con2();
		};
	}, [target]);

	return percent;
}


import { BindingOrValue, getBindingValue, useEventListener } from "@rbxts/pretty-react-hooks";
import { useBinding } from "@rbxts/react";
import { RunService } from "@rbxts/services";

const colors = [
	Color3.fromRGB(255, 0, 4),
	Color3.fromRGB(255, 149, 0),
	Color3.fromRGB(251, 255, 6),
	Color3.fromRGB(28, 255, 26),
	Color3.fromRGB(44, 93, 255),
	Color3.fromRGB(255, 26, 252),
];

function sort(t: ColorSequenceKeypoint[]) {
	const newtab: ColorSequenceKeypoint[] = [];

	for (const i of $range(0, t.size() - 1)) {
		let got: ColorSequenceKeypoint | undefined = undefined;

		for (const v of t) {
			if (!newtab.includes(v)) {
				if (got === undefined || v.Time < got.Time) {
					got = v;
				}
			}
		}

		newtab[i] = got!;
	}

	newtab[0] = new ColorSequenceKeypoint(0, newtab[newtab.size() - 2].Value);
	newtab[newtab.size() - 1] = new ColorSequenceKeypoint(1, newtab[1].Value);

	return newtab;
}

const calc = (i: number, s: number, vc: number) => ((tick() + (s / vc) * i) % s) / s;

export function useRainbow(interval: BindingOrValue<number>) {
	const [color, setColor] = useBinding(new ColorSequence(new Color3(0, 0, 0)));

	useEventListener(RunService.RenderStepped, () => {
		const start = new ColorSequenceKeypoint(0, Color3.fromRGB(0, 0, 0));
		const finish = new ColorSequenceKeypoint(1, Color3.fromRGB(0, 0, 0));

		let tab = [start, finish];

		for (const i of $range(0, colors.size() - 1)) {
			tab.push(new ColorSequenceKeypoint(calc(i, getBindingValue(interval), colors.size()), colors[i]));
		}

		tab = sort(tab);
		const rainbow = new ColorSequence(tab);
		setColor(rainbow);
	});

	return color;
}


import { useRef } from "@rbxts/react";

export function useRandom(seed?: number) {
	const ref = useRef<Random>();
	!ref.current && (ref.current = seed ? new Random(seed) : new Random());

	return ref.current;
}


import { useEventListener } from "@rbxts/pretty-react-hooks";
import { useBinding } from "@rbxts/react";
import { RunService } from "@rbxts/services";

export function useRotation(speed: number, isReverse = false) {
	const [rotation, setRotation] = useBinding(0);

	useEventListener(RunService.Heartbeat, (dt) => {
		let nextValue = rotation.getValue() + (isReverse ? -1 : 1) * dt * speed;
		if (nextValue >= 360) nextValue -= 360;
		if (nextValue < 0) nextValue += 360;

		setRotation(nextValue);
	});

	return rotation;
}


import { useEventListener, getBindingValue, BindingOrValue } from "@rbxts/pretty-react-hooks";
import { useBinding } from "@rbxts/react";
import { RunService } from "@rbxts/services";
import { GetCurrentTime, GetDifferenceNowTime, round } from "shared/utilities/function-utilities";

export function useTimestamp(endTime: BindingOrValue<number>, showMiliseconds = false) {
	const [time, setTime] = useBinding(0);

	useEventListener(RunService.Heartbeat, () => {
		if (getBindingValue(endTime) - GetCurrentTime() < 0) {
			setTime(0);
			return;
		}

		const time = GetDifferenceNowTime(getBindingValue(endTime), false);
		setTime(round(time, showMiliseconds ? 1 : 0));
	});

	return time;
}


import { FunctionComponent } from "@rbxts/react";
import { PlaceNames } from "shared/places";
import { AppGameSlice } from "../app/places/game";

export const Apps: Partial<Record<PlaceNames, { Component: FunctionComponent; IgnoreGuiInset: boolean }>> = {
	Game: { Component: AppGameSlice, IgnoreGuiInset: true },
};


import { InferState, InferActions, combineProducers } from "@rbxts/reflex";
import { Slices } from "./slices";

export type RootState = InferState<typeof RootProducer>;
export type RootActions = InferActions<typeof RootProducer>;
export type RootProducer = typeof RootProducer;
export const RootProducer = combineProducers({
	...Slices,
});

export default RootProducer;


import { createProducer } from "@rbxts/reflex";
import { GameDataSchema } from "shared/schemas/game-data";
import { DeepCloneTable } from "shared/utilities/object-utilities";

export const gameDataSlice = createProducer(DeepCloneTable(GameDataSchema), {});


import { createProducer } from "@rbxts/reflex";
import { Pages } from "client/ui/settings/pages";
import { RootState } from "..";

const initState = {
	Page: Pages.None,
	PreviosPage: Pages.None,
	IsLocked: false,
};

export const PageSlice = createProducer(initState, {
	SetPage: (state, page: Pages) => {
		if (state.IsLocked) return state;

		return {
			...state,
			Page: page,
			PreviosPage: state.Page,
		};
	},

	SwitchPage: (state, page: Pages) => {
		if (state.IsLocked) return state;
		return page === state.Page
			? { ...state, Page: Pages.None, PreviosPage: state.Page }
			: { ...state, Page: page, PreviosPage: state.Page };
	},

	ClearPage: (state) => {
		if (state.IsLocked) return state;
		return { ...state, Page: Pages.None, PreviosPage: state.Page };
	},

	SetLockPage: (state, isLocked: boolean) => {
		if (state.IsLocked === isLocked) return state;

		return {
			...state,
			IsLocked: isLocked,
		};
	},
});

export const SelectPage = (state: RootState) => state.page.Page;
export const SelectIsLockedPage = (state: RootState) => state.page.IsLocked;


import { createProducer } from "@rbxts/reflex";
import { PlayerDataSchema } from "shared/schemas/player-data";
import { DeepCloneTable } from "shared/utilities/object-utilities";
import { DeepWritable } from "types/utility";

export const playerDataSlice = createProducer(
	DeepCloneTable(PlayerDataSchema) as DeepWritable<typeof PlayerDataSchema>,
	{},
);


import React, { PropsWithChildren, Ref, RefObject, forwardRef } from "@rbxts/react";

export const Canvas = forwardRef(
	(
		props: { Key?: string; ref?: RefObject<ScrollingFrame>; ignoreGuiInset?: boolean } & PropsWithChildren,
		ref: Ref<ScreenGui>,
	) => {
		return (
			<screengui
				ZIndexBehavior={Enum.ZIndexBehavior.Sibling}
				IgnoreGuiInset={props.ignoreGuiInset ?? false}
				key={props.Key}
				ResetOnSpawn={false}
				ref={ref}
			>
				{props.children}
			</screengui>
		);
	},
);


import React, { FunctionComponent } from "@rbxts/react";
import { useScaler } from "@rbxts/ui-scaler";
import { ScalerContext } from "client/ui/hooks/scaler/context";

const BASE_RESOLUTION = new Vector2(1920, 1080);

export function withScaler<P extends {}>(Component: FunctionComponent<P>) {
	return (props: P) => {
		const scaleApi = useScaler(BASE_RESOLUTION);
		return <ScalerContext.Provider value={scaleApi}>{<Component {...props} />}</ScalerContext.Provider>;
	};
}


import { BindingOrValue, mapBinding } from "@rbxts/pretty-react-hooks";
import React, { FunctionComponent } from "@rbxts/react";
import { useScalerApi } from "client/ui/hooks/scaler/context";

export function withUnscale<P extends { position: BindingOrValue<UDim2> }>(Component: FunctionComponent<P>) {
	return (props: P) => {
		const scaler = useScalerApi();
		props.position = mapBinding(props.position, (v) => scaler.unscale.udim2(v.X.Offset, v.Y.Offset));
		return <Component {...props} />;
	};
}


import { ITransactEntity, Transaction } from "@rbxts/transact";
import { IPlayerInteraction, PlayerComponent } from "server/components/player-component";
import { logAssert } from "shared/utilities/function-utilities";

const COUNT_REPEATS = 5;

export class PlayerTransaction implements ITransactEntity {
	private static players = new Set<PlayerComponent>();
	private playerInteraction?: IPlayerInteraction;

	public static Create(players: { playerComponent: PlayerComponent; onTransact: () => void }[]) {
		return new Transaction(
			players.map(({ playerComponent, onTransact }) => new PlayerTransaction(playerComponent, onTransact)),
			{
				TransactionRepeats: 1,
				RollbackRepeats: 1,
			},
		);
	}

	constructor(
		private playerComponent: PlayerComponent,
		private onTransact: () => void,
	) {}

	public Init() {
		assert(!PlayerTransaction.players.has(this.playerComponent), "Player already in transaction");
		assert(!this.playerComponent.GetLocked(), "Player is locked");

		PlayerTransaction.players.add(this.playerComponent);
		this.playerComponent.DoCommit();
		this.playerComponent.Keep();
	}

	public async Transact() {
		const [success] = pcall(() => this.onTransact());
		logAssert(success, "[PlayerTransaction]: onTransact failed");
		this.playerInteraction = this.playerComponent.LockComponent();

		return {
			repeats: COUNT_REPEATS,
			callback: () => this.playerInteraction!.SaveProfile(),
		};
	}

	public async Rollback() {
		this.playerComponent.RollbackToLastCommit();

		return (
			this.playerInteraction && {
				repeats: COUNT_REPEATS,
				callback: () => this.playerInteraction!.SaveProfile(),
			}
		);
	}

	public End() {
		this.playerInteraction?.UnlockComponent();
		this.playerComponent.Release();
		PlayerTransaction.players.delete(this.playerComponent);
	}
}


import { CenturionType, Command, CommandContext, Register } from "@rbxts/centurion";

@Register()
class EchoCommand {
	@Command({
		name: "echo",
		description: "Displays text",
		arguments: [
			{
				name: "text",
				description: "The text to display",
				type: CenturionType.String,
			},
		],
	})
	echo(ctx: CommandContext, text: string) {
		ctx.reply(text);
	}
}


import { Component } from "@flamework/components";
import { OnStart } from "@flamework/core";
import { InferSharedComponentState } from "@rbxts/shared-components-flamework/out/types";
import { Collisions } from "shared/collisions";
import { Character } from "shared/components/character";

@Component({})
export class ServerCharacter extends Character implements OnStart {
	public onStart() {
		this.instance.GetDescendants().forEach((instance) => {
			if (instance.IsA("BasePart")) {
				instance.CollisionGroup = Collisions.Player;
			}
		});

		this.instance.AddTag("Character");
	}
}


export function filterWorkspaceBaseParts(instances: Instance[]) {
	return instances.filter(
		(instance) => instance.IsA("BasePart") && instance.FindFirstAncestorOfClass("Workspace") !== undefined,
	) as BasePart[];
}


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

local Constants = require(script.Parent.Constants)

local function defaultBudget()
	local budgets = {}
	local queues = {}

	for requestType, options in Constants.REQUEST_BUDGETS do
		budgets[requestType] = options.INITIAL_BUDGET
		queues[requestType] = {}
	end

	budgets[Enum.DataStoreRequestType.UpdateAsync] =
		math.min(budgets[Enum.DataStoreRequestType.GetAsync], budgets[Enum.DataStoreRequestType.SetIncrementAsync])

	return {
		accumulatedSeconds = 0,
		budgets = budgets,
		queues = queues,
		maxThrottleQueueSize = Constants.MAX_THROTTLE_QUEUE_SIZE,
	}
end

local Budget = {}
Budget.__index = Budget

function Budget.new()
	local self = setmetatable(defaultBudget(), Budget)

	self.manual = false

	RunService.PostSimulation:Connect(function(deltaSeconds)
		self:tick(deltaSeconds)
	end)

	return self
end

function Budget.manual()
	local self = setmetatable(defaultBudget(), Budget)

	self.manual = true

	return self
end

function Budget:setMaxThrottleQueueSize(size)
	self.maxThrottleQueueSize = size
end

function Budget:hasBudget(requestTypes)
	for _, requestType in requestTypes do
		if self.budgets[requestType] < 1 then
			return false
		end
	end

	return true
end

function Budget:consumeBudget(requestTypes)
	for _, requestType in requestTypes do
		self.budgets[requestType] = math.max(self.budgets[requestType] - 1, 0)
	end

	self.budgets[Enum.DataStoreRequestType.UpdateAsync] = math.min(
		self.budgets[Enum.DataStoreRequestType.GetAsync],
		self.budgets[Enum.DataStoreRequestType.SetIncrementAsync]
	)
end

function Budget:updateBudgets()
	local playerCount = #Players:GetPlayers()

	for requestType, options in Constants.REQUEST_BUDGETS do
		local rate = options.RATE + playerCount * options.RATE_PER_PLAYER

		self.budgets[requestType] = math.min(
			self.budgets[requestType] + rate * Constants.BUDGET_UPDATE_INTERVAL,
			options.MAX_BUDGET_FACTOR * rate
		)
	end

	self.budgets[Enum.DataStoreRequestType.UpdateAsync] = math.min(
		self.budgets[Enum.DataStoreRequestType.GetAsync],
		self.budgets[Enum.DataStoreRequestType.SetIncrementAsync]
	)
end

function Budget:updateQueues()
	for index, queue in self.queues do
		local newQueue = {}

		for _, request in queue do
			if self:hasBudget(request.requestTypes) then
				self:consumeBudget(request.requestTypes)
				coroutine.resume(request.thread)
			else
				table.insert(newQueue, request)
			end
		end

		self.queues[index] = newQueue
	end
end

function Budget:update()
	self:updateBudgets()
	self:updateQueues()
end

function Budget:tick(deltaSeconds)
	self.accumulatedSeconds += deltaSeconds

	while self.accumulatedSeconds >= Constants.BUDGET_UPDATE_INTERVAL do
		self:update()
		self.accumulatedSeconds -= Constants.BUDGET_UPDATE_INTERVAL
	end
end

function Budget:yieldForBudget(requestTypes)
	local mainRequestType = requestTypes[1]

	if self:hasBudget(requestTypes) then
		self:consumeBudget(requestTypes)
	elseif #self.queues[mainRequestType] >= self.maxThrottleQueueSize then
		error("Request was throttled due to lack of budget but the throttle queue was full")
	else
		warn("Request was throttled due to lack of budget")

		table.insert(self.queues[mainRequestType], {
			thread = coroutine.running(),
			requestTypes = requestTypes,
		})

		coroutine.yield()
	end
end

return Budget


return {
	MAX_NAME_LENGTH = 50,
	MAX_SCOPE_LENGTH = 50,
	MAX_KEY_LENGTH = 50,

	MAX_THROTTLE_QUEUE_SIZE = 30, -- The amount of requests that can be throttled per request type at once before the request will error.

	BUDGET_UPDATE_INTERVAL = 1,

	GET_CACHE_DURATION = 5,

	REQUEST_BUDGETS = {
		[Enum.DataStoreRequestType.GetAsync] = {
			INITIAL_BUDGET = 100,
			RATE = 60,
			RATE_PER_PLAYER = 10,
			MAX_BUDGET_FACTOR = 3,
		},

		[Enum.DataStoreRequestType.SetIncrementAsync] = {
			INITIAL_BUDGET = 100,
			RATE = 60,
			RATE_PER_PLAYER = 10,
			MAX_BUDGET_FACTOR = 3,
		},

		[Enum.DataStoreRequestType.SetIncrementSortedAsync] = {
			INITIAL_BUDGET = 50,
			RATE = 30,
			RATE_PER_PLAYER = 5,
			MAX_BUDGET_FACTOR = 3,
		},

		[Enum.DataStoreRequestType.OnUpdate] = {
			INITIAL_BUDGET = 30,
			RATE = 30,
			RATE_PER_PLAYER = 5,
			MAX_BUDGET_FACTOR = 1,
		},

		[Enum.DataStoreRequestType.GetSortedAsync] = {
			INITIAL_BUDGET = 10,
			RATE = 5,
			RATE_PER_PLAYER = 2,
			MAX_BUDGET_FACTOR = 3,
		},
	},
}


local DataStoreKeyInfo = {}
DataStoreKeyInfo.__index = DataStoreKeyInfo

function DataStoreKeyInfo.new(createdTime, updatedTime, version, userIds, metadata)
	local userIdsType = typeof(userIds)

	if userIdsType ~= "nil" and userIdsType ~= "table" then
		error(`expected userIds to be a table or nil, got {userIdsType}`)
	end

	if userIdsType == "table" then
		local expectedKey = 1
		for key, value in userIds do
			assert(typeof(value) == "number", "userId must be a number")

			if key ~= expectedKey then
				error(`expected userIds to be an array, got invalid key: {key}`)
			end

			expectedKey += 1
		end
	end

	return setmetatable({
		CreatedTime = createdTime * 1000,
		UpdatedTime = updatedTime * 1000,
		Version = version,
		userIds = if userIds ~= nil then table.clone(userIds) else {},
		metadata = if metadata ~= nil then table.clone(metadata) else {},
	}, DataStoreKeyInfo)
end

function DataStoreKeyInfo:GetUserIds()
	return table.clone(self.userIds)
end

function DataStoreKeyInfo:GetMetadata()
	return table.clone(self.metadata)
end

return DataStoreKeyInfo


local Constants = require(script.Parent.Constants)
local DataStoreKeyInfo = require(script.Parent.DataStoreKeyInfo)
local validateString = require(script.Parent.validateString)

local function copyDeep(value)
	if typeof(value) ~= "table" then
		return value
	end

	local copy = {}

	for a, b in value do
		copy[a] = copyDeep(b)
	end

	return copy
end

local GlobalDataStore = {}
GlobalDataStore.__index = GlobalDataStore

function GlobalDataStore.new(budget, errors, yield)
	return setmetatable({
		data = {},
		keyInfos = {},
		getCache = {},
		budget = budget,
		errors = errors,
		yield = yield,
	}, GlobalDataStore)
end

function GlobalDataStore:write(key, data, userIds, metadata)
	local now = DateTime.now().UnixTimestampMillis

	local keyInfo = self.keyInfos[key]

	if keyInfo ~= nil then
		local version = tostring(tonumber(keyInfo.Version) + 1)

		self.keyInfos[key] = DataStoreKeyInfo.new(keyInfo.CreatedTime, now, version, userIds, metadata)
	else
		self.keyInfos[key] = DataStoreKeyInfo.new(now, now, "0", userIds, metadata)
	end

	-- Data is written after key info because DataStoreKeyInfo.new can throw an error.
	self.data[key] = copyDeep(data)
end

function GlobalDataStore:UpdateAsync(key, transform)
	validateString("key", key, Constants.MAX_KEY_LENGTH)

	if typeof(transform) ~= "function" then
		error("`transform` must be a function")
	end

	if self.errors ~= nil then
		self.errors:simulateError("UpdateAsync")
	end

	local usingGetCache = self.getCache[key] ~= nil and os.clock() < self.getCache[key]

	local requestsTypes = if usingGetCache
		then { Enum.DataStoreRequestType.SetIncrementAsync }
		else { Enum.DataStoreRequestType.GetAsync, Enum.DataStoreRequestType.SetIncrementAsync }

	self.budget:yieldForBudget(requestsTypes)

	local oldValue = self.data[key]

	local ok, transformed, userIds, metadata = pcall(transform, copyDeep(oldValue), self.keyInfos[key])

	if not ok then
		task.spawn(error, transformed)
		return nil
	end

	if transformed == nil then
		return nil
	end

	-- TODO: Make sure transformed data is savable.

	self.yield:yield()

	self:write(key, transformed, userIds, metadata)

	self.getCache[key] = os.clock() + Constants.GET_CACHE_DURATION

	return copyDeep(transformed), self.keyInfos[key]
end

return GlobalDataStore


local SimulatedErrors = {}
SimulatedErrors.__index = SimulatedErrors

function SimulatedErrors.new()
	return setmetatable({
		errorsToSimulate = 0,
	}, SimulatedErrors)
end

function SimulatedErrors:addSimulatedErrors(amount)
	assert(typeof(amount) == "number", "`amount` must be a number")

	self.errorsToSimulate += amount
end

function SimulatedErrors:simulateError(method)
	if self.errorsToSimulate > 0 then
		self.errorsToSimulate -= 1

		error(string.format("`%s` rejected with error (simulated error)", method))
	end
end

return SimulatedErrors


local SimulatedYield = {}
SimulatedYield.__index = SimulatedYield

function SimulatedYield.new()
	return setmetatable({
		yielding = false,
		threads = {},
	}, SimulatedYield)
end

function SimulatedYield:yield()
	if not self.yielding then
		return
	end

	table.insert(self.threads, coroutine.running())

	coroutine.yield()
end

function SimulatedYield:startYield()
	self.yielding = true
end

function SimulatedYield:stopYield()
	self.yielding = false

	for _, thread in self.threads do
		task.spawn(thread)
	end

	table.clear(self.threads)
end

return SimulatedYield


local RunService = game:GetService("RunService")

local Budget = require(script.Budget)
local Constants = require(script.Constants)
local GlobalDataStore = require(script.GlobalDataStore)
local SimulatedErrors = require(script.SimulatedErrors)
local SimulatedYield = require(script.SimulatedYield)
local validateString = require(script.validateString)

local function assertServer()
	if not RunService:IsServer() then
		error("DataStore can't be accessed from the client")
	end
end

local DataStoreServiceMock = {}
DataStoreServiceMock.__index = DataStoreServiceMock

function DataStoreServiceMock.new()
	return setmetatable({
		dataStores = {},
		errors = SimulatedErrors.new(),
		yield = SimulatedYield.new(),
		budget = Budget.new(),
	}, DataStoreServiceMock)
end

function DataStoreServiceMock.manual()
	return setmetatable({
		dataStores = {},
		errors = SimulatedErrors.new(),
		yield = SimulatedYield.new(),
		budget = Budget.manual(),
	}, DataStoreServiceMock)
end

function DataStoreServiceMock:GetDataStore(name, scope)
	assertServer()

	scope = scope or "global"

	validateString("name", name, Constants.MAX_NAME_LENGTH)
	validateString("scope", scope, Constants.MAX_SCOPE_LENGTH)

	if self.dataStores[name] == nil then
		self.dataStores[name] = {}
	end

	if self.dataStores[name][scope] == nil then
		self.dataStores[name][scope] = GlobalDataStore.new(self.budget, self.errors, self.yield)
	end

	return self.dataStores[name][scope]
end

function DataStoreServiceMock:GetRequestBudgetForRequestType(requestType)
	local budget = self.budget.budgets[requestType]

	if budget == nil then
		error("`requestType` must be an Enum.DataStoreRequestType")
	end

	return budget
end

return DataStoreServiceMock


local function validateString(name, value, maxLength)
	if #value == 0 then
		error(string.format("`%s` can't be empty string", name))
	elseif #value > maxLength then
		error(string.format("`%s` is too long (exceeds %d characters)", name, maxLength))
	end
end

return validateString


import { Flamework, Modding } from "@flamework/core";
import { Centurion } from "@rbxts/centurion";
import Log, { Logger } from "@rbxts/log";
import TestEZ from "@rbxts/testez";
import { StartFlameworkUtils } from "shared/flamework-utils";
import { GetPlaceName, IS_ENABLED_MULTIPLE_PLACES } from "shared/places";
import { DataCollectionHandler, LoadGameDataFromReplicatedStorage } from "shared/singletons/data-collection";
import { IS_DEV } from "shared/utilities/constants";
import { SetupLogger } from "shared/utilities/setup-logger";
import { DataStructureMock } from "../shared/game-data/mock-data-structure";
import { isOwner } from "./game-utilities/is-owner";
import { ClearAllTestPlayers } from "./utility-for-tests";
import { SetTestMode } from "./utility-for-tests/test-mode";

Flamework.addPaths("src/server");
Flamework.addPaths("src/shared");

Modding.registerDependency<Logger>((ctor) => {
	SetupLogger();
	return Log.ForContext(ctor);
});

LoadGameDataFromReplicatedStorage();
StartFlameworkUtils();
Flamework.ignite();

Centurion.server({ syncFilter: (player) => isOwner(player) }).start();

if (IS_DEV) {
	DataCollectionHandler.Init(DataStructureMock);

	SetTestMode(true);
	TestEZ.TestBootstrap.run([script.Parent!], TestEZ.Reporters.TextReporter);
	SetTestMode(false);

	LoadGameDataFromReplicatedStorage();
	ClearAllTestPlayers();
}

if (IS_ENABLED_MULTIPLE_PLACES) {
	Log.Debug(`Started on ${GetPlaceName()}`);
}