import { Controller, OnRender, type OnInit } from "@flamework/core";
import { Workspace as World } from "@rbxts/services";
import { Lazy } from "@rbxts/lazy";
import Object from "@rbxts/object-utils";
import Iris from "@rbxts/iris";

import type { LogStart } from "shared/hooks";
import { DefaultCamera } from "client/components/cameras/default";
import { FirstPersonCamera } from "client/components/cameras/first-person";
import { AerialCamera } from "client/components/cameras/aerial";
import { FixedCamera } from "client/components/cameras/fixed";
import { FlyOnTheWallCamera } from "client/components/cameras/fly-on-the-wall";
import { FirstPersonAnimatedCamera } from "client/components/cameras/first-person-animated";
import type { ControlPanelDropdownRenderer } from "shared/structs/control-panel";

import { ControlPanelRenderable } from "./control-panel";
import type { CameraComponent } from "client/base-components/camera";

// add new camera components here
export interface Cameras {
  readonly Default: DefaultCamera;
  readonly FirstPerson: FirstPersonCamera;
  readonly Aerial: AerialCamera;
  readonly Fixed: FixedCamera;
  readonly FlyOnTheWall: FlyOnTheWallCamera;
  readonly FirstPersonAnimated: FirstPersonAnimatedCamera;
}

@Controller()
@ControlPanelRenderable("Camera")
export class CameraController implements OnInit, OnRender, LogStart, ControlPanelDropdownRenderer {
  public readonly cameraStorage = new Instance("Actor", World);
  public readonly cameras = new Lazy<Cameras>(() => ({
    Default: DefaultCamera.create(this),
    FirstPerson: FirstPersonCamera.create(this),
    Aerial: AerialCamera.create(this),
    Fixed: FixedCamera.create(this),
    FlyOnTheWall: FlyOnTheWallCamera.create(this),
    FirstPersonAnimated: FirstPersonAnimatedCamera.create(this)
  }));

  public currentName!: keyof Cameras;

  public onInit(): void {
    this.cameraStorage.Name = "Cameras";
    this.set("Default");
  }

  public onRender(dt: number): void {
    const camera = this.get();
    if (camera !== undefined && "onRender" in camera && typeOf(camera.onRender) === "function") {
      const update = <(camera: CameraComponent, dt: number) => void>camera.onRender;
      update(camera, dt);
    }
  }

  public renderControlPanelDropdown(): void {
    const currentCamera = this.get().instance;
    const fov = Iris.SliderNum(["FOV", 0.25, 1, 120], { number: Iris.State(currentCamera.FieldOfView) });
    if (fov.numberChanged())
      currentCamera.FieldOfView = fov.state.number.get();

    const cameraComponents = Object.keys(this.cameras.getValue()).sort();
    const componentIndex = Iris.State<keyof Cameras>(this.currentName);
    Iris.Combo(["Camera Component"], { index: componentIndex });
    {
      for (const component of cameraComponents)
        Iris.Selectable([component, component], { index: componentIndex });
    }
    Iris.End();

    if (this.currentName !== componentIndex.get())
      this.set(componentIndex.get());
  }

  public set(cameraName: keyof Cameras): void {
    this.currentName = cameraName;
    for (const [otherCameraName] of pairs(this.cameras.getValue()))
      this.get(otherCameraName).toggle(cameraName === otherCameraName);
  }

  public get<T extends CameraComponent>(cameraName: keyof Cameras = this.currentName): T {
    return <T>this.cameras.getValue()[cameraName];
  }
}


import { Controller, type OnStart } from "@flamework/core";
import { StandardActionBuilder } from "@rbxts/mechanism";
import { processDependency } from "@rbxts/flamework-meta-utils";
import Iris from "@rbxts/iris";

import type { LogStart } from "shared/hooks";
import { OnInput } from "client/decorators";
import { Player } from "client/utility";
import { isDeveloper } from "shared/constants";
import { createMappingDecorator } from "shared/utility/meta";
import type { ControlPanelDropdownRenderer } from "shared/structs/control-panel";

import type { MouseController } from "./mouse";

const [renderableMeta, ControlPanelRenderable] = createMappingDecorator<ControlPanelDropdownRenderer, never[], [dropdownName: string, order?: number]>();
export { ControlPanelRenderable };

interface Renderable {
  readonly renderer: ControlPanelDropdownRenderer;
  readonly dropdownName: string;
  readonly order?: number;
}

@Controller()
export class ControlPanelController implements OnStart, LogStart {
  private readonly windowSize = new Vector2(300, 400);
  private readonly windowOpened = Iris.State(false);
  private renderables: Renderable[] = [];
  private mouseUnlocked = false;

  public constructor(
    private readonly mouse: MouseController
  ) { }

  public async onStart(): Promise<void> {
    for (const [_, [ctor, [dropdownName, order]]] of renderableMeta)
      processDependency(ctor, renderable => this.renderables.push({ renderer: renderable, dropdownName, order }));

    this.renderables.sort((a, b) => (a.order ?? math.huge + 1) < (b.order ?? math.huge + 1));

    Iris.Init();
    Iris.UpdateGlobalConfig(Iris.TemplateConfig.colorDark);
    Iris.UpdateGlobalConfig(Iris.TemplateConfig.sizeClear);
    Iris.Connect(() => this.render());
  }

  @OnInput(new StandardActionBuilder("Comma"))
  public open(): void {
    if (!isDeveloper(Player)) return;
    this.windowOpened.set(!this.windowOpened.get());
  }

  @OnInput(new StandardActionBuilder("P"))
  public unlockMouse(): void {
    if (!isDeveloper(Player)) return;
    this.mouseUnlocked = !this.mouseUnlocked;
    this.mouse.iconEnabled(this.mouseUnlocked);
    this.mouse.behavior(this.mouseUnlocked ? Enum.MouseBehavior.Default : Enum.MouseBehavior.LockCenter);
    Player.CameraMode = this.mouseUnlocked ? Enum.CameraMode.Classic : Enum.CameraMode.LockFirstPerson;
  }

  private render(): void {
    Iris.Window(["Control Panel"], {
      size: Iris.State(this.windowSize),
      isOpened: this.windowOpened
    });
    for (const renderable of this.renderables) {
      Iris.Tree([renderable.dropdownName]);
      renderable.renderer.renderControlPanelDropdown();
      Iris.End();
    }
    Iris.End();
  }
}


import { Controller } from "@flamework/core";
import { StandardActionBuilder } from "@rbxts/mechanism";

import type { LogStart } from "shared/hooks";
import { OnInput, OnInputRelease } from "client/decorators";

export const enum ActionID {
  LMB,
  RMB,
  MMB,
  LeftTrigger,
  RightTrigger,
  Crouch
}

/** Handles all game input */
@Controller()
export class InputController implements LogStart {
  @OnInput(
    new StandardActionBuilder("C")
      .setID(ActionID.Crouch)
  )
  public crouch(): void {
    // code for crouching
    print("crouched")
  }

  @OnInputRelease(ActionID.Crouch)
  public stand(): void {
    // code for standing
    print("stood")
  }
}


import { Controller, type OnInit, type OnRender } from "@flamework/core";
import { UserInputService as UserInput, Workspace as World } from "@rbxts/services";
import { AxisAction, AxisActionBuilder, StandardActionBuilder } from "@rbxts/mechanism";
import { RaycastParamsBuilder } from "@rbxts/builders";
import Charm, { atom } from "@rbxts/charm";
import Signal from "@rbxts/signal";

import { OnInput, OnAxisInput, OnInputRelease, inputManager } from "client/decorators";
import { Player } from "client/utility";
import { ActionID } from "./input";

const { abs } = math;

const MOUSE_RAY_DISTANCE = 1000;

/** Mouse controller that emulates cross-platform actions */
@Controller({ loadOrder: 0 })
export class MouseController implements OnInit, OnRender {
  public readonly scrolled = new Signal<(delta: number) => void>;
  public readonly isLmbDown = atom(false);
  public readonly isRmbDown = atom(false);
  public readonly isMmbDown = atom(false);
  public readonly iconEnabled = atom(true);
  public readonly behavior = atom<Enum.MouseBehavior>(Enum.MouseBehavior.Default);

  private readonly playerMouse = Player.GetMouse();
  private readonly rightThumbstickAxis = new AxisActionBuilder("Thumbstick2");
  private readonly thumbstickDeadzone = 0.1;

  private lastInput?: Enum.UserInputType;
  private delta = new Vector2;

  public onInit(): void {
    inputManager
      .bind(this.rightThumbstickAxis)
      .bind(new StandardActionBuilder("L2").setID(ActionID.LeftTrigger))
      .bind(new StandardActionBuilder("R2").setID(ActionID.RightTrigger));

    // Touch controls
    UserInput.TouchPinch.Connect((_, scale) => this.scrolled.Fire((scale < 1 ? 1 : -1) * abs(scale - 2)));
    UserInput.TouchStarted.Connect(() => this.lmbDown());
    UserInput.TouchEnded.Connect(() => this.lmbUp());
    UserInput.InputChanged.Connect(input => this.lastInput = input.UserInputType);
  }

  public onRender(dt: number): void {
    const behavior = this.behavior();
    if (UserInput.MouseBehavior !== behavior && behavior !== Enum.MouseBehavior.Default)
      UserInput.MouseBehavior = behavior;

    UserInput.MouseIconEnabled = this.iconEnabled();
    switch (this.lastInput) {
      case Enum.UserInputType.MouseMovement: {
        this.delta = UserInput.GetMouseDelta();
        break;
      }
      case Enum.UserInputType.Gamepad1:
      case Enum.UserInputType.Gamepad2:
      case Enum.UserInputType.Gamepad3:
      case Enum.UserInputType.Gamepad4:
      case Enum.UserInputType.Gamepad5:
      case Enum.UserInputType.Gamepad6:
      case Enum.UserInputType.Gamepad7:
      case Enum.UserInputType.Gamepad8: {
        const { X, Y } = this.rightThumbstickAxis.position;
        this.delta = new Vector2(
          this.applyThumbstickDeadzone(X),
          this.applyThumbstickDeadzone(-Y)
        ).mul(10);
        break;
      }
    }
  }

  public getScreenPosition(): Vector2 {
    return UserInput.GetMouseLocation();
  }

  public getWorldPosition(distance = MOUSE_RAY_DISTANCE): Vector3 {
    const { X, Y } = UserInput.GetMouseLocation();
    const { Origin, Direction } = World.CurrentCamera!.ViewportPointToRay(X, Y);
    const raycastResult = this.createRay(distance);
    return raycastResult !== undefined ? raycastResult.Position : Origin.add(Direction.mul(distance));
  }

  public target(distance = MOUSE_RAY_DISTANCE): Maybe<BasePart> {
    return this.createRay(distance)?.Instance;
  }

  public getDelta(): Vector2 {
    return this.delta;
  }

  public setTargetFilter(filterInstance: Instance): void {
    this.playerMouse.TargetFilter = filterInstance;
  }

  public setIcon(icon: string): void {
    UserInput.MouseIcon = icon;
  }

  /** @hidden */
  @OnAxisInput(new AxisActionBuilder("MouseWheel"))
  public onScroll(axis: AxisAction): void {
    this.scrolled.Fire(-axis.position.Z);
  }

  /** @hidden */
  @OnAxisInput(new AxisActionBuilder("R2"))
  public onR2AxisChange(axis: AxisAction): void {
    this.triggerAxesChange(axis, this.isLmbDown);
  }

  /** @hidden */
  @OnAxisInput(new AxisActionBuilder("L2"))
  public onL2AxisChange(axis: AxisAction): void {
    this.triggerAxesChange(axis, this.isLmbDown);
  }

  /** @hidden */
  @OnInputRelease(ActionID.RightTrigger)
  public onR2Release(): void {
    this.rmbUp();
  }

  /** @hidden */
  @OnInputRelease(ActionID.LeftTrigger)
  public onL2Release(): void {
    this.lmbUp();
  }

  /** @hidden */
  @OnInputRelease(ActionID.MMB)
  public mmbUp(): void {
    this.isMmbDown(false);
  }

  /** @hidden */
  @OnInput(
    new StandardActionBuilder("MouseButton3")
      .setID(ActionID.MMB)
  )
  public mmbDown(): void {
    this.isMmbDown(true);
  }

  /** @hidden */
  @OnInputRelease(ActionID.RMB)
  public rmbUp(): void {
    this.isRmbDown(false);
  }

  /** @hidden */
  @OnInput(
    new StandardActionBuilder("MouseButton2")
      .setID(ActionID.RMB)
  )
  public rmbDown(): void {
    this.isRmbDown(true);
  }

