Преглед на файлове

Removed all included libraries

Iñigo Valentin преди 1 седмица
родител
ревизия
383986c1f1

+ 3 - 2
.github/copilot-instructions.md

@@ -13,9 +13,10 @@ V-Gears is a free, cross-platform reimplementation of the Final Fantasy VII engi
 - `src/common/` — application base class and file utilities shared by all targets.
 - `src/core/` — engine runtime: audio, console, entities, walkmesh, UI, particles, savemap, `ScriptManager` (Lua bindings via sol2 and system LuaJIT).
 - `src/data/` — FF7 binary format parsers (LGP, LZS, HRC, TEX, field files, worldmap TXZ...). Most formats follow a triplet: `*File` (data), `*FileSerializer` (read/write), `*FileManager` (Ogre resource loading).
-- `src/installer/` — Qt data installer (`v-gears-installer`); includes the script `decompiler/` that converts field/world scripts into Lua.
+- `src/installer/` — Qt data installer (`v-gears-installer`); includes the script `decompiler/` that converts field/world scripts into Lua and the SfxDump audio converter.
 - `src/map/`, `src/modules/`, `src/viewer/` — field map code, game modules (e.g. worldmap), and the model viewer.
-- `lib/` — bundled third-party code (sol2 single header, sfxdump). Do not restyle, modernize, or refactor these. LuaJIT is a system dependency.
+- `src/third_party/sol/` — vendored sol2 single header. Do not restyle, modernize, or refactor it.
+- LuaJIT is a system dependency.
 - `test/` — Boost.Test suite mirroring `src/` paths; sources are listed manually in `test/CMakeLists.txt`.
 - `data/` — runtime configuration only (Ogre plugins/resources cfgs). Not game data.
 - `doc/` — all guides; `doc/code/FFVII/` documents the original binary formats.

+ 1 - 4
CMakeLists.txt

@@ -47,7 +47,7 @@ if(NOT CMAKE_BUILD_TYPE)
     set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build" FORCE)
 endif()
 
-# LuaJIT is a system dependency. sol2 is header-only; sfxdump remains bundled.
+# LuaJIT is a system dependency; sol2 is header-only.
 find_package(PkgConfig QUIET)
 if(PKG_CONFIG_FOUND)
     pkg_check_modules(LUAJIT QUIET luajit)
@@ -74,9 +74,6 @@ else()
     set(VGEARS_LUAJIT_LIBRARIES ${VGEARS_LUAJIT_LIBRARY})
 endif()
 
-# Build remaining bundled external libraries.
-add_subdirectory(lib)
-
 # Build v-gears (and, if requested, the installer).
 add_subdirectory(src)
 

+ 0 - 3
lib/CMakeLists.txt

@@ -1,3 +0,0 @@
-add_subdirectory(sfxdump) # SFXDump has its own configuration.
-
-# sol2 (lib/sol2) is header-only, and LuaJIT is provided by the system.

+ 0 - 1
lib/sfxdump/CMakeLists.txt

@@ -1 +0,0 @@
-add_executable ( sfxdump sfxdump.c )

+ 0 - 120
lib/sfxdump/sfxdump.c

