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
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
dist_electron
src/routes.ts
node_modules
# Agent worktrees hold whole copies of this repo, which the unscoped check would walk into.
.claude
typed-router.d.ts
vendor
dist
Expand Down
20 changes: 7 additions & 13 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginVue from "eslint-plugin-vue";

import { contentLayerRules } from "./eslint/content-layer.mjs";
import { restrictedSyntaxRules } from "./eslint/restricted-syntax.mjs";

export default [
{ files: ["**/*.{js,mjs,cjs,ts,vue}"] },
{
Expand All @@ -13,6 +16,8 @@ export default [
"**/out",
"**/dist_electron",
"**/node_modules",
// Agent worktrees hold whole copies of this repo, which the unscoped lint would walk into.
"**/.claude",
"**/working-files",
"**/typed-router.d.ts",
"**/vendor",
Expand Down Expand Up @@ -54,17 +59,6 @@ export default [
"@typescript-eslint/no-unsafe-function-type": "warn",
},
},
{
files: ["src/renderer/**/*.{ts,vue}"],
ignores: ["src/renderer/api/shell.ts"],
rules: {
"no-restricted-syntax": [
"error",
{
selector: "MemberExpression[object.name='window'][property.name='shell']",
message: "Use shellApi from @renderer/api/shell so failures get reported instead of reaching the fatal error modal.",
},
],
},
},
...contentLayerRules,
...restrictedSyntaxRules,
];
45 changes: 45 additions & 0 deletions eslint/content-layer.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: 2026 The BAR Lobby Authors
//
// SPDX-License-Identifier: MIT

// Lint rules that exist because of how the content layer is put together, rather than for style. Each one
// stands in for a boundary the language cannot express, so the reason it exists lives next to it.

// TypeScript cannot scope an export to a directory, so the content providers stay exported for
// content-api.ts to import and this is what keeps anything else from reaching them.
const noContentProviderImports = [
"error",
{
patterns: [
{
group: ["**/content/engine/engine-provider", "**/content/game/game-provider", "**/content/maps/map-provider"],
message: "Import contentAPI from @main/content/content-api instead of reaching a content provider directly.",
},
],
},
];

// content-state.ts is where the predicates are written, and content-api.ts is what assigns a status in
// the first place.
export const contentStatusScope = {
files: ["src/**/*.{ts,vue}"],
ignores: ["src/main/content/content-state.ts", "src/main/content/content-api.ts"],
};

// The predicates in content-state.ts answer different questions about a content status, and consumers
// reading the status themselves is how they ended up disagreeing about what counts as active.
// "failed" is left out of the pattern because tachyon responses and setup stages use that word too.
export const noContentStatusComparisons = {
selector: "BinaryExpression[operator=/^[!=]==$/] > Literal[value=/^(queued|acquiring|removing)$/]",
message: "Use isUnsettled or isInProgress from @main/content/content-state rather than comparing status.",
};

export const contentLayerRules = [
{
files: ["src/**/*.{ts,vue}"],
ignores: ["src/main/content/**"],
rules: {
"no-restricted-imports": noContentProviderImports,
},
},
];
20 changes: 20 additions & 0 deletions eslint/restricted-syntax.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: 2026 The BAR Lobby Authors
//
// SPDX-License-Identifier: MIT

import { contentStatusScope, noContentStatusComparisons } from "./content-layer.mjs";
import { noDirectShellCalls, shellApiScope } from "./shell-api.mjs";

// eslint replaces no-restricted-syntax rather than merging it, so two blocks matching one file means
// the later one silently drops the other's selectors. Every selector is assembled in this one place so
// that cannot happen, and each block repeats the selectors of every block it overlaps.
export const restrictedSyntaxRules = [
{
...contentStatusScope,
rules: { "no-restricted-syntax": ["error", noContentStatusComparisons] },
},
{
...shellApiScope,
rules: { "no-restricted-syntax": ["error", noDirectShellCalls, noContentStatusComparisons] },
},
];
15 changes: 15 additions & 0 deletions eslint/shell-api.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: 2026 The BAR Lobby Authors
//
// SPDX-License-Identifier: MIT

// window.shell throws straight past the renderer into the fatal error modal, so the renderer goes
// through a wrapper that reports the failure instead. shell.ts is the wrapper.
export const shellApiScope = {
files: ["src/renderer/**/*.{ts,vue}"],
ignores: ["src/renderer/api/shell.ts"],
};

export const noDirectShellCalls = {
selector: "MemberExpression[object.name='window'][property.name='shell']",
message: "Use shellApi from @renderer/api/shell so failures get reported instead of reaching the fatal error modal.",
};
7 changes: 4 additions & 3 deletions lang/dev/lobby.json
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,6 @@
},
"downloads": {
"noDownloads": "No dlodnwoas avctie",
"moreDownloads": "and {count} more...",
"starting": "Sattnrig...",
"extracting": "Erixtanctg...",
"progressMB": "{current} / {total} MB ({percent}%)",
Expand All @@ -261,7 +260,8 @@
"speedBps": "{speed} B/s",
"etaHoursMinutes": "{hours}h {minutes}m left",
"etaMinutesSeconds": "{minutes}m {seconds}s left",
"etaSeconds": "{seconds}s left"
"etaSeconds": "{seconds}s left",
"queued": "Quueed"
},
"serverStatus": {
"playersOnline": "Plyreas Oinnle",
Expand Down Expand Up @@ -442,7 +442,8 @@
"playOffline": "Paly Oniflfe",
"stepFailed": "{step} — Failed",
"stepRetrying": "{step} — Rtyneirg…",
"contentRequired": "Egnnie and gmae must be dodolnwaed borefe stnartig a btalte."
"contentRequired": "Egnnie and gmae must be dodolnwaed borefe stnartig a btalte.",
"pathUnusable": "That fdelor caonnt be used for gmae fleis. Pick a dnieferft one."
}
},
"battle": {
Expand Down
8 changes: 4 additions & 4 deletions lang/en/lobby.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@
"customLobbies": "Custom Lobbies",
"tournaments": "Tournaments"
},