  /** @hidden */
  @OnInputRelease(ActionID.LMB)
  public lmbUp(): void {
    this.isLmbDown(false);
  }

  /** @hidden */
  @OnInput(
    new StandardActionBuilder("MouseButton1")
      .setID(ActionID.LMB)
  )
  public lmbDown(): void {
    this.isLmbDown(true);
  }

  private triggerAxesChange(axis: AxisAction, isDown: Charm.Atom<boolean>): void {
    if (axis.delta.Z < 0) return void isDown(false);
    if (axis.delta.Z < 0.05) return;
    isDown(true);
  }

  private applyThumbstickDeadzone(coord: number): number {
    if (abs(coord) < this.thumbstickDeadzone) return 0;
    return (coord > 0 ? coord - this.thumbstickDeadzone : coord + this.thumbstickDeadzone) / (1 - this.thumbstickDeadzone);
  }

  private createRay(distance: number, filter: Instance[] = []): Maybe<RaycastResult> {
    const { X, Y } = UserInput.GetMouseLocation();
    const { Origin, Direction } = World.CurrentCamera!.ViewportPointToRay(X, Y);

    const raycastParams = new RaycastParamsBuilder()
      .SetIgnoreWater(true)
      .AddToFilter(...filter)
      .Build();

    return World.Raycast(Origin, Direction.mul(distance), raycastParams);
  }
}


import { Controller } from "@flamework/core";
import { Workspace as World, Stats } from "@rbxts/services";
import { eMath } from "@rbxts/atlas";
import Iris from "@rbxts/iris";

import { roundDecimal } from "shared/utility/numbers";
import type { ControlPanelDropdownRenderer } from "shared/structs/control-panel";

import { ControlPanelRenderable } from "./control-panel";

const STAT_UPDATE_RATE = 1; // second

@Controller()
@ControlPanelRenderable("Performance Statistics", 0)
export class PerformanceStatsController implements ControlPanelDropdownRenderer {
  private readonly sendSizes: number[] = [];
  private readonly recvSizes: number[] = [];
  private lastSendRecvUpdate = 0;
  private avgSend = 0;
  private avgRecv = 0;
  private highestSend = 0;
  private highestRecv = 0;
  private lowestSend = 0;
  private lowestRecv = 0;

  public renderControlPanelDropdown(): void {
    const now = os.clock();
    if (now - this.lastSendRecvUpdate >= STAT_UPDATE_RATE) {
      this.sendSizes.sort();
      this.recvSizes.sort();
      this.highestSend = this.sendSizes[this.sendSizes.size() - 1];
      this.lowestSend = this.sendSizes[0];
      this.highestRecv = this.recvSizes[this.recvSizes.size() - 1];
      this.lowestRecv = this.recvSizes[0];
      this.avgSend = eMath.avg(...this.sendSizes);
      this.avgRecv = eMath.avg(...this.recvSizes);

      this.sendSizes.clear();
      this.recvSizes.clear();
      this.lastSendRecvUpdate = now;
    }

    const recv = Stats.DataReceiveKbps;
    const send = Stats.DataSendKbps;
    this.recvSizes.push(recv);
    this.sendSizes.push(send);

    const formatKBsPerSecond = (kbps: number) => (kbps === undefined ? "..." : (kbps < 0.1 ? "<0.1" : roundDecimal(kbps, 2))) + " kb/s";
    Iris.Text([`Network Receive: ${formatKBsPerSecond(recv)}`]);
    Iris.Text([`Network Receive (High): ${formatKBsPerSecond(this.highestRecv)}`]);
    Iris.Text([`Network Receive (Low): ${formatKBsPerSecond(this.lowestRecv)}`]);
    Iris.Text([`Network Receive (Average): ${formatKBsPerSecond(this.avgRecv)}`]);
    Iris.Text([`Network Send: ${formatKBsPerSecond(send)}`]);
    Iris.Text([`Network Send (High): ${formatKBsPerSecond(this.highestSend)}`]);
    Iris.Text([`Network Send (Low): ${formatKBsPerSecond(this.lowestSend)}`]);
    Iris.Text([`Network Send (Average): ${formatKBsPerSecond(this.avgSend)}`]);
    Iris.Text([`Heartbeat Time: ${roundDecimal(Stats.HeartbeatTimeMs, 2)} ms`]);
    Iris.Text([`Memory Usage (Signals): ${math.floor(Stats.GetMemoryUsageMbForTag("Signals"))} mb`]);
    Iris.Text([`Memory Usage (Script): ${math.floor(Stats.GetMemoryUsageMbForTag("Script"))} mb`]);
    Iris.Text([`Memory Usage (Total): ${math.floor(Stats.GetTotalMemoryUsageMb())} mb`]);
    Iris.Text([`Workspace Instance Count: ${World.GetDescendants().size()}`]);
  }
}


import { Controller } from "@flamework/core";
import { TweenInfoBuilder } from "@rbxts/builders";
import { Lazy } from "@rbxts/lazy";
import { tween } from "@rbxts/instance-utility";

import type { LogStart } from "shared/hooks";
import { PlayerGui } from "client/utility";

@Controller()
export class UIEffectsController implements LogStart {
  private readonly screen = new Lazy<ScreenGui>(() => {
    const screen = new Instance("ScreenGui", PlayerGui);
    screen.Name = "UIEffects";
    screen.DisplayOrder = 10;
    screen.ScreenInsets = Enum.ScreenInsets.DeviceSafeInsets;
    return screen;
  }).getValue();
  private readonly blackFrame = new Lazy<Frame>(() => {
    const frame = new Instance("Frame", this.screen);
    frame.Name = "Black";
    frame.Size = UDim2.fromScale(1, 1);
    frame.BackgroundColor3 = new Color3;
    frame.Transparency = 1;
    return frame;
  }).getValue();

  public async blackFade<Disable extends boolean = false>(manualDisable: Disable = <Disable>false, timeBetween = 0.5, fadeTime = 0.65): Promise<Disable extends true ? () => Tween : void> {
    type RType = Disable extends true ? () => Tween : void;
    const info = new TweenInfoBuilder()
      .SetTime(fadeTime)
      .SetEasingStyle(Enum.EasingStyle.Sine)
      .Build();

    const toggle = (on: boolean) => tween(this.blackFrame, info, { Transparency: on ? 0 : 1 });
    const fadeIn = toggle(true);
    fadeIn.Completed.Wait();
    fadeIn.Destroy();
    task.wait(timeBetween);

    if (!manualDisable)
      toggle(false);

    return new Promise(resolve =>
      resolve(<RType>(manualDisable === true ? () => toggle(false) : void 0))
    );
  }
}


import { Modding } from "@flamework/core";
import { InputManager, StandardAction, AxisAction } from "@rbxts/mechanism";
import { callMethodOnDependency } from "@rbxts/flamework-meta-utils";
import type { ClientReceiver as ClientEventReceiver } from "@flamework/networking/out/events/types";
import type { ClientReceiver as ClientFunctionReceiver } from "@flamework/networking/out/functions/types";
import type { Serializer } from "@rbxts/flamework-binary-serializer";

import { FlameworkIgnited } from "shared/constants";
import Log from "shared/log";

export const inputManager = new InputManager;

export const OnInput = Modding.createDecorator<[binding: StandardAction]>(
  "Method",
  (descriptor, [action]) => {
    FlameworkIgnited.Once(() => {
      inputManager.bind(action);
      action.activated.Connect(() => {
        const object = <Record<string, Callback>>Modding.resolveSingleton(descriptor.constructor!);
        void task.spawn(object[descriptor.property], object, action);
      });
    });
  }
);

export const OnAxisInput = Modding.createDecorator<[binding: AxisAction]>(
  "Method",
  (descriptor, [axis]) => {
    FlameworkIgnited.Once(() => {
      inputManager.bind(axis);
      axis.updated.Connect(() => {
        const object = <Record<string, Callback>>Modding.resolveSingleton(descriptor.constructor!);
        void task.spawn(object[descriptor.property], object, axis);
      });
    });
  }
);

/** **Note:** You need to provide an action name to the OnInput decorator to use this decorator, with which you will use the same action name. */
export const OnInputRelease = Modding.createDecorator<[actionID: string | number]>(
  "Method",
  (descriptor, [actionID]) => task.spawn(() => {
    FlameworkIgnited.Once(() => {
      let action = inputManager.getActionByID(actionID, StandardAction);
      if (action === undefined) {
        task.wait(0.1);
        action = inputManager.getActionByID(actionID, StandardAction);
      }

      if (action === undefined)
        throw Log.fatal(`Failed to bind method "${descriptor.property}" using @OnInputRelease decorator: No input action with ID "${actionID}" exists`);

      action.deactivated.Connect(() => {
        const object = <Record<string, Callback>>Modding.resolveSingleton(descriptor.constructor!);
        void task.spawn(object[descriptor.property], object, action);
      });
    });
  })
);

type ClientReceiver<I extends unknown[] = unknown[], O = void> = ClientEventReceiver<I> | ClientFunctionReceiver<I, O>;

/** @metadata reflect identifier flamework:parameters */
export function LinkRemote<I extends unknown[] = unknown[], O = void>(remote: ClientReceiver<I, O>) {
  return (ctor: object, propertyKey: string, descriptor: TypedPropertyDescriptor<(this: unknown, ...input: I) => O>) => {
    FlameworkIgnited.Once(() => {
      (<UnionToIntersection<ClientReceiver<I, O>>>remote)["setCallback" in remote ? "setCallback" : "connect"]((...args) => callMethodOnDependency(ctor, descriptor, ...args));
    });
  }
}

/** @metadata reflect identifier flamework:parameters */
export function LinkSerializedRemote<PacketStruct extends object, I extends [packet: SerializedPacket] = [packet: SerializedPacket], O = void>(remote: ClientReceiver<I, O>, deserializer: Serializer<PacketStruct>) {
  return (ctor: object, propertyKey: string, descriptor: TypedPropertyDescriptor<(this: unknown, struct: PacketStruct, ...otherArgs: never[]) => O>) => {
    FlameworkIgnited.Once(() => {
      (<UnionToIntersection<ClientReceiver<I, O>>>remote)["setCallback" in remote ? "setCallback" : "connect"]((...args) => {
        const [{ buffer, blobs }] = args;
        args.shift();

        const struct = deserializer.deserialize(buffer, blobs);
        return callMethodOnDependency(ctor, descriptor, struct, ...<never[]><unknown>args);
      });
    });
  }
}


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

export const Player = Players.LocalPlayer;
export const PlayerGui = Player.WaitForChild<PlayerGui>("PlayerGui");


import { TestRunner } from "@rbxts/runit";
import { ServerScriptService } from "@rbxts/services";

const testRunner = new TestRunner(
  ServerScriptService.WaitForChild("Tests")
);

testRunner.run({ colors: true });


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

export = identity<CommandDefinition>({
  Name: "set-data",
  Aliases: ["data-set"],
  Description: "Set the data at the given directory to the given value",
  Group: "Dev",
  Args: [
    {
      Type: "string",
      Name: "Directory",
      Description: "Directory of the data being set",
    },
    {
      Type: "any",
      Name: "Value",
      Description: "The value to set the data at the given directory to",
    }
  ],
});


import { Firebase } from "@rbxts/firebase";
import type { CommandContext } from "@rbxts/cmdr";

import { getFirebaseInfo } from "server/services/third-party/data";

export = async function (context: CommandContext, directory: string, value: unknown): Promise<void> {
  try {
    const firebase = new Firebase(...await getFirebaseInfo());
    firebase.set(directory, value);
    context.Reply(`Successfully set data at "${directory}" to "${value}"!`, Color3.fromRGB(0, 255, 0));
  } catch (err) {
    context.Reply(`Error: ${err}`, Color3.fromRGB(255, 0, 0));
  }
}


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

const parent = script.Parent as Folder;
Cmdr.RegisterDefaultCommands();
Cmdr.RegisterCommandsIn(parent.WaitForChild<Folder>("commands"));
Cmdr.RegisterTypesIn(parent.WaitForChild<Folder>("types"));
Cmdr.RegisterHooksIn(parent.WaitForChild<Folder>("hooks"))


import type { Registry } from "@rbxts/cmdr";

import { isNaN } from "shared/utility/numbers";

export = function (registry: Registry): void {
  registry.RegisterType("any", {
    Parse: value => {
      if (tonumber(value) !== undefined && !isNaN(tonumber(value)!))
        return tonumber(value)!;
      else if (value === "true")
        return true;
      else if (value === "false")
        return false;
      else if (["undefined", "null", "nil"].includes(<string>value))
        return undefined;
      else
        return value;
    }
  })
}