@@ -1,120 +0,0 @@
-#include "structs.h"
-#include <stdlib.h>
-#include <stdio.h>
-#include <string.h>
-
-
-
-Fmt_chunk read_Fmt_chunk(FILE* fmt)
-{
-	Fmt_chunk chunk = {.size = sizeof chunk.adpcm};
-
-	memcpy(chunk.id, "fmt ", sizeof chunk.id);
-	fread(&chunk.adpcm, sizeof chunk.adpcm, 1, fmt);
-
-	return chunk;
-}
-
-
-
-Loop_chunk read_Loop_chunk(FfWav_header const* header)
-{
-	Loop_chunk chunk = {
-		.size  = sizeof(uint32_t) * 2,
-		.start = header->start,
-		.end   = header->end
-	};
-
-	memcpy(chunk.id, "fflp", sizeof chunk.id);
-
-	return chunk;
-}
-
-
-
-Data_chunk* read_Data_chunk(FfWav_header const* header, FILE* data)
-{
-	Data_chunk* chunk = malloc(sizeof *chunk * header->len);
-
-	memcpy(chunk->id, "data", sizeof chunk->id);
-	chunk->size = header->len;
-	fread(chunk->data, header->len, 1, data);
-
-	return chunk;
-}
-
-
-
-Riff_header init_riff_header()
-{
-	Riff_header riff;
-	memcpy(&riff.id, "RIFF", sizeof riff.id);
-	memcpy(&riff.format, "WAVE", sizeof riff.format);
-	riff.size = 0;
-	return riff;
-}
-
-
-
-
-int main(int argc, char* argv[])
-{
-	if (argc != 4) {
-		printf("Usage: sfxdump fmt_path dat_path target_dir");
-		return 1;
-	}
-
-	FILE* fmt  = fopen(argv[1], "rb");
-	FILE* dat  = fopen(argv[2], "rb");
-
-	if (!fmt || !dat) {
-		printf("Could not open .fmt and / or .dat file");
-		return 1;
-	}
-
-	//printf("Dumping sfx 0 - 750 to %s\n", argv[3]);
-
-	for (int count = 0; count < 750; ++count) {
-
-		FfWav_header header = {0};
-		fread(&header, sizeof header, 1, fmt);
-
-		if (!header.len) {
-			fseek(fmt, sizeof(WAVEFORMATEX), SEEK_CUR);
-			continue;
-		}
-
-		Riff_header riff = init_riff_header();
-		Fmt_chunk format = read_Fmt_chunk(fmt);
-		Loop_chunk loop  = read_Loop_chunk(&header);
-		Data_chunk* data = read_Data_chunk(&header, dat);
-		riff.size = sizeof riff.format + sizeof format + sizeof *data + data->size;
-
-		if (header.loop)
-			riff.size += sizeof loop;
-
-		char path[260];
-		sprintf(path, "%s/%d.wav", argv[3], count);
-		FILE* out_wav = fopen(path, "wb");
-
-		if (!out_wav) {
-			//printf("Error opening %s\n", path);
-			continue;
-		}
-
-		fwrite(&riff, sizeof riff, 1, out_wav);
-		fwrite(&format, sizeof format, 1, out_wav);
-		fwrite(data, sizeof *data + data->size, 1, out_wav);
-
-		if (header.loop) {
-			//printf("Appending loop data for %d\n", count);
-			fwrite(&loop, sizeof loop, 1, out_wav);
-		}
-
-		fclose(out_wav);
-		free(data);
-	}
-
-	fclose(fmt);
-	fclose(dat);
-}

+ 1 - 2
src/CMakeLists.txt

@@ -834,6 +834,7 @@ if(_test_removed_count GREATER 0)
 endif()
 
 add_library(libvgears STATIC ${VGEARS_SOURCE_FILES})
+target_sources(libvgears PRIVATE installer/SfxDump.c)
 # Ensure generated ui headers are created before compiling libvgears
 if(DEFINED _GENERATED_UI_TARGET)
     add_dependencies(libvgears ${_GENERATED_UI_TARGET})
@@ -862,7 +863,6 @@ target_include_directories(libvgears
         ${OGRE_INCLUDE_DIRS}
         ${OGRE_INCLUDE_DIRS}/Overlay
         ${OGRE_INCLUDE_DIRS}/Bites
-        ${CMAKE_SOURCE_DIR}/lib/sol2
         ${VGEARS_LUAJIT_INCLUDE_DIRS}
         ${QT_INCLUDE_DIRS}
 )
