diff --git a/Makefile b/Makefile index 265f57e16..d42ba9be3 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,14 @@ ifeq ($(shell uname -s),Linux) LDFLAGS += -Wl,-z,defs -lm -lEGL -lGL endif +ifeq ($(shell uname -s),OpenBSD) + CFLAGS += -I/usr/local/include + LDFLAGS += -L/usr/local/lib -lm -lGL + MPQ_LIB := -lmpq +else + MPQ_LIB := -lstorm +endif + ifeq ($(shell uname -s),Darwin) ifeq ($(shell uname -m),arm64) HOMEBREW_PREFIX := /opt/homebrew @@ -56,10 +64,10 @@ $(LIB_DIR)/libgame.so: $(GAME_OBJS) $(LIB_DIR) $(CC) -shared -o $@ $(GAME_OBJS) $(LDFLAGS) -lcmath3 $(LIB_DIR)/librenderer.so: cmath3 $(RENDERER_OBJS) $(LIB_DIR) - $(CC) -shared -o $@ $(RENDERER_OBJS) $(LDFLAGS) -lcmath3 -lSDL2 -lstorm -ljpeg + $(CC) -shared -o $@ $(RENDERER_OBJS) $(LDFLAGS) -lcmath3 -lSDL2 $(MPQ_LIB) -ljpeg $(BIN_DIR)/openwarcraft3: cmath3 game renderer $(APP_OBJS) $(BIN_DIR) - $(CC) -o $@ $(APP_OBJS) -Wl,-rpath,'$$ORIGIN/../lib' $(LDFLAGS) -lcmath3 -lSDL2 -lstorm -lgame -lrenderer + $(CC) -o $@ $(APP_OBJS) -Wl,-rpath,'$$ORIGIN/../lib' $(LDFLAGS) -lcmath3 -lSDL2 $(MPQ_LIB) -lgame -lrenderer $(OBJ_DIR)/%.o: src/%.c @mkdir -p $(dir $@) diff --git a/README.md b/README.md index f18a05e75..065c4b45b 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ This will run the built `openwarcraft3` executable from the `build/bin` folder. The Makefile build requires the following libraries installed on your system: -* **StormLib** — for MPQ archive support +* **StormLib** (or **libmpq** on OpenBSD) — for MPQ archive support * **SDL2** — for windowing and input * **libjpeg** — for JPEG image decoding @@ -91,6 +91,14 @@ On Linux, use your distribution package manager, e.g., for Ubuntu: sudo apt-get install libsdl2-dev libjpeg-dev libstorm-dev ``` +On OpenBSD, install the packages using pkg_add: + +```bash +doas pkg_add sdl2 libmpq jpeg +``` + +**Note:** OpenBSD support uses libmpq instead of StormLib, as StormLib is not well-supported on OpenBSD. The code automatically detects OpenBSD and uses the appropriate library. + --- # Project Architecture & Code Structure diff --git a/premake5.lua b/premake5.lua index 3ec14d6c4..e53a0d749 100644 --- a/premake5.lua +++ b/premake5.lua @@ -33,6 +33,9 @@ solution "openwarcraft3" ["ALWAYS_SEARCH_USER_PATHS"] = "YES", -- This is the minimum version of macos we'll be able to run on }; + filter {"system:bsd"} + defines { "OW3_PLATFORM_BSD" } + filter {"system:windows"} defines { "OW3_PLATFORM_WINDOWS" } @@ -65,6 +68,10 @@ project "openwarcraft3" links { "gdi32", "kernel32", "psapi" } filter "system:linux" links { "dl", "GL", "pthread", "X11" } + filter "system:bsd" + links { "GL", "pthread", "mpq" } + includedirs { "/usr/local/include" } + libdirs { "/usr/local/lib" } filter "system:macosx" links { "QuartzCore.framework", "Metal.framework", "Cocoa.framework", "IOKit.framework", "CoreVideo.framework" } linkoptions { diff --git a/src/common/common.c b/src/common/common.c index 4ee3a52b2..01a39d1d2 100644 --- a/src/common/common.c +++ b/src/common/common.c @@ -1,6 +1,6 @@ #include "common.h" -#include +#include "mpq_adapter.h" #define MAXPRINTMSG 4096 diff --git a/src/common/mpq_adapter.c b/src/common/mpq_adapter.c new file mode 100644 index 000000000..8bbd5e591 --- /dev/null +++ b/src/common/mpq_adapter.c @@ -0,0 +1,296 @@ +#include "mpq_adapter.h" + +#ifdef USE_LIBMPQ + +#include +#include +#include +#include + +/* Windows types compatibility - these should be defined in shared.h */ +#ifndef TRUE +#define TRUE 1 +#endif +#ifndef FALSE +#define FALSE 0 +#endif + +/* SIZE_MAX may not be defined in older systems */ +#ifndef SIZE_MAX +#define SIZE_MAX ((size_t)-1) +#endif + +/* Open an MPQ archive */ +BOOL MPQ_OpenArchive(LPCSTR szMpqName, DWORD dwPriority, DWORD dwFlags, MPQ_ARCHIVE* phMpq) { + mpq_archive_s* archive = NULL; + int result = libmpq__archive_open(&archive, (char*)szMpqName, -1); + + if (result != 0 || archive == NULL) { + return FALSE; + } + + *phMpq = archive; + return TRUE; +} + +/* Close an MPQ archive */ +BOOL MPQ_CloseArchive(MPQ_ARCHIVE hMpq) { + if (hMpq == NULL) { + return FALSE; + } + + int result = libmpq__archive_close(hMpq); + return (result == 0) ? TRUE : FALSE; +} + +/* Open a file within an MPQ archive */ +BOOL MPQ_OpenFileEx(MPQ_ARCHIVE hMpq, LPCSTR szFileName, DWORD dwSearchScope, MPQ_FILE* phFile) { + if (hMpq == NULL || szFileName == NULL || phFile == NULL) { + return FALSE; + } + + MPQ_FILE_HANDLE* file_handle = (MPQ_FILE_HANDLE*)malloc(sizeof(MPQ_FILE_HANDLE)); + if (file_handle == NULL) { + return FALSE; + } + + /* Get file number from filename */ + int file_number = 0; + int result = libmpq__file_number(hMpq, (char*)szFileName, &file_number); + + if (result != 0) { + free(file_handle); + return FALSE; + } + + /* Get file size */ + off_t file_size = 0; + result = libmpq__file_unpacked_size(hMpq, file_number, &file_size); + + if (result != 0) { + free(file_handle); + return FALSE; + } + + /* Allocate buffer and read entire file for efficient random access */ + /* Check for overflow - file_size should fit in size_t */ + if (file_size < 0 || (sizeof(size_t) < sizeof(off_t) && file_size > (off_t)SIZE_MAX)) { + free(file_handle); + return FALSE; + } + + /* Handle zero-size files */ + unsigned char* buffer = NULL; + if (file_size > 0) { + buffer = (unsigned char*)malloc((size_t)file_size); + if (buffer == NULL) { + free(file_handle); + return FALSE; + } + + result = libmpq__file_read(hMpq, file_number, buffer, (off_t)file_size); + + if (result != 0) { + free(buffer); + free(file_handle); + return FALSE; + } + } + + file_handle->archive = hMpq; + file_handle->file_number = file_number; + file_handle->offset = 0; + file_handle->cached_data = buffer; + file_handle->cached_size = file_size; + + *phFile = file_handle; + return TRUE; +} + +/* Close a file handle */ +BOOL MPQ_CloseFile(MPQ_FILE hFile) { + if (hFile == NULL) { + return FALSE; + } + + if (hFile->cached_data != NULL) { + free(hFile->cached_data); + } + + free(hFile); + return TRUE; +} + +/* Read data from a file */ +BOOL MPQ_ReadFile(MPQ_FILE hFile, LPVOID lpBuffer, DWORD dwToRead, LPDWORD pdwRead, LPOVERLAPPED lpOverlapped) { + if (hFile == NULL || lpBuffer == NULL) { + return FALSE; + } + + /* Check if we're trying to read beyond file size */ + if (hFile->offset >= hFile->cached_size) { + if (pdwRead != NULL) { + *pdwRead = 0; + } + return TRUE; /* Not an error, just EOF */ + } + + /* Calculate bytes to read safely to avoid overflow */ + off_t bytes_to_read = dwToRead; + off_t remaining = hFile->cached_size - hFile->offset; + + /* Ensure we don't read past end of file */ + if (bytes_to_read > remaining) { + bytes_to_read = remaining; + } + + /* Additional safety check for pointer arithmetic */ + if (hFile->offset < 0 || bytes_to_read < 0 || + hFile->offset > hFile->cached_size || + bytes_to_read > (hFile->cached_size - hFile->offset)) { + return FALSE; + } + + /* Copy from cached data */ + memcpy(lpBuffer, hFile->cached_data + hFile->offset, (size_t)bytes_to_read); + hFile->offset += bytes_to_read; + + if (pdwRead != NULL) { + /* Check if bytes_to_read fits in DWORD before casting */ + if (bytes_to_read > 0xFFFFFFFF) { + /* This shouldn't happen with DWORD dwToRead input, but be safe */ + *pdwRead = 0xFFFFFFFF; + } else { + *pdwRead = (DWORD)bytes_to_read; + } + } + + return TRUE; +} + +/* Get file size */ +DWORD MPQ_GetFileSize(MPQ_FILE hFile, LPDWORD pdwFileSizeHigh) { + if (hFile == NULL) { + return 0; + } + + if (pdwFileSizeHigh != NULL) { + /* Safe handling for both 32-bit and 64-bit off_t */ + #if defined(_FILE_OFFSET_BITS) && _FILE_OFFSET_BITS == 64 + *pdwFileSizeHigh = (DWORD)(hFile->cached_size >> 32); + #else + *pdwFileSizeHigh = 0; + #endif + } + + return (DWORD)(hFile->cached_size & 0xFFFFFFFF); +} + +/* Set file pointer position */ +DWORD MPQ_SetFilePointer(MPQ_FILE hFile, LONG lFilePos, LONG* plFilePosHigh, DWORD dwMoveMethod) { + if (hFile == NULL) { + return 0xFFFFFFFF; + } + + off_t new_offset = 0; + off_t file_pos_offset = (off_t)lFilePos; + + switch (dwMoveMethod) { + case FILE_BEGIN: + new_offset = file_pos_offset; + break; + case FILE_CURRENT: + /* Check for overflow in addition */ + if (lFilePos > 0 && hFile->offset > (off_t)(SIZE_MAX - lFilePos)) { + return 0xFFFFFFFF; /* Overflow would occur */ + } + if (lFilePos < 0 && hFile->offset < (off_t)lFilePos) { + return 0xFFFFFFFF; /* Underflow would occur */ + } + new_offset = hFile->offset + file_pos_offset; + break; + case FILE_END: + /* Check for overflow in addition */ + if (lFilePos > 0 && hFile->cached_size > (off_t)(SIZE_MAX - lFilePos)) { + return 0xFFFFFFFF; /* Overflow would occur */ + } + if (lFilePos < 0 && hFile->cached_size < (off_t)(-lFilePos)) { + return 0xFFFFFFFF; /* Underflow would occur */ + } + new_offset = hFile->cached_size + file_pos_offset; + break; + default: + return 0xFFFFFFFF; + } + + if (new_offset < 0) { + return 0xFFFFFFFF; + } + + hFile->offset = new_offset; + + if (plFilePosHigh != NULL) { + /* Safe handling for both 32-bit and 64-bit off_t */ + #if defined(_FILE_OFFSET_BITS) && _FILE_OFFSET_BITS == 64 + *plFilePosHigh = (LONG)(new_offset >> 32); + #else + *plFilePosHigh = 0; + #endif + } + + return (DWORD)(new_offset & 0xFFFFFFFF); +} + +/* Extract a file from the archive */ +BOOL MPQ_ExtractFile(MPQ_ARCHIVE hMpq, LPCSTR szToExtract, LPCSTR szExtracted, DWORD dwSearchScope) { + if (hMpq == NULL || szToExtract == NULL || szExtracted == NULL) { + return FALSE; + } + + int file_number = 0; + int result = libmpq__file_number(hMpq, (char*)szToExtract, &file_number); + + if (result != 0) { + return FALSE; + } + + result = libmpq__file_extract(hMpq, file_number, (char*)szExtracted); + return (result == 0) ? TRUE : FALSE; +} + +/* Find first file in archive - simplified implementation */ +MPQ_FIND_HANDLE MPQ_FindFirstFile(MPQ_ARCHIVE hMpq, LPCSTR szMask, LPMPQ_FIND_DATA lpFindFileData, LPCSTR szListFile) { + /* Note: libmpq doesn't have native file enumeration like StormLib. + * This is a simplified implementation that may not work for all cases. + * For full functionality, you would need to read and parse the (listfile) + * from the MPQ archive. */ + + if (hMpq == NULL || lpFindFileData == NULL) { + return NULL; + } + + /* Initialize find data structure */ + lpFindFileData->archive = hMpq; + lpFindFileData->current_file = 0; + + /* Try to get the number of files in the archive */ + /* This is a limitation - libmpq doesn't provide easy file enumeration */ + lpFindFileData->file_count = 0; + + /* Return NULL to indicate file enumeration is not fully supported with libmpq */ + return NULL; +} + +/* Find next file */ +BOOL MPQ_FindNextFile(MPQ_FIND_HANDLE hFind, LPMPQ_FIND_DATA lpFindFileData) { + /* Not fully implemented - libmpq limitation */ + return FALSE; +} + +/* Close find handle */ +BOOL MPQ_FindClose(MPQ_FIND_HANDLE hFind) { + /* Nothing to clean up in our simplified implementation */ + return TRUE; +} + +#endif /* USE_LIBMPQ */ diff --git a/src/common/mpq_adapter.h b/src/common/mpq_adapter.h new file mode 100644 index 000000000..217b135e7 --- /dev/null +++ b/src/common/mpq_adapter.h @@ -0,0 +1,99 @@ +#ifndef __MPQ_ADAPTER_H__ +#define __MPQ_ADAPTER_H__ + +/* + * MPQ Adapter - Abstraction layer for MPQ archive access + * + * This header provides a unified interface for MPQ archive operations, + * supporting both StormLib (default) and libmpq (OpenBSD). + * + * On OpenBSD, use libmpq since StormLib is not well supported. + * On other platforms, use StormLib which is more feature-complete. + */ + +#if defined(__OpenBSD__) + #define USE_LIBMPQ 1 + #include +#else + #define USE_STORMLIB 1 + #include +#endif + +#include "shared.h" + +#ifdef USE_LIBMPQ + +/* libmpq types and constants */ +typedef mpq_archive_s* MPQ_ARCHIVE; +typedef struct { + mpq_archive_s* archive; + int file_number; + off_t offset; /* Current read position */ + unsigned char* cached_data; /* Cached file data for efficient seeking */ + off_t cached_size; +} MPQ_FILE_HANDLE; + +typedef MPQ_FILE_HANDLE* MPQ_FILE; + +/* File opening flags (StormLib compatibility) */ +#define SFILE_OPEN_FROM_MPQ 0 + +/* File pointer positioning methods */ +#ifndef FILE_BEGIN +#define FILE_BEGIN 0 +#endif +#ifndef FILE_CURRENT +#define FILE_CURRENT 1 +#endif +#ifndef FILE_END +#define FILE_END 2 +#endif + +/* Find file data structure */ +typedef struct { + char cFileName[260]; + mpq_archive_s* archive; + int current_file; + int file_count; +} MPQ_FIND_DATA, *LPMPQ_FIND_DATA; + +/* Find handle type */ +typedef void* MPQ_FIND_HANDLE; + +/* Adapter function prototypes */ +BOOL MPQ_OpenArchive(LPCSTR szMpqName, DWORD dwPriority, DWORD dwFlags, MPQ_ARCHIVE* phMpq); +BOOL MPQ_CloseArchive(MPQ_ARCHIVE hMpq); +BOOL MPQ_OpenFileEx(MPQ_ARCHIVE hMpq, LPCSTR szFileName, DWORD dwSearchScope, MPQ_FILE* phFile); +BOOL MPQ_CloseFile(MPQ_FILE hFile); +BOOL MPQ_ReadFile(MPQ_FILE hFile, LPVOID lpBuffer, DWORD dwToRead, LPDWORD pdwRead, LPOVERLAPPED lpOverlapped); +DWORD MPQ_GetFileSize(MPQ_FILE hFile, LPDWORD pdwFileSizeHigh); +DWORD MPQ_SetFilePointer(MPQ_FILE hFile, LONG lFilePos, LONG* plFilePosHigh, DWORD dwMoveMethod); +BOOL MPQ_ExtractFile(MPQ_ARCHIVE hMpq, LPCSTR szToExtract, LPCSTR szExtracted, DWORD dwSearchScope); +MPQ_FIND_HANDLE MPQ_FindFirstFile(MPQ_ARCHIVE hMpq, LPCSTR szMask, LPMPQ_FIND_DATA lpFindFileData, LPCSTR szListFile); +BOOL MPQ_FindNextFile(MPQ_FIND_HANDLE hFind, LPMPQ_FIND_DATA lpFindFileData); +BOOL MPQ_FindClose(MPQ_FIND_HANDLE hFind); + +/* Macro definitions for transparent API */ +#define SFileOpenArchive MPQ_OpenArchive +#define SFileCloseArchive MPQ_CloseArchive +#define SFileOpenFileEx MPQ_OpenFileEx +#define SFileCloseFile MPQ_CloseFile +#define SFileReadFile MPQ_ReadFile +#define SFileGetFileSize MPQ_GetFileSize +#define SFileSetFilePointer MPQ_SetFilePointer +#define SFileExtractFile MPQ_ExtractFile +#define SFileFindFirstFile MPQ_FindFirstFile +#define SFileFindNextFile MPQ_FindNextFile +#define SFileFindClose MPQ_FindClose + +/* Type aliases */ +#define SFILE_FIND_DATA MPQ_FIND_DATA + +#else /* USE_STORMLIB */ + +/* StormLib is used as-is, no wrapper needed */ +typedef struct _SFILE_FIND_DATA SFILE_FIND_DATA; + +#endif /* USE_LIBMPQ */ + +#endif /* __MPQ_ADAPTER_H__ */ diff --git a/src/common/parser.c b/src/common/parser.c index d8ca21aea..6d10b5081 100644 --- a/src/common/parser.c +++ b/src/common/parser.c @@ -1,4 +1,4 @@ -#include +#include "mpq_adapter.h" #include "common.h" diff --git a/src/common/shared.h b/src/common/shared.h index 1e8e528eb..06d9e6c54 100644 --- a/src/common/shared.h +++ b/src/common/shared.h @@ -180,6 +180,7 @@ typedef long long LONGLONG; typedef unsigned long long ULONGLONG; typedef float FLOAT; typedef void * HANDLE; +typedef void * LPVOID; typedef void * LPOVERLAPPED; // Unsupported on Linux and Mac typedef char TCHAR; typedef unsigned int LCID; diff --git a/src/common/sheet.c b/src/common/sheet.c index 9b4c3b090..ca6a192bb 100644 --- a/src/common/sheet.c +++ b/src/common/sheet.c @@ -1,5 +1,5 @@ #include -#include +#include "mpq_adapter.h" #include "../common/common.h" diff --git a/src/common/world.c b/src/common/world.c index 5e43a2f7c..5bddbc31b 100644 --- a/src/common/world.c +++ b/src/common/world.c @@ -1,5 +1,5 @@ #include "common.h" -#include +#include "mpq_adapter.h" static struct { LPWAR3MAP map; diff --git a/src/renderer/r_local.h b/src/renderer/r_local.h index 7c7943fb8..c9f3b791a 100644 --- a/src/renderer/r_local.h +++ b/src/renderer/r_local.h @@ -2,7 +2,7 @@ #define r_local_h #include -#include +#include "../common/mpq_adapter.h" // TODO: M1 doesn't link without these includes diff --git a/src/server/server.h b/src/server/server.h index a13690bc2..25d1bef2d 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -4,7 +4,7 @@ #include "../common/common.h" #include "game.h" -#include +#include "../common/mpq_adapter.h" #define EDICT_NUM(n) ((edict_t *)((LPSTR)ge->edicts + ge->edict_size*(n))) #define NUM_FOR_EDICT(e) (DWORD)(((LPSTR)(e)-(LPSTR)ge->edicts) / ge->edict_size)