Refactor menu rendering and asset registry structure
- Updated SoftwareRenderer to incorporate MenuHeaderBand for handling spear variant menus and improved backdrop drawing. - Refactored asset registry imports to organize menu-related assets under a dedicated menu structure. - Enhanced game session snapshot tests to validate menu theme restoration for spear variant games. - Added tests for classic menu presentation module to ensure palette consistency with canonical constants. - Implemented tests for spear asset registry to verify correct menu VGA index resolutions. - Created unit tests for MenuHeaderBand to validate functionality in rendering menu headers and sidebars. - Adjusted HUD module imports to align with new menu structure. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/built_in/menu/spear/spear_asset_registry.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_input.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_menu.dart';
|
||||
|
||||
void main() {
|
||||
test(
|
||||
@@ -122,6 +124,104 @@ void main() {
|
||||
expect(engine.entities.last, isA<SmallAmmoCollectible>());
|
||||
},
|
||||
);
|
||||
|
||||
test('restoreSaveState applies menu theme from restored active game', () {
|
||||
final engine = _buildEngineWithTwoGames();
|
||||
engine.init();
|
||||
|
||||
final GameSessionSnapshot snapshot = engine.captureSaveState();
|
||||
final GameSessionSnapshot restoredSnapshot = GameSessionSnapshot(
|
||||
currentGameIndex: 1,
|
||||
currentEpisodeIndex: snapshot.currentEpisodeIndex,
|
||||
currentLevelIndex: snapshot.currentLevelIndex,
|
||||
returnLevelIndex: snapshot.returnLevelIndex,
|
||||
difficulty: snapshot.difficulty,
|
||||
timeAliveMs: snapshot.timeAliveMs,
|
||||
lastAcousticAlertTime: snapshot.lastAcousticAlertTime,
|
||||
isMapOverlayVisible: snapshot.isMapOverlayVisible,
|
||||
isMenuOverlayVisible: snapshot.isMenuOverlayVisible,
|
||||
player: snapshot.player,
|
||||
currentLevel: snapshot.currentLevel,
|
||||
areaGrid: snapshot.areaGrid,
|
||||
areasByPlayer: snapshot.areasByPlayer,
|
||||
entities: snapshot.entities,
|
||||
doors: snapshot.doors,
|
||||
pushwalls: snapshot.pushwalls,
|
||||
);
|
||||
|
||||
engine.restoreSaveState(restoredSnapshot);
|
||||
|
||||
final presentation = WolfMenuPresentation(engine.data);
|
||||
final bool isSpear =
|
||||
engine.data.version == GameVersion.spearOfDestiny ||
|
||||
engine.data.version == GameVersion.spearOfDestinyDemo;
|
||||
|
||||
final int expectedBackground = isSpear
|
||||
? _rgb24FromVgaIndex(
|
||||
_resolvedMenuColorIndex(
|
||||
presentation.backgroundIndex,
|
||||
engine.data.version,
|
||||
),
|
||||
)
|
||||
: _paletteMappedRgb24(0x890000);
|
||||
|
||||
final int expectedPanel = isSpear
|
||||
? 0x000359
|
||||
: _paletteMappedRgb24(0x590002);
|
||||
expect(engine.currentGameIndex, 1);
|
||||
expect(engine.menuBackgroundRgb, expectedBackground);
|
||||
expect(engine.menuPanelRgb, expectedPanel);
|
||||
expect(engine.menuManager.menuBackgroundRgb, expectedBackground);
|
||||
});
|
||||
}
|
||||
|
||||
int _rgb24FromVgaIndex(int paletteIndex) {
|
||||
final int argb = ColorPalette.argbFromVgaIndex(paletteIndex);
|
||||
final int r = (argb >> 16) & 0xFF;
|
||||
final int g = (argb >> 8) & 0xFF;
|
||||
final int b = argb & 0xFF;
|
||||
return (r << 16) | (g << 8) | b;
|
||||
}
|
||||
|
||||
int _resolvedMenuColorIndex(int paletteIndex, GameVersion version) {
|
||||
final bool isSpear =
|
||||
version == GameVersion.spearOfDestiny ||
|
||||
version == GameVersion.spearOfDestinyDemo;
|
||||
if (!isSpear && paletteIndex >= 0x20 && paletteIndex <= 0x2F) {
|
||||
return paletteIndex + 0x70;
|
||||
}
|
||||
return paletteIndex;
|
||||
}
|
||||
|
||||
int _paletteMappedRgb24(int rgb) {
|
||||
final int index = _closestVgaIndexForRgb24(rgb);
|
||||
return _rgb24FromVgaIndex(index);
|
||||
}
|
||||
|
||||
int _closestVgaIndexForRgb24(int rgb24) {
|
||||
final int targetR = (rgb24 >> 16) & 0xFF;
|
||||
final int targetG = (rgb24 >> 8) & 0xFF;
|
||||
final int targetB = rgb24 & 0xFF;
|
||||
|
||||
int bestIndex = 0;
|
||||
int bestDistance = 0x7FFFFFFF;
|
||||
|
||||
for (int index = 0; index < 256; index++) {
|
||||
final int argb = ColorPalette.argbFromVgaIndex(index);
|
||||
final int r = (argb >> 16) & 0xFF;
|
||||
final int g = (argb >> 8) & 0xFF;
|
||||
final int b = argb & 0xFF;
|
||||
final int dr = targetR - r;
|
||||
final int dg = targetG - g;
|
||||
final int db = targetB - b;
|
||||
final int distance = (dr * dr) + (dg * dg) + (db * db);
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance;
|
||||
bestIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
return bestIndex;
|
||||
}
|
||||
|
||||
class _TestInput extends Wolf3dInput {
|
||||
@@ -162,6 +262,35 @@ class _SilentAudio implements EngineAudio {
|
||||
}
|
||||
|
||||
WolfEngine _buildEngine() {
|
||||
final data = _buildTestData(version: GameVersion.retail);
|
||||
|
||||
return WolfEngine(
|
||||
data: data,
|
||||
difficulty: Difficulty.medium,
|
||||
startingEpisode: 0,
|
||||
frameBuffer: FrameBuffer(64, 64),
|
||||
input: _TestInput(),
|
||||
onGameWon: () {},
|
||||
engineAudio: _SilentAudio(),
|
||||
);
|
||||
}
|
||||
|
||||
WolfEngine _buildEngineWithTwoGames() {
|
||||
final retail = _buildTestData(version: GameVersion.retail);
|
||||
final spear = _buildTestData(version: GameVersion.spearOfDestiny);
|
||||
|
||||
return WolfEngine(
|
||||
availableGames: <WolfensteinData>[retail, spear],
|
||||
difficulty: Difficulty.medium,
|
||||
startingEpisode: 0,
|
||||
frameBuffer: FrameBuffer(64, 64),
|
||||
input: _TestInput(),
|
||||
onGameWon: () {},
|
||||
engineAudio: _SilentAudio(),
|
||||
);
|
||||
}
|
||||
|
||||
WolfensteinData _buildTestData({required GameVersion version}) {
|
||||
final wallGrid = _buildGrid();
|
||||
final objectGrid = _buildGrid();
|
||||
_fillBoundaries(wallGrid, 2);
|
||||
@@ -171,43 +300,38 @@ WolfEngine _buildEngine() {
|
||||
wallGrid[2][3] = 90;
|
||||
wallGrid[4][4] = 5;
|
||||
|
||||
return WolfEngine(
|
||||
data: WolfensteinData(
|
||||
version: GameVersion.retail,
|
||||
dataVersion: DataVersion.unknown,
|
||||
registry: RetailAssetRegistry(),
|
||||
walls: [
|
||||
_solidSprite(1),
|
||||
_solidSprite(1),
|
||||
_solidSprite(2),
|
||||
_solidSprite(2),
|
||||
],
|
||||
sprites: List.generate(436, (_) => _solidSprite(255)),
|
||||
sounds: List.generate(200, (_) => PcmSound(Uint8List(1))),
|
||||
adLibSounds: const [],
|
||||
music: const [],
|
||||
vgaImages: const [],
|
||||
episodes: [
|
||||
Episode(
|
||||
name: 'Episode 1',
|
||||
levels: [
|
||||
WolfLevel(
|
||||
name: 'Level 1',
|
||||
wallGrid: wallGrid,
|
||||
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
||||
objectGrid: objectGrid,
|
||||
music: Music.level01,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
difficulty: Difficulty.medium,
|
||||
startingEpisode: 0,
|
||||
frameBuffer: FrameBuffer(64, 64),
|
||||
input: _TestInput(),
|
||||
onGameWon: () {},
|
||||
engineAudio: _SilentAudio(),
|
||||
return WolfensteinData(
|
||||
version: version,
|
||||
dataVersion: DataVersion.unknown,
|
||||
registry: switch (version) {
|
||||
GameVersion.spearOfDestiny => SpearAssetRegistry(),
|
||||
_ => RetailAssetRegistry(),
|
||||
},
|
||||
walls: [
|
||||
_solidSprite(1),
|
||||
_solidSprite(1),
|
||||
_solidSprite(2),
|
||||
_solidSprite(2),
|
||||
],
|
||||
sprites: List.generate(436, (_) => _solidSprite(255)),
|
||||
sounds: List.generate(200, (_) => PcmSound(Uint8List(1))),
|
||||
adLibSounds: const [],
|
||||
music: const [],
|
||||
vgaImages: const [],
|
||||
episodes: [
|
||||
Episode(
|
||||
name: 'Episode 1',
|
||||
levels: [
|
||||
WolfLevel(
|
||||
name: 'Level 1',
|
||||
wallGrid: wallGrid,
|
||||
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
||||
objectGrid: objectGrid,
|
||||
music: Music.level01,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -159,6 +159,23 @@ void main() {
|
||||
expect(manager.transitionEffect, WolfTransitionEffect.none);
|
||||
expect(manager.activeMenu, WolfMenuScreen.difficultySelect);
|
||||
});
|
||||
|
||||
test('spear variant main menu omits READ THIS row', () {
|
||||
final manager = MenuManager();
|
||||
|
||||
manager.beginSelectionFlow(
|
||||
gameCount: 1,
|
||||
initialGameIsSpear: true,
|
||||
);
|
||||
manager.showMainMenu(hasResumableGame: false, hasLoadableSave: true);
|
||||
|
||||
expect(
|
||||
manager.mainMenuEntries.any(
|
||||
(entry) => entry.action == WolfMenuMainAction.readThis,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:test/test.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/built_in/menu/wolf/classic_menu_presentation_module.dart';
|
||||
|
||||
void main() {
|
||||
test('classic menu palette matches canonical WL_MENU.H constants', () {
|
||||
const module = ClassicMenuPresentationModule();
|
||||
|
||||
expect(module.backgroundIndex, 0x2D); // BKGDCOLOR
|
||||
expect(module.panelIndex, 0x23); // BORD2COLOR
|
||||
expect(module.borderIndex, 0x29); // BORDCOLOR
|
||||
expect(module.disabledTextIndex, 0x2B); // DEACTIVE
|
||||
|
||||
expect(module.unselectedTextIndex, 0x17); // TEXTCOLOR
|
||||
expect(module.selectedTextIndex, 0x13); // HIGHLIGHT
|
||||
expect(module.headerTextIndex, 0x47); // READHCOLOR
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:test/test.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/built_in/shareware_hud_module.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/built_in/menu/wolf/shareware_hud_module.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
|
||||
void main() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:test/test.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/built_in/shareware_menu_module.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/built_in/menu/wolf/shareware_menu_module.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
|
||||
void main() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:test/test.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/built_in/spear_menu_presentation_module.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/built_in/menu/spear/spear_menu_presentation_module.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'package:test/test.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/built_in/menu/spear/spear_asset_registry.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/built_in/menu/spear/spear_menu_presentation_module.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
|
||||
void main() {
|
||||
group('SpearAssetRegistry', () {
|
||||
test('resolves full SOD menu VGA indices', () {
|
||||
final SpearAssetRegistry registry = SpearAssetRegistry();
|
||||
|
||||
expect(registry.menu.resolve(MenuPicKey.title)?.pictureIndex, 76);
|
||||
expect(registry.menu.resolve(MenuPicKey.pg13)?.pictureIndex, 88);
|
||||
expect(registry.menu.resolve(MenuPicKey.credits)?.pictureIndex, 89);
|
||||
expect(
|
||||
registry.menu.resolve(MenuPicKey.controlBackground)?.pictureIndex,
|
||||
12,
|
||||
);
|
||||
expect(registry.menu.resolve(MenuPicKey.optionsLabel)?.pictureIndex, 13);
|
||||
expect(registry.menuPresentation, isA<SpearMenuPresentationModule>());
|
||||
});
|
||||
});
|
||||
|
||||
group('BuiltInAssetRegistryResolver full SOD selection', () {
|
||||
test('uses full Spear registry for game-version fallback', () {
|
||||
const BuiltInAssetRegistryResolver resolver =
|
||||
BuiltInAssetRegistryResolver();
|
||||
|
||||
final AssetRegistry registry = resolver.resolve(
|
||||
const RegistrySelectionContext(
|
||||
gameVersion: GameVersion.spearOfDestiny,
|
||||
dataVersion: DataVersion.unknown,
|
||||
),
|
||||
);
|
||||
|
||||
expect(registry, isA<SpearAssetRegistry>());
|
||||
expect(registry.menu.resolve(MenuPicKey.title)?.pictureIndex, 76);
|
||||
expect(registry.menu.resolve(MenuPicKey.pg13)?.pictureIndex, 88);
|
||||
expect(registry.menu.resolve(MenuPicKey.credits)?.pictureIndex, 89);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:wolf_3d_dart/src/rendering/menu_header_band.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
|
||||
void main() {
|
||||
test('shared Spear variant utility is correct', () {
|
||||
expect(
|
||||
MenuHeaderBand.isSpearVariant(GameVersion.retail),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
MenuHeaderBand.isSpearVariant(GameVersion.shareware),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
MenuHeaderBand.isSpearVariant(GameVersion.spearOfDestiny),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
MenuHeaderBand.isSpearVariant(GameVersion.spearOfDestinyDemo),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'black rows and rows sandwiched between them are extended to screen sides',
|
||||
() {
|
||||
// Interior column (x=1): row 0 → 9 (leading non-black, skip),
|
||||
// row 1 → 0 (black, fill),
|
||||
// row 2 → 0 (black, fill),
|
||||
// row 3 → 4 (trailing, skip), row 4 → 3 (trailing, skip).
|
||||
final image = _imageFromRows([
|
||||
[5, 9, 9, 5],
|
||||
[6, 0, 8, 5],
|
||||
[6, 0, 8, 0],
|
||||
[7, 4, 4, 7],
|
||||
[7, 3, 3, 7],
|
||||
]);
|
||||
|
||||
final List<(int, int, int, int)> edgeRows = <(int, int, int, int)>[];
|
||||
|
||||
MenuHeaderBand.applyFromHeadingImage(
|
||||
image: image,
|
||||
imageX320: 10,
|
||||
fillSideEdgesRow:
|
||||
(
|
||||
int y200,
|
||||
int leftWidth320,
|
||||
int rightStartX320,
|
||||
int paletteIndex,
|
||||
) {
|
||||
edgeRows.add((y200, leftWidth320, rightStartX320, paletteIndex));
|
||||
},
|
||||
);
|
||||
|
||||
// Only rows 1 and 2 have interior-column value 0 (black) → side fills.
|
||||
expect(
|
||||
edgeRows,
|
||||
<(int, int, int, int)>[
|
||||
(1, 10, 14, 0),
|
||||
(2, 10, 14, 0),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('no side fills when all interior rows are non-black', () {
|
||||
final image = _imageFromRows([
|
||||
[9, 1, 1, 3],
|
||||
[8, 2, 2, 3],
|
||||
[7, 4, 5, 6],
|
||||
[6, 6, 6, 6],
|
||||
[5, 7, 7, 2],
|
||||
]);
|
||||
|
||||
final List<(int, int, int, int)> edgeRows = <(int, int, int, int)>[];
|
||||
|
||||
MenuHeaderBand.applyFromHeadingImage(
|
||||
image: image,
|
||||
imageX320: 50,
|
||||
fillSideEdgesRow:
|
||||
(
|
||||
int y200,
|
||||
int leftWidth320,
|
||||
int rightStartX320,
|
||||
int paletteIndex,
|
||||
) {
|
||||
edgeRows.add((y200, leftWidth320, rightStartX320, paletteIndex));
|
||||
},
|
||||
);
|
||||
|
||||
expect(edgeRows, isEmpty);
|
||||
});
|
||||
|
||||
test('algorithm trims long trailing rows after final black run', () {
|
||||
final image = _imageFromRows([
|
||||
[9, 1, 1, 3],
|
||||
[8, 2, 2, 3],
|
||||
[7, 0, 0, 7],
|
||||
[7, 0, 0, 7],
|
||||
[7, 0, 0, 7],
|
||||
[6, 6, 6, 6],
|
||||
[5, 5, 5, 5],
|
||||
[4, 4, 4, 4],
|
||||
[3, 3, 3, 3],
|
||||
[2, 2, 2, 2],
|
||||
[1, 1, 1, 1],
|
||||
]);
|
||||
|
||||
final List<(int, int, int, int)> edgeRows = <(int, int, int, int)>[];
|
||||
|
||||
MenuHeaderBand.applyFromHeadingImage(
|
||||
image: image,
|
||||
imageX320: 50,
|
||||
fillSideEdgesRow:
|
||||
(
|
||||
int y200,
|
||||
int leftWidth320,
|
||||
int rightStartX320,
|
||||
int paletteIndex,
|
||||
) {
|
||||
edgeRows.add((y200, leftWidth320, rightStartX320, paletteIndex));
|
||||
},
|
||||
);
|
||||
|
||||
// Rows 2-4 are black (interior x=1 → 0). Rows 5-10 are non-black trailing;
|
||||
// effectiveBandRowCount trims to lastBlack(4)+1+3 = 8 rows processed.
|
||||
// Only the 3 black rows produce fills (no interior non-black rows here).
|
||||
expect(edgeRows.length, 3);
|
||||
expect(
|
||||
edgeRows,
|
||||
<(int, int, int, int)>[
|
||||
(2, 50, 54, 0),
|
||||
(3, 50, 54, 0),
|
||||
(4, 50, 54, 0),
|
||||
],
|
||||
);
|
||||
|
||||
// firstColumn is trimmed to 8 entries (rows 0-7).
|
||||
expect(
|
||||
MenuHeaderBand.firstColumn(image),
|
||||
<int>[1, 2, 0, 0, 0, 6, 5, 4],
|
||||
);
|
||||
});
|
||||
|
||||
test('interior non-black rows sandwiched between black rows are extended', () {
|
||||
// Simulates a heading image with a decorative non-black stripe between
|
||||
// two black sections (e.g. Wolf3D row 32).
|
||||
// Interior column (x=1): row 0 → 5 (leading non-black, skip),
|
||||
// row 1 → 0 (first black, fill with palette 0),
|
||||
// row 2 → 7 (interior non-black stripe, fill with palette 7),
|
||||
// row 3 → 0 (last black, fill with palette 0),
|
||||
// rows 4–7 → trailing non-black (4 rows > maxTrailing=3,
|
||||
// so effectiveBandRowCount trims to 7 rows).
|
||||
final image = _imageFromRows([
|
||||
[9, 5, 5, 9],
|
||||
[6, 0, 0, 6],
|
||||
[6, 7, 7, 6],
|
||||
[6, 0, 0, 6],
|
||||
[8, 3, 3, 8],
|
||||
[8, 4, 4, 8],
|
||||
[8, 2, 2, 8],
|
||||
[8, 1, 1, 8],
|
||||
]);
|
||||
|
||||
final List<(int, int, int, int)> edgeRows = <(int, int, int, int)>[];
|
||||
|
||||
MenuHeaderBand.applyFromHeadingImage(
|
||||
image: image,
|
||||
imageX320: 10,
|
||||
fillSideEdgesRow:
|
||||
(
|
||||
int y200,
|
||||
int leftWidth320,
|
||||
int rightStartX320,
|
||||
int paletteIndex,
|
||||
) {
|
||||
edgeRows.add((y200, leftWidth320, rightStartX320, paletteIndex));
|
||||
},
|
||||
);
|
||||
|
||||
// Rows 1 and 3 are black → fill with palette 0.
|
||||
// Row 2 is interior non-black between two black rows → fill with palette 7.
|
||||
// Row 0 is before firstBlack → skip (background shows through at top).
|
||||
// Rows 4–6 are after lastBlack → skip (background shows through at bottom).
|
||||
expect(
|
||||
edgeRows,
|
||||
<(int, int, int, int)>[
|
||||
(1, 10, 14, 0),
|
||||
(2, 10, 14, 7),
|
||||
(3, 10, 14, 0),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
VgaImage _imageFromRows(List<List<int>> rows) {
|
||||
const int width = 4;
|
||||
final int height = rows.length;
|
||||
final Uint8List pixels = Uint8List(width * height);
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
final List<int> row = rows[y];
|
||||
if (row.length != width) {
|
||||
throw ArgumentError('Each row must have width=$width entries.');
|
||||
}
|
||||
for (int x = 0; x < width; x++) {
|
||||
pixels[(x * height) + y] = row[x];
|
||||
}
|
||||
}
|
||||
|
||||
return VgaImage(width: width, height: height, pixels: pixels);
|
||||
}
|
||||
Reference in New Issue
Block a user