@@ -1009,7 +1009,6 @@ target_include_directories(v-gears
         ${OGRE_INCLUDE_DIRS}
         ${OGRE_INCLUDE_DIRS}/Overlay
         ${OGRE_INCLUDE_DIRS}/Bites
-        ${CMAKE_SOURCE_DIR}/lib/sol2
         ${VGEARS_LUAJIT_INCLUDE_DIRS}
     ${QT_INCLUDE_DIRS}
 )

+ 1 - 1
src/LuaIncludes.h

@@ -34,5 +34,5 @@
 // headers (lua.h, lualib.h, lauxlib.h) are always available after this.
 // Note: on LuaJIT, sol.hpp bundles a compat53 shim that exposes the 5.2-style
 // lua_resume(L, from, nargs) signature.
-#include <sol.hpp>
+#include <third_party/sol/sol.hpp>
 

+ 20 - 62
src/installer/MediaDataInstaller.cpp

@@ -22,28 +22,14 @@
 #include <OgreColourValue.h>
 #include <boost/format.hpp>
 #include <boost/algorithm/string.hpp>
-#include <boost/predef/os.h>
 #include <boost/filesystem.hpp>
 #include <tinyxml.h>
 #include "DiskImage.h"
 #include "MediaDataInstaller.h"
+#include "SfxDump.h"
 #include "data/VGearsLGPArchive.h"
 #include "data/VGearsTexFile.h"
 #include "TexFile.h"
-#if (BOOST_OS_WINDOWS)
-#include <stdlib>
-#elif (BOOST_OS_SOLARIS)
-#include <stdlib>
-#include <limits>
-#elif (BOOST_OS_LINUX)
-#include <unistd.h>
-#include <limits.h>
-#elif (BOOST_OS_MACOS)
-#include <mach-o/dyld.h>
-#elif (BOOST_OS_BSD_FREE)
-#include <sys/types>
-#include <sys/sysctl>
-#endif
 
 int MediaDataInstaller::TOTAL_SOUNDS = 750;
 
@@ -64,46 +50,6 @@ MediaDataInstaller::MediaDataInstaller(
 }
 
 