import { callMethodOnDependency } from "@rbxts/flamework-meta-utils";
import type { Serializer } from "@rbxts/flamework-binary-serializer";
import type { ServerReceiver as ServerEventReceiver } from "@flamework/networking/out/events/types";
import type { ServerReceiver as ServerFunctionReceiver } from "@flamework/networking/out/functions/types";

import { FlameworkIgnited } from "shared/constants";

type ServerReceiver<I extends unknown[] = unknown[], O = void> = ServerEventReceiver<I> | ServerFunctionReceiver<I, O>;

/** @metadata reflect identifier flamework:parameters */
export function LinkRemote<I extends unknown[] = unknown[], O = void>(remote: ServerReceiver<I, O>) {
  return (ctor: object, propertyKey: string, descriptor: TypedPropertyDescriptor<(this: unknown, player: Player, ...input: I) => O>) => {
    FlameworkIgnited.Once(() => {
      (<UnionToIntersection<ServerReceiver<I, O>>>remote)["setCallback" in remote ? "setCallback" : "connect"]((...args) => callMethodOnDependency(ctor, descriptor, ...args));
    });
  }
}

/** @metadata reflect identifier flamework:parameters */
export function LinkSerializedRemote<PacketStruct extends object, I extends [packet: SerializedPacket] = [packet: SerializedPacket], O = void>(remote: ServerReceiver<I, O>, deserializer: Serializer<PacketStruct>) {
  return (ctor: object, propertyKey: string, descriptor: TypedPropertyDescriptor<(this: unknown, player: Player, struct: PacketStruct, ...otherArgs: never[]) => O>) => {
    FlameworkIgnited.Once(() => {
      (<UnionToIntersection<ServerReceiver<I, O>>>remote)["setCallback" in remote ? "setCallback" : "connect"]((...args) => {
        const [player, { buffer, blobs }] = args;
        args.shift();

        const struct = deserializer.deserialize(buffer, blobs);
        return callMethodOnDependency(ctor, descriptor, player, struct, ...<never[]><unknown>args);
      });
    });
  }
}


import { Service, Modding, type OnStart } from "@flamework/core";
import { Players } from "@rbxts/services";

import type { OnPlayerJoin, OnPlayerLeave } from "server/hooks";

@Service({ loadOrder: 1 })
export class PlayersService implements OnStart {
  public onStart(): void {
    const joinListeners = new Set<OnPlayerJoin>;
    const leaveListeners = new Set<OnPlayerLeave>;
    Modding.onListenerAdded<OnPlayerJoin>(object => joinListeners.add(object));
    Modding.onListenerRemoved<OnPlayerJoin>(object => joinListeners.delete(object));
    Modding.onListenerAdded<OnPlayerLeave>(object => leaveListeners.add(object));
    Modding.onListenerRemoved<OnPlayerLeave>(object => leaveListeners.delete(object));

    Players.PlayerAdded.Connect(player => {
      for (const listener of joinListeners)
        task.spawn(() => listener.onPlayerJoin(player));
    });

    Players.PlayerRemoving.Connect(player => {
      for (const listener of leaveListeners)
        task.spawn(() => listener.onPlayerLeave(player));
    });

    for (const player of Players.GetPlayers())
      for (const listener of joinListeners)
        task.spawn(() => listener.onPlayerJoin(player));
  }
}


import { Service, type OnInit } from "@flamework/core";
import { DataStoreService as Data } from "@rbxts/services";
import { Firebase } from "@rbxts/firebase";
import Signal from "@rbxts/signal";

import type { LogStart } from "shared/hooks";
import type { OnPlayerJoin, OnPlayerLeave } from "server/hooks";
import { Events } from "server/network";
import { Serializers } from "shared/network";
import { LinkRemote } from "server/decorators";
import { LogBenchmark, SpawnTask } from "shared/decorators";
import { roundDecimal } from "shared/utility/numbers";
import { type PlayerData, type GlobalData, INITIAL_DATA } from "shared/data-models/player-data";

export async function getFirebaseInfo(): Promise<[string, string]> {
  return new Promise((resolve, reject) => {
    const env = Data.GetDataStore("EnvironmentInfo");
    try {
      const authKey = env.GetAsync<string>("FIREBASE_AUTH")[0]!;
      const dbUrl = env.GetAsync<string>("FIREBASE_URL")[0]!;

      // Complain if we don't have values to input
      if (authKey === undefined)
        return reject("FIREBASE_AUTH was not found in EnvironmentInfo data store");
      if (dbUrl === undefined)
        return reject("FIREBASE_URL was not found in EnvironmentInfo data store");

      resolve([dbUrl, authKey]);
    } catch (err) {
      reject(`Failed to fetch Firebase info because DataStoreService errored: ${err}`);
    }
  });
}

@Service({ loadOrder: -1 })
export class DataService implements OnInit, OnPlayerJoin, OnPlayerLeave, LogStart {
  public readonly loaded = new Signal<(player: Player, data: PlayerData) => void>;
  public readonly updated = new Signal<(player: Player, data: PlayerData) => void>;
  public readonly updatedGlobal = new Signal<(data: GlobalData) => void>;
  public readonly firebaseCreated = new Signal;
  public playerData: Record<number, PlayerData> = {};

  private firebase!: Firebase;

  @SpawnTask()
  public async onInit(): Promise<void> {
    this.firebase = new Firebase(...await getFirebaseInfo());
    this.firebaseCreated.Fire();
    this.playerData = await this.getDatabase();
  }

  public async onPlayerJoin(player: Player): Promise<void> {
    if (this.firebase === undefined)
      this.firebaseCreated.Wait();

    const data = await this.firebase.get<PlayerData>(`playerData/${player.UserId}`, table.clone(INITIAL_DATA));
    this.playerData[player.UserId] = data;
  }

  public async onPlayerLeave(player: Player): Promise<void> {
    await this.firebase.set(`playerData/${player.UserId}`, this.get(player));
  }

  @LinkRemote(Events.data.initialize)
  @LogBenchmark((_, msElapsed, player) => `Initialized ${player}'s data in ${roundDecimal(msElapsed, 2)} ms`)
  public async setup(player: Player): Promise<void> {
    const data = await this.initialize(player);
    this.loaded.Fire(player, data);
    Events.data.loaded(player, Serializers.playerData.serialize(data));
  }

  public async getGlobal<T>(directory: string, defaultValue?: T): Promise<Maybe<T>> {
    return this.firebase.get(directory, defaultValue);
  }

  public async setGlobal<T>(directory: string, value: T): Promise<void> {
    await this.firebase.set<T>(directory, value);
    this.updatedGlobal.Fire((await this.getGlobal(""))!);
  }

  public async addToArrayGlobal<T extends defined>(directory: string, value: T): Promise<void> {
    await this.firebase.addToArray(directory, value);
    this.updatedGlobal.Fire((await this.getGlobal(""))!);
  }

  public get(player: Player, initialValue = INITIAL_DATA): PlayerData {
    return this.playerData[player.UserId] ?? initialValue;
  }

  public set(player: Player, data: PlayerData): PlayerData {
    this.playerData[player.UserId] = data;
    this.update(player, data);
    return data;
  }

  private async getDatabase(): Promise<Record<string, PlayerData>> {
    if (this.firebase === undefined)
      this.firebaseCreated.Wait();

    return await this.firebase.get("playerData", {});
  }

  @SpawnTask()
  private update(player: Player, data: PlayerData): void {
    this.updated.Fire(player, data);
    Events.data.updated(player, Serializers.playerData.serialize(data));
  }

  private async initialize(player: Player): Promise<PlayerData> {
    const database = await this.getDatabase();
    const data = <Writable<PlayerData>>this.get(player, database[tostring(player.UserId)]);
    for (const [key, value] of pairs(INITIAL_DATA))
      if (!(key in data))
        data[key] = <never>value;

    return this.set(player, data);
  }
}


import { OnInit, Service } from "@flamework/core";
import { MarketplaceService as Market, Players } from "@rbxts/services";

import { DataService } from "./third-party/data";
import Log from "shared/log";

type RewardHandler = (player: Player) => void;
const enum ProductIDs {
  // Gold5000 = 1620635951,
}

@Service()
export class TransactionsService implements OnInit {
  private readonly rewardHandlers: Record<number, RewardHandler> = {
    // [ProductIDs.Gold5000]: player => this.db.increment(player, "gold", 5000)
  }

  public constructor(
    private readonly data: DataService
  ) { }

  public onInit(): void {
    Market.ProcessReceipt = ({ PlayerId, ProductId, PurchaseId }) => {
      const productKey = `${PlayerId}_${PurchaseId}`;
      const player = Players.GetPlayerByUserId(PlayerId);
      const playerExists = player !== undefined;

      let purchaseRecorded: Maybe<boolean> = true;
      if (playerExists) {
        const data = this.data.get(player);
        const alreadyPurchased = data.purchaseHistory.includes(productKey);
        if (alreadyPurchased)
          return Enum.ProductPurchaseDecision.PurchaseGranted;
      } else
        purchaseRecorded = undefined;

      let success = true;
      try {
        const grantReward = this.rewardHandlers[ProductId];
        if (playerExists)
          grantReward?.(player);
      } catch (err) {
        success = false;
        purchaseRecorded = undefined;
        Log.warn(`Failed to process purchase for product ${ProductId}: ${err}`);
      }

      return Enum.ProductPurchaseDecision[(!success || purchaseRecorded === undefined) ? "NotProcessedYet" : "PurchaseGranted"];
    }
  }
}


import { BaseComponent } from "@flamework/components";
import { Janitor } from "@rbxts/janitor";

export default class DestroyableComponent<A extends {} = {}, I extends Instance = Instance> extends BaseComponent<A, I> {
  protected readonly janitor = new Janitor;

  public destroy(): void {
    if (!("Destroy" in this.janitor)) return;
    this.janitor.Destroy();
  }
}


import type {
  ClientReceiver as ClientEventReceiver,
  ClientSender as ClientEventSender,
  ServerSender as ServerEventSender,
  ServerReceiver as ServerEventReceiver
} from "@flamework/networking/out/events/types";
import type { Serializer } from "@rbxts/flamework-binary-serializer";
import { Players, ServerScriptService } from "@rbxts/services";
import LazyIterator from "@rbxts/lazy-iterator";
import Destroyable from "@rbxts/destroyable";

interface WithRange {
  readonly rangeOrigin: Vector3;
  readonly range: number;
}

interface WithoutRange {
  readonly rangeOrigin?: undefined;
  readonly range?: undefined;
}

interface BaseReplicationOptions { }

export type ReplicationOptions = BaseReplicationOptions
  & (WithRange | WithoutRange);

const DEFAULT_REPLICATION_OPTIONS: ReplicationOptions = {};

/** The replicable to create on the client */
export abstract class Replicable<I extends unknown[]> extends Destroyable {
  protected readonly useReplicationOptions: ClientEventSender<[options: ReplicationOptions]>;

  public constructor(
    receiver: ClientEventReceiver<I>,
    protected readonly sender: ClientEventSender<I>
  ) {
    super();
    const { Events } = require<typeof import("client/network")>(Players.LocalPlayer.WaitForChild("PlayerScripts").WaitForChild("TS").WaitForChild<ModuleScript>("network"));
    this.useReplicationOptions = Events.useReplicationOptions;
    this.janitor.Add(receiver.connect((...args) => this.replicate(...args)));
  }

  protected abstract replicate(...args: I): void;

  protected requestReplication(replicationOptions = DEFAULT_REPLICATION_OPTIONS, ...args: I): void {
    this.useReplicationOptions(replicationOptions);
    this.sender(...args);
    this.replicate(...args);
  }
}

/** Replicates to client */
export class Replicator<I extends unknown[]> extends Destroyable {
  public static replicationOptions = DEFAULT_REPLICATION_OPTIONS;
  public static initialize(): void {
    const { Events } = require<typeof import("server/network")>(ServerScriptService.WaitForChild("TS").WaitForChild<ModuleScript>("network"));
    Events.useReplicationOptions.connect((_, options) => Replicator.replicationOptions = options);
  }

  public constructor(receiver: ServerEventReceiver<I>, sender: ServerEventSender<I>) {
    super();

    this.janitor.Add(receiver.connect((player, ...args) => {
      const players = LazyIterator.fromArray(Players.GetPlayers())
        .filter(p => p !== player);

      if (Replicator.replicationOptions.range !== undefined)
        players.filter(player => {
          const character = <Maybe<CharacterModel>>player.Character;
          if (character === undefined) return false;

          const distance = character.HumanoidRootPart.Position.sub(Replicator.replicationOptions.rangeOrigin!).Magnitude;
          return distance < Replicator.replicationOptions.range!;
        });

      sender(players.collect(), ...args);
    }));
  }
}

