Move brand images to proprietary/ and support multi-dir asset pipeline (#3662)

## Description:

* Move proprietary brand images (logos, favicon) from resources/images/
to proprietary/images/ to separate open-source assets from proprietary
ones
* Extend the asset pipeline (PublicAssetManifest, vite.config.ts) to
support multiple source directories (resources/ + proprietary/), so
buildAssetUrl resolves assets from either location transparently
* In dev, serve proprietary/ as a fallback middleware (registered after
Vite's publicDir handler) so resources/ takes precedence when files
exist in both. The idea is we could have placeholder assets placeholders
that can be used by forks, and only the production build uses
proprietary assets.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

evan
This commit is contained in:
Evan
2026-04-16 08:42:11 -07:00
committed by GitHub
parent 31baeacdf4
commit 1ebac8e854
13 changed files with 100 additions and 62 deletions
+4 -6
View File
@@ -1,7 +1,5 @@
import { Howl } from "howler";
import of4 from "../../../proprietary/sounds/music/of4.mp3";
import openfront from "../../../proprietary/sounds/music/openfront.mp3";
import war from "../../../proprietary/sounds/music/war.mp3";
import { assetUrl } from "../../core/AssetUrls";
import { EventBus } from "../../core/EventBus";
import { UserSettings } from "../../core/game/UserSettings";
import {
@@ -33,19 +31,19 @@ export class SoundManager {
this.safely("initialize background music", () => {
this.backgroundMusic = [
new Howl({
src: [of4],
src: [assetUrl("sounds/music/of4.mp3")],
loop: false,
onend: this.playNext.bind(this),
volume: 0,
}),
new Howl({
src: [openfront],
src: [assetUrl("sounds/music/openfront.mp3")],
loop: false,
onend: this.playNext.bind(this),
volume: 0,
}),
new Howl({
src: [war],
src: [assetUrl("sounds/music/war.mp3")],
loop: false,
onend: this.playNext.bind(this),
volume: 0,
+44 -18
View File
@@ -233,20 +233,44 @@ export function getResourcesDir(rootDir: string = process.cwd()): string {
return path.join(rootDir, "resources");
}
export function getProprietaryDir(rootDir: string = process.cwd()): string {
return path.join(rootDir, "proprietary");
}
// Scans directories with synchronous fs.existsSync — assumes a small number of sourceDirs.
function resolveSourceDir(relativePath: string, sourceDirs: string[]): string {
for (const dir of sourceDirs) {
const candidate = path.join(dir, relativePath);
if (fs.existsSync(candidate)) {
return dir;
}
}
throw new Error(
`Asset ${relativePath} not found in any source directory: ${sourceDirs.join(", ")}`,
);
}
function resolveSourceFile(relativePath: string, sourceDirs: string[]): string {
return path.join(resolveSourceDir(relativePath, sourceDirs), relativePath);
}
export function shouldKeepRootPublicFile(relativePath: string): boolean {
return ROOT_PUBLIC_FILES.has(normalizeAssetPath(relativePath));
}
export function listHashedPublicAssetPaths(resourcesDir: string): string[] {
export function listHashedPublicAssetPaths(sourceDirs: string[]): string[] {
const files = new Set<string>();
for (const pattern of HASHED_PUBLIC_ASSET_GLOBS) {
for (const file of globSync(pattern, {
cwd: resourcesDir,
nodir: true,
dot: false,
posix: true,
})) {
files.add(normalizeAssetPath(file));
for (const dir of sourceDirs) {
if (!fs.existsSync(dir)) continue;
for (const pattern of HASHED_PUBLIC_ASSET_GLOBS) {
for (const file of globSync(pattern, {
cwd: dir,
nodir: true,
dot: false,
posix: true,
})) {
files.add(normalizeAssetPath(file));
}
}
}
return [...files].sort();
@@ -264,13 +288,14 @@ export function listRootPublicFiles(resourcesDir: string): string[] {
.sort();
}
export function buildPublicAssetManifest(resourcesDir: string): AssetManifest {
const cached = manifestCache.get(resourcesDir);
export function buildPublicAssetManifest(sourceDirs: string[]): AssetManifest {
const cacheKey = sourceDirs.join("\0");
const cached = manifestCache.get(cacheKey);
if (cached) {
return cached;
}
const hashedPublicAssetPaths = listHashedPublicAssetPaths(resourcesDir);
const hashedPublicAssetPaths = listHashedPublicAssetPaths(sourceDirs);
const rawAssetPaths = hashedPublicAssetPaths.filter(
(relativePath) => !isDerivedPublicAsset(relativePath),
);
@@ -280,14 +305,14 @@ export function buildPublicAssetManifest(resourcesDir: string): AssetManifest {
const manifest: AssetManifest = {};
for (const relativePath of rawAssetPaths) {
const absolutePath = path.join(resourcesDir, relativePath);
const absolutePath = resolveSourceFile(relativePath, sourceDirs);
const hash = createContentHash(absolutePath);
manifest[relativePath] = createHashedAssetUrl(relativePath, hash);
}
for (const relativePath of derivedAssetPaths) {
const renderedAsset = renderDerivedPublicAsset(
resourcesDir,
resolveSourceDir(relativePath, sourceDirs),
relativePath,
manifest,
);
@@ -301,7 +326,7 @@ export function buildPublicAssetManifest(resourcesDir: string): AssetManifest {
);
}
manifestCache.set(resourcesDir, manifest);
manifestCache.set(cacheKey, manifest);
return manifest;
}
@@ -310,17 +335,18 @@ export function clearPublicAssetManifestCache(): void {
}
export function createHashedPublicAssetFiles(
resourcesDir: string,
sourceDirs: string[],
outDir: string,
assetManifest: AssetManifest,
): void {
for (const [relativePath, hashedUrl] of Object.entries(assetManifest)) {
const sourcePath = path.join(resourcesDir, relativePath);
const sourceDir = resolveSourceDir(relativePath, sourceDirs);
const sourcePath = path.join(sourceDir, relativePath);
const outputPath = path.join(outDir, normalizeAssetPath(hashedUrl));
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
const renderedAsset = renderDerivedPublicAsset(
resourcesDir,
sourceDir,
relativePath,
assetManifest,
);