-std::string MediaDataInstaller::GetExecutablePath(){
-    #if (BOOST_OS_WINDOWS)
-    char *exe_path;
-    if (_get_pgmptr(&exe_path) != 0) exe_path = "";
-    #elif (BOOST_OS_SOLARIS)
-    char exe_path[PATH_MAX];
-    if (realpath(getexecname(), exe_path) == NULL) exe_path[0] = '\0';
-    #elif (BOOST_OS_LINUX)
-    char exe_path[PATH_MAX];
-    ssize_t len = ::readlink("/proc/self/exe", exe_path, sizeof(exe_path));
-    if (len == -1 || len == sizeof(exe_path)) len = 0;
-    exe_path[len] = '\0';
-    #elif (BOOST_OS_MACOS)
-    char exe_path[PATH_MAX];
-    uint32_t len = sizeof(exe_path);
-    if (_NSGetExecutablePath(exe_path, &len) != 0) exe_path[0] = '\0'; // buffer too small (!)
-    else {
-        // resolve symlinks, ., .. if possible
-        char *canonical_path = realpath(exe_path, NULL);
-        if (canonical_path != NULL){
-            strncpy(exe_path,canonical_path,len);
-            free(canonical_path);
-        }
-    }
-    #elif (BOOST_OS_BSD_FREE)
-    char exe_path[2048];
-    int mib[4];
-    mib[0] = CTL_KERN;
-    mib[1] = KERN_PROC;
-    mib[2] = KERN_PROC_PATHNAME;
-    mib[3] = -1;
-    size_t len = sizeof(exe_path);
-    if (sysctl(mib, 4, exe_path, &len, NULL, 0) != 0) exe_path[0] = '\0';
-    #endif
-    return
-      strlen(exe_path) > 0
-      ? boost::filesystem::path(exe_path).remove_filename().make_preferred().string() : std::string();
-}
-
-
 void MediaDataInstaller::PopulateMaps(){
     // Most data here comes from https://forums.qhimm.com/index.php?topic=15786.0
     sound_map_[0] = "Cursor";
@@ -1369,11 +1315,23 @@ void MediaDataInstaller::InstallSprites(){
 }
 
 int MediaDataInstaller::InstallSoundsInit(){
-    // SFXDump handles the wav conversion
-    std::string command = (boost::format(
-      "%1%/sfxdump %2%data/sound/audio.fmt %2%data/sound/audio.dat %3%audio/sounds/"
-    ) % GetExecutablePath() % disk_image_.getContentPath() % output_dir_).str();    
-    std::system(command.c_str());
+    std::unique_ptr<std::string> fmt_path = disk_image_.filePath("data/sound/audio.fmt");
+    std::unique_ptr<std::string> data_path = disk_image_.filePath("data/sound/audio.dat");
+    std::string sound_dir =
+      (boost::filesystem::path(output_dir_) / "audio" / "sounds").string();
+
+    if (!fmt_path){
+        std::cerr << "MediaDataInstaller: could not find data/sound/audio.fmt in the selected game data."
+                  << std::endl;
+    }
+    else if (!data_path){
+        std::cerr << "MediaDataInstaller: could not find data/sound/audio.dat in the selected game data."
+                  << std::endl;
+    }
+    else if (SfxDumpConvert(fmt_path->c_str(), data_path->c_str(), sound_dir.c_str()) != 0){
+        std::cerr << "MediaDataInstaller: failed to convert FFVII sound effects."
+                  << std::endl;
+    }
     processed_sounds_ = 0;
     return TOTAL_SOUNDS;
 }
@@ -1389,7 +1347,7 @@ bool MediaDataInstaller::InstallSounds(){
               "ffmpeg -hide_banner -loglevel panic -y  -i %1%audio/sounds/%2%.wav %1%audio/sounds/%2%.ogg"
             ) % output_dir_ % processed_sounds_).str();
             std::system(command.c_str());
-            //std::cout << "    ADD SOUND " << processed_sounds_ << ".ogg" << std::endl;
+            std::cout << "    ADD SOUND " << processed_sounds_ << ".ogg" << std::endl;
             sounds_.push_back("audio/sounds/" + std::to_string(processed_sounds_) + ".ogg");
             // Remove the wav file.
             if (!keep_originals_)
@@ -1397,7 +1355,7 @@ bool MediaDataInstaller::InstallSounds(){
         }
         else{
             sounds_.push_back("audio/sounds/INVALID.ogg");
-            //std::cout << "    -- ADD SOUND INVALID.ogg" << std::endl;
+            std::cout << "    -- ADD SOUND INVALID.ogg" << std::endl;
         }
     }
     processed_sounds_ ++;

+ 0 - 8
src/installer/MediaDataInstaller.h