/** The replicable to create on the client */
export abstract class SerializedReplicable<Packet extends object = object> extends Replicable<[packet: SerializedPacket | Packet]> {
  public constructor(
    receiver: ClientEventReceiver<[packet: SerializedPacket]>,
    sender: ClientEventSender<[packet: SerializedPacket]>,
    private readonly serializer: Serializer<Packet>
  ) {
    super(receiver, <ClientEventSender<[packet: SerializedPacket | Packet]>>sender);
  }

  protected abstract replicate(packet: Packet): void;

  protected override requestReplication(packet: Packet, replicationOptions = DEFAULT_REPLICATION_OPTIONS): void {
    const serialized = this.serializer.serialize(packet);
    this.useReplicationOptions(replicationOptions);
    this.sender(serialized);
    this.replicate(packet);
  }
}

/** Replicates to client */
export class SerializedReplicator extends Replicator<[packet: SerializedPacket]> {

}


import Iris from "@rbxts/iris";

import { isNaN } from "shared/utility/numbers";
import type { ControlPanelDropdownRenderer } from "shared/structs/control-panel";

const { ceil, min, huge: INF } = math;

/*
  If delta time in the spring class is lower than this value,
  it will split up the update into multiple smaller updates
*/
const MAX_SPRING_DELTA = 1 / 40;

export class Spring implements ControlPanelDropdownRenderer {
  static readonly iterations = 8;

  public defaultPosition: Vector3;
  public target: Vector3;
  public position: Vector3;
  public velocity: Vector3;
  private lastPosition: Vector3;

  public constructor(
    public mass = 5,
    public force = 50,
    public damping = 4,
    public speed = 4,
    defaultPosition = new Vector3(0, 0, 0)
  ) {
    this.defaultPosition = defaultPosition;
    this.target = defaultPosition;
    this.position = defaultPosition;
    this.velocity = new Vector3();
    this.lastPosition = defaultPosition;
  }

  /**
   * Shove the spring off equilibrium
   *
   * @param force Force vector
   */
  public shove(force: Vector3): void {
    let { X, Y, Z } = force;
    if (isNaN(X) || X === INF || X === -INF) X = 0;
    if (isNaN(Y) || Y === INF || Y === -INF) Y = 0;
    if (isNaN(Z) || Z === INF || Z === -INF) Z = 0;
    this.velocity = this.velocity.add(new Vector3(X, Y, Z));
  }

  /**
   * Reset the spring to its default position
   */
  public resetTarget(): void {
    this.target = this.defaultPosition;
  }

  /**
   * Update the spring
   *
   * @param dt Delta time
   * @returns The difference between the current and last positions
   */
  public update(dt: number): Vector3 {
    if (dt > MAX_SPRING_DELTA) {
      const iter = ceil(dt / MAX_SPRING_DELTA);
      for (let i = 0; i < iter; i++) {
        this.update(dt / iter);
      }

      return this.position.sub(this.lastPosition);
    }

    const scaledDt: number = (min(dt, 1) * this.speed) / Spring.iterations;
    for (let i = 0; i < Spring.iterations; i++) {
      const force: Vector3 = this.target.sub(this.position);
      const acceleration: Vector3 = force
        .mul(this.force)
        .div(this.mass)
        .sub(this.velocity.mul(this.damping));

      this.velocity = this.velocity.add(acceleration.mul(scaledDt));
      this.position = this.position.add(this.velocity.mul(scaledDt));
    }

    const positionDifference = this.position.sub(this.lastPosition);
    this.lastPosition = this.position;
    return positionDifference;
  }

  public renderControlPanelDropdown(prefix?: string): void {
    Iris.Tree([(prefix !== undefined ? prefix + " " : "") + "Spring"]);

    const mass = Iris.SliderNum(["Mass", 0.25, 0.25, 100], { number: Iris.State(this.mass) });
    if (mass.numberChanged())
      this.mass = mass.state.number.get();

    const force = Iris.SliderNum(["Force", 0.25, 0.25, 100], { number: Iris.State(this.force) });
    if (force.numberChanged())
      this.force = force.state.number.get();

    const damping = Iris.SliderNum(["Damping", 0.25, 0.25, 100], { number: Iris.State(this.damping) });
    if (damping.numberChanged())
      this.damping = damping.state.number.get();

    const speed = Iris.SliderNum(["Speed", 0.25, 0.25, 100], { number: Iris.State(this.speed) });
    if (speed.numberChanged())
      this.speed = speed.state.number.get();

    Iris.End();
  }
}


import { ReplicatedFirst, RunService as Runtime } from "@rbxts/services";
import Object from "@rbxts/object-utils";
import Signal from "@rbxts/signal";

enum DevID {
  Runic = 44966864
}

const DEVELOPERS = new Set(Object.values(DevID)); // add extra developer user IDs here

export function isDeveloper(player: Player): boolean {
  return Runtime.IsStudio() || DEVELOPERS.has(player.UserId);
}

export const Assets = ReplicatedFirst.Assets;

export const FlameworkIgnited = new Signal;


import type { DataType } from "@rbxts/flamework-binary-serializer";

export const INITIAL_GLOBAL_DATA: GlobalData = {

};

export interface GlobalData {

}

export const INITIAL_DATA: PlayerData = {
  coins: 0,
  purchaseHistory: []
};

export interface PlayerData {
  readonly coins: DataType.u32;
  readonly purchaseHistory: string[];
}


import { Modding, Reflect } from "@flamework/core";
import { RunService as Runtime } from "@rbxts/services";

import { roundDecimal } from "./utility/numbers";
import Log from "./log";

/**
 * Request the required metadata for lifecycle events and dependency resolution.
 * @metadata flamework:implements flamework:parameters
 */
export const Singleton = Modding.createDecorator("Class", descriptor => {
  Reflect.defineMetadata(descriptor.object, "flamework:singleton", true);
});

/**
 * Wraps the function in a `task.spawn()`
 *
 * **Note:** Voids the return value
 * */
export const SpawnTask = Modding.createDecorator(
  "Method",
  descriptor => {
    const object = <Record<string, Callback>><unknown>descriptor.constructor!;
    const originalMethod = object[descriptor.property];
    object[descriptor.property] = (...args: unknown[]) => task.spawn(originalMethod, ...args);
  }
);

/** Calls `whenInvalid` when `validator` returns false */
export const ValidateReturn = Modding.createDecorator<[validator: (returnValue: unknown) => boolean, whenInvalid?: (returnValue: unknown) => void, warnNotError?: boolean]>(
  "Method",
  (descriptor, [validator, whenInvalid, warnNotError]) => {
    // FlameworkIgnited.Once(() => {
    const object = <Record<string, Callback>><unknown>descriptor.constructor!;
    const originalMethod = object[descriptor.property];
    warnNotError ??= false;
    whenInvalid ??= value => Log[warnNotError ? "warn" : "fatal"](`Invalid return value ${value} returned by ${tostring(descriptor.constructor)}.${descriptor.property}()`);

    object[descriptor.property] = function (...args: unknown[]) {
      const value = originalMethod(...args);
      const isValid = validator(value);
      if (!isValid)
        whenInvalid!(value);

      return value;
    };
    // });
  }
);

/** Retries the function every time `retryCondition` returns true, `times` times, with `delay` seconds in between */
export const Retry = Modding.createDecorator<[times: number, delay?: number, retryCondition?: (fn: () => void) => boolean]>(
  "Method",
  (descriptor, [times, delay, retryCondition]) => {
    // FlameworkIgnited.Once(() => {
    const object = <Record<string, Callback>><unknown>descriptor.constructor!;
    const originalMethod = object[descriptor.property];
    delay ??= 0;
    retryCondition ??= fn => {
      try {
        fn();
        return false;
      } catch (e) {
        Log.fatal(`Failed to retry ${tostring(descriptor.constructor!)}.${descriptor.property}(): ${e}`);
        return true;
      }
    };

    object[descriptor.property] = function (...args: unknown[]) {
      const wrappedMethod = (...retryArgs: unknown[]) => retryArgs.size() > 0 ? originalMethod(...retryArgs) : originalMethod(...args);
      let shouldRetry = retryCondition!(wrappedMethod); // calls originalMethod
      let retries = 0;
      while (shouldRetry && retries < times) {
        retries++;
        if (delay! > 0)
          task.wait(delay);

        shouldRetry = retryCondition!(wrappedMethod);
      }
    };
    // });
  }
);

/** Only allows the function to be executed once every `length` seconds */
export const Cooldown = Modding.createDecorator<[length: number]>(
  "Method",
  (descriptor, [length]) => {
    // FlameworkIgnited.Once(() => {
    let canCall = true;
    const object = <Record<string, Callback>><unknown>descriptor.constructor!;
    const originalMethod = object[descriptor.property];
    object[descriptor.property] = function (...args: unknown[]) {
      if (!canCall) return;
      canCall = false;
      task.delay(length, () => canCall = true);

      return originalMethod(...args);
    };
    // });
  }
);

/** Cache the first result the function returns and return the cached result from then on out */
export const Memoize = Modding.createDecorator<[]>(
  "Method",
  descriptor => {
    // FlameworkIgnited.Once(() => {
    const object = <Record<string, Callback> & { __memoizationCache: Record<string, unknown> }><unknown>descriptor.constructor!;
    const originalMethod = object[descriptor.property];
    const key = tostring(originalMethod);

    object[descriptor.property] = function (this, ...args: unknown[]) {
      this.__memoizationCache ??= {};
      if (this.__memoizationCache[key] === undefined) {
        const result = originalMethod(this, ...args);
        this.__memoizationCache[key] = result;
      }

      return this.__memoizationCache[key];
    };
    // });
  }
);

/** Only allows the function to be executed in studio */
export const StudioOnly = Modding.createDecorator<[]>(
  "Method",
  descriptor => {
    // FlameworkIgnited.Once(() => {
    const object = <Record<string, Callback>><unknown>descriptor.constructor!;
    const originalMethod = object[descriptor.property];

    object[descriptor.property] = function (...args: unknown[]) {
      if (Runtime.IsStudio())
        originalMethod(...args);
    };
    // });
  }
);

/**
 * Benchmark how long the function takes to run
 * @metadata reflect identifier flamework:parameters
 */
export function LogBenchmark<Args extends unknown[] = unknown[]>(formatter?: (methodName: string, msElapsed: number, ...args: Args) => string) {
  return (ctor: object, propertyKey: string, descriptor: TypedPropertyDescriptor<(this: unknown, ...args: Args) => unknown>) => {
    const object = <Record<string, Callback>>ctor;
    formatter ??= (name, elapsed, ..._) => `Method "${name}" took ${roundDecimal(elapsed, 2)} ms to execute.`;

    object[propertyKey] = function (_: unknown, ...args: Args) {
      const startTime = os.clock();
      const result = descriptor.value(_, ...args);
      const endTime = os.clock();
      const elapsed = (endTime - startTime) * 1000;
      Log.info(formatter!(propertyKey, elapsed, ...args));
      return result;
    };
  }
}


import Wave from "@rbxts/wave";
import Iris from "@rbxts/iris";

export function createWaveTree(wave: Wave, name = "Wave"): Wave {
  let resultWave = wave;

  Iris.Tree([name]);
  {
    const useSin = Iris.Checkbox(["Is Sine Wave?"], { isChecked: Iris.State(wave.waveFunction === math.sin) });
    const amplitude = Iris.SliderNum(["Amplitude", 0.1, 0.1, 20], { number: Iris.State(wave.amplitude) });
    const frequency = Iris.SliderNum(["Frequency", 0.1, 0, 20], { number: Iris.State(wave.frequency) });
    const phaseShift = Iris.SliderNum(["Phase Shift", 0.01, 0, 5], { number: Iris.State(wave.phaseShift) });
    const verticalShift = Iris.SliderNum(["Vertical Shift", 0.01, 0, 5], { number: Iris.State(wave.verticalShift) });
    const damping = Iris.SliderNum(["Damping", 0.1, 1, 10], { number: Iris.State(wave.damping) });

    resultWave = new Wave(
      amplitude.state.number.get(),
      frequency.state.number.get(),
      phaseShift.state.number.get(),
      verticalShift.state.number.get(),
      damping.state.number.get(),
      useSin.state.isChecked.get() ? math.sin : math.cos
    );
  }
  Iris.End();

  return resultWave;
}

export function createVector3Tree(
  vector: Vector3,
  name = "Vector3",
  increment = Vector3.one.mul(0.1),
  minimum = Vector3.one.mul(-100),
  maximum = Vector3.one.mul(100)
): Vector3 {
  let resultVector = vector;

  Iris.Tree([name]);
  {
    const x = Iris.SliderNum(["X", increment.X, minimum.X, maximum.X], { number: vector.X });
    const y = Iris.SliderNum(["Y", increment.Y, minimum.Y, maximum.Y], { number: vector.Y });
    const z = Iris.SliderNum(["Z", increment.Z, minimum.Z, maximum.Z], { number: vector.Z });
    resultVector = new Vector3(x.state.number.get(), y.state.number.get(), z.state.number.get());
  }
  Iris.End();

  return resultVector;
}


