diff --git a/scripts/convert-system-font.mjs b/scripts/convert-system-font.mjs index 22daded..d454dc9 100644 --- a/scripts/convert-system-font.mjs +++ b/scripts/convert-system-font.mjs @@ -148,6 +148,8 @@ function generateTypeScript(bitmaps) { lines.push(' * This font data is in the public domain.'); lines.push(' */'); lines.push(''); + lines.push('// #region Bitmap Data'); + lines.push(''); lines.push('// prettier-ignore'); lines.push('export const SYSTEM_FONT_BITMAPS: readonly number[] = ['); @@ -163,7 +165,12 @@ function generateTypeScript(bitmaps) { lines.push('];'); lines.push(''); + lines.push('// #endregion'); + lines.push(''); + lines.push('// #region Constants'); + lines.push(''); lines.push('/** First character code in the bitmap array. */'); + lines.push('export const SYSTEM_FONT_FIRST_CHAR = 32;'); lines.push(''); lines.push('/** Last character code in the bitmap array (inclusive). */'); @@ -181,6 +188,7 @@ function generateTypeScript(bitmaps) { lines.push('/** Number of bytes per glyph (one byte per row). */'); lines.push('export const SYSTEM_FONT_BYTES_PER_GLYPH = 14;'); lines.push(''); + lines.push('// #endregion'); return lines.join('\n'); } diff --git a/src/assets/SpriteSheet.test.ts b/src/assets/SpriteSheet.test.ts index b5e3ca5..2bb952d 100644 --- a/src/assets/SpriteSheet.test.ts +++ b/src/assets/SpriteSheet.test.ts @@ -386,6 +386,15 @@ describe('SpriteSheet', () => { expect(() => sheet.getImage()).toThrow('not available for sheets created from raw indexed data'); }); + it('throws RangeError when pixel array length mismatches dimensions', () => { + const pixels = new Uint8Array(10) as Uint8Array; + + expect(() => SpriteSheet.fromIndexedPixels(4, 4, pixels)).toThrow(RangeError); + expect(() => SpriteSheet.fromIndexedPixels(4, 4, pixels)).toThrow( + 'indexedPixels length 10 does not match 4x4 (expected 16)', + ); + }); + it('creates r8uint GPU texture via getTexture', () => { const device = createMockGPUDevice(); const pixels = new Uint8Array(4 * 4) as Uint8Array; diff --git a/src/assets/SpriteSheet.ts b/src/assets/SpriteSheet.ts index 0e54e4d..ce94eaa 100644 --- a/src/assets/SpriteSheet.ts +++ b/src/assets/SpriteSheet.ts @@ -115,6 +115,14 @@ export class SpriteSheet { * @returns Sprite sheet ready for rendering. */ static fromIndexedPixels(width: number, height: number, indexedPixels: Uint8Array): SpriteSheet { + const expectedLength = width * height; + + if (indexedPixels.length !== expectedLength) { + throw new RangeError( + `[SpriteSheet] indexedPixels length ${indexedPixels.length} does not match ${width}x${height} (expected ${expectedLength}).`, + ); + } + const sheet = new SpriteSheet(null, new Vector2i(width, height)); sheet.indexedPixels = indexedPixels; diff --git a/src/assets/SystemFont.test.ts b/src/assets/SystemFont.test.ts index 4bdda96..0f746e1 100644 --- a/src/assets/SystemFont.test.ts +++ b/src/assets/SystemFont.test.ts @@ -12,6 +12,8 @@ import { describe, expect, it } from 'vitest'; import { + SYSTEM_FONT_BITMAPS, + SYSTEM_FONT_BYTES_PER_GLYPH, SYSTEM_FONT_FIRST_CHAR, SYSTEM_FONT_GLYPH_COUNT, SYSTEM_FONT_GLYPH_HEIGHT, @@ -31,6 +33,10 @@ describe('createSystemFont', () => { expect(font.name).toBe('System'); }); + it('bitmap data has correct total length', () => { + expect(SYSTEM_FONT_BITMAPS.length).toBe(SYSTEM_FONT_GLYPH_COUNT * SYSTEM_FONT_BYTES_PER_GLYPH); + }); + it('has correct metadata', () => { const font = createSystemFont(); diff --git a/src/assets/SystemFont.ts b/src/assets/SystemFont.ts index 8baf09a..a975040 100644 --- a/src/assets/SystemFont.ts +++ b/src/assets/SystemFont.ts @@ -57,6 +57,14 @@ const ATLAS_HEIGHT = ATLAS_ROWS * SYSTEM_FONT_GLYPH_HEIGHT; * @returns Uint8Array of `ATLAS_WIDTH * ATLAS_HEIGHT` palette indices. */ function buildAtlasPixels(): Uint8Array { + const expectedLength = SYSTEM_FONT_GLYPH_COUNT * SYSTEM_FONT_BYTES_PER_GLYPH; + + if (SYSTEM_FONT_BITMAPS.length < expectedLength) { + throw new Error( + `[SystemFont] SYSTEM_FONT_BITMAPS has ${SYSTEM_FONT_BITMAPS.length} entries, expected at least ${expectedLength}.`, + ); + } + const pixels = new Uint8Array(ATLAS_WIDTH * ATLAS_HEIGHT) as Uint8Array; for (let i = 0; i < SYSTEM_FONT_GLYPH_COUNT; i++) { @@ -67,7 +75,9 @@ function buildAtlasPixels(): Uint8Array { const bitmapOffset = i * SYSTEM_FONT_BYTES_PER_GLYPH; for (let y = 0; y < SYSTEM_FONT_GLYPH_HEIGHT; y++) { - const rowByte = SYSTEM_FONT_BITMAPS[bitmapOffset + y] ?? 0; + // Safe: length validated above guarantees bitmapOffset + y is in bounds. + + const rowByte = SYSTEM_FONT_BITMAPS[bitmapOffset + y] as number; for (let x = 0; x < SYSTEM_FONT_GLYPH_WIDTH; x++) { // Bit 7 is leftmost pixel. diff --git a/src/assets/fonts/systemFontData.ts b/src/assets/fonts/systemFontData.ts index 4e24c95..422b697 100644 --- a/src/assets/fonts/systemFontData.ts +++ b/src/assets/fonts/systemFontData.ts @@ -18,6 +18,8 @@ * This font data is in the public domain. */ +// #region Bitmap Data + // prettier-ignore export const SYSTEM_FONT_BITMAPS: readonly number[] = [ // Space (32) @@ -212,6 +214,10 @@ export const SYSTEM_FONT_BITMAPS: readonly number[] = [ 0x00, 0x00, 0x40, 0xa8, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; +// #endregion + +// #region Constants + /** First character code in the bitmap array. */ export const SYSTEM_FONT_FIRST_CHAR = 32; @@ -229,3 +235,5 @@ export const SYSTEM_FONT_GLYPH_HEIGHT = 14; /** Number of bytes per glyph (one byte per row). */ export const SYSTEM_FONT_BYTES_PER_GLYPH = 14; + +// #endregion diff --git a/src/core/BTAPI.ts b/src/core/BTAPI.ts index 2fdcf6c..56eed0d 100644 --- a/src/core/BTAPI.ts +++ b/src/core/BTAPI.ts @@ -433,12 +433,15 @@ export class BTAPI { public drawSystemText(pos: Vector2i, paletteIndex: number, text: string): void { this.assertPaletteIndex(paletteIndex); + // Palette index 0 is transparent -- nothing to draw. + if (paletteIndex === 0) { + return; + } + if (this.systemFont) { // Offset math: font stores foreground as index 1. // Shader computes 1 + (paletteIndex - 1) = paletteIndex. - const offset = paletteIndex > 0 ? paletteIndex - 1 : 0; - - this.renderer?.drawBitmapText(this.systemFont, pos, text, offset); + this.renderer?.drawBitmapText(this.systemFont, pos, text, paletteIndex - 1); } }