@@ -125,14 +125,6 @@ class MediaDataInstaller{
          */
         static int TOTAL_SOUNDS;
 		
-		/*
-		 * Retrieves the path to the currently running executable.
-		 *
-		 * @return Path to the folder containig the current executable, or an empty string in case
-		 * of failure.
-		 */
-		std::string GetExecutablePath();
-
         /**
          * The disk image to read the original data from.
          */

+ 174 - 0
src/installer/SfxDump.c

@@ -0,0 +1,174 @@
+#include "SfxDump.h"
+#include "SfxDumpStructures.h"
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define SFX_DUMP_SOUND_COUNT 750
+
+static int read_exact(FILE* file, void* buffer, size_t size){
+	return fread(buffer, 1, size, file) == size;
+}
+
+static int write_exact(FILE* file, const void* buffer, size_t size){
+	return fwrite(buffer, 1, size, file) == size;
+}
+
+static int convert_sound(
+	FILE* fmt_file,
+	FILE* data_file,
+	const FfWav_header* header,
+	const char* target_dir,
+	int sound_index
+){
+	Fmt_chunk format = {0};
+	Loop_chunk loop = {0};
+	Riff_header riff = {0};
+	Data_chunk* data_chunk = NULL;
+	FILE* output_file = NULL;
+	char* output_path = NULL;
+	int result = 1;
+	size_t dir_length = strlen(target_dir);
+	int add_separator = dir_length > 0
+		&& target_dir[dir_length - 1] != '/'
+		&& target_dir[dir_length - 1] != '\\';
+	size_t path_capacity;
+	uint64_t riff_size;
+
+	if (dir_length > SIZE_MAX - 16 - (size_t)add_separator) {
+		fprintf(stderr, "SfxDump: output directory path is too long.\n");
+		return 1;
+	}
+	path_capacity = dir_length + (size_t)add_separator + 16;
+	output_path = malloc(path_capacity);
+	if (!output_path) {
+		fprintf(stderr, "SfxDump: unable to allocate output path.\n");
+		return 1;
+	}
+
+	if (snprintf(
+		output_path, path_capacity, "%s%s%d.wav", target_dir,
+		add_separator ? "/" : "", sound_index
+	) >= (int)path_capacity){
+		fprintf(stderr, "SfxDump: output path is too long.\n");
+		goto cleanup;
+	}
+
+	memcpy(format.id, "fmt ", sizeof format.id);
+	format.size = sizeof format.adpcm;
+	if (!read_exact(fmt_file, &format.adpcm, sizeof format.adpcm)){
+		fprintf(stderr, "SfxDump: truncated format record for sound %d.\n", sound_index);
+		goto cleanup;
+	}
+
+	memcpy(loop.id, "fflp", sizeof loop.id);
+	loop.size = sizeof loop.start + sizeof loop.end;
+	loop.start = header->start;
+	loop.end = header->end;
+
+	if ((uint64_t)header->len > (uint64_t)SIZE_MAX - sizeof *data_chunk){
+		fprintf(stderr, "SfxDump: sound %d is too large.\n", sound_index);
+		goto cleanup;
+	}
+	data_chunk = malloc(sizeof *data_chunk + (size_t)header->len);
+	if (!data_chunk){
+		fprintf(stderr, "SfxDump: unable to allocate sound %d.\n", sound_index);
+		goto cleanup;
+	}
+	memcpy(data_chunk->id, "data", sizeof data_chunk->id);
+	data_chunk->size = header->len;
+	if (!read_exact(data_file, data_chunk->data, header->len)){
+		fprintf(stderr, "SfxDump: truncated audio data for sound %d.\n", sound_index);
+		goto cleanup;
+	}
+
+	memcpy(riff.id, "RIFF", sizeof riff.id);
+	memcpy(riff.format, "WAVE", sizeof riff.format);
+	riff_size = sizeof riff.format + sizeof format + sizeof *data_chunk + header->len;
+	if (header->loop) riff_size += sizeof loop;
+	if (riff_size > UINT32_MAX){
+		fprintf(stderr, "SfxDump: WAV file for sound %d is too large.\n", sound_index);
+		goto cleanup;
+	}
+	riff.size = (uint32_t)riff_size;
+
+	output_file = fopen(output_path, "wb");
+	if (!output_file){
+		fprintf(stderr, "SfxDump: unable to create %s.\n", output_path);
+		goto cleanup;
+	}
+	if (!write_exact(output_file, &riff, sizeof riff)
+		|| !write_exact(output_file, &format, sizeof format)
+		|| !write_exact(output_file, data_chunk, sizeof *data_chunk + header->len)
+		|| (header->loop && !write_exact(output_file, &loop, sizeof loop))){
+		fprintf(stderr, "SfxDump: failed writing %s.\n", output_path);
+		goto cleanup;
+	}
+	if (fclose(output_file) != 0){
+		output_file = NULL;
+		fprintf(stderr, "SfxDump: failed closing %s.\n", output_path);
+		goto cleanup;
+	}
+	output_file = NULL;
+	result = 0;
+
+cleanup:
+	if (output_file) fclose(output_file);
+	free(data_chunk);
+	free(output_path);
+	return result;
+}
+
+int SfxDumpConvert(const char* fmt_path, const char* data_path, const char* target_dir){
+	FILE* fmt_file;
+	FILE* data_file;
+	int result = 0;
+
+	if (!fmt_path || !data_path || !target_dir || target_dir[0] == '\0'){
+		fprintf(stderr, "SfxDump: invalid conversion path.\n");
+		return 1;
+	}
+
+	fmt_file = fopen(fmt_path, "rb");
+	if (!fmt_file){
+		fprintf(stderr, "SfxDump: unable to open %s.\n", fmt_path);
+		return 1;
+	}
+	data_file = fopen(data_path, "rb");
+	if (!data_file){
+		fprintf(stderr, "SfxDump: unable to open %s.\n", data_path);
+		fclose(fmt_file);
+		return 1;
+	}
+
+	for (int sound_index = 0; sound_index < SFX_DUMP_SOUND_COUNT; ++sound_index){
+		FfWav_header header = {0};
+		size_t header_size = fread(&header, 1, sizeof header, fmt_file);
+
+		if (header_size == 0 && feof(fmt_file)) break;
+		if (header_size != sizeof header){
+			fprintf(stderr, "SfxDump: truncated format table at sound %d.\n", sound_index);
+			result = 1;
+			break;
+		}
+
+		if (!header.len){
+			if (fseek(fmt_file, (long)sizeof(WAVEFORMATEX), SEEK_CUR) != 0){
+				fprintf(stderr, "SfxDump: truncated empty format record at sound %d.\n", sound_index);
+				result = 1;
+				break;
+			}
+			continue;
+		}
+
+		if (convert_sound(fmt_file, data_file, &header, target_dir, sound_index) != 0){
+			result = 1;
+			break;
+		}
+	}
+
+	if (fclose(fmt_file) != 0) result = 1;
+	if (fclose(data_file) != 0) result = 1;
+	return result;
+}

+ 19 - 0
src/installer/SfxDump.h

@@ -0,0 +1,19 @@
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Converts FFVII audio data into individual WAV files.
+ *
+ * @param fmt_path Path to the audio format table.
+ * @param data_path Path to the audio data archive.
+ * @param target_dir Directory where WAV files are written.
+ * @return 0 on success, nonzero if conversion fails.
+ */
+int SfxDumpConvert(const char* fmt_path, const char* data_path, const char* target_dir);
+
+#ifdef __cplusplus
+}
+#endif