import { BaseComponent } from "@flamework/components";
import { RunService as Runtime, Players } from "@rbxts/services";
import { Logger, OutputStream } from "@rbxts/ez-log";
import { getName } from "@rbxts/flamework-meta-utils";

function getInstancePath(instance: Instance): string {
  let path = instance.GetFullName()
    .gsub("Workspace", "World")[0]
    .gsub("PlayerGui", "UI")[0];

  if (Runtime.IsClient())
    [path] = path.gsub(`Players.${Players.LocalPlayer.Name}.`, "");

  return path;
}

class FlameworkLogger extends Logger {
  /**
   * @param name Name of the component class
   * @param component The component itself
   */
  public async clientComponent(component: BaseComponent): Promise<void> {
    this.info(`Started ${getName(component)} on ${await getInstancePath(component.instance)}`, ["client_component"]);
  }

  /**
   * @param name Name of the component class
   * @param component The component itself
   */
  public async serverComponent(component: BaseComponent): Promise<void> {
    this.info(`Started ${getName(component)} on ${await getInstancePath(component.instance)}`, ["server_component"]);
  }

  /**
   * @param name Name of the controller
   */
  public controller(controller: object): void {
    this.info(`Started ${getName(controller)}`, ["controller"]);
  }

  /**
   * @param name Name of the service
   */
  public service(service: object): void {
    this.info(`Started ${getName(service)}`, ["service"]);
  }
}

const Log = new FlameworkLogger(OutputStream.RobloxConsole);
export = Log;


import { Networking } from "@flamework/networking";
import { createBinarySerializer } from "@rbxts/flamework-binary-serializer";

import type { PlayerData } from "./data-models/player-data";
import type { AudioPacket } from "./structs/packets";
import type { ReplicationOptions } from "./classes/replicable";

type SerializedRemote = (packet: SerializedPacket) => void;
type UnreliableSerializedRemote = Networking.Unreliable<(packet: SerializedPacket) => void>;

interface ServerEvents {
  useReplicationOptions(options: ReplicationOptions): void;
  audio: {
    replicate: SerializedRemote;
  };
  data: {
    initialize(): void;
  };
  character: {
    toggleDefaultMovement(on: boolean): void;
  };
}

interface ClientEvents {
  audio: {
    played: SerializedRemote;
  };
  data: {
    loaded: SerializedRemote;
    updated: SerializedRemote;
  };
}

interface ServerFunctions { }

interface ClientFunctions { }

export const Serializers = {
  playerData: createBinarySerializer<PlayerData>(),
  audio: createBinarySerializer<AudioPacket>()
};

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


import { Modding } from "@flamework/core";
import { t } from "@rbxts/t";

/** @hidden */
function _getInstanceAtPath([paths]: string[][]): Maybe<Instance> {
  let instance: Maybe<Instance> = game;

  for (const path of paths) {
    if (instance === undefined) break;
    instance = instance.FindFirstChild(path);
  }

  return instance;
}

/**
 * Resolves the instance at the given path using Rojo
 *
 * @metadata macro intrinsic-arg-shift
 */
export function getInstanceAtPath<T extends string>(path: T, meta?: Modding.Intrinsic<"path", [T]>): Maybe<Instance> {
  return _getInstanceAtPath(path as unknown as string[][]);
}

/**
 * Macro that generates a type guard (if one is not specified) then if the guard passes, returns the casted value
 *
 * @metadata macro
 */
export function safeCast<T>(value: unknown, guard?: t.check<T> | Modding.Generic<T, "guard">): Maybe<T> {
  return guard !== undefined ?
    (guard(value) ? value : undefined)
    : undefined;
}

/**
 * Generates a map and a decorator that adds the target to the map, for the given object type and constructor arguments
 */
export function createMappingDecorator<T extends object, CtorArgs extends unknown[] = never[], Args extends unknown[] = []>() {
  type ObjectConstructor = new (...args: CtorArgs) => T;

  const map = new Map<string, [ObjectConstructor, Args]>;
  const decorator = (...args: Args) => <K extends ObjectConstructor>(ctor: K) => void map.set(tostring(ctor), [ctor, args]);
  return [map, decorator] as const;
}


import { Modding } from "@flamework/core";
import { Fact, Assert } from "@rbxts/runit";
import { t } from "@rbxts/t";

import { safeCast } from "shared/utility/meta";

interface Foo {
  readonly foo: number;
}

class SafeCastTest {
  @Fact
  public generatesTypeGuard(): void {
    this.checkFoo();
  }

  @Fact
  public usesExistingTypeGuard(): void {
    const fooGuard = t.interface({
      foo: t.number
    });

    this.checkFoo(fooGuard);
  }

  /** @metadata macro */
  private checkFoo<T extends Foo>(guard?: t.check<T> | Modding.Generic<T, "guard">): void {
    const invalidFoo = safeCast<T>({}, guard);
    const validFoo = safeCast<T>({ foo: 123 }, guard);

    Assert.undefined(invalidFoo);
    Assert.notUndefined(validFoo);
    if (Assert.isType<T>(validFoo)) {
      Assert.equal(123, validFoo.foo);
    }
  }
}

export = SafeCastTest;


import { Theory, InlineData, Assert } from "@rbxts/runit";

import { doubleSidedLimit, roundDecimal, toNearestFiveOrTen, zeroIfClose } from "shared/utility/numbers";

class NumberUtilityTest {
  @Theory
  @InlineData(69, 10)
  @InlineData(-69, -10)
  public limitsNumberOnBothSigns(input: number, expected: number): void {
    Assert.equal(expected, doubleSidedLimit(input, 10));
  }

  @Theory
  @InlineData(0.001, 0)
  @InlineData(-0.001, 0)
  @InlineData(0.01, 0.01)
  public flattensToZeroIfClose(input: number, expected: number): void {
    Assert.equal(expected, zeroIfClose(input))
  }

  @Theory
  @InlineData(8, 10)
  @InlineData(14, 15)
  @InlineData(18, 20)
  @InlineData(3, 5)
  @InlineData(2, 0)
  public roundsToNearestFiveOrTen(input: number, expected: number): void {
    Assert.equal(expected, toNearestFiveOrTen(input))
  }

  @Theory
  @InlineData(10.678, 1, 10.7)
  @InlineData(10.678, 2, 10.68)
  public roundsDecimalPoints(input: number, decimalPoints: number, expected: number): void {
    Assert.equal(expected, roundDecimal(input, decimalPoints));
  }
}

export = NumberUtilityTest;


import type TestEZType from "@rbxts/testez";
const TestEZ = require(game.GetService("ReplicatedStorage").include.node_modules["@rbxts"].testez.src) as TestEZType;
const results = TestEZ.TestBootstrap.run([game.GetService("ServerScriptService").tests]);
if (results.errors.size() > 0 || results.failureCount > 0) {
	error("Tests failed!");
}


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

local placePath = process.args[1]

local game = roblox.deserializePlace(fs.readFile(placePath))
local Workspace = game:GetService("Workspace")

for _, folder in Workspace:GetChildren() do
    if folder.ClassName == "Folder" then
        local modelContents = folder:GetChildren()
        if #modelContents == 0 then
            error("No model contents in " .. folder.Name .. "!")
        elseif #modelContents > 1 then
            error("Multiple roots in " .. folder.Name .. "!")
        end
        fs.writeFile(folder.Name, roblox.serializeModel(modelContents))
    end
end


-- luacheck: globals Instance

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

local Instance = roblox.Instance

local game = Instance.new("DataModel")
local Workspace = game:GetService("Workspace")

local args = table.clone(process.args)
local placePath = table.remove(args, 1)

for _, modelPath in args do
    local folder = Instance.new("Folder")
    folder.Name = modelPath
    folder.Parent = Workspace
    for _, instance in roblox.deserializeModel(fs.readFile(modelPath)) do
        instance.Parent = folder
    end
end

fs.writeFile(placePath, roblox.serializePlace(game))


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

local placeFilePath = process.args[1]
local outPath = process.args[2]

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

local SERVICE_BLACKLIST = {
	StarterPlayer = true,
}

local output = ""
local indent = ""

local function pushIndent()
	indent = indent .. "\t"
end

local function popIndent()
	indent = string.sub(indent, 2)
end

local function writeLine(str)
	if str == "" then
		output = output .. "\n"
	else
		output = output .. indent .. str .. "\n"
	end
end

local function writeService(service)
	local serviceChildren = service:GetChildren()

	if next(serviceChildren) == nil then
		return
	end

	writeLine("interface " .. service.ClassName .. " {")
	pushIndent()

	local function search(instance)
		local name = instance.Name
		if name == "node_modules" then
			return
		end
		local prefix = ""
		if name == "Terrain" and instance.Parent == game.Workspace then
			prefix = "readonly "
		end
		if not string.match(name, "^[_a-zA-Z][_a-zA-Z0-9]*$") then
			name = '"' .. name .. '"'
		end
		local definition = prefix .. name .. ": " .. instance.ClassName
		local children = instance:GetChildren()
		if next(children) == nil then
			writeLine(definition .. ";")
		else
			writeLine(definition .. " & {")
			pushIndent()
			local seen = {}
			for _, child in ipairs(children) do
				if not seen[child.Name] then
					seen[child.Name] = true
					search(child)
				end
			end
			popIndent()
			writeLine("};")
		end
	end
	for _, child in ipairs(serviceChildren) do
		search(child)
	end

	popIndent()
	writeLine("}")
	writeLine("")
end

for _, service in ipairs(game:GetChildren()) do
	if not SERVICE_BLACKLIST[service.Name] then
		writeService(service)
	end
end

fs.writeFile(outPath, string.match(output, "^(.-)%s*$") .. "\n")


--[[
  uint64_t seed = uintptr_t(L);
  seed ^= time(NULL);
  seed ^= clock();

  pcg32_seed(&L->global->rngstate, seed);

  The only unknown variable we have is the (curtime - startup time) offset in the seeding.
  But it has a relatively small range, so we can brute-force it.
--]]

local expected = math.random(2 ^ 28) -- Just enough to not appear twice in our range
local curtime = os.time()

local ASLR_ANCHOR = tostring(assert)
local ANCHOR_TO_L_OFFSET = 0x11900 -- (luaB_assert_ptr - L_ptr), may be different per version/build

local state_hi, state_lo = 0, 0

local function u32(x)
	return bit32.band(x, 0xFFFFFFFF)
end

local function add64(ahi, alo, bhi, blo)
	local lo = u32(alo + blo)
	local carry = (alo + blo) >= 2 ^ 32 and 1 or 0
	local hi = u32(ahi + bhi + carry)
	return hi, lo
end

local function bxor64(ahi, alo, bhi, blo)
	return bit32.bxor(ahi, bhi), bit32.bxor(alo, blo)
end

-- Heavily optimized, further modifications will probably slow it down.
local function pcg32_next_u32()
	-- uint64_t oldstate = *state;
	-- *state = oldstate * 6364136223846793005ULL + (PCG32_INC | 1);
	-- uint32_t xorshifted = uint32_t(((oldstate >> 18u) ^ oldstate) >> 27u);
	-- uint32_t rot = uint32_t(oldstate >> 59u);
	-- return (xorshifted >> rot) | (xorshifted << ((-int32_t(rot)) & 31));

	local old_hi, old_lo = state_hi, state_lo

	-- state = old * 0x5851F42D4C957F2D + 105  (mod 2^64), base-2^16 limbs
	local o0 = bit32.band(old_lo, 0xFFFF)
	local o1 = old_lo // 2 ^ 16
	local o2 = bit32.band(old_hi, 0xFFFF)

	local s0 = o0 * 32557 + 105 -- 105 = PCG32_INC | 1
	local s1 = o0 * 19605 + o1 * 32557 + s0 // 2 ^ 16
	local s2 = o0 * 62509 + o1 * 19605 + o2 * 32557 + s1 // 2 ^ 16

	state_hi, state_lo =
		bit32.band(s2, 0xFFFF) + bit32.band(
			o0 * 22609 + o1 * 62509 + o2 * 19605 + old_hi // 2 ^ 16 * 32557 + s2 // 2 ^ 16,
			0xFFFF
		) * 2 ^ 16,
		bit32.band(s0, 0xFFFF) + bit32.band(s1, 0xFFFF) * 2 ^ 16

	-- output transform from old state (XSH-RR)
	-- uint32_t xorshifted = uint32_t(((oldstate >> 18u) ^ oldstate) >> 27u);
	-- uint32_t rot = uint32_t(oldstate >> 59u);
	-- return (xorshifted >> rot) | (xorshifted << ((-int32_t(rot)) & 31));
	return bit32.rrotate(
		bit32.bor(
			bit32.bxor(bit32.bor(old_lo / 2 ^ 18, old_hi * 2 ^ 14), old_lo) / 2 ^ 27,
			bit32.bxor(old_hi / 2 ^ 18, old_hi) * 32
		),
		bit32.band((old_hi / 2 ^ 27), 31)
	)
