Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,33 @@ jobs:

- name: Quality checks
run: npm run check

sonarcloud:
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
continue-on-error: true
permissions:
contents: read
pull-requests: write

steps:
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Build
run: npm run build

- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@v5
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
10 changes: 10 additions & 0 deletions .sourcery.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
reviews:
request_review: true
approve: false
auto_merge: false
ignore_paths:
- dist/**
- node_modules/**
- plugin/**
- "*.json"
- "*.md"
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ Current report areas:
- Package drift
- Performance hotspots

### Genre-Specific Checks (Opt-In)

- **Shooter/Sniper** — weapon remote trust analysis, spawn fairness heuristics, combat content review, weapon equip and respawn cycle smoke tests

Genre checks are opt-in and heuristic. They use pattern matching and static analysis, not runtime simulation. See [docs/shooter-checks.md](docs/shooter-checks.md) for details.

Each finding includes:
- Severity: `blocker`, `warning`, or `info`
- Confidence: `high`, `medium`, `heuristic`, or `manual_review`
Expand Down Expand Up @@ -136,20 +142,22 @@ See [examples/](examples/) for full sample reports in Markdown and JSON.
- The verdict is a scoring rule based on issue counts, not a comprehensive release policy.
- It can miss issues and it can raise false positives. A passing report means "nothing obvious was flagged," not "safe to publish."
- Some checks depend on Open Cloud API keys for full coverage. Without them, metadata-based checks are skipped.
- Genre-specific checks use keyword and pattern matching. They work best with conventional naming and value-instance configs. Unconventional architectures may produce false positives or missed detections.

## Studio-Tested

All 43 tools have been integration-tested against a live Roblox Studio session (2026-03-29).
All 46 tools have been integration-tested against a live Roblox Studio session (2026-03-29).

| Category | Tools | Pass | Skip | Partial |
|----------|-------|------|------|---------|
| Core | 18 | 17 | 0 | 1 |
| Shipcheck | 14 | 14 | 0 | 0 |
| Shooter Genre | 3 | 3 | 0 | 0 |
| Automation | 4 | 3 | 1 | 0 |
| Building | 3 | 3 | 0 | 0 |
| Cloud | 3 | 0 | 3 | 0 |
| Playtester | 1 | 1 | 0 | 0 |
| **Total** | **43** | **38** | **4** | **1** |
| **Total** | **46** | **41** | **4** | **1** |

- **Skip:** Cloud tools require an Open Cloud API key (schema validated, not callable without credentials).
- **Partial:** `start_playtest` returns a plugin capability error (`StartDecal`). Playtest control may require manual interaction in some Studio configurations.
Expand Down
12 changes: 12 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@ All 43 tools tested against a live Roblox Studio session on 2026-03-29.
|------|--------|-------|
| `rbx_playtester` | PASS | list_scenarios, run_scenario (spawn_flow PASS), get_result all working |

## Shooter Genre (3 tools + 2 presets)

Tested against a non-shooter Roblox project (expected: clean output, no false positives).

| Tool | Result | Notes |
|------|--------|-------|
| `rbx_shooter_weapon_remote_trust` | PASS | 0 weapon remotes found (correct for non-shooter) |
| `rbx_shooter_spawn_clustering` | PASS | 0 spawns found (correct) |
| `rbx_shooter_combat_content_maturity` | PASS | 30 scripts + 37 UI elements scanned, 0 findings |
| `shooter_weapon_equip` preset | PARTIAL | No weapons in StarterPack (expected for non-shooter) |
| `shooter_respawn_cycle` preset | PASS | CharacterAutoLoads=true, RespawnTime=3, 7 CharacterAdded handlers |

## Known Limitations

- **`start_playtest`**: Plugin cannot invoke playtest in all Studio configurations due to `StartDecal` capability error. Use manual Play button as a workaround.
Expand Down
107 changes: 107 additions & 0 deletions docs/shooter-checks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Shooter/Sniper Genre Checks

Genre-specific checks for Roblox shooter and sniper games. All checks are **opt-in** and **heuristic** — they use pattern matching and static analysis, not runtime simulation.

## Shipcheck Rules

### `rbx_shooter_weapon_remote_trust`

Audits weapon-related RemoteEvents and RemoteFunctions for server-side validation.

**What it checks:**
- Finds remotes with weapon-related names (fire, shoot, damage, hit, reload, equip, weapon, gun, bullet, projectile)
- Verifies each remote has a server-side handler in ServerScriptService
- Checks handlers for argument type validation (typeof, tonumber, assert, etc.)
- Checks handlers for rate limiting patterns (tick, cooldown, debounce, throttle)

**Issues raised:**

| Rule | Severity | Confidence | Description |
|------|----------|------------|-------------|
| `no_server_handler` | medium | medium | Weapon remote has no server-side handler |
| `missing_type_validation` | medium | heuristic | Handler lacks argument type checks |
| `no_rate_limiting` | low | heuristic | Handler lacks rate limiting patterns |

**Limitations:**
- Pattern-based name matching — non-standard naming may be missed
- Cannot verify validation is *correct*, only that patterns *exist*
- Obfuscated or minified scripts won't be analyzed effectively
- Only scans ServerScriptService for handlers

---

### `rbx_shooter_spawn_clustering`

Analyzes SpawnLocation distribution for fairness issues.

**What it checks:**
- Measures pairwise distances between all SpawnLocations
- Flags clustering when average spread is below threshold (default: 30 studs)
- Checks team balance — flags if team spawn counts differ by more than 2x
- Detects suspicious spawn heights (Y < -10 or Y > 1000)

**Issues raised:**

| Rule | Severity | Confidence | Description |
|------|----------|------------|-------------|
| `spawn_clustering` | warning | heuristic | Spawns are clustered below minimum spread |
| `team_spawn_imbalance` | warning | medium | Teams have unequal spawn point counts |
| `suspicious_spawn_height` | info | medium | Spawn at extreme Y position |

**Limitations:**
- Position-only heuristic — cannot assess line-of-sight or cover
- Intentionally clustered spawns (lobby areas) will be flagged
- FFA games without teams may trigger false positives on team balance

---

### `rbx_shooter_combat_content_maturity`

Scans scripts and UI text for combat-related content that may affect age rating.

**What it checks:**
- Reads all script source code and scans for keyword categories
- Scans TextLabel and TextButton text properties
- Categories: violence_explicit, violence_moderate, weapon_refs, social_risk

**Keyword categories:**

| Category | Examples | Severity |
|----------|----------|----------|
| violence_explicit | gore, dismember, decapitate, torture | warning |
| violence_moderate | blood, bleed, corpse, dead body | info |
| weapon_refs | AK-47, shotgun, sniper rifle, RPG | info |
| social_risk | discord.gg, youtube.com, twitter.com | warning |

**Limitations:**
- Keyword-based only — no semantic understanding
- Common game terms may be flagged (e.g., "headshot" is normal in shooters)
- All findings are `manual_review` confidence — flags for human review, not violations

## Playtester Presets

### `shooter_weapon_equip`

Verifies weapon Tools exist with proper configuration.

**Flow:**
1. Check StarterPack for Tool instances
2. Verify weapons have Handle parts
3. Check for config values (Damage, Ammo, FireRate)
4. Confirm at least one configured weapon exists

**Expected result:** PASS for shooter projects with value-instance weapon configs. PARTIAL or FAIL for non-shooter projects or script-based configs.

---

### `shooter_respawn_cycle`

Validates respawn infrastructure.

**Flow:**
1. Check Players.CharacterAutoLoads and RespawnTime
2. Count SpawnLocations and team assignments
3. Search for CharacterAdded handlers in scripts
4. Confirm spawn infrastructure exists

**Expected result:** PASS for most games with standard respawn setup. PARTIAL if CharacterAutoLoads is disabled (may be intentional for custom respawn).
58 changes: 58 additions & 0 deletions examples/shooter-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Example: Shooter Audit Report

Sample output from running shooter-specific checks on a Roblox FPS project.

## Shipcheck Report — MyShooterGame

**Date:** 2026-03-29T20:00:00Z
**Verdict:** REVIEW — Score: 72/100

### Summary
- Blockers: 0
- Warnings: 3
- Info: 1
- Manual review needed: 2

### Shooter-Specific Findings

#### [shooter-weapon-001] Unvalidated weapon remote
**Confidence:** medium | **Category:** security | **Remediation:** assisted
Handler for "FireWeapon" in ServerScriptService.WeaponHandler may lack argument type checks.
**Evidence:** ServerScriptService.WeaponHandler — no typeof/tonumber patterns found
**Recommendation:** Validate argument types (typeof, tonumber, etc.) in OnServerEvent handlers.

#### [shooter-weapon-002] No rate limiting on damage remote
**Confidence:** heuristic | **Category:** security | **Remediation:** assisted
No rate limiting patterns detected for "ApplyDamage" handler.
**Evidence:** ServerScriptService.DamageHandler — no tick()/cooldown/debounce patterns
**Recommendation:** Add server-side rate limiting to prevent fire-rate exploitation.

#### [shooter-spawn-001] Spawn point clustering detected
**Confidence:** heuristic | **Category:** gameplay | **Remediation:** manual
Average spawn spread is 18.4 studs, below the 30-stud minimum.
**Evidence:** 8 SpawnLocations in Workspace.Spawns, avg distance: 18.4 studs
**Recommendation:** Spread spawn points to reduce spawn-kill risk.

#### [shooter-spawn-002] Team spawn imbalance
**Confidence:** medium | **Category:** gameplay | **Remediation:** manual
Team "Red" has 5 spawns, team "Blue" has 2 spawns.
**Evidence:** SpawnLocation TeamColor distribution
**Recommendation:** Balance spawn counts across teams.

### Playtester Results

#### shooter_weapon_equip — PASS
- StarterPack: 3 Tools found (Rifle, Pistol, Knife)
- All weapons have Handle parts
- Config values: Rifle (Damage=25, Ammo=30), Pistol (Damage=15, Ammo=12)

#### shooter_respawn_cycle — PASS
- CharacterAutoLoads: true
- RespawnTime: 5 seconds
- SpawnLocations: 8 (Red: 5, Blue: 2, Neutral: 1)
- CharacterAdded handlers: 3 found

---

*This is a sample report. Actual output depends on your project's structure and configuration.*
*All findings are heuristic — they flag review candidates, not definitive issues.*
9 changes: 3 additions & 6 deletions src/tools/building/lighting-preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,9 @@ const lightingResultSchema = z.object({

function filterTechnology(config: PresetConfig): PresetConfig {
if (config["lighting"]) {
const { Technology: _, ...safe } = config["lighting"];
return { ...config, lighting: safe };
const filtered = { ...config["lighting"] };
delete filtered["Technology"];
return { ...config, lighting: filtered };
}
return config;
}
Expand All @@ -180,17 +181,13 @@ registerTool({
const client = new StudioBridgeClient({ port: input.studio_port });

let config: PresetConfig;
let presetName: string;

if (input.preset && input.preset !== "custom" && PRESETS[input.preset]) {
config = filterTechnology({ ...PRESETS[input.preset] });
presetName = input.preset;
} else if (input.custom_config) {
config = filterTechnology({ ...input.custom_config } as PresetConfig);
presetName = input.preset ?? "custom";
} else {
config = {};
presetName = input.preset ?? "custom";
}

const result = await client.applyLighting(undefined, config);
Expand Down
75 changes: 74 additions & 1 deletion src/tools/playtester/playtester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,16 @@ const schema = z.object({
studio_port: z.number().int().positive().default(33796),
action: z.enum(["run_scenario", "list_scenarios", "get_result"]),
scenario: playtestScenarioSchema.optional(),
scenario_preset: z.enum(["spawn_flow", "shop_flow", "tutorial_flow", "mobile_ux"]).optional(),
scenario_preset: z
.enum([
"spawn_flow",
"shop_flow",
"tutorial_flow",
"mobile_ux",
"shooter_weapon_equip",
"shooter_respawn_cycle",
])
.optional(),
result_id: z.string().min(1).optional(),
});

Expand Down Expand Up @@ -179,6 +188,70 @@ function scenarioPresets(): Record<string, PlaytestScenario> {
],
timeout_seconds: 45,
},
shooter_weapon_equip: {
name: "shooter_weapon_equip",
description: "Verify weapon tools exist with proper configuration for a shooter game.",
steps: [
{
type: "execute_code",
description: "Check StarterPack for Tool instances",
code: "local starterPack = game:FindFirstChild('StarterPack'); local names = {}; local count = 0; if starterPack then for _, child in ipairs(starterPack:GetChildren()) do if child:IsA('Tool') then count += 1; table.insert(names, child.Name) end end end return { ok = count > 0, count = count, names = names }",
},
{
type: "execute_code",
description: "Verify weapons have Handle parts",
code: "local starterPack = game:FindFirstChild('StarterPack'); local missing = {}; if starterPack then for _, child in ipairs(starterPack:GetChildren()) do if child:IsA('Tool') and child:FindFirstChild('Handle') == nil then table.insert(missing, child.Name) end end end return { ok = #missing == 0, missing_handles = missing }",
},
{
type: "execute_code",
description: "Check for weapon config values",
code: "local starterPack = game:FindFirstChild('StarterPack'); local weaponConfigs = {}; if starterPack then for _, child in ipairs(starterPack:GetChildren()) do if child:IsA('Tool') then local config = { name = child.Name, Damage = false, Ammo = false, FireRate = false }; for _, descendant in ipairs(child:GetDescendants()) do if descendant:IsA('NumberValue') or descendant:IsA('IntValue') then local lowered = string.lower(descendant.Name); if lowered == 'damage' then config.Damage = true end; if lowered == 'ammo' then config.Ammo = true end; if lowered == 'firerate' or lowered == 'fire_rate' then config.FireRate = true end end end; if config.Damage or config.Ammo or config.FireRate then table.insert(weaponConfigs, config) end end end end return { ok = #weaponConfigs > 0, weapon_configs = weaponConfigs }",
},
{
type: "verify_state",
description: "Confirm at least one configured weapon exists",
expected: '"ok":true',
},
{
type: "note",
description: "Record weapon equip note",
note: "Weapon equip flow verified",
},
],
timeout_seconds: 30,
},
shooter_respawn_cycle: {
name: "shooter_respawn_cycle",
description: "Validate respawn infrastructure for a shooter game.",
steps: [
{
type: "execute_code",
description: "Check Players.CharacterAutoLoads and RespawnTime",
code: "local players = game:GetService('Players'); return { auto_loads = players.CharacterAutoLoads, respawn_time = players.RespawnTime }",
},
{
type: "execute_code",
description: "Check SpawnLocation count and team assignment",
code: "local count = 0; local teams = {}; for _, descendant in ipairs(game:GetDescendants()) do if descendant:IsA('SpawnLocation') then count += 1; local key = descendant.Neutral and 'Neutral' or tostring(descendant.TeamColor); teams[key] = (teams[key] or 0) + 1 end end return { ok = count > 0, count = count, teams = teams }",
},
{
type: "execute_code",
description: "Search for CharacterAdded handlers in scripts",
code: "local handlers = 0; for _, descendant in ipairs(game:GetDescendants()) do if descendant:IsA('LuaSourceContainer') and string.find(string.lower(descendant.Source), 'characteradded') then handlers += 1 end end return { ok = handlers > 0, handlers_found = handlers }",
},
{
type: "verify_state",
description: "Confirm spawn infrastructure exists",
expected: '"ok":true',
},
{
type: "note",
description: "Record respawn cycle note",
note: "Respawn cycle infrastructure verified",
},
],
timeout_seconds: 30,
},
};
}

Expand Down
1 change: 1 addition & 0 deletions src/tools/register-all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ import "./building/terrain-generate.js";
import "./building/ui-builder.js";
import "./shipcheck/validate-mobile-ui.js";
import "./shipcheck/shipcheck-report.js";
import "./shipcheck/genre/shooter/index.js";
Loading
Loading