Add built-in asset modules for Wolfenstein 3D v1.4 Shareware release

- Implement RetailSfxModule to map sound effects to numeric slots.
- Create SharewareAssetRegistry to manage assets for the Shareware version.
- Introduce SharewareEntityModule to define available enemies in Shareware.
- Add SharewareMenuPicModule to handle menu pictures with runtime offset computation.
- Implement SharewareMusicModule for music routing in Shareware.
- Define keys for entities, HUD elements, menu pictures, and music tracks.
- Create abstract modules for entity, HUD, menu picture, and music assets.
- Add registry resolver to select appropriate asset registry based on game version and data version.
- Update WolfensteinData to include new asset registry exports.
- Modify tests to utilize the new asset registry structure for Shareware and Retail versions.

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-19 13:45:19 +01:00
parent de0d99588e
commit fcda0f9ff4
38 changed files with 1411 additions and 119 deletions

View File

@@ -2,7 +2,6 @@ import 'dart:io';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:wolf_3d_dart/src/data/data_version.dart';
import 'package:wolf_3d_dart/src/data/wl_parser.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';

View File

@@ -2,7 +2,6 @@ import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart' show md5;
import 'package:wolf_3d_dart/src/data/data_version.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
/// The primary parser for Wolfenstein 3D data formats.
@@ -99,6 +98,70 @@ abstract class WLParser {
);
}
/// Async file-by-file equivalent of [loadAsync]; exposed for use from
/// external callers that need a [registryOverride] in async contexts.
static Future<WolfensteinData> loadAsyncWithOverride(
Future<ByteData> Function(String filename) fileFetcher, {
AssetRegistry? registryOverride,
}) async {
GameVersion? detectedVersion;
ByteData? vswap;
for (final version in GameVersion.values) {
try {
vswap = await fileFetcher('VSWAP.${version.fileExtension}');
detectedVersion = version;
break;
} catch (_) {}
}
if (detectedVersion == null || vswap == null) {
throw Exception('Could not locate a valid VSWAP file.');
}
final ext = detectedVersion.fileExtension;
final vswapBytes = vswap.buffer.asUint8List(
vswap.offsetInBytes,
vswap.lengthInBytes,
);
final vswapHash = md5.convert(vswapBytes).toString();
final dataIdentity = DataVersion.fromChecksum(vswapHash);
ByteData gameMapsData;
if (dataIdentity == DataVersion.version10Retail) {
try {
gameMapsData = await fileFetcher('MAPTEMP.$ext');
} catch (_) {
gameMapsData = await fileFetcher('GAMEMAPS.$ext');
}
} else {
gameMapsData = await fileFetcher('GAMEMAPS.$ext');
}
final rawFiles = {
'MAPHEAD.$ext': await fileFetcher('MAPHEAD.$ext'),
'VGADICT.$ext': await fileFetcher('VGADICT.$ext'),
'VGAHEAD.$ext': await fileFetcher('VGAHEAD.$ext'),
'VGAGRAPH.$ext': await fileFetcher('VGAGRAPH.$ext'),
'AUDIOHED.$ext': await fileFetcher('AUDIOHED.$ext'),
'AUDIOT.$ext': await fileFetcher('AUDIOT.$ext'),
};
return load(
version: detectedVersion,
dataIdentity: dataIdentity,
vswap: vswap,
mapHead: rawFiles['MAPHEAD.$ext']!,
gameMaps: gameMapsData,
vgaDict: rawFiles['VGADICT.$ext']!,
vgaHead: rawFiles['VGAHEAD.$ext']!,
vgaGraph: rawFiles['VGAGRAPH.$ext']!,
audioHed: rawFiles['AUDIOHED.$ext']!,
audioT: rawFiles['AUDIOT.$ext']!,
registryOverride: registryOverride,
);
}
/// Parses all raw ByteData upfront and returns a fully populated [WolfensteinData] object.
///
/// By using named parameters, the compiler guarantees no files are missing or misnamed.
@@ -115,28 +178,61 @@ abstract class WLParser {
required ByteData audioHed,
required ByteData audioT,
required DataVersion dataIdentity,
AssetRegistry? registryOverride,
}) {
final isShareware = version == GameVersion.shareware;
// v1.0/1.1 used different HUD strings and had different secret wall bugs
final isLegacy =
dataIdentity == DataVersion.version10Retail ||
dataIdentity == DataVersion.version11Retail;
final audio = parseAudio(audioHed, audioT, version);
final vgaImages = parseVgaImages(vgaDict, vgaHead, vgaGraph);
// Resolve the appropriate registry for this data identity, unless the
// caller has supplied an explicit override (e.g. a modded asset pack).
final registry =
registryOverride ??
_resolveRegistry(
version: version,
dataVersion: dataIdentity,
vgaImages: vgaImages,
);
return WolfensteinData(
version: version,
dataVersion: dataIdentity,
registry: registry,
walls: parseWalls(vswap),
sprites: parseSprites(vswap),
sounds: parseSounds(vswap).map((bytes) => PcmSound(bytes)).toList(),
episodes: parseEpisodes(mapHead, gameMaps, isShareware: isShareware),
vgaImages: parseVgaImages(vgaDict, vgaHead, vgaGraph),
vgaImages: vgaImages,
adLibSounds: audio.adLib,
music: audio.music,
);
}
/// Selects the registry for [version]/[dataVersion] and, for shareware,
/// initialises the menu module's runtime image-offset heuristic.
static AssetRegistry _resolveRegistry({
required GameVersion version,
required DataVersion dataVersion,
required List<VgaImage> vgaImages,
}) {
final context = RegistrySelectionContext(
gameVersion: version,
dataVersion: dataVersion,
);
final registry = const BuiltInAssetRegistryResolver().resolve(context);
// Initialise the shareware menu heuristic now that images are available.
if (registry is SharewareAssetRegistry) {
final sizes = vgaImages
.map((img) => (width: img.width, height: img.height))
.toList();
registry.sharewareMenu.initWithImageSizes(sizes);
}
return registry;
}
/// Extracts the 64x64 wall textures from the VSWAP file.
///
/// Wall textures are stored sequentially at the beginning of the chunks array.

