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
39 changes: 39 additions & 0 deletions src/release-package-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,21 @@ export interface ReleasePackageDialogOptions {
}

interface StoredRelease {
target: ReleaseTarget;
metadata: ArenaMetadata;
readme: string;
license: string;
attribution: string;
includeSource: boolean;
}

export type ReleaseTarget = 'map' | 'game';

let releaseDialogRequest = 0;

function defaults(mapName: string): StoredRelease {
return {
target: 'map',
metadata: {
title: mapName, gameTypes: ['ffa'], botSupport: true,
recommendedPlayers: '', author: '', description: '',
Expand All @@ -49,6 +53,12 @@ function defaults(mapName: string): StoredRelease {
};
}

export function gameReleaseBuildIssue(target: ReleaseTarget, build: BuildRecord | null): string | null {
if (target !== 'game' || !build) return null;
if (!build.aas) return 'Maker game levels require a successful AAS bot-navigation stage. Recompile with Generate AAS enabled.';
return null;
}

function readStored(editor: Editor, mapName: string): StoredRelease {
const source = editor.worldspawn.properties[RELEASE_METADATA_KEY];
if (!source) return defaults(mapName);
Expand Down Expand Up @@ -253,6 +263,17 @@ export async function openReleasePackageDialog(options: ReleasePackageDialogOpti

const filesSection = document.createElement('section');
filesSection.innerHTML = '<h3>Release files</h3>';
const targetInput = document.createElement('select');
for (const [value, label] of [
['map', 'Quake 3 map package'],
['game', 'Maker game level (BSP + AAS + source)'],
] as const) {
const option = document.createElement('option');
option.value = value;
option.textContent = label;
targetInput.appendChild(option);
}
targetInput.value = stored.target;
const readmeInput = textarea(stored.readme);
const licenseInput = textarea(stored.license);
const attributionInput = textarea(stored.attribution);
Expand All @@ -262,6 +283,7 @@ export async function openReleasePackageDialog(options: ReleasePackageDialogOpti
const fileGrid = document.createElement('div');
fileGrid.className = 'release-fields';
fileGrid.append(
field('Export target', targetInput),
field('README', readmeInput),
field('License', licenseInput),
field('Attribution', attributionInput),
Expand Down Expand Up @@ -298,7 +320,22 @@ export async function openReleasePackageDialog(options: ReleasePackageDialogOpti
);
body.append(metadataSection, levelshotSection, filesSection, auditSection, packageSection);

const applyTargetDefaults = () => {
const gameTarget = targetInput.value === 'game';
if (gameTarget) {
includeSource.checked = true;
botInput.checked = true;
}
includeSource.disabled = gameTarget;
const issue = gameReleaseBuildIssue(targetInput.value as ReleaseTarget, build);
packageStatus.textContent = issue ?? (build ? 'Ready to validate and build.' : 'A compiled BSP is required.');
packageStatus.className = issue ? 'error' : '';
};
targetInput.onchange = applyTargetDefaults;
applyTargetDefaults();

const collectStored = (): StoredRelease => ({
target: targetInput.value as ReleaseTarget,
metadata: {
title: titleInput.value.trim(),
gameTypes: gameTypesInput.value.split(/[\s,]+/).filter(Boolean),
Expand All @@ -323,6 +360,8 @@ export async function openReleasePackageDialog(options: ReleasePackageDialogOpti
if (!build?.bsp) return null;
saveMetadata();
try {
const gameBuildIssue = gameReleaseBuildIssue(targetInput.value as ReleaseTarget, build);
if (gameBuildIssue) throw new Error(gameBuildIssue);
const activeTextureManager = editor.textureManager ?? textureManager;
const packageResult = buildReleasePackage({
mapName,
Expand Down
9 changes: 8 additions & 1 deletion tests/release-package-dialog.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { buildSourceFingerprint, type BuildRecord } from '../src/build-history';
import { selectReleaseBuild } from '../src/release-package-dialog';
import { gameReleaseBuildIssue, selectReleaseBuild } from '../src/release-package-dialog';

function build(documentRevision: number, source?: string, region = false): BuildRecord {
return {
Expand Down Expand Up @@ -36,4 +36,11 @@ describe('release package build selection', () => {
expect(selectReleaseBuild([build(2, source)], 3, buildSourceFingerprint(`${source}// changed`))).toBeNull();
expect(selectReleaseBuild([build(3, source, true)], 3, buildSourceFingerprint(source))).toBeNull();
});

it('requires AAS for Maker game-level exports', () => {
const withoutAas = build(2);
expect(gameReleaseBuildIssue('map', withoutAas)).toBeNull();
expect(gameReleaseBuildIssue('game', withoutAas)).toMatch(/require.*AAS/i);
expect(gameReleaseBuildIssue('game', { ...withoutAas, aas: new Uint8Array([1]) })).toBeNull();
});
});
4 changes: 3 additions & 1 deletion tests/release-package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,16 @@ describe('release packaging', () => {
mapName: 'test_map', bsp: strToU8('bsp'), aas: strToU8('aas'), entities: [world],
assets, textures: textureAdapter(assets),
metadata: { title: 'Test Arena', gameTypes: ['ffa', 'team'], botSupport: true, recommendedPlayers: '2-4', author: 'Mapper', description: 'Test' },
levelshot: strToU8('png'), files: { readme: 'read me' },
levelshot: strToU8('png'), files: { readme: 'read me' }, includeSourceMap: '{\n"classname" "worldspawn"\n}\n',
};
const first = buildReleasePackage(input);
const second = buildReleasePackage(input);
expect(first.pk3).toEqual(second.pk3);
const files = unzipSync(first.pk3);
expect(Object.keys(files)).toEqual([...Object.keys(files)].sort((a, b) => a.localeCompare(b)));
expect(files['maps/test_map.bsp']).toBeTruthy();
expect(files['maps/test_map.aas']).toBeTruthy();
expect(files['maps/test_map.map']).toBeTruthy();
expect(files['scripts/test_map.arena']).toBeTruthy();
expect(files['textures/custom/wall.tga']).toBeTruthy();
expect(first.report.archiveValidation.valid).toBe(true);
Expand Down
Loading