end

local function random(upperLim)
	-- Let's target only math.random(upperLim) at the moment.
	--   case 1:
	-- { // only upper limit
	--     int u = luaL_checkinteger(L, 1);
	--     luaL_argcheck(L, 1 <= u, 1, "interval is empty");

	--     uint64_t x = uint64_t(u) * pcg32_output(&g->rngstate);
	--     int r = int(1 + (x >> 32));
	--     lua_pushinteger(L, r); // int between 1 and `u'
	--     break;
	-- }
	-- Inline: x_hi = hi32( (uint32(upperLim) * rand) )
	local rand = pcg32_next_u32()
	local a = bit32.band(upperLim, 0xFFFFFFFF)
	if a == 0 then
		return 1
	end

	local a0 = bit32.band(a, 0xFFFF)
	local b0 = bit32.band(rand, 0xFFFF)
	local a1 = a // 2 ^ 16
	local b1 = rand // 2 ^ 16
	local mid = a0 * b1 + a1 * b0

	return 1 + (a1 * b1 + mid // 2 ^ 16) + a0 * b0 + bit32.band(mid, 0xFFFF) * 2 ^ 16 // 2 ^ 32 % 2 ^ 32
end

local function seed_from_number(hi, low)
	state_hi, state_lo = add64(0, 105, hi, low)
	pcg32_next_u32()
end

local function ptr_str_to_u64(str)
	local hex = str:match("0x(%x+)$")

	if #hex > 8 then
		local hi_hex = hex:sub(1, #hex - 8)
		local lo_hex = hex:sub(#hex - 7)
		return u32(tonumber(hi_hex, 16) or 0), u32(tonumber(lo_hex, 16) or 0)
	end

	return 0, u32(tonumber(hex, 16) or 0)
end

print("----------------------------")

-- uint64_t seed = uintptr_t(L);
-- seed ^= time(NULL);
-- seed ^= clock(); (we don't have access to clock() value, but we can brute-force it)

state_hi, state_lo = ptr_str_to_u64(ASLR_ANCHOR)
state_hi, state_lo = add64(state_hi, state_lo, 0, -ANCHOR_TO_L_OFFSET)
print(string.format("aslr: 0x%08X%08X", state_hi, state_lo))

local time_hi = curtime // 2 ^ 32
local time_lo = curtime % 2 ^ 32
state_hi, state_lo = bxor64(state_hi, state_lo, time_hi, time_lo)
print(string.format("time: 0x%08X%08X", time_hi, time_lo))

local saved_state_hi, saved_state_lo = state_hi, state_lo
print(string.format("seed: 0x%08X%08X", state_hi, state_lo))

print("----------------------------")

local found = false
for clockTime = 0, 50000 do
	-- seed ^= clock();
	seed_from_number(bxor64(saved_state_hi, saved_state_lo, 0, u32(clockTime)))

	if random(2 ^ 28) == expected then
		print(`Found matching seed with offset: {clockTime}`)
		found = true
		break
	end
end

if not found then
	error("Could not find matching seed!!")
end

for i = 1, 5 do
	print("----------------------------")
	print(`Predicted: {random(2 ^ 28)}`)
	print(`Actual:    {math.random(2 ^ 28)}`)
end


local fs = require("@lune/fs") :: fs
local path = "coverage/lcov.info"
local lcov = fs.readFile(path)
lcov = lcov:gsub("\\", "/")
fs.writeFile(path, lcov)


--[[
Copyright © 2024 Stephen Leitnick

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
-- -----------------------------------------------------------------------------
--               Batched Yield-Safe Signal Implementation                     --
-- This is a Signal class which has effectively identical behavior to a       --
-- normal RBXScriptSignal, with the only difference being a couple extra      --
-- stack frames at the bottom of the stack trace when an error is thrown.     --
-- This implementation caches runner coroutines, so the ability to yield in   --
-- the signal handlers comes at minimal extra cost over a naive signal        --
-- implementation that either always or never spawns a thread.                --
--                                                                            --
-- License:                                                                   --
--   Licensed under the MIT license.                                          --
--                                                                            --
-- Authors:                                                                   --
--   stravant - July 31st, 2021 - Created the file.                           --
--   sleitnick - August 3rd, 2021 - Modified for Knit.                        --
-- -----------------------------------------------------------------------------
local task = require("@lune/task")

-- Signal types
export type Connection = {
	Disconnect: (self: Connection) -> (),
	Destroy: (self: Connection) -> (),
	Connected: boolean,
}

export type Signal<T...> = {
	Fire: (self: Signal<T...>, T...) -> (),
	FireDeferred: (self: Signal<T...>, T...) -> (),
	Connect: (self: Signal<T...>, fn: (T...) -> ()) -> Connection,
	Once: (self: Signal<T...>, fn: (T...) -> ()) -> Connection,
	DisconnectAll: (self: Signal<T...>) -> (),
	GetConnections: (self: Signal<T...>) -> { Connection },
	Destroy: (self: Signal<T...>) -> (),
	Wait: (self: Signal<T...>) -> T...,
}

-- The currently idle thread to run the next handler on
local freeRunnerThread = nil

-- Function which acquires the currently idle handler runner thread, runs the
-- function fn on it, and then releases the thread, returning it to being the
-- currently idle one.
-- If there was a currently idle runner thread already, that's okay, that old
-- one will just get thrown and eventually GCed.
local function acquireRunnerThreadAndCallEventHandler(fn, ...)
	local acquiredRunnerThread = freeRunnerThread
	freeRunnerThread = nil
	fn(...)
	-- The handler finished running, this runner thread is free again.
	freeRunnerThread = acquiredRunnerThread
end

-- Coroutine runner that we create coroutines of. The coroutine can be
-- repeatedly resumed with functions to run followed by the argument to run
-- them with.
local function runEventHandlerInFreeThread(...)
	acquireRunnerThreadAndCallEventHandler(...)
	while true do
		acquireRunnerThreadAndCallEventHandler(coroutine.yield())
	end
end

--[=[
	@within Signal
	@interface SignalConnection
	.Connected boolean
	.Disconnect (SignalConnection) -> ()

	Represents a connection to a signal.
	```lua
	local connection = signal:Connect(function() end)
	print(connection.Connected) --> true
	connection:Disconnect()
	print(connection.Connected) --> false
	```
]=]

-- Connection class
local Connection = {}
Connection.__index = Connection

function Connection:Disconnect()
	if not self.Connected then
		return
	end
	self.Connected = false

	-- Unhook the node, but DON'T clear it. That way any fire calls that are
	-- currently sitting on this node will be able to iterate forwards off of
	-- it, but any subsequent fire calls will not hit it, and it will be GCed
	-- when no more fire calls are sitting on it.
	if self._signal._handlerListHead == self then
		self._signal._handlerListHead = self._next
	else
		local prev = self._signal._handlerListHead
		while prev and prev._next ~= self do
			prev = prev._next
		end
		if prev then
			prev._next = self._next
		end
	end
end

Connection.Destroy = Connection.Disconnect

-- Make Connection strict
setmetatable(Connection, {
	__index = function(_tb, key)
		error(("Attempt to get Connection::%s (not a valid member)"):format(tostring(key)), 2)
	end,
	__newindex = function(_tb, key, _value)
		error(("Attempt to set Connection::%s (not a valid member)"):format(tostring(key)), 2)
	end,
})

--[=[
	@within Signal
	@type ConnectionFn (...any) -> ()

	A function connected to a signal.
]=]

--[=[
	@class Signal

	A Signal is a data structure that allows events to be dispatched
	and observed.

	This implementation is a direct copy of the de facto standard, [GoodSignal](https://devforum.roblox.com/t/lua-signal-class-comparison-optimal-goodsignal-class/1387063),
	with some added methods and typings.

	For example:
	```lua
	local signal = Signal.new()

	-- Subscribe to a signal:
	signal:Connect(function(msg)
		print("Got message:", msg)
	end)

	-- Dispatch an event:
	signal:Fire("Hello world!")
	```
]=]
local Signal = {}
Signal.__index = Signal

--[=[
	Constructs a new Signal

	@return Signal
]=]
function Signal.new<T...>(): Signal<T...>
	local self = setmetatable({
		_handlerListHead = false,
		_proxyHandler = nil,
		_yieldedThreads = nil,
	}, Signal)

	return self
end

--[=[
	Constructs a new Signal that wraps around an RBXScriptSignal.

	@param rbxScriptSignal RBXScriptSignal -- Existing RBXScriptSignal to wrap
	@return Signal

	For example:
	```lua
	local signal = Signal.Wrap(workspace.ChildAdded)
	signal:Connect(function(part) print(part.Name .. " added") end)
	Instance.new("Part").Parent = workspace
	```
]=]
function Signal.Wrap<T...>(rbxScriptSignal: RBXScriptSignal): Signal<T...>
	assert(
		typeof(rbxScriptSignal) == "RBXScriptSignal",
		"Argument #1 to Signal.Wrap must be a RBXScriptSignal; got " .. typeof(rbxScriptSignal)
	)

	local signal = Signal.new()
	signal._proxyHandler = rbxScriptSignal:Connect(function(...)
		signal:Fire(...)
	end)

	return signal
end

--[=[
	Checks if the given object is a Signal.

	@param obj any -- Object to check
	@return boolean -- `true` if the object is a Signal.
]=]
function Signal.Is(obj: any): boolean
	return type(obj) == "table" and getmetatable(obj) == Signal
end

--[=[
	@param fn ConnectionFn
	@return SignalConnection

	Connects a function to the signal, which will be called anytime the signal is fired.
	```lua
	signal:Connect(function(msg, num)
		print(msg, num)
	end)

	signal:Fire("Hello", 25)
	```
]=]
function Signal:Connect(fn)
	local connection = setmetatable({
		Connected = true,
		_signal = self,
		_fn = fn,
		_next = false,
	}, Connection)

	if self._handlerListHead then
		connection._next = self._handlerListHead
		self._handlerListHead = connection
	else
		self._handlerListHead = connection
	end

	return connection
end

--[=[
	@deprecated v1.3.0 -- Use `Signal:Once` instead.
	@param fn ConnectionFn
	@return SignalConnection
]=]
function Signal:ConnectOnce(fn)
	return self:Once(fn)
end

--[=[
	@param fn ConnectionFn
	@return SignalConnection

	Connects a function to the signal, which will be called the next time the signal fires. Once
	the connection is triggered, it will disconnect itself.
	```lua
	signal:Once(function(msg, num)
		print(msg, num)
	end)

	signal:Fire("Hello", 25)
	signal:Fire("This message will not go through", 10)
	```
]=]
function Signal:Once(fn)
	local connection
	local done = false

	connection = self:Connect(function(...)
		if done then
			return
		end

		done = true
		connection:Disconnect()
		fn(...)
	end)

	return connection
end

function Signal:GetConnections()
	local items = {}

	local item = self._handlerListHead
	while item do
		table.insert(items, item)
		item = item._next
	end

	return items
end

-- Disconnect all handlers. Since we use a linked list it suffices to clear the
-- reference to the head handler.
--[=[
	Disconnects all connections from the signal.
	```lua
	signal:DisconnectAll()
	```
]=]
function Signal:DisconnectAll()
	local item = self._handlerListHead
	while item do
		item.Connected = false
		item = item._next
	end
	self._handlerListHead = false

	local yieldedThreads = rawget(self, "_yieldedThreads")
	if yieldedThreads then
		for thread in yieldedThreads do
			if coroutine.status(thread) == "suspended" then
				warn(debug.traceback(thread, "signal disconnected; yielded thread cancelled", 2))
				task.cancel(thread)
			end
		end
		table.clear(self._yieldedThreads)
	end
end

-- Signal:Fire(...) implemented by running the handler functions on the
-- coRunnerThread, and any time the resulting thread yielded without returning
-- to us, that means that it yielded to the Roblox scheduler and has been taken
-- over by Roblox scheduling, meaning we have to make a new coroutine runner.
--[=[
	@param ... any

	Fire the signal, which will call all of the connected functions with the given arguments.
	```lua
	signal:Fire("Hello")

	-- Any number of arguments can be fired:
	signal:Fire("Hello", 32, {Test = "Test"}, true)
	```
]=]
function Signal:Fire(...)
	local item = self._handlerListHead
	while item do
		if item.Connected then
			if not freeRunnerThread then
				freeRunnerThread = coroutine.create(runEventHandlerInFreeThread)
			end
			task.spawn(freeRunnerThread, item._fn, ...)
		end
		item = item._next
	end
end

--[=[
	@param ... any

	Same as `Fire`, but uses `task.defer` internally & doesn't take advantage of thread reuse.
	```lua
	signal:FireDeferred("Hello")
	```
]=]
function Signal:FireDeferred(...)
	local item = self._handlerListHead
	while item do
		local conn = item
		task.defer(function(...)
			if conn.Connected then
				conn._fn(...)
			end
		end, ...)
		item = item._next
	end
end

--[=[
	@return ... any
	@yields

	Yields the current thread until the signal is fired, and returns the arguments fired from the signal.
	Yielding the current thread is not always desirable. If the desire is to only capture the next event
	fired, using `Once` might be a better solution.
	```lua
	task.spawn(function()
		local msg, num = signal:Wait()
		print(msg, num) --> "Hello", 32
	end)
	signal:Fire("Hello", 32)
	```
]=]
function Signal:Wait()
	local yieldedThreads = rawget(self, "_yieldedThreads")
	if not yieldedThreads then
		yieldedThreads = {}
		rawset(self, "_yieldedThreads", yieldedThreads)
	end

	local thread = coroutine.running()
	yieldedThreads[thread] = true

	self:Once(function(...)
		yieldedThreads[thread] = nil
		task.spawn(thread, ...)
	end)

	return coroutine.yield()
end

--[=[
	Cleans up the signal.

	Technically, this is only necessary if the signal is created using
	`Signal.Wrap`. Connections should be properly GC'd once the signal
	is no longer referenced anywhere. However, it is still good practice
	to include ways to strictly clean up resources. Calling `Destroy`
	on a signal will also disconnect all connections immediately.
	```lua
	signal:Destroy()
	```
]=]
function Signal:Destroy()
	self:DisconnectAll()

	local proxyHandler = rawget(self, "_proxyHandler")
	if proxyHandler then
		proxyHandler:Disconnect()
	end
end

-- Make signal strict
setmetatable(Signal, {
	__index = function(_tb, key)
		error(("Attempt to get Signal::%s (not a valid member)"):format(tostring(key)), 2)
	end,
	__newindex = function(_tb, key, _value)
		error(("Attempt to set Signal::%s (not a valid member)"):format(tostring(key)), 2)
	end,
})

return table.freeze({
	new = Signal.new,
	Wrap = Signal.Wrap,
	Is = Signal.Is,
})


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

local signal = require("./mock-signal")

local testPlacePath = process.args[1]
local game = roblox.deserializePlace(fs.readFile(testPlacePath))
local threads: { thread } = {}

-- rUnit uses lemon signal
roblox.implementProperty("BindableEvent", "Event", function(get)
	return signal.new()
end)

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

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

-- Promise.lua indexes RunService.Heartbeat, but only uses it in Promise.defer and Promise.delay
local heartbeat = signal.new()
roblox.implementProperty("RunService", "Heartbeat", function()
	return heartbeat
end, function() end)

table.insert(
	threads,
	task.spawn(function()
		local lastFrame = os.clock()
		while true do
			task.wait()
			local dt = os.clock() - lastFrame
			heartbeat:Fire(dt)
		end
	end)
)

roblox.implementMethod("RunService", "IsStudio", function()
	return true
end)

roblox.implementMethod("RunService", "IsServer", function()
	return true
end)

roblox.implementMethod("RunService", "IsClient", function()
	return false
end)

roblox.implementMethod("RunService", "IsRunning", function()
	return false
end)

roblox.implementMethod("DataModel", "IsLoaded", function()
	return true
end)

roblox.implementProperty("Players", "LocalPlayer", function()
	return roblox.Instance.new("Player")
end)

roblox.implementMethod("HttpService", "JSONEncode", function(self, value)
	return serde.encode("json", value)
end)

local requireCache = {}
local requireFrom
local runRobloxScript

local function createRobloxRequire(script: Instance)
	local function robloxRequire(moduleScript: ModuleScript)
		if typeof(moduleScript) == "string" then
			return requireFrom(script, moduleScript, robloxRequire)
		end

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

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

	return robloxRequire
end

function runRobloxScript(script: Script | LocalScript | ModuleScript)
	assert(typeof(script) == "Instance", "script is not an instance")
	local robloxRequire = createRobloxRequire(script)
	local callableFn = luau.load(luau.compile(script.Source), {
		debugName = script:GetFullName(),
		environment = tableJoin(roblox, {
			Instance = roblox.Instance,
			game = game,
			script = script,
			require = robloxRequire,
			tick = tick,
			task = task,
			debug = debug,
		}),
	})

	return callableFn()
end

local function split(path): { string }
	local t = {}
	for part in string.gmatch(path, "[^/]+") do
		table.insert(t, part)
	end
	return t
end

local function resolveRelative(base: Instance, path: string): Instance
	local parts = split(path)
	local current = base

	for _, part in ipairs(parts) do
		if part == "." then
			-- stay
		elseif part == ".." then
			current = current.Parent
			assert(current, "Invalid require path: too many '..'")
		else
			local old = current
			current = current:FindFirstChild(part)
			assert(current, "Module not found: " .. old:GetFullName() .. "." .. part)
		end
	end

	return current
end

function requireFrom(script: Instance, path: string, robloxRequire)
	if not robloxRequire then
		robloxRequire = createRobloxRequire(script)
	end
	path = string.gsub(path, "@self", "./src/")

	-- relative paths
	if string.sub(path, 1, 1) == "." then
		local instance = resolveRelative(script.Parent, path)
		assert(instance, "no instance found at path " .. path .. " in " .. script.Parent:GetFullName())
		return robloxRequire(instance)
	end

	-- fallback
	return robloxRequire(path)
end

runRobloxScript(game.ReplicatedStorage.Tests["run-tests"])
local reportJSON = game.ReplicatedStorage.coverage.Value
fs.writeDir("coverage")
fs.writeFile("coverage/coverage-final.json", reportJSON)

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


import { sizeOfNumberType, sign, CF__add } from "./utility";
import { f24 } from "./utility/f24";
import { f16 } from "./utility/f16";
import { u24 } from "./utility/u24";
import { u12 } from "./utility/u12";
import { i24 } from "./utility/i24";
import { i12 } from "./utility/i12";
import { AXIS_ALIGNED_ORIENTATIONS, COMMON_UDIM2S, COMMON_VECTORS, IS_LUNE } from "./constants";
import type { ProcessedInfo } from "./info-processing";
import type { SerializedData, SerializerSchema } from "./types";
import { f8 } from "./utility/f8";

const { ceil, map, pi: PI } = math;
const { create: createVector } = vector;
const {
  create: createBuffer,
  readi8, readi16, readi32, readu8, readu16, readu32, readf32, readf64, readstring
} = buffer;
const { fromRGB } = Color3;

const orig = CFrame.fromAxisAngle;
const fromAxisAngle = IS_LUNE
  ? (unit: Vector3, rotation: number) => orig(new Vector3(unit.X, unit.Y, unit.Z), rotation)
  : orig;

const LIMIT_16_BITS = 2 ** 16 - 1;
const LIMIT_15_BITS = 2 ** 15 - 1;

export function getDeserializeFunction<T>(
  { schema, containsPacking, containsUnknownPacking, minimumPackedBits, minimumPackedBytes, sortedEnums }: ProcessedInfo
): (data: SerializedData) => T {
  const bitsLength = ceil(minimumPackedBits / 8) * 8;
  let bits = table.create<boolean>(bitsLength);
  let bitIndex = 0;
  let buf!: buffer;
  let offset!: number;
  let blobs: defined[] | undefined;
  let blobIndex = 0;
  let packing = false;

  function deserialize(meta: SerializerSchema, serializeOffset = offset): unknown {
    const currentOffset = serializeOffset;

    switch (meta[0]) {
      case "u8":
        offset += 1;
        return readu8(buf, currentOffset);
      case "u12":
        offset += 1;
        const u12Bits = [bits[bitIndex++], bits[bitIndex++], bits[bitIndex++], bits[bitIndex++]] as const;
        return u12.read(buf, currentOffset, u12Bits);
      case "u16":
        offset += 2;
        return readu16(buf, currentOffset);
      case "u24":
        offset += 3;
        return u24.read(buf, currentOffset);
      case "u32":
        offset += 4;
        return readu32(buf, currentOffset);
      case "i8":
        offset += 1;
        return readi8(buf, currentOffset);
      case "i12":
        offset += 1;
        const i12Bits = [bits[bitIndex++], bits[bitIndex++], bits[bitIndex++], bits[bitIndex++]] as const;
        return i12.read(buf, currentOffset, i12Bits);
      case "i16":
        offset += 2;
        return readi16(buf, currentOffset);
      case "i24":
        offset += 3;
        return i24.read(buf, currentOffset);
      case "i32":
        offset += 4;
        return readi32(buf, currentOffset);
      case "f8":
        offset += 1;
        return f8.read(buf, currentOffset);
      case "f16":
        offset += 2;
        return f16.read(buf, currentOffset);
      case "f24":
        offset += 3;
        return f24.read(buf, currentOffset);
      case "f32":
        offset += 4;
        return readf32(buf, currentOffset);
      case "f64":
        offset += 8;
        return readf64(buf, currentOffset);
      case "bool":
        if (packing)
          return bits[bitIndex++];

        offset += 1;
        return readu8(buf, currentOffset) === 1;
      case "string": {
        const [_, lengthType] = meta;
        const lengthSize = sizeOfNumberType(lengthType);
        const length = deserialize(lengthType) as number;
        offset += length;

        return readstring(buf, currentOffset + lengthSize, length);
      }
      case "enum": {
        const index = readu8(buf, currentOffset);
        offset += 1;

        return sortedEnums[meta[1]!][index];
      }
      case "numbersequence": {
        const keypointCount = readu8(buf, currentOffset);
        const keypoints: NumberSequenceKeypoint[] = [];
        let bytesRead = 1;

        for (const i of $range(1, keypointCount)) {
          const keypointOffset = currentOffset + 1 + 6 * (i - 1);
          const time = readu16(buf, keypointOffset) / 0xFFFF;
          const value = readu16(buf, keypointOffset + 2) / 0xFFFF;
          const envelope = readu16(buf, keypointOffset + 4) / 0xFFFF;

          bytesRead += 6;
          keypoints.push(new NumberSequenceKeypoint(time, value, envelope));
        }

        offset += bytesRead;
        return new NumberSequence(keypoints);
      }
      case "colorsequence": {
        const keypointCount = readu8(buf, currentOffset);
        const keypoints: ColorSequenceKeypoint[] = [];
        let bytesRead = 1;

        for (const i of $range(1, keypointCount)) {
          const keypointOffset = currentOffset + 1 + 5 * (i - 1);
          const time = readu16(buf, keypointOffset) / 0xFFFF;
          const value = deserialize(["color"], keypointOffset + 2) as Color3;

          bytesRead += 5;
          keypoints.push(new ColorSequenceKeypoint(time, value));
        }

        offset += bytesRead;
        return new ColorSequence(keypoints);
      }
      case "color": {
        const r = readu8(buf, currentOffset) as number;
        const g = readu8(buf, currentOffset + 1) as number;
        const b = readu8(buf, currentOffset + 2) as number;
        offset += 3;

        return fromRGB(r, g, b);
      }
      case "udim": {
        const [_, scaleType, offsetType] = meta;
        const scale = deserialize(scaleType) as number;
        const offset = deserialize(offsetType) as number;
        return new UDim(scale, offset);
      }
      case "udim2": {
        const [_, scaleXType, offsetXType, scaleYType, offsetYType] = meta;
        if (packing) {
          const isOptimized = bits[bitIndex++];
          if (isOptimized) {
            const packed = readu8(buf, currentOffset);
            offset += 1;

            const index = packed & 0x7
            if (index !== 0x7)
              return COMMON_UDIM2S[index];
          }
        }

        const scaleX = deserialize(scaleXType) as number;
        const offsetX = deserialize(offsetXType) as number;
        const scaleY = deserialize(scaleYType) as number;
        const offsetY = deserialize(offsetYType) as number;
        return new UDim2(scaleX, offsetX, scaleY, offsetY);
      }
      case "vector": {
        const [_, xType, yType, zType] = meta;
        if (packing) {
          const isOptimized = bits[bitIndex++];
          if (isOptimized) {
            const packed = readu8(buf, currentOffset);
            offset += 1;

            const index = packed & 0x3F
            if (index !== 0x3F)
              return COMMON_VECTORS[index];
          }
        }

        const x = deserialize(xType) as number;
        const y = deserialize(yType) as number;
        const z = deserialize(zType) as number;
        return createVector(x, y, z);
      }
      case "cframe": {
        const [_, xType, yType, zType] = meta;
        if (packing) {
          const isOptimized = bits[bitIndex++];
          if (isOptimized) {
            const packed = readu8(buf, currentOffset);
            offset += 1;

            const optimizedPosition = packed & 0x60;
            const optimizedRotation = packed & 0x1F;
            const rotation = optimizedRotation !== 0x1F
              ? AXIS_ALIGNED_ORIENTATIONS[optimizedRotation]
              : readCFrameAngles();

            let position: Vector3;
            if (optimizedPosition === 0x20)
              position = Vector3.zero;
            else if (optimizedPosition === 0x60)
              position = Vector3.one;
            else
              position = deserialize(["vector", xType, yType, zType]) as Vector3;

            return CF__add(rotation, position);
          }
        }

        const rotation = readCFrameAngles();
        const position = deserialize(["vector", xType, yType, zType]) as Vector3;
        return CF__add(rotation, position);
      }
      case "list": {
        const [_, elementMeta, sizeMeta] = meta;
        const size = deserialize(sizeMeta) as number;
        const list: defined[] = [];
        for (const _ of $range(1, size))
          list.push(deserialize(elementMeta)!);

        return list
      }
      case "object": {
        const [_, elements, preallocation] = meta;
        const object = table.clone(preallocation);
        for (const i of $range(1, elements.size(), 2)) {
          const key = elements[i - 1] as string;
          object.set(key, deserialize(elements[i] as SerializerSchema));
        }

        return object;
      }
      case "union": {
        const [_, tagName, tagged, byteSize] = meta;

        let tagIndex;
        if (byteSize === 1) {
          offset += 1;
          tagIndex = readu8(buf, currentOffset);
        } else if (byteSize === 2) {
          offset += 2;
          tagIndex = readu16(buf, currentOffset);
        } else
          tagIndex = bits[bitIndex++] ? 0 : 1;

        const [tagValue, tagMetadata] = tagged[tagIndex];
        const object = deserialize(tagMetadata);
        (object as Record<string, unknown>)[tagName] = tagValue;

        return object;
      }
      case "guard_union": {
        const [_, serializers] = meta;
        const [deserializer] = serializers[readu8(buf, currentOffset)];
        offset += 1;

        return deserialize(deserializer);
      }
      case "literal": {
        const [_, literals, byteSize] = meta;
        if (byteSize === 1) {
          offset += 1;
          return literals[readu8(buf, currentOffset)];
        } else if (byteSize === 2) {
          offset += 2;
          return literals[readu16(buf, currentOffset)];
        } else if (byteSize === -1)
          return literals[bits[bitIndex++] ? 0 : 1];

        return literals[0];
      }
      case "tuple": {
        const [_, elements, restMetadata, restLengthType] = meta;
        let restLength = 0;
        if (restMetadata !== undefined)
          restLength = deserialize(restLengthType!) as number;

        const tuple = new Array<defined>(elements.size() + restLength);
        for (const element of elements)
          tuple.push(deserialize(element) as defined);

        if (restMetadata !== undefined)
          for (const _ of $range(1, restLength))
            tuple.push(deserialize(restMetadata) as defined);

        return tuple;
      }
      case "set": {
        const [_, elementType, lengthType] = meta;
        const length = deserialize(lengthType) as number;
        const set = new Set<unknown>;

        if (length > 0)
          for (const _ of $range(1, length))
            set.add(deserialize(elementType));

        return set;
      }
      case "map": {
        const [_, keyType, valueType, lengthType] = meta;
        const length = deserialize(lengthType) as number;
        const map = new Map<unknown, unknown>;

        if (length > 0)
          for (const _ of $range(1, length))
            map.set(deserialize(keyType), deserialize(valueType));

        return map;
      }

      case "optional": {
        const [_, valueMeta] = meta;
        const exists = packing
          ? bits[bitIndex++]
          : readu8(buf, offset++) === 1;

        return exists ? deserialize(valueMeta) : undefined;
      }
      case "packed": {
        const [_, innerType] = meta;
        const enclosing = packing;
        packing = true;
        const value = deserialize(innerType);
        packing = enclosing;

        return value;
      }

      case "blob":
        return blobs![blobIndex++];

      default:
        throw `[@rbxts/serio]: Cannot deserialize unknown schema type '${meta[0]}'`;
    }
  }

  function readCFrameAngles(): CFrame {
    const currentOffset = offset;
    const mappedX = readu16(buf, currentOffset);
    let mappedY = readi16(buf, currentOffset + 2);
    const mappedAngle = readu16(buf, currentOffset + 4);
    offset += 6;

    const zSign = sign(mappedY);
    mappedY *= zSign;

    const axisX = map(mappedX, 0, LIMIT_16_BITS, -1, 1);
    let derivedMaximumSquared = 1 - axisX ** 2;
    const derivedMaximum = derivedMaximumSquared ** 0.5;
    const axisY = map(mappedY, 0, LIMIT_15_BITS, -derivedMaximum, derivedMaximum);
    derivedMaximumSquared -= axisY ** 2;

    const axisZ = (derivedMaximumSquared ** 0.5) * zSign;
    const axis = createVector(axisX, axisY, axisZ) as unknown as Vector3;
    const angle = map(mappedAngle, 0, LIMIT_16_BITS, 0, PI);
    return fromAxisAngle(axis, angle);
  }

  function readBits(): void {
    const guaranteedBytes = minimumPackedBytes;

    while (true) {
      const currentByte = readu8(buf, offset);
      const guaranteedByte = offset++ < guaranteedBytes;

      for (const bit of $range(guaranteedByte ? 0 : 1, 7)) {
        const value = (currentByte >>> bit) % 2 === 1;
        bits.push(value);
      }

      // Variable bit indicated the end.
      if (!guaranteedByte && currentByte % 2 === 0)
        break;

      // We only have guaranteed bits and we reached the end.
      if (!containsUnknownPacking && offset === guaranteedBytes)
        break;
    }
  }

  return (data: SerializedData) => {
    ({
      buf = createBuffer(0),
      blobs =[]
    } = data);
    offset = 0;
    blobIndex = 0;
    bitIndex = 0;

    if (containsPacking && buffer.len(buf) > 0) {
      bits = [];
      readBits();
    }

    return deserialize(schema) as T;
  };
}


import { SerializerSchema } from "./types";
import { getSortedEnumItems } from "./utility";

const { max, ceil } = math;

const enum IterationFlags {
  Default = 0,
  SizeUnknown = 1 << 0,
  Packed = 1 << 1
}

export interface ProcessedInfo {
  readonly flags: IterationFlags;
  readonly schema: SerializerSchema;
  readonly containsPacking: boolean;
  readonly containsUnknownPacking: boolean;
  readonly minimumPackedBits: number;
  readonly minimumPackedBytes: number;
  readonly sortedEnums: Record<string, EnumItem[]>;
}

const packing = (info: ProcessedInfo) => (info.flags & IterationFlags.Packed) !== 0;
function addPackedBit(info: Writable<ProcessedInfo>, count = 1): void {
  if (!packing(info)) return;
  info.containsPacking = true;

  if ((info.flags & IterationFlags.SizeUnknown) !== 0)
    info.containsUnknownPacking = true;
  else
    // We only keep track of guaranteed packing bits, which we can use for optimization.
    info.minimumPackedBits += count;
}

/**
 * Mutates the given ProcessedInfo object while traversing the given schema in
 * order to collect information about it.
 *
 * This function is used to prepare the schema for the serialization and
 * deserialization processes. It traverses the schema and adds information to
 * the ProcessedInfo object about the schema's layout. This information is
 * necessary for the serialization and deserialization processes to work
 * correctly.
 */
function schemaPass(schema: SerializerSchema, info: Writable<ProcessedInfo>): SerializerSchema {
  const [kind] = schema;

  switch (kind) {
    case "object_raw": {
      // We transform objects as an array of tuples, but this is slow to iterate over.
      // We flatten the raw generated metadata into a single array, which can be iterated much quicker.
      // We also create a preallocated object that we can clone as we already know the structure ahead of time.
      const preallocation = new Map<unknown, unknown>;
      const transformed: (string | SerializerSchema)[] = [];
      for (const [key, meta] of schema[1]) {
        transformed.push(key, schemaPass(meta, info));
        preallocation.set(key, true);
      }

      schema = ["object", transformed, preallocation];
      break;
    }
    case "u12":
    case "i12": {
      if (!packing(info)) {
        throw `[@rbxts/serio]: 12-bit types may not be used without packing!`;
      }

      addPackedBit(info, 4);
      break;
    }
    case "optional": {
      addPackedBit(info);
      info.flags |= IterationFlags.SizeUnknown;

      schema = [kind, schemaPass(schema[1], info)];
      break;
    }
    case "packed": {
      info.flags |= IterationFlags.Packed;

      schema = [kind, schemaPass(schema[1], info)];
      break;
    }
    case "union": {
      const [_, name, options] = schema;
      const optionsSize = options.size();
      // Whenever we only have two options, we can use a single bit.
      // We use a byte size of `-1` to indicate a packable union.
      const isPackable = packing(info) && optionsSize === 2;
      if (isPackable)
        addPackedBit(info);

      info.flags |= IterationFlags.SizeUnknown;
      schema = [
        kind,
        name,
        options.map(([key, schema]): [unknown, SerializerSchema] => [key, schemaPass(schema, info)]),
        isPackable ? -1 : (optionsSize > 256 ? 2 : 1),
      ];
      break;
    }
    case "guard_union": {
      info.flags |= IterationFlags.SizeUnknown;

      schema = [kind, schema[1].map((v) => [schemaPass(v[0], info), v[1]] as const)];
      break;
    }
    case "literal": {
      const [_, possibleValues, size] = schema;
      const possibleValuesSize = possibleValues.size();
      // Whenever we only have two options, we can use a single bit.
      // We exclude undefined using `data[2] === 0` as it complicates thing.
      if (packing(info) && possibleValuesSize === 2 && size === 0) {
        addPackedBit(info);

        // We use `-1` as the size to signify that this union can be packed,
        // as it's not a valid value otherwise.
        return [kind, possibleValues, -1];
      }

      // Since `undefined` is not included in the size of `data[1]`,
      // we add the existing value of `data[3]` (which is 1 if undefined is in the union) to `data[1]`
      // to determine the final required size.
      // A size of -1 means this isn't a union.
      const finalSize = size === -1
        ? 0
        : (size + possibleValuesSize > 256 ? 2 : 1);

      schema = [kind, possibleValues, finalSize];
      break;
    }
    case "udim2":
    case "vector":
    case "cframe":
    case "bool":
      addPackedBit(info);
      break;
    case "set":
    case "list": {
      info.flags |= IterationFlags.SizeUnknown;

      schema = [kind, schemaPass(schema[1], info), schema[2]];
      break;
    }
    case "map": {
      info.flags |= IterationFlags.SizeUnknown;

      schema = [kind, schemaPass(schema[1], info), schemaPass(schema[2], info), schema[3]];
      break;
    }
    case "tuple": {
      const [_, elementTypes, restElementType] = schema;
      const fixedElements = elementTypes.map(v => schemaPass(v, info));

      let restElement;
      if (restElementType !== undefined) {
        info.flags |= IterationFlags.SizeUnknown;

        restElement = schemaPass(restElementType, info);
      }

      schema = [kind, fixedElements, restElement];
      break;
    }
    case "enum": {
      const [_, index] = schema;
      if (info.sortedEnums[index] === undefined)
        info.sortedEnums[index] = getSortedEnumItems(Enum[index as never]);

      break;
    }
  }

  return schema;
}

function getMinimumPackedBytes({ minimumPackedBits, containsUnknownPacking }: ProcessedInfo): number {
  return max(0, ceil(minimumPackedBits / 8) - (containsUnknownPacking ? 1 : 0));
}

export function processInfo(rawSchema: SerializerSchema): ProcessedInfo {
  const info: Writable<ProcessedInfo> = {
    schema: rawSchema,
    flags: IterationFlags.Default,
    containsPacking: false,
    containsUnknownPacking: false,
    minimumPackedBits: 0,
    minimumPackedBytes: 0,
    sortedEnums: {}
  };

  info.schema = schemaPass(rawSchema, info);
  info.minimumPackedBytes = getMinimumPackedBytes(info);

  return info;
}