feat(template): initialize libraries and release v1.0.0

This commit is contained in:
Infuseting 2026-10-14 09:16:43 +02:00
parent fcdd2d7a50
commit 76d200f068
13 changed files with 278 additions and 24 deletions

View File

@ -2,7 +2,9 @@
"version": 1, "version": 1,
"flameworkVersion": "1.3.2", "flameworkVersion": "1.3.2",
"identifiers": { "identifiers": {
"crownstudio-roblox-ts-template:out/server/services/startup-service@StartupService": "server/services/startup-service@StartupService" "crownstudio-roblox-ts-template:out/server/services/startup-service@StartupService": "server/services/startup-service@StartupService",
"crownstudio-roblox-ts-template:out/client/controllers/cmdr-controller@CmdrController": "client/controllers/cmdr-controller@CmdrController",
"crownstudio-roblox-ts-template:out/server/services/cmdr-service@CmdrService": "server/services/cmdr-service@CmdrService"
}, },
"metadata": {} "metadata": {}
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "crownstudio-roblox-ts-template", "name": "crownstudio-roblox-ts-template",
"version": "0.1.0", "version": "1.0.0",
"private": true, "private": true,
"packageManager": "pnpm@11.21.0", "packageManager": "pnpm@11.21.0",
"engines": { "engines": {
@ -18,7 +18,8 @@
"lint": "eslint .", "lint": "eslint .",
"format:check": "prettier --check .", "format:check": "prettier --check .",
"format": "prettier --write .", "format": "prettier --write .",
"check": "pnpm lint && pnpm compile", "test:cmdr": "node --test scripts/cmdr-permissions.test.mjs",
"check": "pnpm lint && pnpm test:cmdr && pnpm compile",
"prepare": "husky" "prepare": "husky"
}, },
"rbxts-build": { "rbxts-build": {

View File

@ -4,24 +4,19 @@ Template Roblox TypeScript avec Flamework, Rojo et une sélection de bibliothèq
## Démarrage ## Démarrage
Prérequis : [Roblox Studio](https://create.roblox.com/docs/studio/setup), [mise](https://mise.jdx.dev/getting-started.html) et [Rokit](https://github.com/rojo-rbx/rokit#readme). Sous Windows, rouvrir le terminal après leur installation pour actualiser `PATH`. Sous Windows 10/11, lancer depuis le dépôt :
```powershell ```powershell
mise install powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\setup.ps1
rokit install
pnpm install --frozen-lockfile
pnpm check
pnpm build
pnpm serve
``` ```
`mise` épingle Node.js 24.16.0, pnpm 11.21.0 et uv 0.13.0. `rokit` installe Rojo 7.7.1, Asphalt 2.0.2, darklua 0.19.0 et Mantle 0.11.18. `pnpm build` compile le TypeScript et crée `game.rbxl` avec Rojo. Ouvrir ce fichier dans Studio, ou utiliser `pnpm start` pour le flux [`rbxts-build`](https://github.com/osyrisrblx/rbxts-build#readme). `pnpm watch` lance la compilation et le serveur Rojo en continu. Le projet Rojo est [`default.project.json`](default.project.json) et les entrées client/serveur sont sous [`src`](src/). Le [script d'installation](scripts/setup.ps1) nécessite WinGet (App Installer du Microsoft Store). Il installe Git, [mise](https://mise.jdx.dev/installing-mise.html), Rokit, Roblox Studio et Blender via WinGet, puis les versions définies dans `mise.toml` et `rokit.toml`, et les dépendances pnpm verrouillées. Il termine par `pnpm check` et `pnpm build` et s'arrête dès qu'une commande échoue. Windows peut demander une élévation pour certains installateurs. Une relance conserve les applications déjà installées ; elle ne les met pas à jour automatiquement. Les applications WinGet utilisent la version disponible dans son catalogue, les outils du projet restent épinglés.
Autres commandes : `pnpm lint`, `pnpm format:check` et `pnpm format`. [Husky](https://typicode.github.io/husky/) installe un hook local `pre-commit` qui exécute `pnpm lint`. [Renovate](https://docs.renovatebot.com/) lit [`renovate.json`](renovate.json) une fois son application activée sur le dépôt distant.
## Bibliothèques installées ## Bibliothèques installées
Chaque paquet de cette section est une dépendance directe. Les liens mènent à la documentation du projet ou à son README actuel ; les versions exécutables sont épinglées dans le lockfile. Les paquets ne sont pas automatiquement utilisés par le code de démarrage. Chaque paquet de cette section est une dépendance directe. Les liens mènent à la documentation du projet ou à son README actuel ; les versions exécutables sont épinglées dans le lockfile. Les 22 bibliothèques ont été examinées : Flamework, son gestionnaire de composants, Cmdr et le logger sont branchés au démarrage ; les autres bibliothèques s'utilisent à l'import ou se configurent avec les données de leur fonctionnalité.
**Console Cmdr** : en mode Play dans Studio, ouvrir avec **F2**. Les commandes intégrées sont enregistrées et protégées côté serveur. Hors Studio, seuls le propriétaire individuel de l'expérience et les UserIds renseignés dans [`cmdr-permissions.ts`](src/server/config/cmdr-permissions.ts) sont autorisés. Pour une expérience de groupe, renseigner les administrateurs explicitement. `pnpm test:cmdr` vérifie cette politique ; ce test fait aussi partie de `pnpm check`. La configuration client est dans [`cmdr-controller.ts`](src/client/controllers/cmdr-controller.ts).
- `@flamework/core` ([documentation](https://flamework.fireboltofdeath.dev/docs/introduction/)) — services, contrôleurs et injection ; le service de démarrage en donne un exemple. - `@flamework/core` ([documentation](https://flamework.fireboltofdeath.dev/docs/introduction/)) — services, contrôleurs et injection ; le service de démarrage en donne un exemple.
- `@flamework/components` ([documentation](https://flamework.fireboltofdeath.dev/docs/additional-modules/components/creating-a-component/)) — composants d'instances. - `@flamework/components` ([documentation](https://flamework.fireboltofdeath.dev/docs/additional-modules/components/creating-a-component/)) — composants d'instances.
@ -34,17 +29,17 @@ Chaque paquet de cette section est une dépendance directe. Les liens mènent à
- `@rbxts/lapis` ([documentation](https://github.com/nezuo/lapis/tree/master/docs)) — persistance de données. - `@rbxts/lapis` ([documentation](https://github.com/nezuo/lapis/tree/master/docs)) — persistance de données.
- `@rbxts/trove` ([README et API](https://github.com/Sleitnick/rbxts-trove#readme)) — nettoyage des ressources. - `@rbxts/trove` ([README et API](https://github.com/Sleitnick/rbxts-trove#readme)) — nettoyage des ressources.
- `@rbxts/log` ([README](https://github.com/roblox-aurora/rbx-log#readme)) — journalisation. - `@rbxts/log` ([README](https://github.com/roblox-aurora/rbx-log#readme)) — journalisation.
- `@rbxts/sift` ([documentation](https://cxmeel.github.io/sift/)) — collections immuables - `@rbxts/sift` ([documentation](https://cxmeel.github.io/sift/)) — collections immuables
- `@rbxts/t` ([README et API](https://github.com/osyrisrblx/t#readme)) — validation de valeurs à l'exécution. - `@rbxts/t` ([README et API](https://github.com/osyrisrblx/t#readme)) — validation de valeurs à l'exécution.
- `@rbxts/cmdr` ([documentation](https://eryn.io/Cmdr/), [types roblox-ts](https://github.com/sasial-dev/Cmdr#readme)) — commandes de développement. - `@rbxts/cmdr` ([documentation](https://eryn.io/Cmdr/), [types roblox-ts](https://github.com/sasial-dev/Cmdr#readme)) — commandes de développement.
- `@rbxts/gamejoy` ([README](https://github.com/Rimuy/GameJoy#readme)) — contrôles de jeu. - `@rbxts/gamejoy` ([README](https://github.com/Rimuy/GameJoy#readme)) — contrôles de jeu.
- `@rbxts/tween` ([README et API](https://github.com/Validark/Roblox-TS-Libraries/blob/master/tween/README.md)) — interpolations. - `@rbxts/tween` ([README et API](https://github.com/Validark/Roblox-TS-Libraries/blob/master/tween/README.md)) — interpolations.
- `@rbxts/timer` ([README](https://github.com/Bytebit-Org/roblox-Timer#readme)) — temporisation. - `@rbxts/timer` ([README](https://github.com/Bytebit-Org/roblox-Timer#readme)) — temporisation.
- `@rbxts/ui-labs` ([documentation](https://ui-labs.luau.page/docs/getstarted.html)) — prévisualisation d'interface. - `@rbxts/ui-labs` ([documentation](https://ui-labs.luau.page/docs/getstarted.html)) — prévisualisation d'interface.
- `@rbxts/sound-manager` ([documentation du projet](https://github.com/xelnae-r/Sound-Manager#readme)) — gestion des sons - `@rbxts/sound-manager` ([documentation du projet](https://github.com/xelnae-r/Sound-Manager#readme)) — gestion des sons
- `@rbxts/object-cache` ([documentation amont](https://github.com/Pyseph/ObjectCache#readme)) — cache de pièces/objets - `@rbxts/object-cache` ([documentation amont](https://github.com/Pyseph/ObjectCache#readme)) — cache de pièces/objets
- `@rbxts/net` ([README](https://github.com/roblox-aurora/rbx-net#readme)) — contient `ExperienceBroadcastEvent` pour les messages entre serveurs. - `@rbxts/net` ([README](https://github.com/roblox-aurora/rbx-net#readme)) — contient `ExperienceBroadcastEvent` pour les messages entre serveurs.
- `@rbxts-js/roblox-lua-promise` ([README](https://github.com/evaera/roblox-lua-promise#readme)) — implémentation de promesses - `@rbxts-js/roblox-lua-promise` ([README](https://github.com/evaera/roblox-lua-promise#readme)) — implémentation de promesses
Pour choisir une approche lors d'une nouvelle fonctionnalité, le skill local [`roblox-feature-docs`](.agents/skills/roblox-feature-docs/SKILL.md) compare les bibliothèques pertinentes, leurs documentations et les API réellement disponibles dans les versions verrouillées. Pour choisir une approche lors d'une nouvelle fonctionnalité, le skill local [`roblox-feature-docs`](.agents/skills/roblox-feature-docs/SKILL.md) compare les bibliothèques pertinentes, leurs documentations et les API réellement disponibles dans les versions verrouillées.
@ -79,6 +74,4 @@ Le serveur Blender peut exécuter du code Python dans Blender : utiliser uniquem
L'[API Cloud Creator Store](https://create.roblox.com/docs/cloud/reference/features/creator-store) est une API Roblox distincte des serveurs MCP ci-dessus. Elle n'est pas activée ici : son usage nécessite une clé et un cas d'utilisation précis. L'[API Cloud Creator Store](https://create.roblox.com/docs/cloud/reference/features/creator-store) est une API Roblox distincte des serveurs MCP ci-dessus. Elle n'est pas activée ici : son usage nécessite une clé et un cas d'utilisation précis.
## Vérification du template <sub>Version du template : 1.0.0</sub>
Sur Windows, l'installation verrouillée, `pnpm check`, `pnpm format:check`, `pnpm build` et `pnpm start -- --no-watch` ont réussi. `pnpm build` produit `game.rbxl` et `pnpm start` a lancé Roblox Studio. `codex mcp list` reconnaît les deux serveurs. Un test de jeu dans Studio et un appel MCP complet restent à effectuer.

View File

@ -0,0 +1,61 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { URL } from "node:url";
import { runInNewContext } from "node:vm";
import ts from "typescript";
// Run the actual, engine-independent permission policy, not a duplicate of it.
const source = readFileSync(
new URL("../src/server/config/cmdr-permissions.ts", import.meta.url),
"utf8",
);
const compiled = ts.transpileModule(source, {
compilerOptions: { module: ts.ModuleKind.CommonJS },
}).outputText;
const policyModule = { exports: {} };
runInNewContext(compiled, {
exports: policyModule.exports,
module: policyModule,
});
const { isCmdrAllowed, CMDR_PERMISSIONS } = policyModule.exports;
test("published game denies ordinary players by default", () => {
assert.equal(isCmdrAllowed(42, false, 99), false);
});
test("Studio allows local testing without publishing or configuring an admin", () => {
assert.equal(isCmdrAllowed(-1, true, undefined), true);
});
test("experience owner is allowed in a user-owned published game", () => {
assert.equal(isCmdrAllowed(99, false, 99), true);
});
test("no user owner means no automatic permission in a group-owned game", () => {
assert.equal(isCmdrAllowed(99, false, undefined), false);
});
test("only explicitly listed users get allowlist access", () => {
const permissions = {
allowInStudio: false,
allowExperienceOwner: false,
adminUserIds: new Set([42]),
};
assert.equal(isCmdrAllowed(42, false, undefined, permissions), true);
assert.equal(isCmdrAllowed(43, false, undefined, permissions), false);
});
test("Studio and owner exceptions can both be disabled", () => {
const permissions = {
allowInStudio: false,
allowExperienceOwner: false,
adminUserIds: new Set(),
};
assert.equal(isCmdrAllowed(99, true, 99, permissions), false);
assert.equal(isCmdrAllowed(99, false, 99, permissions), false);
});
test("published default configuration contains no placeholder administrator", () => {
assert.equal(CMDR_PERMISSIONS.adminUserIds.size, 0);
});

73
scripts/setup.ps1 Normal file
View File

@ -0,0 +1,73 @@
#Requires -Version 5.1
[CmdletBinding()]
param(
[switch]$DryRun,
[switch]$SkipChecks
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$projectRoot = Split-Path -Parent $PSScriptRoot
function Invoke-Tool {
param([string]$Name, [string[]]$Arguments)
Write-Host "> $Name $($Arguments -join ' ')"
if ($DryRun) { return }
& $Name @Arguments
if ($LASTEXITCODE -ne 0) {
throw "$Name a echoue (code $LASTEXITCODE). Corriger l'erreur puis relancer le script."
}
}
function Update-SessionPath {
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
$env:Path = "$machinePath;$userPath;$env:Path;$env:USERPROFILE\.rokit\bin"
}
function Install-Package {
param([string]$Id)
if (-not $DryRun) {
& winget list --id $Id --exact --source winget --accept-source-agreements --disable-interactivity
if ($LASTEXITCODE -eq 0) {
Write-Host "$Id est deja installe."
return
}
# APP_NOT_INSTALLED: other errors must not be mistaken for a missing package.
if ($LASTEXITCODE -ne -1978335212) {
throw "Impossible de verifier $Id avec winget (code $LASTEXITCODE)."
}
}
Invoke-Tool 'winget' @('install', '--id', $Id, '--exact', '--source', 'winget', '--silent', '--accept-package-agreements', '--accept-source-agreements', '--disable-interactivity')
if (-not $DryRun) { Update-SessionPath }
}
Push-Location $projectRoot
try {
if (-not $DryRun -and -not (Get-Command winget -ErrorAction SilentlyContinue)) {
throw "Installer App Installer (winget) depuis le Microsoft Store, puis relancer ce script."
}
foreach ($package in @('Git.Git', 'jdx.mise', 'Rojo.Rokit', 'Roblox.RobloxStudio', 'BlenderFoundation.Blender')) {
Install-Package $package
}
Invoke-Tool 'mise' @('trust', 'mise.toml')
Invoke-Tool 'mise' @('install')
Invoke-Tool 'rokit' @('install')
Invoke-Tool 'mise' @('exec', '--', 'pnpm', 'install', '--frozen-lockfile')
if (-not $SkipChecks) {
Invoke-Tool 'mise' @('exec', '--', 'pnpm', 'check')
Invoke-Tool 'mise' @('exec', '--', 'pnpm', 'build')
}
if ($DryRun) {
Write-Host 'Apercu termine : aucune installation effectuee.'
} else {
Write-Host 'Installation terminee. Rouvrir le terminal et les clients MCP.'
Write-Host 'Pour les commandes pnpm/uvx, utiliser mise exec -- <commande> ou activer mise dans le shell.'
Write-Host 'Activer le MCP dans Studio et installer/activer l extension MCP officielle dans Blender (voir README).'
}
} finally {
Pop-Location
}

View File

@ -0,0 +1,30 @@
import { Controller, OnStart } from "@flamework/core";
import { CmdrClient } from "@rbxts/cmdr";
import { Players } from "@rbxts/services";
import { Trove } from "@rbxts/trove";
import { CMDR_ACCESS_ATTRIBUTE } from "shared/cmdr";
import { TEMPLATE_NAME } from "shared/template-name";
@Controller()
export class CmdrController implements OnStart {
private readonly trove = new Trove();
public onStart(): void {
CmdrClient.SetActivationKeys([Enum.KeyCode.F2]);
CmdrClient.SetPlaceName(TEMPLATE_NAME);
CmdrClient.SetActivationUnlocksMouse(true);
this.trove.connect(Players.LocalPlayer.GetAttributeChangedSignal(CMDR_ACCESS_ATTRIBUTE), () =>
this.updateAccess(),
);
this.trove.attachToInstance(script);
this.updateAccess();
}
private updateAccess(): void {
const enabled = Players.LocalPlayer.GetAttribute(CMDR_ACCESS_ATTRIBUTE) === true;
CmdrClient.SetEnabled(enabled);
if (!enabled) {
CmdrClient.Hide();
}
}
}

View File

@ -1,5 +1,10 @@
import { Flamework } from "@flamework/core"; import { Flamework } from "@flamework/core";
import "@flamework/components";
import Log from "@rbxts/log";
import { initializeLogging } from "shared/logging";
import { TEMPLATE_NAME } from "shared/template-name"; import { TEMPLATE_NAME } from "shared/template-name";
initializeLogging();
Flamework.addPaths("src/client/controllers");
Flamework.ignite(); Flamework.ignite();
print(`${TEMPLATE_NAME} ready (client)`); Log.Info("{Template} ready (client)", TEMPLATE_NAME);

View File

@ -0,0 +1,27 @@
export interface CmdrPermissions {
readonly allowInStudio: boolean;
readonly allowExperienceOwner: boolean;
readonly adminUserIds: ReadonlySet<number>;
}
export const CMDR_PERMISSIONS: CmdrPermissions = {
allowInStudio: true,
allowExperienceOwner: true,
// Add individual Roblox UserIds here, especially for group-owned experiences.
adminUserIds: new Set<number>(),
};
export function isCmdrAllowed(
userId: number,
isStudio: boolean,
experienceOwnerUserId: number | undefined,
permissions: CmdrPermissions = CMDR_PERMISSIONS,
): boolean {
return (
(isStudio && permissions.allowInStudio) ||
permissions.adminUserIds.has(userId) ||
(permissions.allowExperienceOwner &&
experienceOwnerUserId !== undefined &&
userId === experienceOwnerUserId)
);
}

View File

@ -1,6 +1,10 @@
import { Flamework } from "@flamework/core"; import { Flamework } from "@flamework/core";
import "@flamework/components";
import Log from "@rbxts/log";
import { initializeLogging } from "shared/logging";
import { TEMPLATE_NAME } from "shared/template-name"; import { TEMPLATE_NAME } from "shared/template-name";
initializeLogging();
Flamework.addPaths("src/server/services"); Flamework.addPaths("src/server/services");
Flamework.ignite(); Flamework.ignite();
print(`${TEMPLATE_NAME} ready (server)`); Log.Info("{Template} ready (server)", TEMPLATE_NAME);

View File

@ -0,0 +1,43 @@
import { OnStart, Service } from "@flamework/core";
import { Cmdr } from "@rbxts/cmdr";
import Log from "@rbxts/log";
import { Players, RunService } from "@rbxts/services";
import { Trove } from "@rbxts/trove";
import { isCmdrAllowed } from "server/config/cmdr-permissions";
import { CMDR_ACCESS_ATTRIBUTE } from "shared/cmdr";
@Service()
export class CmdrService implements OnStart {
private readonly trove = new Trove();
public onStart(): void {
// Register authorization before exposing the built-in commands.
Cmdr.Registry.RegisterHook("BeforeRun", (context) => {
if (!this.canUseConsole(context.Executor)) {
return "You do not have permission to run this command.";
}
});
Cmdr.Registry.RegisterDefaultCommands();
this.trove.connect(Players.PlayerAdded, (player) => {
this.updateAccess(player);
});
for (const player of Players.GetPlayers()) {
this.updateAccess(player);
}
game.BindToClose(() => this.trove.destroy());
Log.Info("Cmdr ready with {CommandCount} commands", Cmdr.Registry.GetCommands().size());
}
private canUseConsole(player: Player): boolean {
return isCmdrAllowed(
player.UserId,
RunService.IsStudio(),
game.CreatorType === Enum.CreatorType.User ? game.CreatorId : undefined,
);
}
private updateAccess(player: Player): void {
player.SetAttribute(CMDR_ACCESS_ATTRIBUTE, this.canUseConsole(player));
}
}

View File

@ -1,8 +1,9 @@
import { OnStart, Service } from "@flamework/core"; import { OnStart, Service } from "@flamework/core";
import Log from "@rbxts/log";
@Service() @Service()
export class StartupService implements OnStart { export class StartupService implements OnStart {
public onStart(): void { public onStart(): void {
print("CrownStudio template ready (server)"); Log.Debug("StartupService started");
} }
} }

2
src/shared/cmdr.ts Normal file
View File

@ -0,0 +1,2 @@
// This attribute controls visibility only. Authorization is checked on the server.
export const CMDR_ACCESS_ATTRIBUTE = "CmdrEnabled";

12
src/shared/logging.ts Normal file
View File

@ -0,0 +1,12 @@
import Log, { LogLevel } from "@rbxts/log";
import { RunService } from "@rbxts/services";
export function initializeLogging(): void {
Log.SetLogger(
Log.Configure()
.SetMinLogLevel(RunService.IsStudio() ? LogLevel.Debugging : LogLevel.Information)
.EnrichWithProperty("Side", RunService.IsServer() ? "server" : "client")
.WriteTo(Log.RobloxOutput())
.Create(),
);
}