"party": {
"title": "Party",
"partyTab": "Active Party",
Expand Down Expand Up @@ -253,7 +252,6 @@
},
"downloads": {
"noDownloads": "No downloads active",
"moreDownloads": "and {count} more...",
"starting": "Starting...",
"extracting": "Extracting...",
"progressMB": "{current} / {total} MB ({percent}%)",
Expand All @@ -262,7 +260,8 @@
"speedBps": "{speed} B/s",
"etaHoursMinutes": "{hours}h {minutes}m left",
"etaMinutesSeconds": "{minutes}m {seconds}s left",
"etaSeconds": "{seconds}s left"
"etaSeconds": "{seconds}s left",
"queued": "Queued"
},
"serverStatus": {
"playersOnline": "Players Online",
Expand Down Expand Up @@ -443,7 +442,8 @@
"playOffline": "Play Offline",
"stepFailed": "{step} — Failed",
"stepRetrying": "{step} — Retrying…",
"contentRequired": "Engine and game must be downloaded before starting a battle."
"contentRequired": "Engine and game must be downloaded before starting a battle.",
"pathUnusable": "That folder cannot be used for game files. Pick a different one."
}
},
"battle": {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "bar-lobby",
"type": "module",
"version": "0.16.1",
"version": "0.16.2",
"private": true,
"description": "Lobby client for the RTS game Beyond All Reason",
"author": "The BAR Lobby Authors",
Expand Down
17 changes: 17 additions & 0 deletions src/main/config/content-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: 2026 The BAR Lobby Authors
//
// SPDX-License-Identifier: MIT

// Held at one until an engine ships with the pr-downloader fix for concurrent invocations corrupting
// rapid's repo index. #423 asks for 2-4, which is what this goes back to once that lands.
// Counts invocations, not connections: a game is one slot and fans out inside prd.
export const MAX_CONCURRENT_DOWNLOADS = 1;

// How long content nothing is holding on to is kept before a sweep will remove it. Deliberately not a
// user setting: nobody can reason about the right number, and getting it wrong deletes their content.
export const CONTENT_RETENTION_DAYS = 90;

// Refuse to start acquiring with less than this free on the assets volume. A floor rather than a real
// size estimate, because rapid does not tell us how big a game is until it is fetching it; this is
// aimed at the disk that is already full, which is the case that actually fails.
export const MIN_FREE_BYTES_TO_ACQUIRE = 2 * 1024 * 1024 * 1024;
3 changes: 3 additions & 0 deletions src/main/content/auto-updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export class AutoUpdaterAPI extends Downloader {
autoUpdater.on("download-progress", (progressInfo) => {
this.downloadProgress({
type: "update",
id: this.updateInfo?.version ?? "unknown",
name: this.updateInfo?.version ?? "unknown",
currentBytes: progressInfo.transferred,
totalBytes: progressInfo.total,
Expand All @@ -77,6 +78,7 @@ export class AutoUpdaterAPI extends Downloader {
// 'info' does not have download details, so we just falsify the values since it's complete anyway.
this.downloadComplete({
type: "update",
id: this.updateInfo?.version ?? "unknown",
name: this.updateInfo?.version ?? "unknown",
currentBytes: 1,
totalBytes: 1,
Expand All @@ -90,6 +92,7 @@ export class AutoUpdaterAPI extends Downloader {
// As above, 'info' does not have download details, so we just falsify the values since it's stopped already
this.downloadFailed({
type: "update",
id: this.updateInfo?.version ?? "unknown",
name: this.updateInfo?.version ?? "unknown",
currentBytes: 0,
totalBytes: 0,
Expand Down
Loading
Loading