+ 0 - 0
lib/sfxdump/structs.h → src/installer/SfxDumpStructures.h


+ 0 - 0
lib/sol2/sol.hpp → src/third_party/sol/sol.hpp


+ 3 - 1
test/CMakeLists.txt

@@ -1,5 +1,5 @@
 # Find packages required to compile tests.
-find_package(Boost COMPONENTS unit_test_framework REQUIRED)
+find_package(Boost COMPONENTS filesystem unit_test_framework REQUIRED)
 
 
 # Compiler options.
@@ -163,6 +163,7 @@ set(SOURCE_FILES
     installer/MediaDataInstaller.cpp
     installer/ModelsAndAnimationsDb.cpp
     installer/ScopedLgp.cpp
+    installer/SfxDump.cpp
     installer/TexFile.cpp
     installer/VGearsUtility.cpp
     installer/WorldInstaller.cpp
@@ -270,6 +271,7 @@ target_link_libraries(v-gears-tests
     ${OGGVORBIS_LIBRARIES}
     ${TinyXML_LIBRARIES}
     ${Boost_LIBRARIES}
+    ${BOOST_LINK_LIBS}
     ${OGRE_LIBRARIES}
     ${ZLIB_LIBRARIES}
 )

+ 69 - 0
test/installer/SfxDump.cpp

@@ -0,0 +1,69 @@
+#include <boost/filesystem.hpp>
+#include <boost/test/unit_test.hpp>
+#include <cstdint>
+#include <cstring>
+#include <fstream>
+#include <iterator>
+#include <string>
+#include <vector>
+
+#include "SfxDump.h"
+#include "SfxDumpStructures.h"
+
+BOOST_AUTO_TEST_CASE(SfxDumpWritesLoopedWaveFile){
+    const boost::filesystem::path temp_dir = boost::filesystem::temp_directory_path()
+      / boost::filesystem::unique_path("vgears-sfxdump-%%%%-%%%%");
+    boost::filesystem::create_directories(temp_dir);
+
+    const boost::filesystem::path fmt_path = temp_dir / "audio.fmt";
+    const boost::filesystem::path data_path = temp_dir / "audio.dat";
+    const boost::filesystem::path sound_dir = temp_dir / "sounds";
+    boost::filesystem::create_directories(sound_dir);
+
+    {
+        std::ofstream fmt_file(fmt_path.string().c_str(), std::ios::binary);
+        FfWav_header empty_header = {0};
+        WAVEFORMATEX empty_format = {0};
+        FfWav_header sound_header = {4, 0, 1, 0, 2, 3};
+        ADPCMWAVEFORMAT adpcm_format = {0};
+
+        fmt_file.write(reinterpret_cast<const char*>(&empty_header), sizeof empty_header);
+        fmt_file.write(reinterpret_cast<const char*>(&empty_format), sizeof empty_format);
+        fmt_file.write(reinterpret_cast<const char*>(&sound_header), sizeof sound_header);
+        fmt_file.write(reinterpret_cast<const char*>(&adpcm_format), sizeof adpcm_format);
+        BOOST_REQUIRE(fmt_file.good());
+    }
+    {
+        std::ofstream data_file(data_path.string().c_str(), std::ios::binary);
+        const unsigned char sample_data[] = {0x10, 0x20, 0x30, 0x40};
+        data_file.write(reinterpret_cast<const char*>(sample_data), sizeof sample_data);
+        BOOST_REQUIRE(data_file.good());
+    }
+
+    BOOST_REQUIRE_EQUAL(
+      SfxDumpConvert(fmt_path.string().c_str(), data_path.string().c_str(), sound_dir.string().c_str()),
+      0
+    );
+
+    const boost::filesystem::path wav_path = sound_dir / "1.wav";
+    BOOST_REQUIRE(boost::filesystem::exists(wav_path));
+    BOOST_CHECK(!boost::filesystem::exists(sound_dir / "0.wav"));
+
+    std::ifstream wav_file(wav_path.string().c_str(), std::ios::binary);
+    std::vector<char> wav_bytes(
+      (std::istreambuf_iterator<char>(wav_file)), std::istreambuf_iterator<char>()
+    );
+    const size_t expected_size = sizeof(Riff_header) + sizeof(Fmt_chunk)
+      + sizeof(Data_chunk) + 4 + sizeof(Loop_chunk);
+    BOOST_REQUIRE_EQUAL(wav_bytes.size(), expected_size);
+    BOOST_CHECK_EQUAL(std::string(wav_bytes.data(), 4), "RIFF");
+    BOOST_CHECK_EQUAL(std::string(wav_bytes.data() + 8, 4), "WAVE");
+    BOOST_CHECK_EQUAL(std::string(wav_bytes.data() + 70, 4), "data");
+    BOOST_CHECK_EQUAL(std::string(wav_bytes.data() + 82, 4), "fflp");
+
+    uint32_t riff_size = 0;
+    std::memcpy(&riff_size, wav_bytes.data() + 4, sizeof riff_size);
+    BOOST_CHECK_EQUAL(riff_size, expected_size - 8);
+
+    boost::filesystem::remove_all(temp_dir);
+}