View File

@@ -1,7 +1,6 @@
import 'dart:typed_data';
import 'package:crypto/crypto.dart'; // Import for MD5
import 'package:wolf_3d_dart/src/data/data_version.dart'; // Import your enum
import 'package:crypto/crypto.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'io/discovery_stub.dart'
@@ -33,6 +32,10 @@ class WolfensteinLoader {
/// 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.
///
/// Supply [registryOverride] to use a fully custom [AssetRegistry] instead
/// of the built-in version-detected registry. This is the primary extension
/// point for modded or custom asset packs.
static WolfensteinData loadFromBytes({
required GameVersion version,
required ByteData? vswap,
@@ -43,6 +46,7 @@ class WolfensteinLoader {
required ByteData? vgaGraph,
required ByteData? audioHed,
required ByteData? audioT,
AssetRegistry? registryOverride,
}) {
// 1. Validation Check
final Map<String, ByteData?> files = {
@@ -67,7 +71,7 @@ class WolfensteinLoader {
);
}
// 2. Identify identity for specialized parsing (e.g., MAPTEMP vs GAMEMAPS)
// 2. Identify identity for specialised parsing (e.g., MAPTEMP vs GAMEMAPS).
final vswapBytes = vswap!.buffer.asUint8List(
vswap.offsetInBytes,
vswap.lengthInBytes,
@@ -75,10 +79,9 @@ class WolfensteinLoader {
final hash = md5.convert(vswapBytes).toString();
final dataIdentity = DataVersion.fromChecksum(hash);
// 3. Pass-through to parser with the detected identity
// 3. Pass-through to parser with the detected identity and optional override.
return WLParser.load(
version: version,
// Correctly identifies v1.0/1.1/1.4
dataIdentity: dataIdentity,
vswap: vswap,
mapHead: mapHead!,
@@ -88,6 +91,7 @@ class WolfensteinLoader {
vgaGraph: vgaGraph!,
audioHed: audioHed!,
audioT: audioT!,
registryOverride: registryOverride,
);
}
}