Added some dartdoc comments and an (untested) sixel rasterizer
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -33,6 +33,7 @@
|
|||||||
**/build/
|
**/build/
|
||||||
**/coverage/
|
**/coverage/
|
||||||
**/flutter/ephemeral/
|
**/flutter/ephemeral/
|
||||||
|
**/generated_plugin*
|
||||||
|
|
||||||
# Symbolication related
|
# Symbolication related
|
||||||
**/app.*.symbols
|
**/app.*.symbols
|
||||||
|
|||||||
@@ -1,23 +1,31 @@
|
|||||||
|
/// Represents the specific version identity of the Wolfenstein 3D data files.
|
||||||
|
///
|
||||||
|
/// Since files like 'VSWAP.WL6' exist across multiple releases (v1.0, v1.1, v1.4),
|
||||||
|
/// we use MD5 checksums of the VSWAP file to determine specific engine behaviors,
|
||||||
|
/// such as MAPTEMP support or HUD differences.
|
||||||
enum DataVersion {
|
enum DataVersion {
|
||||||
/// V1.0 Retail (VSWAP.WL6)
|
/// Original v1.0 Retail release.
|
||||||
version10Retail('a6d901dfb455dfac96db5e4705837cdb'),
|
version10Retail('a6d901dfb455dfac96db5e4705837cdb'),
|
||||||
|
|
||||||
/// v1.1 Retail (VSWAP.WL6)
|
/// v1.1 Retail update.
|
||||||
version11Retail('a80904e0283a921d88d977b56c279b9d'),
|
version11Retail('a80904e0283a921d88d977b56c279b9d'),
|
||||||
|
|
||||||
/// v1.4 Shareware (VSWAP.WL1)
|
/// v1.4 Shareware release (VSWAP.WL1).
|
||||||
version14Shareware('6efa079414b817c97db779cecfb081c9'),
|
version14Shareware('6efa079414b817c97db779cecfb081c9'),
|
||||||
|
|
||||||
/// v1.4 Retail (VSWAP.WL6) - GOG/Steam version
|
/// v1.4 Retail release (found on GOG and Steam).
|
||||||
version14Retail('b8ff4997461bafa5ef2a94c11f9de001'),
|
version14Retail('b8ff4997461bafa5ef2a94c11f9de001'),
|
||||||
|
|
||||||
unknown('unknown'),
|
/// Default state if the file hash is unrecognized.
|
||||||
|
unknown('unknown')
|
||||||
;
|
;
|
||||||
|
|
||||||
|
/// The MD5 hash of the VSWAP file associated with this version.
|
||||||
final String checksum;
|
final String checksum;
|
||||||
|
|
||||||
const DataVersion(this.checksum);
|
const DataVersion(this.checksum);
|
||||||
|
|
||||||
|
/// Matches a provided [hash] string to a known [DataVersion].
|
||||||
static DataVersion fromChecksum(String hash) {
|
static DataVersion fromChecksum(String hash) {
|
||||||
return DataVersion.values.firstWhere(
|
return DataVersion.values.firstWhere(
|
||||||
(v) => v.checksum == hash,
|
(v) => v.checksum == hash,
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ Future<Map<GameVersion, WolfensteinData>> discoverInDirectory({
|
|||||||
return fileName == expectedName;
|
return fileName == expectedName;
|
||||||
}).firstOrNull;
|
}).firstOrNull;
|
||||||
|
|
||||||
// v1.0 FIX: Search for MAPTEMP if GAMEMAPS is missing
|
// v1.0 FIX: In the very first retail release, the maps were named
|
||||||
|
// MAPTEMP.WL6 instead of GAMEMAPS.WL6. We check for this alias
|
||||||
|
// to ensure v1.0 data is properly discovered.
|
||||||
if (match == null && requiredFile == GameFile.gameMaps) {
|
if (match == null && requiredFile == GameFile.gameMaps) {
|
||||||
final altName = 'MAPTEMP.$ext';
|
final altName = 'MAPTEMP.$ext';
|
||||||
match = allFiles.where((file) {
|
match = allFiles.where((file) {
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ import 'package:crypto/crypto.dart' show md5;
|
|||||||
import 'package:wolf_3d_dart/src/data/data_version.dart';
|
import 'package:wolf_3d_dart/src/data/data_version.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
|
/// The primary parser for Wolfenstein 3D data formats.
|
||||||
|
///
|
||||||
|
/// This abstract class serves as the extraction and decompression engine for
|
||||||
|
/// legacy id Software game assets. It processes raw byte streams into usable
|
||||||
|
/// objects (walls, sprites, audio, levels, and UI elements) using original
|
||||||
|
/// algorithms like Carmackization, RLEW, and Huffman expansion.
|
||||||
abstract class WLParser {
|
abstract class WLParser {
|
||||||
// --- Original Song Lookup Tables ---
|
// --- Original Song Lookup Tables ---
|
||||||
static const List<int> _sharewareMusicMap = [
|
static const List<int> _sharewareMusicMap = [
|
||||||
@@ -21,10 +27,11 @@ abstract class WLParser {
|
|||||||
];
|
];
|
||||||
|
|
||||||
/// Asynchronously discovers the game version and loads all necessary files.
|
/// Asynchronously discovers the game version and loads all necessary files.
|
||||||
/// Provide a [fileFetcher] callback (e.g., Flutter's rootBundle.load) that
|
///
|
||||||
/// takes a filename and returns its ByteData.
|
/// Provide a [fileFetcher] callback (e.g., Flutter's `rootBundle.load` or
|
||||||
/// Asynchronously discovers the game version and loads all necessary files.
|
/// standard `File.readAsBytes`) that takes a filename string and returns
|
||||||
/// Asynchronously discovers the game version and loads all necessary files.
|
/// its [ByteData]. The parser will probe for the `VSWAP` file to determine
|
||||||
|
/// the game version and then fetch the corresponding data files.
|
||||||
static Future<WolfensteinData> loadAsync(
|
static Future<WolfensteinData> loadAsync(
|
||||||
Future<ByteData> Function(String filename) fileFetcher,
|
Future<ByteData> Function(String filename) fileFetcher,
|
||||||
) async {
|
) async {
|
||||||
@@ -92,9 +99,11 @@ abstract class WLParser {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses all raw ByteData upfront and returns a fully populated
|
/// Parses all raw ByteData upfront and returns a fully populated [WolfensteinData] object.
|
||||||
/// WolfensteinData object. By using named parameters, the compiler
|
///
|
||||||
/// guarantees no files are missing or misnamed.
|
/// By using named parameters, the compiler guarantees no files are missing or misnamed.
|
||||||
|
/// The [dataIdentity] parameter is used to apply version-specific bug fixes or
|
||||||
|
/// legacy parsing adjustments (e.g., HUD string differences in v1.0).
|
||||||
static WolfensteinData load({
|
static WolfensteinData load({
|
||||||
required GameVersion version,
|
required GameVersion version,
|
||||||
required ByteData vswap,
|
required ByteData vswap,
|
||||||
@@ -128,7 +137,9 @@ abstract class WLParser {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts the 64x64 wall textures from VSWAP.WL1
|
/// Extracts the 64x64 wall textures from the VSWAP file.
|
||||||
|
///
|
||||||
|
/// Wall textures are stored sequentially at the beginning of the chunks array.
|
||||||
static List<Sprite> parseWalls(ByteData vswap) {
|
static List<Sprite> parseWalls(ByteData vswap) {
|
||||||
final header = _VswapHeader(vswap);
|
final header = _VswapHeader(vswap);
|
||||||
|
|
||||||
@@ -139,7 +150,10 @@ abstract class WLParser {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts the compiled scaled sprites from VSWAP.WL1
|
/// Extracts the compiled scaled sprites from the VSWAP file.
|
||||||
|
///
|
||||||
|
/// Sprites use a column-based "sparse" format to allow for transparency
|
||||||
|
/// and efficient vertical scaling in the raycaster.
|
||||||
static List<Sprite> parseSprites(ByteData vswap) {
|
static List<Sprite> parseSprites(ByteData vswap) {
|
||||||
final header = _VswapHeader(vswap);
|
final header = _VswapHeader(vswap);
|
||||||
final sprites = <Sprite>[];
|
final sprites = <Sprite>[];
|
||||||
@@ -155,7 +169,9 @@ abstract class WLParser {
|
|||||||
return sprites;
|
return sprites;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts digitized sound effects (PCM Audio) from VSWAP.WL1
|
/// Extracts digitized sound effects (PCM Audio) from the VSWAP file.
|
||||||
|
///
|
||||||
|
/// Sounds are stored as raw 8-bit PCM audio bytes.
|
||||||
static List<Uint8List> parseSounds(ByteData vswap) {
|
static List<Uint8List> parseSounds(ByteData vswap) {
|
||||||
final header = _VswapHeader(vswap);
|
final header = _VswapHeader(vswap);
|
||||||
final lengthStart = 6 + (header.chunks * 4);
|
final lengthStart = 6 + (header.chunks * 4);
|
||||||
@@ -235,7 +251,10 @@ abstract class WLParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts and decodes the UI graphics, Title Screens, and Menu items
|
/// Extracts and decodes the UI graphics, Title Screens, and Menu items.
|
||||||
|
///
|
||||||
|
/// The images are built using a dictionary (`VGADICT`), metadata (`VGAHEAD`),
|
||||||
|
/// and Huffman-compressed image chunks (`VGAGRAPH`).
|
||||||
static List<VgaImage> parseVgaImages(
|
static List<VgaImage> parseVgaImages(
|
||||||
ByteData vgaDict,
|
ByteData vgaDict,
|
||||||
ByteData vgaHead,
|
ByteData vgaHead,
|
||||||
@@ -287,7 +306,10 @@ abstract class WLParser {
|
|||||||
"Episode 6\nConfrontation",
|
"Episode 6\nConfrontation",
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Parses MAPHEAD and GAMEMAPS to extract the raw level data.
|
/// Parses `MAPHEAD` and `GAMEMAPS` to extract the raw level data.
|
||||||
|
///
|
||||||
|
/// Level files undergo two layers of decompression (Carmack + RLEW). The map
|
||||||
|
/// data is then translated into 64x64 grids representing the walls and objects.
|
||||||
static List<Episode> parseEpisodes(
|
static List<Episode> parseEpisodes(
|
||||||
ByteData mapHead,
|
ByteData mapHead,
|
||||||
ByteData gameMaps, {
|
ByteData gameMaps, {
|
||||||
@@ -400,6 +422,9 @@ abstract class WLParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts AdLib sounds and IMF music tracks from the audio files.
|
/// Extracts AdLib sounds and IMF music tracks from the audio files.
|
||||||
|
///
|
||||||
|
/// Splits the sequential chunks in `AUDIOT` based on the offsets provided
|
||||||
|
/// in `AUDIOHED`. Converts raw music bytes into structured [ImfMusic] objects.
|
||||||
static ({List<PcmSound> adLib, List<ImfMusic> music}) parseAudio(
|
static ({List<PcmSound> adLib, List<ImfMusic> music}) parseAudio(
|
||||||
ByteData audioHed,
|
ByteData audioHed,
|
||||||
ByteData audioT,
|
ByteData audioT,
|
||||||
@@ -452,40 +477,41 @@ abstract class WLParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- ALGORITHM 1: CARMACK EXPANSION ---
|
// --- ALGORITHM 1: CARMACK EXPANSION ---
|
||||||
|
/// Expands data using the "Carmackization" near/far pointer compression.
|
||||||
|
///
|
||||||
|
/// This is the first layer of decompression used for level data in GAMEMAPS.
|
||||||
|
/// It works similarly to LZSS, matching near (`0xA7`) and far (`0xA8`) pointers
|
||||||
|
/// to replay previously decompressed data words.
|
||||||
static Uint16List _expandCarmack(Uint8List compressed) {
|
static Uint16List _expandCarmack(Uint8List compressed) {
|
||||||
ByteData data = ByteData.sublistView(compressed);
|
ByteData data = ByteData.sublistView(compressed);
|
||||||
|
int expandedLengthWords = data.getUint16(0, Endian.little) ~/ 2;
|
||||||
// The first 16-bit word is the total length of the expanded data in BYTES.
|
|
||||||
int expandedLengthBytes = data.getUint16(0, Endian.little);
|
|
||||||
int expandedLengthWords = expandedLengthBytes ~/ 2;
|
|
||||||
Uint16List expanded = Uint16List(expandedLengthWords);
|
Uint16List expanded = Uint16List(expandedLengthWords);
|
||||||
|
|
||||||
int inIdx = 2; // Skip the length word we just read
|
int inIdx = 2; // Word-based index
|
||||||
int outIdx = 0;
|
int outIdx = 0;
|
||||||
|
|
||||||
while (outIdx < expandedLengthWords && inIdx < compressed.length) {
|
while (outIdx < expandedLengthWords) {
|
||||||
int word = data.getUint16(inIdx, Endian.little);
|
int word = data.getUint16(inIdx, Endian.little);
|
||||||
inIdx += 2;
|
inIdx += 2;
|
||||||
|
|
||||||
int highByte = word >> 8;
|
int highByte = word >> 8;
|
||||||
int lowByte = word & 0xFF;
|
int lowByte = word & 0xFF;
|
||||||
|
|
||||||
// 0xA7 and 0xA8 are the Carmack Pointer Tags
|
// Carmack pointers: 0xA7 (Near) and 0xA8 (Far)
|
||||||
if (highByte == 0xA7 || highByte == 0xA8) {
|
if (highByte == 0xA7 || highByte == 0xA8) {
|
||||||
if (lowByte == 0) {
|
if (lowByte == 0) {
|
||||||
// Exception Rule: If the length (lowByte) is 0, it's not a pointer.
|
// Edge case: if low byte is 0, it's a literal tag byte
|
||||||
// It's literally just the tag byte followed by another byte.
|
|
||||||
int nextByte = data.getUint8(inIdx++);
|
int nextByte = data.getUint8(inIdx++);
|
||||||
expanded[outIdx++] = (nextByte << 8) | highByte;
|
expanded[outIdx++] = (nextByte << 8) | highByte;
|
||||||
} else if (highByte == 0xA7) {
|
} else if (highByte == 0xA7) {
|
||||||
// 0xA7 = Near Pointer (look back a few spaces)
|
// Near pointer: Copy [lowByte] words from [offset] back in the output
|
||||||
int offset = data.getUint8(inIdx++);
|
int offset = data.getUint8(inIdx++);
|
||||||
int copyFrom = outIdx - offset;
|
int copyFrom = outIdx - offset;
|
||||||
for (int i = 0; i < lowByte; i++) {
|
for (int i = 0; i < lowByte; i++) {
|
||||||
expanded[outIdx++] = expanded[copyFrom++];
|
expanded[outIdx++] = expanded[copyFrom++];
|
||||||
}
|
}
|
||||||
} else if (highByte == 0xA8) {
|
} else {
|
||||||
// 0xA8 = Far Pointer (absolute offset from the very beginning)
|
// Far pointer: Copy [lowByte] words from absolute [offset] in the output
|
||||||
int offset = data.getUint16(inIdx, Endian.little);
|
int offset = data.getUint16(inIdx, Endian.little);
|
||||||
inIdx += 2;
|
inIdx += 2;
|
||||||
for (int i = 0; i < lowByte; i++) {
|
for (int i = 0; i < lowByte; i++) {
|
||||||
@@ -493,7 +519,6 @@ abstract class WLParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Normal, uncompressed word
|
|
||||||
expanded[outIdx++] = word;
|
expanded[outIdx++] = word;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -501,6 +526,10 @@ abstract class WLParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- ALGORITHM 2: RLEW EXPANSION ---
|
// --- ALGORITHM 2: RLEW EXPANSION ---
|
||||||
|
/// Expands data using Run-Length Encoding on Word boundaries.
|
||||||
|
///
|
||||||
|
/// This is the second layer of decompression for level maps. It identifies a
|
||||||
|
/// special marker ([rlewTag]) and expands the subsequent count and value.
|
||||||
static List<int> _expandRlew(Uint16List carmackExpanded, int rlewTag) {
|
static List<int> _expandRlew(Uint16List carmackExpanded, int rlewTag) {
|
||||||
// The first word is the expanded length in BYTES
|
// The first word is the expanded length in BYTES
|
||||||
int expandedLengthBytes = carmackExpanded[0];
|
int expandedLengthBytes = carmackExpanded[0];
|
||||||
@@ -530,6 +559,9 @@ abstract class WLParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts decompressed VGA data chunks (UI, Fonts, Pictures)
|
/// Extracts decompressed VGA data chunks (UI, Fonts, Pictures)
|
||||||
|
///
|
||||||
|
/// Safely manages offsets and handles out-of-bounds safety checks when
|
||||||
|
/// reading from the `VGAGRAPH` blob.
|
||||||
static List<Uint8List> _parseVgaRaw(
|
static List<Uint8List> _parseVgaRaw(
|
||||||
ByteData vgaDict,
|
ByteData vgaDict,
|
||||||
ByteData vgaHead,
|
ByteData vgaHead,
|
||||||
@@ -610,6 +642,11 @@ abstract class WLParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- ALGORITHM 3: HUFFMAN EXPANSION ---
|
// --- ALGORITHM 3: HUFFMAN EXPANSION ---
|
||||||
|
/// Decompresses data using id Software's Huffman algorithm.
|
||||||
|
///
|
||||||
|
/// Traverses a binary tree derived from `VGADICT`. Bits from the [compressed]
|
||||||
|
/// data stream dictate left/right traversal until a leaf node is found, at which
|
||||||
|
/// point the data byte is appended to the output buffer.
|
||||||
static Uint8List _expandHuffman(
|
static Uint8List _expandHuffman(
|
||||||
Uint8List compressed,
|
Uint8List compressed,
|
||||||
List<_HuffmanNode> dict,
|
List<_HuffmanNode> dict,
|
||||||
@@ -620,34 +657,31 @@ abstract class WLParser {
|
|||||||
int outIdx = 0;
|
int outIdx = 0;
|
||||||
int byteIdx = 0;
|
int byteIdx = 0;
|
||||||
int bitMask = 1;
|
int bitMask = 1;
|
||||||
int currentNode = 254; // id Software's Huffman root is always node 254
|
int currentNode = 254; // Root node is always 254 in id's implementation
|
||||||
|
|
||||||
while (outIdx < expandedLength && byteIdx < compressed.length) {
|
while (outIdx < expandedLength && byteIdx < compressed.length) {
|
||||||
// Read the current bit (LSB to MSB)
|
// Extract bits from Least Significant Bit (LSB) to Most (MSB)
|
||||||
int bit = (compressed[byteIdx] & bitMask) == 0 ? 0 : 1;
|
int bit = (compressed[byteIdx] & bitMask) == 0 ? 0 : 1;
|
||||||
|
|
||||||
// Advance to the next bit/byte
|
// Move to next bit/byte
|
||||||
bitMask <<= 1;
|
bitMask <<= 1;
|
||||||
if (bitMask > 128) {
|
if (bitMask > 128) {
|
||||||
bitMask = 1;
|
bitMask = 1;
|
||||||
byteIdx++;
|
byteIdx++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Traverse the tree
|
// If bit is 0, take the left path; if 1, take the right path
|
||||||
int nextVal = bit == 0 ? dict[currentNode].bit0 : dict[currentNode].bit1;
|
int nextVal = bit == 0 ? dict[currentNode].bit0 : dict[currentNode].bit1;
|
||||||
|
|
||||||
if (nextVal < 256) {
|
if (nextVal < 256) {
|
||||||
// If the value is < 256, we've hit a leaf node (an actual character byte!)
|
// Found a leaf node: write the byte and reset to root
|
||||||
expanded[outIdx++] = nextVal;
|
expanded[outIdx++] = nextVal;
|
||||||
currentNode =
|
currentNode = 254;
|
||||||
254; // Reset to the root of the tree for the next character
|
|
||||||
} else {
|
} else {
|
||||||
// If the value is >= 256, it's a pointer to the next internal node.
|
// Internal node: pointer to the next branch (offset by 256)
|
||||||
// Node indexes are offset by 256.
|
|
||||||
currentNode = nextVal - 256;
|
currentNode = nextVal - 256;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return expanded;
|
return expanded;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,15 @@ import 'io/discovery_stub.dart'
|
|||||||
as platform;
|
as platform;
|
||||||
import 'wl_parser.dart';
|
import 'wl_parser.dart';
|
||||||
|
|
||||||
|
/// The main entry point for loading Wolfenstein 3D data.
|
||||||
|
///
|
||||||
|
/// This class provides high-level methods to either scan a local filesystem
|
||||||
|
/// (on supported platforms) or load data directly from memory (Web/Flutter).
|
||||||
class WolfensteinLoader {
|
class WolfensteinLoader {
|
||||||
/// Scans a directory for Wolfenstein 3D data files and loads all available versions.
|
/// Scans [directoryPath] for Wolfenstein data files.
|
||||||
|
///
|
||||||
|
/// On Web, this will throw an [UnsupportedError]. On Desktop/Mobile, it will
|
||||||
|
/// attempt to identify and load all valid game versions found in the path.
|
||||||
static Future<Map<GameVersion, WolfensteinData>> discover({
|
static Future<Map<GameVersion, WolfensteinData>> discover({
|
||||||
String? directoryPath,
|
String? directoryPath,
|
||||||
bool recursive = false,
|
bool recursive = false,
|
||||||
@@ -21,8 +28,11 @@ class WolfensteinLoader {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses WolfensteinData from raw ByteData.
|
/// Manually loads game data from provided [ByteData] buffers.
|
||||||
/// Throws an [ArgumentError] if any required file is null.
|
///
|
||||||
|
/// Use this for Web or Flutter assets where automated directory scanning
|
||||||
|
/// is not possible. It automatically detects the [DataVersion] (v1.0, v1.4, etc.)
|
||||||
|
/// by hashing the [vswap] buffer.
|
||||||
static WolfensteinData loadFromBytes({
|
static WolfensteinData loadFromBytes({
|
||||||
required GameVersion version,
|
required GameVersion version,
|
||||||
required ByteData? vswap,
|
required ByteData? vswap,
|
||||||
@@ -57,7 +67,7 @@ class WolfensteinLoader {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Identify the DataVersion via Checksum
|
// 2. Identify identity for specialized parsing (e.g., MAPTEMP vs GAMEMAPS)
|
||||||
final vswapBytes = vswap!.buffer.asUint8List(
|
final vswapBytes = vswap!.buffer.asUint8List(
|
||||||
vswap.offsetInBytes,
|
vswap.offsetInBytes,
|
||||||
vswap.lengthInBytes,
|
vswap.lengthInBytes,
|
||||||
|
|||||||
@@ -1,17 +1,34 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
/// Represents the four primary compass directions used for movement and orientation.
|
||||||
|
///
|
||||||
|
/// The [radians] values are aligned with standard 2D Cartesian coordinates where
|
||||||
|
/// East is 0.0 and rotations proceed clockwise.
|
||||||
enum CardinalDirection {
|
enum CardinalDirection {
|
||||||
|
/// 0 degrees (pointing Right)
|
||||||
east(0.0),
|
east(0.0),
|
||||||
|
|
||||||
|
/// 90 degrees (pointing Down)
|
||||||
south(math.pi / 2),
|
south(math.pi / 2),
|
||||||
|
|
||||||
|
/// 180 degrees (pointing Left)
|
||||||
west(math.pi),
|
west(math.pi),
|
||||||
|
|
||||||
|
/// 270 degrees (pointing Up)
|
||||||
north(3 * math.pi / 2)
|
north(3 * math.pi / 2)
|
||||||
;
|
;
|
||||||
|
|
||||||
|
/// The rotation value in radians associated with this direction.
|
||||||
final double radians;
|
final double radians;
|
||||||
|
|
||||||
const CardinalDirection(this.radians);
|
const CardinalDirection(this.radians);
|
||||||
|
|
||||||
/// Helper to decode Wolf3D enemy directional blocks
|
/// Decodes Wolf3D enemy directional orientation from map tile indices.
|
||||||
|
///
|
||||||
|
/// In the original Wolfenstein 3D map format, enemies are stored in
|
||||||
|
/// blocks where the specific index (modulo 4) determines their starting facing.
|
||||||
static CardinalDirection fromEnemyIndex(int index) {
|
static CardinalDirection fromEnemyIndex(int index) {
|
||||||
|
// The engine uses a specific pattern: 0=East, 1=North, 2=West, 3=South
|
||||||
switch (index % 4) {
|
switch (index % 4) {
|
||||||
case 0:
|
case 0:
|
||||||
return CardinalDirection.east;
|
return CardinalDirection.east;
|
||||||
@@ -22,6 +39,7 @@ enum CardinalDirection {
|
|||||||
case 3:
|
case 3:
|
||||||
return CardinalDirection.south;
|
return CardinalDirection.south;
|
||||||
default:
|
default:
|
||||||
|
// Fallback safety, though mathematically unreachable with % 4
|
||||||
return CardinalDirection.east;
|
return CardinalDirection.east;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
/// Provides the standard VGA color palette used by Wolfenstein 3D.
|
||||||
abstract class ColorPalette {
|
abstract class ColorPalette {
|
||||||
|
/// The 256-color palette converted to 32-bit ARGB values for modern rendering.
|
||||||
static final Uint32List vga32Bit = Uint32List.fromList([
|
static final Uint32List vga32Bit = Uint32List.fromList([
|
||||||
0xFF000000,
|
0xFF000000,
|
||||||
0xFFAA0000,
|
0xFFAA0000,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
/// A lightweight, immutable 2D Vector/Coordinate system.
|
/// A lightweight, immutable 2D Vector/Coordinate system.
|
||||||
|
///
|
||||||
|
/// Used for entity positioning, raycasting calculations, and direction vectors.
|
||||||
class Coordinate2D implements Comparable<Coordinate2D> {
|
class Coordinate2D implements Comparable<Coordinate2D> {
|
||||||
final double x;
|
final double x;
|
||||||
final double y;
|
final double y;
|
||||||
@@ -8,13 +10,18 @@ class Coordinate2D implements Comparable<Coordinate2D> {
|
|||||||
const Coordinate2D(this.x, this.y);
|
const Coordinate2D(this.x, this.y);
|
||||||
|
|
||||||
/// Returns the angle in radians between this coordinate and [other].
|
/// Returns the angle in radians between this coordinate and [other].
|
||||||
/// Useful for "Look At" logic or determining steering direction.
|
///
|
||||||
|
/// Useful for "Look At" logic or determining steering direction for AI.
|
||||||
/// Result is between -pi and pi.
|
/// Result is between -pi and pi.
|
||||||
double angleTo(Coordinate2D other) {
|
double angleTo(Coordinate2D other) {
|
||||||
return math.atan2(other.y - y, other.x - x);
|
return math.atan2(other.y - y, other.x - x);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rotates the coordinate around (0,0) by [radians].
|
/// Rotates the coordinate around the origin (0,0) by [radians].
|
||||||
|
///
|
||||||
|
/// This uses a standard rotation matrix:
|
||||||
|
/// x' = x*cos(θ) - y*sin(θ)
|
||||||
|
/// y' = x*sin(θ) + y*cos(θ)
|
||||||
Coordinate2D rotate(double radians) {
|
Coordinate2D rotate(double radians) {
|
||||||
final cos = math.cos(radians);
|
final cos = math.cos(radians);
|
||||||
final sin = math.sin(radians);
|
final sin = math.sin(radians);
|
||||||
@@ -24,7 +31,9 @@ class Coordinate2D implements Comparable<Coordinate2D> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Linear Interpolation: Slides between this and [target] by [t] (0.0 to 1.0).
|
/// Linear Interpolation (LERP): Slides between this and [target] by [t].
|
||||||
|
///
|
||||||
|
/// [t] should be a value between 0.0 and 1.0.
|
||||||
/// Perfect for smooth camera follows or "lerping" an object to a new spot.
|
/// Perfect for smooth camera follows or "lerping" an object to a new spot.
|
||||||
Coordinate2D lerp(Coordinate2D target, double t) {
|
Coordinate2D lerp(Coordinate2D target, double t) {
|
||||||
return Coordinate2D(
|
return Coordinate2D(
|
||||||
@@ -33,18 +42,24 @@ class Coordinate2D implements Comparable<Coordinate2D> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the length of the vector.
|
||||||
double get magnitude => math.sqrt(x * x + y * y);
|
double get magnitude => math.sqrt(x * x + y * y);
|
||||||
|
|
||||||
|
/// Returns a vector with the same direction but a magnitude of 1.0.
|
||||||
Coordinate2D get normalized {
|
Coordinate2D get normalized {
|
||||||
final m = magnitude;
|
final m = magnitude;
|
||||||
if (m == 0) return const Coordinate2D(0, 0);
|
if (m == 0) return const Coordinate2D(0, 0);
|
||||||
return Coordinate2D(x / m, y / m);
|
return Coordinate2D(x / m, y / m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calculates the Dot Product between this and [other].
|
||||||
double dot(Coordinate2D other) => (x * other.x) + (y * other.y);
|
double dot(Coordinate2D other) => (x * other.x) + (y * other.y);
|
||||||
|
|
||||||
|
/// Calculates the Euclidean distance between two points.
|
||||||
double distanceTo(Coordinate2D other) => (this - other).magnitude;
|
double distanceTo(Coordinate2D other) => (this - other).magnitude;
|
||||||
|
|
||||||
|
// --- Operator Overloads for Vector Math ---
|
||||||
|
|
||||||
Coordinate2D operator +(Coordinate2D other) =>
|
Coordinate2D operator +(Coordinate2D other) =>
|
||||||
Coordinate2D(x + other.x, y + other.y);
|
Coordinate2D(x + other.x, y + other.y);
|
||||||
Coordinate2D operator -(Coordinate2D other) =>
|
Coordinate2D operator -(Coordinate2D other) =>
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
|
/// Defines the game difficulty levels, matching the original titles.
|
||||||
enum Difficulty {
|
enum Difficulty {
|
||||||
canIPlayDaddy(0, "Can I play, Daddy?"),
|
canIPlayDaddy(0, "Can I play, Daddy?"),
|
||||||
dontHurtMe(0, "Don't hurt me."),
|
dontHurtMe(0, "Don't hurt me."),
|
||||||
bringEmOn(1, "Bring em' on!"),
|
bringEmOn(1, "Bring em' on!"),
|
||||||
iAmDeathIncarnate(2, "I am Death incarnate!");
|
iAmDeathIncarnate(2, "I am Death incarnate!"),
|
||||||
|
;
|
||||||
|
|
||||||
|
/// The friendly string shown in menus.
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
|
/// The numeric level used for map object filtering logic.
|
||||||
final int level;
|
final int level;
|
||||||
|
|
||||||
const Difficulty(this.level, this.title);
|
const Difficulty(this.level, this.title);
|
||||||
|
|||||||
@@ -1,24 +1,36 @@
|
|||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
|
/// Handles identity and difficulty-based logic for enemies in map data.
|
||||||
|
///
|
||||||
|
/// In Wolf3D, each enemy type (Guard, SS, etc.) occupies a block of 36 IDs
|
||||||
|
/// in the map object layer to account for behavior, direction, and difficulty.
|
||||||
class EnemyMapData {
|
class EnemyMapData {
|
||||||
|
/// The starting ID for this specific enemy type (e.g., 108 for Guards).
|
||||||
final int baseId;
|
final int baseId;
|
||||||
|
|
||||||
const EnemyMapData(this.baseId);
|
const EnemyMapData(this.baseId);
|
||||||
|
|
||||||
/// True if the ID falls anywhere within this enemy's 36-ID block
|
/// Returns true if the provided [id] belongs to this enemy type's 36-ID block.
|
||||||
bool claimsId(int id) => id >= baseId && id < baseId + 36;
|
bool claimsId(int id) => id >= baseId && id < baseId + 36;
|
||||||
|
|
||||||
|
/// Returns true if the ID represents a standing (static) version for the [difficulty].
|
||||||
bool isStaticForDifficulty(int id, Difficulty difficulty) {
|
bool isStaticForDifficulty(int id, Difficulty difficulty) {
|
||||||
|
// Standing enemies occupy the first 12 IDs (4 per difficulty level)
|
||||||
int start = baseId + (difficulty.level * 4);
|
int start = baseId + (difficulty.level * 4);
|
||||||
return id >= start && id < start + 4;
|
return id >= start && id < start + 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if the ID represents a patrolling version for the [difficulty].
|
||||||
bool isPatrolForDifficulty(int id, Difficulty difficulty) {
|
bool isPatrolForDifficulty(int id, Difficulty difficulty) {
|
||||||
|
// Patrolling enemies occupy the next 12 IDs
|
||||||
int start = baseId + 12 + (difficulty.level * 4);
|
int start = baseId + 12 + (difficulty.level * 4);
|
||||||
return id >= start && id < start + 4;
|
return id >= start && id < start + 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if the ID represents an "Ambush" version for the [difficulty].
|
||||||
|
/// Ambush enemies wait for the player to enter their line of sight before moving.
|
||||||
bool isAmbushForDifficulty(int id, Difficulty difficulty) {
|
bool isAmbushForDifficulty(int id, Difficulty difficulty) {
|
||||||
|
// Ambush enemies occupy the final 12 IDs of the block
|
||||||
int start = baseId + 24 + (difficulty.level * 4);
|
int start = baseId + 24 + (difficulty.level * 4);
|
||||||
return id >= start && id < start + 4;
|
return id >= start && id < start + 4;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
|
/// Represents a collection of levels grouped into an Episode.
|
||||||
class Episode {
|
class Episode {
|
||||||
|
/// The display name (e.g., "Episode 1: Escape from Wolfenstein").
|
||||||
final String name;
|
final String name;
|
||||||
|
|
||||||
|
/// The list of levels associated with this episode.
|
||||||
final List<WolfLevel> levels;
|
final List<WolfLevel> levels;
|
||||||
|
|
||||||
const Episode({required this.name, required this.levels});
|
const Episode({required this.name, required this.levels});
|
||||||
|
|||||||
@@ -1,19 +1,32 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
/// A 32-bit pixel buffer representing the raw screen output.
|
||||||
|
///
|
||||||
|
/// This buffer is intended to be filled by the raycaster and then
|
||||||
|
/// pushed to the GPU (via Texture) or displayed on a Canvas.
|
||||||
class FrameBuffer {
|
class FrameBuffer {
|
||||||
|
/// Screen width in pixels.
|
||||||
final int width;
|
final int width;
|
||||||
|
|
||||||
|
/// Screen height in pixels.
|
||||||
final int height;
|
final int height;
|
||||||
|
|
||||||
// A 1D array representing the 2D screen.
|
/// A 1D array representing the 2D screen in ABGR/RGBA format.
|
||||||
// Length = width * height.
|
/// Length is always [width] * [height].
|
||||||
final Uint32List pixels;
|
final Uint32List pixels;
|
||||||
|
|
||||||
FrameBuffer(this.width, this.height) : pixels = Uint32List(width * height);
|
FrameBuffer(this.width, this.height) : pixels = Uint32List(width * height);
|
||||||
|
|
||||||
// Helper to clear the screen (e.g., draw ceiling and floor)
|
/// Clears the screen by painting the top half as the ceiling and the bottom half as the floor.
|
||||||
|
///
|
||||||
|
/// [ceilingColor32] and [floorColor32] should be 32-bit hex colors (e.g., 0xFF383838).
|
||||||
void clear(int ceilingColor32, int floorColor32) {
|
void clear(int ceilingColor32, int floorColor32) {
|
||||||
int half = (width * height) ~/ 2;
|
int half = (width * height) ~/ 2;
|
||||||
|
|
||||||
|
// Fill the ceiling (indices 0 to middle)
|
||||||
pixels.fillRange(0, half, ceilingColor32);
|
pixels.fillRange(0, half, ceilingColor32);
|
||||||
|
|
||||||
|
// Fill the floor (indices middle to end)
|
||||||
pixels.fillRange(half, pixels.length, floorColor32);
|
pixels.fillRange(half, pixels.length, floorColor32);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/// Standard internal filenames for Wolfenstein 3D data components.
|
||||||
enum GameFile {
|
enum GameFile {
|
||||||
vswap('VSWAP'),
|
vswap('VSWAP'),
|
||||||
mapHead('MAPHEAD'),
|
mapHead('MAPHEAD'),
|
||||||
@@ -6,9 +7,10 @@ enum GameFile {
|
|||||||
vgaHead('VGAHEAD'),
|
vgaHead('VGAHEAD'),
|
||||||
vgaGraph('VGAGRAPH'),
|
vgaGraph('VGAGRAPH'),
|
||||||
audioHed('AUDIOHED'),
|
audioHed('AUDIOHED'),
|
||||||
audioT('AUDIOT');
|
audioT('AUDIOT')
|
||||||
|
;
|
||||||
|
|
||||||
|
/// The filename without the extension (e.g., 'VSWAP').
|
||||||
final String baseName;
|
final String baseName;
|
||||||
|
|
||||||
const GameFile(this.baseName);
|
const GameFile(this.baseName);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
|
/// Supported game releases and their associated file extensions.
|
||||||
enum GameVersion {
|
enum GameVersion {
|
||||||
|
/// Wolfenstein 3D Shareware (.WL1)
|
||||||
shareware("WL1"),
|
shareware("WL1"),
|
||||||
|
|
||||||
|
/// Wolfenstein 3D Full Retail (.WL6)
|
||||||
retail("WL6"),
|
retail("WL6"),
|
||||||
|
|
||||||
|
/// Spear of Destiny Full Version (.SOD)
|
||||||
spearOfDestiny("SOD"),
|
spearOfDestiny("SOD"),
|
||||||
spearOfDestinyDemo("SDM"),
|
|
||||||
|
/// Spear of Destiny Demo (.SDM)
|
||||||
|
spearOfDestinyDemo("SDM")
|
||||||
;
|
;
|
||||||
|
|
||||||
final String fileExtension;
|
final String fileExtension;
|
||||||
|
|
||||||
const GameVersion(this.fileExtension);
|
const GameVersion(this.fileExtension);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,23 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
/// Represents a graphical asset decoded from the VGA data chunks.
|
||||||
|
///
|
||||||
|
/// Unlike walls or sprites which are fixed at 64x64, [VgaImage] objects
|
||||||
|
/// have variable dimensions and are used for UI elements like the
|
||||||
|
/// status bar, fonts, title screens, and end-of-episode pictures.
|
||||||
class VgaImage {
|
class VgaImage {
|
||||||
|
/// The horizontal width of the image in pixels.
|
||||||
final int width;
|
final int width;
|
||||||
|
|
||||||
|
/// The vertical height of the image in pixels.
|
||||||
final int height;
|
final int height;
|
||||||
final Uint8List pixels; // 8-bit paletted pixel data
|
|
||||||
|
/// The raw 8-bit paletted pixel data.
|
||||||
|
///
|
||||||
|
/// Each byte in this list represents an index into the game's
|
||||||
|
/// 256-color VGA palette. To render this image, these indices
|
||||||
|
/// must be mapped to 32-bit colors using a [ColorPalette].
|
||||||
|
final Uint8List pixels;
|
||||||
|
|
||||||
VgaImage({
|
VgaImage({
|
||||||
required this.width,
|
required this.width,
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
||||||
|
|
||||||
|
/// Static definitions and logic for objects found in the map's second plane (Object Layer).
|
||||||
|
///
|
||||||
|
/// This includes player start positions, collectibles, static decorations,
|
||||||
|
/// and interactive triggers like level exits.
|
||||||
abstract class MapObject {
|
abstract class MapObject {
|
||||||
// --- Player Spawns ---
|
// --- Player Spawns ---
|
||||||
static const int playerNorth = 19;
|
static const int playerNorth = 19;
|
||||||
@@ -9,6 +13,7 @@ abstract class MapObject {
|
|||||||
static const int playerWest = 22;
|
static const int playerWest = 22;
|
||||||
|
|
||||||
// --- Static Decorations ---
|
// --- Static Decorations ---
|
||||||
|
// These are purely visual sprites that don't move or interact.
|
||||||
static const int waterPuddle = 23;
|
static const int waterPuddle = 23;
|
||||||
static const int greenBarrel = 24;
|
static const int greenBarrel = 24;
|
||||||
static const int chairTable = 25;
|
static const int chairTable = 25;
|
||||||
@@ -84,6 +89,8 @@ abstract class MapObject {
|
|||||||
static const int bossFettgesicht = 223;
|
static const int bossFettgesicht = 223;
|
||||||
|
|
||||||
// --- Enemy Range Constants ---
|
// --- Enemy Range Constants ---
|
||||||
|
// Every enemy type occupies a block of 36 IDs.
|
||||||
|
// Modulo math is used on these ranges to determine orientation and patrol status.
|
||||||
static const int guardStart = 108; // 108-143
|
static const int guardStart = 108; // 108-143
|
||||||
static const int dogStart = 144; // 144-179
|
static const int dogStart = 144; // 144-179
|
||||||
static const int ssStart = 180; // 180-215
|
static const int ssStart = 180; // 180-215
|
||||||
@@ -94,6 +101,7 @@ abstract class MapObject {
|
|||||||
static const int deadGuard = 124; // Decorative only in WL1
|
static const int deadGuard = 124; // Decorative only in WL1
|
||||||
static const int deadAardwolf = 125; // Decorative only in WL1
|
static const int deadAardwolf = 125; // Decorative only in WL1
|
||||||
|
|
||||||
|
/// Calculates the rotation in radians for a player or enemy based on their Map ID.
|
||||||
static double getAngle(int id) {
|
static double getAngle(int id) {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case playerNorth:
|
case playerNorth:
|
||||||
@@ -111,24 +119,23 @@ abstract class MapObject {
|
|||||||
final EnemyType? type = EnemyType.fromMapId(id);
|
final EnemyType? type = EnemyType.fromMapId(id);
|
||||||
if (type == null) return 0.0;
|
if (type == null) return 0.0;
|
||||||
|
|
||||||
// Because all enemies are in blocks of 4, modulo 4 gets the exact angle
|
// Because all enemies are grouped in blocks of 4 (one for each CardinalDirection),
|
||||||
|
// modulo 4 extracts the specific orientation index.
|
||||||
return CardinalDirection.fromEnemyIndex(id % 4).radians;
|
return CardinalDirection.fromEnemyIndex(id % 4).radians;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determines if an object should be spawned on the current difficulty.
|
/// Validates if an object should exist on the map given the current [difficulty].
|
||||||
static bool isDifficultyAllowed(int objId, Difficulty difficulty) {
|
static bool isDifficultyAllowed(int objId, Difficulty difficulty) {
|
||||||
// 1. Dead bodies always spawn
|
// 1. Decorative dead bodies (used in WL1) always spawn.
|
||||||
if (objId == deadGuard || objId == deadAardwolf) return true;
|
if (objId == deadGuard || objId == deadAardwolf) return true;
|
||||||
|
|
||||||
// 2. If it's an enemy, we return true to let it pass through to the
|
// 2. Enemy filtering is handled by the Enemy factory logic,
|
||||||
// Enemy.spawn factory. The factory will safely return null if the
|
// so we pass them through here.
|
||||||
// enemy does not belong on this difficulty.
|
|
||||||
if (EnemyType.fromMapId(objId) != null) {
|
if (EnemyType.fromMapId(objId) != null) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. All non-enemy map objects (keys, ammo, puddles, plants)
|
// 3. Static items (ammo, health, plants) are constant across all difficulties.
|
||||||
// are NOT difficulty-tiered. They always spawn.
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
/// Represents a raw 8-bit PCM digitized sound effect.
|
||||||
class PcmSound {
|
class PcmSound {
|
||||||
final Uint8List bytes;
|
final Uint8List bytes;
|
||||||
PcmSound(this.bytes);
|
PcmSound(this.bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A single instruction for the OPL2 (AdLib) FM synthesis chip.
|
||||||
class ImfInstruction {
|
class ImfInstruction {
|
||||||
|
/// The OPL2 register to write to.
|
||||||
final int register;
|
final int register;
|
||||||
|
|
||||||
|
/// The data value to write to the register.
|
||||||
final int data;
|
final int data;
|
||||||
final int delay; // Delay in 1/700ths of a second
|
|
||||||
|
/// The wait time (delay) before the next instruction, measured in 1/700ths of a second.
|
||||||
|
final int delay;
|
||||||
|
|
||||||
ImfInstruction({
|
ImfInstruction({
|
||||||
required this.register,
|
required this.register,
|
||||||
@@ -17,21 +24,27 @@ class ImfInstruction {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents a music track in the iD Music Format (IMF).
|
||||||
class ImfMusic {
|
class ImfMusic {
|
||||||
final List<ImfInstruction> instructions;
|
final List<ImfInstruction> instructions;
|
||||||
|
|
||||||
ImfMusic(this.instructions);
|
ImfMusic(this.instructions);
|
||||||
|
|
||||||
|
/// Decodes IMF data from a raw byte buffer.
|
||||||
|
///
|
||||||
|
/// IMF files in Wolfenstein 3D are prefixed with a 2-byte little-endian
|
||||||
|
/// length header followed by 4-byte instruction chunks.
|
||||||
factory ImfMusic.fromBytes(Uint8List bytes) {
|
factory ImfMusic.fromBytes(Uint8List bytes) {
|
||||||
List<ImfInstruction> instructions = [];
|
List<ImfInstruction> instructions = [];
|
||||||
|
|
||||||
// Wolfenstein 3D IMF chunks start with a 16-bit length header (little-endian)
|
// The first 2 bytes are the length of the data portion (excluding header).
|
||||||
int actualSize = bytes[0] | (bytes[1] << 8);
|
int actualSize = bytes[0] | (bytes[1] << 8);
|
||||||
|
|
||||||
// Start parsing at index 2 to skip the size header
|
// Calculate limit and ensure we don't overflow the source buffer.
|
||||||
int limit = 2 + actualSize;
|
int limit = 2 + actualSize;
|
||||||
if (limit > bytes.length) limit = bytes.length; // Safety bounds
|
if (limit > bytes.length) limit = bytes.length;
|
||||||
|
|
||||||
|
// Instructions are 4 bytes each: [Reg] [Data] [Delay Low] [Delay High]
|
||||||
for (int i = 2; i < limit - 3; i += 4) {
|
for (int i = 2; i < limit - 3; i += 4) {
|
||||||
instructions.add(
|
instructions.add(
|
||||||
ImfInstruction(
|
ImfInstruction(
|
||||||
@@ -47,8 +60,7 @@ class ImfMusic {
|
|||||||
|
|
||||||
typedef WolfMusicMap = List<int>;
|
typedef WolfMusicMap = List<int>;
|
||||||
|
|
||||||
/// Maps to the original sound indices from the Wolfenstein 3D source code.
|
/// Map indices to original sound effects as defined in the Wolfenstein 3D source.
|
||||||
/// Use these to index into `activeGame.sounds[id]`.
|
|
||||||
abstract class WolfSound {
|
abstract class WolfSound {
|
||||||
// --- Doors & Environment ---
|
// --- Doors & Environment ---
|
||||||
static const int openDoor = 8;
|
static const int openDoor = 8;
|
||||||
|
|||||||
@@ -3,16 +3,22 @@ import 'dart:typed_data';
|
|||||||
typedef Matrix<T> = List<List<T>>;
|
typedef Matrix<T> = List<List<T>>;
|
||||||
typedef SpriteMap = Matrix<int>;
|
typedef SpriteMap = Matrix<int>;
|
||||||
|
|
||||||
|
/// A 64x64 graphical asset (wall texture or enemy sprite).
|
||||||
class Sprite {
|
class Sprite {
|
||||||
|
/// 1D array of 8-bit palette indices (4096 total pixels).
|
||||||
final Uint8List pixels;
|
final Uint8List pixels;
|
||||||
|
|
||||||
Sprite(this.pixels);
|
Sprite(this.pixels);
|
||||||
|
|
||||||
// Factory to convert your 2D matrices into a 1D array during load time
|
/// Factory to convert a 2D [matrix] (x,y) into a 1D column-major array.
|
||||||
|
///
|
||||||
|
/// In Wolf3D, textures are often stored/processed in columns (x * 64 + y)
|
||||||
|
/// to optimize the vertical scaling used in raycasting.
|
||||||
factory Sprite.fromMatrix(Matrix<int> matrix) {
|
factory Sprite.fromMatrix(Matrix<int> matrix) {
|
||||||
final pixels = Uint8List(64 * 64);
|
final pixels = Uint8List(64 * 64);
|
||||||
for (int y = 0; y < 64; y++) {
|
for (int y = 0; y < 64; y++) {
|
||||||
for (int x = 0; x < 64; x++) {
|
for (int x = 0; x < 64; x++) {
|
||||||
|
// Linear index for vertical-strip rendering
|
||||||
pixels[x * 64 + y] = matrix[x][y];
|
pixels[x * 64 + y] = matrix[x][y];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
/// Defines the exact start and end sprite indices for an animation state.
|
/// Represents a slice of indices within a sprite list for animation states.
|
||||||
class SpriteFrameRange {
|
class SpriteFrameRange {
|
||||||
final int start;
|
final int start;
|
||||||
final int end;
|
final int end;
|
||||||
|
|
||||||
const SpriteFrameRange(this.start, this.end);
|
const SpriteFrameRange(this.start, this.end);
|
||||||
|
|
||||||
|
/// Total number of frames in this animation range.
|
||||||
int get length => end - start + 1;
|
int get length => end - start + 1;
|
||||||
|
|
||||||
|
/// Checks if a specific sprite [index] is part of this animation range.
|
||||||
bool contains(int index) => index >= start && index <= end;
|
bool contains(int index) => index >= start && index <= end;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
|
/// Represents a single playable level (map) in the Wolfenstein 3D engine.
|
||||||
|
///
|
||||||
|
/// A level consists of two primary 64x64 grids: one for structural walls
|
||||||
|
/// and one for dynamic objects (entities, pickups, and player starts).
|
||||||
class WolfLevel {
|
class WolfLevel {
|
||||||
|
/// The internal name of the level (e.g., "Level 1" or "Floor 1").
|
||||||
final String name;
|
final String name;
|
||||||
|
|
||||||
|
/// A 64x64 grid of indices pointing to [WolfensteinData.walls].
|
||||||
|
///
|
||||||
|
/// In the original engine, a value of 0 usually indicates an empty space.
|
||||||
final SpriteMap wallGrid;
|
final SpriteMap wallGrid;
|
||||||
|
|
||||||
|
/// A 64x64 grid of indices pointing to game objects, entities, and triggers.
|
||||||
final SpriteMap objectGrid;
|
final SpriteMap objectGrid;
|
||||||
|
|
||||||
|
/// The index of the [ImfMusic] track to play while this level is active.
|
||||||
final int musicIndex;
|
final int musicIndex;
|
||||||
|
|
||||||
const WolfLevel({
|
const WolfLevel({
|
||||||
|
|||||||
@@ -1,13 +1,34 @@
|
|||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
|
/// A complete, immutable container for all assets belonging to a specific
|
||||||
|
/// version of Wolfenstein 3D.
|
||||||
|
///
|
||||||
|
/// This object holds the extracted textures, sounds, music, and levels,
|
||||||
|
/// serving as the primary data source for the game engine's renderer
|
||||||
|
/// and audio system.
|
||||||
class WolfensteinData {
|
class WolfensteinData {
|
||||||
|
/// The version of the game these assets belong to (e.g., Shareware or Retail).
|
||||||
final GameVersion version;
|
final GameVersion version;
|
||||||
|
|
||||||
|
/// The collection of 64x64 wall textures extracted from VSWAP.
|
||||||
final List<Sprite> walls;
|
final List<Sprite> walls;
|
||||||
|
|
||||||
|
/// The collection of billboarding sprites (enemies, items, decorations).
|
||||||
final List<Sprite> sprites;
|
final List<Sprite> sprites;
|
||||||
|
|
||||||
|
/// Digitized 8-bit PCM sound effects.
|
||||||
final List<PcmSound> sounds;
|
final List<PcmSound> sounds;
|
||||||
|
|
||||||
|
/// AdLib (FM Synthesis) sound effects for the OPL2 chip.
|
||||||
final List<PcmSound> adLibSounds;
|
final List<PcmSound> adLibSounds;
|
||||||
|
|
||||||
|
/// The collection of IMF music tracks.
|
||||||
final List<ImfMusic> music;
|
final List<ImfMusic> music;
|
||||||
|
|
||||||
|
/// UI elements, fonts, and full-screen pictures extracted from VGA assets.
|
||||||
final List<VgaImage> vgaImages;
|
final List<VgaImage> vgaImages;
|
||||||
|
|
||||||
|
/// The game's levels, organized into episodes (e.g., Episode 1-6).
|
||||||
final List<Episode> episodes;
|
final List<Episode> episodes;
|
||||||
|
|
||||||
const WolfensteinData({
|
const WolfensteinData({
|
||||||
|
|||||||
@@ -0,0 +1,308 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
|
||||||
|
class SixelRasterizer extends Rasterizer {
|
||||||
|
late Uint8List _screen;
|
||||||
|
late WolfEngine _engine;
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic render(WolfEngine engine, FrameBuffer buffer) {
|
||||||
|
_engine = engine;
|
||||||
|
// We only need 8-bit indices for the 256 VGA colors
|
||||||
|
_screen = Uint8List(buffer.width * buffer.height);
|
||||||
|
return super.render(engine, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void prepareFrame(WolfEngine engine) {
|
||||||
|
// Top half is ceiling color index (25), bottom half is floor color index (29)
|
||||||
|
for (int y = 0; y < viewHeight; y++) {
|
||||||
|
int colorIndex = (y < viewHeight / 2) ? 25 : 29;
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
_screen[y * width + x] = colorIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void drawWallColumn(
|
||||||
|
int x,
|
||||||
|
int drawStart,
|
||||||
|
int drawEnd,
|
||||||
|
int columnHeight,
|
||||||
|
Sprite texture,
|
||||||
|
int texX,
|
||||||
|
double perpWallDist,
|
||||||
|
int side,
|
||||||
|
) {
|
||||||
|
for (int y = drawStart; y < drawEnd; y++) {
|
||||||
|
double relativeY =
|
||||||
|
(y - (-columnHeight ~/ 2 + viewHeight ~/ 2)) / columnHeight;
|
||||||
|
int texY = (relativeY * 64).toInt().clamp(0, 63);
|
||||||
|
|
||||||
|
int colorByte = texture.pixels[texX * 64 + texY];
|
||||||
|
|
||||||
|
// Note: Directional shading is omitted here to preserve strict VGA palette indices.
|
||||||
|
// Sixel uses a fixed 256-color palette, so real-time shading requires a lookup table.
|
||||||
|
_screen[y * width + x] = colorByte;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void drawSpriteStripe(
|
||||||
|
int stripeX,
|
||||||
|
int drawStartY,
|
||||||
|
int drawEndY,
|
||||||
|
int spriteHeight,
|
||||||
|
Sprite texture,
|
||||||
|
int texX,
|
||||||
|
double transformY,
|
||||||
|
) {
|
||||||
|
for (
|
||||||
|
int y = math.max(0, drawStartY);
|
||||||
|
y < math.min(viewHeight, drawEndY);
|
||||||
|
y++
|
||||||
|
) {
|
||||||
|
double relativeY = (y - drawStartY) / spriteHeight;
|
||||||
|
int texY = (relativeY * 64).toInt().clamp(0, 63);
|
||||||
|
|
||||||
|
int colorByte = texture.pixels[texX * 64 + texY];
|
||||||
|
|
||||||
|
// 255 is the "transparent" color index
|
||||||
|
if (colorByte != 255) {
|
||||||
|
_screen[y * width + stripeX] = colorByte;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void drawWeapon(WolfEngine engine) {
|
||||||
|
int spriteIndex = engine.player.currentWeapon.getCurrentSpriteIndex(
|
||||||
|
engine.data.sprites.length,
|
||||||
|
);
|
||||||
|
Sprite weaponSprite = engine.data.sprites[spriteIndex];
|
||||||
|
|
||||||
|
int weaponWidth = (width * 0.5).toInt();
|
||||||
|
int weaponHeight = (viewHeight * 0.8).toInt();
|
||||||
|
|
||||||
|
int startX = (width ~/ 2) - (weaponWidth ~/ 2);
|
||||||
|
int startY =
|
||||||
|
viewHeight - weaponHeight + (engine.player.weaponAnimOffset ~/ 4);
|
||||||
|
|
||||||
|
for (int dy = 0; dy < weaponHeight; dy++) {
|
||||||
|
for (int dx = 0; dx < weaponWidth; dx++) {
|
||||||
|
int texX = (dx * 64 ~/ weaponWidth).clamp(0, 63);
|
||||||
|
int texY = (dy * 64 ~/ weaponHeight).clamp(0, 63);
|
||||||
|
|
||||||
|
int colorByte = weaponSprite.pixels[texX * 64 + texY];
|
||||||
|
if (colorByte != 255) {
|
||||||
|
int drawX = startX + dx;
|
||||||
|
int drawY = startY + dy;
|
||||||
|
if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < viewHeight) {
|
||||||
|
_screen[drawY * width + drawX] = colorByte;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void drawHud(WolfEngine engine) {
|
||||||
|
int statusBarIndex = engine.data.vgaImages.indexWhere(
|
||||||
|
(img) => img.width == 320 && img.height == 40,
|
||||||
|
);
|
||||||
|
if (statusBarIndex == -1) return;
|
||||||
|
|
||||||
|
_blitVgaImage(engine.data.vgaImages[statusBarIndex], 0, 160);
|
||||||
|
|
||||||
|
_drawNumber(1, 32, 176, engine.data.vgaImages);
|
||||||
|
_drawNumber(engine.player.score, 96, 176, engine.data.vgaImages);
|
||||||
|
_drawNumber(3, 120, 176, engine.data.vgaImages);
|
||||||
|
_drawNumber(engine.player.health, 192, 176, engine.data.vgaImages);
|
||||||
|
_drawNumber(engine.player.ammo, 232, 176, engine.data.vgaImages);
|
||||||
|
|
||||||
|
_drawFace(engine);
|
||||||
|
_drawWeaponIcon(engine);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic finalizeFrame() {
|
||||||
|
return toSixelString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// SIXEL ENCODER
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// Converts the 8-bit index buffer into a standard Sixel sequence
|
||||||
|
String toSixelString() {
|
||||||
|
StringBuffer sb = StringBuffer();
|
||||||
|
|
||||||
|
// Start Sixel sequence (q = Sixel format)
|
||||||
|
sb.write('\x1bPq');
|
||||||
|
|
||||||
|
// 1. Define the Palette (and apply damage flash directly to the palette!)
|
||||||
|
double damageIntensity = _engine.player.damageFlash;
|
||||||
|
int redBoost = (150 * damageIntensity).toInt();
|
||||||
|
double colorDrop = 1.0 - (0.5 * damageIntensity);
|
||||||
|
|
||||||
|
for (int i = 0; i < 256; i++) {
|
||||||
|
int color = ColorPalette.vga32Bit[i];
|
||||||
|
int r = color & 0xFF;
|
||||||
|
int g = (color >> 8) & 0xFF;
|
||||||
|
int b = (color >> 16) & 0xFF;
|
||||||
|
|
||||||
|
if (damageIntensity > 0) {
|
||||||
|
r = (r + redBoost).clamp(0, 255);
|
||||||
|
g = (g * colorDrop).toInt().clamp(0, 255);
|
||||||
|
b = (b * colorDrop).toInt().clamp(0, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sixel RGB ranges from 0 to 100
|
||||||
|
int sixelR = (r * 100) ~/ 255;
|
||||||
|
int sixelG = (g * 100) ~/ 255;
|
||||||
|
int sixelB = (b * 100) ~/ 255;
|
||||||
|
|
||||||
|
sb.write('#$i;2;$sixelR;$sixelG;$sixelB');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Encode Image in 6-pixel vertical bands
|
||||||
|
for (int band = 0; band < height; band += 6) {
|
||||||
|
Map<int, Uint8List> colorMap = {};
|
||||||
|
|
||||||
|
// Map out which pixels use which color in this 6px high band
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
for (int yOffset = 0; yOffset < 6; yOffset++) {
|
||||||
|
int y = band + yOffset;
|
||||||
|
if (y >= height) break;
|
||||||
|
|
||||||
|
int colorIdx = _screen[y * width + x];
|
||||||
|
if (!colorMap.containsKey(colorIdx)) {
|
||||||
|
colorMap[colorIdx] = Uint8List(width);
|
||||||
|
}
|
||||||
|
// Set the bit corresponding to the vertical position (0-5)
|
||||||
|
colorMap[colorIdx]![x] |= (1 << yOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the encoded Sixel characters for each color present in the band
|
||||||
|
bool firstColor = true;
|
||||||
|
for (var entry in colorMap.entries) {
|
||||||
|
if (!firstColor)
|
||||||
|
sb.write('\$'); // Carriage return to overlay colors on the same band
|
||||||
|
firstColor = false;
|
||||||
|
|
||||||
|
// Select color index
|
||||||
|
sb.write('#${entry.key}');
|
||||||
|
|
||||||
|
Uint8List cols = entry.value;
|
||||||
|
int currentVal = -1;
|
||||||
|
int runLength = 0;
|
||||||
|
|
||||||
|
// Run-Length Encoding (RLE) loop
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
int val = cols[x];
|
||||||
|
if (val == currentVal) {
|
||||||
|
runLength++;
|
||||||
|
} else {
|
||||||
|
if (runLength > 0) _writeSixelRle(sb, currentVal, runLength);
|
||||||
|
currentVal = val;
|
||||||
|
runLength = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (runLength > 0) _writeSixelRle(sb, currentVal, runLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.write('-'); // Move down to the next 6-pixel band
|
||||||
|
}
|
||||||
|
|
||||||
|
// End Sixel sequence
|
||||||
|
sb.write('\x1b\\');
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _writeSixelRle(StringBuffer sb, int value, int runLength) {
|
||||||
|
String char = String.fromCharCode(value + 63);
|
||||||
|
// Sixel RLE format: !<count><char> (only worth it if count > 3)
|
||||||
|
if (runLength > 3) {
|
||||||
|
sb.write('!$runLength$char');
|
||||||
|
} else {
|
||||||
|
sb.write(char * runLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// PRIVATE HUD HELPERS (Adapted for 8-bit index buffer)
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
void _blitVgaImage(VgaImage image, int startX, int startY) {
|
||||||
|
int planeWidth = image.width ~/ 4;
|
||||||
|
int planeSize = planeWidth * image.height;
|
||||||
|
|
||||||
|
for (int dy = 0; dy < image.height; dy++) {
|
||||||
|
for (int dx = 0; dx < image.width; dx++) {
|
||||||
|
int drawX = startX + dx;
|
||||||
|
int drawY = startY + dy;
|
||||||
|
|
||||||
|
if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < height) {
|
||||||
|
int srcX = dx.clamp(0, image.width - 1);
|
||||||
|
int srcY = dy.clamp(0, image.height - 1);
|
||||||
|
|
||||||
|
int plane = srcX % 4;
|
||||||
|
int sx = srcX ~/ 4;
|
||||||
|
int index = (plane * planeSize) + (srcY * planeWidth) + sx;
|
||||||
|
|
||||||
|
int colorByte = image.pixels[index];
|
||||||
|
if (colorByte != 255) {
|
||||||
|
_screen[drawY * width + drawX] = colorByte;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _drawNumber(
|
||||||
|
int value,
|
||||||
|
int rightAlignX,
|
||||||
|
int startY,
|
||||||
|
List<VgaImage> vgaImages,
|
||||||
|
) {
|
||||||
|
const int zeroIndex = 96;
|
||||||
|
String numStr = value.toString();
|
||||||
|
int currentX = rightAlignX - (numStr.length * 8);
|
||||||
|
|
||||||
|
for (int i = 0; i < numStr.length; i++) {
|
||||||
|
int digit = int.parse(numStr[i]);
|
||||||
|
if (zeroIndex + digit < vgaImages.length) {
|
||||||
|
_blitVgaImage(vgaImages[zeroIndex + digit], currentX, startY);
|
||||||
|
}
|
||||||
|
currentX += 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _drawFace(WolfEngine engine) {
|
||||||
|
int health = engine.player.health;
|
||||||
|
int faceIndex = (health <= 0)
|
||||||
|
? 127
|
||||||
|
: 106 + (((100 - health) ~/ 16).clamp(0, 6) * 3);
|
||||||
|
if (faceIndex < engine.data.vgaImages.length) {
|
||||||
|
_blitVgaImage(engine.data.vgaImages[faceIndex], 136, 164);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _drawWeaponIcon(WolfEngine engine) {
|
||||||
|
int weaponIndex = 89;
|
||||||
|
if (engine.player.hasChainGun) {
|
||||||
|
weaponIndex = 91;
|
||||||
|
} else if (engine.player.hasMachineGun) {
|
||||||
|
weaponIndex = 90;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (weaponIndex < engine.data.vgaImages.length) {
|
||||||
|
_blitVgaImage(engine.data.vgaImages[weaponIndex], 256, 164);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,5 +11,6 @@ export 'src/engine/managers/pushwall_manager.dart';
|
|||||||
export 'src/engine/player/player.dart';
|
export 'src/engine/player/player.dart';
|
||||||
export 'src/engine/rasterizer/ascii_rasterizer.dart';
|
export 'src/engine/rasterizer/ascii_rasterizer.dart';
|
||||||
export 'src/engine/rasterizer/rasterizer.dart';
|
export 'src/engine/rasterizer/rasterizer.dart';
|
||||||
|
export 'src/engine/rasterizer/sixel_rasterizer.dart';
|
||||||
export 'src/engine/rasterizer/software_rasterizer.dart';
|
export 'src/engine/rasterizer/software_rasterizer.dart';
|
||||||
export 'src/engine/wolf_3d_engine_base.dart';
|
export 'src/engine/wolf_3d_engine_base.dart';
|
||||||
|
|||||||
Reference in New Issue
Block a user