first commit for versioning

This commit is contained in:
Natsirt867
2026-01-05 10:03:07 -06:00
commit 987343376d
348 changed files with 89570 additions and 0 deletions

1833
src/glad.c Normal file

File diff suppressed because it is too large Load Diff

123
src/logger/gl_log.c Normal file
View File

@@ -0,0 +1,123 @@
//
// Created by Tristan on 11/26/2025.
//
#include <time.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include "gl_log.h"
#include "glad/glad.h"
#define GL_LOG_FILE "gl.log"
bool restart_gl_log() {
FILE* file = fopen(GL_LOG_FILE, "w");
if (!file) {
fprintf(
stderr,
"ERROR: could not open GL_LOG_FILE log file %s for writing\n",
GL_LOG_FILE);
return false;
};
time_t now = time(NULL);
char *date = ctime(&now);
fprintf(file, "GL_LOG_FILE log. local time %s\n", date);
fclose(file);
return true;
}
bool gl_log(const char *message, ...) {
va_list argptr;
FILE* file = fopen(GL_LOG_FILE, "a");
if (!file) {
fprintf(
stderr,
"ERROR: could not open GL_LOG_FILE %s file for appending\n",
GL_LOG_FILE);
return false;
};
va_start(argptr, message);
vfprintf(file, message, argptr);
va_end(argptr);
fclose(file);
return true;
}
bool gl_log_err(const char *message, ...) {
va_list argptr;
FILE* file = fopen(GL_LOG_FILE, "a");
if (!file) {
fprintf(
stderr,
"ERROR: could not open GL_LOG_FILE %s file for appending\n",
GL_LOG_FILE);
return false;
}
va_start(argptr, message);
vfprintf(file, message, argptr);
va_end(argptr);
va_start(argptr, message);
vfprintf(stderr, message, argptr);
va_end(argptr);
fclose(file);
return true;
}
void log_gl_params() {
GLenum params[] = {
GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS,
GL_MAX_CUBE_MAP_TEXTURE_SIZE,
GL_MAX_DRAW_BUFFERS,
GL_MAX_FRAGMENT_UNIFORM_COMPONENTS,
GL_MAX_TEXTURE_IMAGE_UNITS,
GL_MAX_TEXTURE_SIZE,
GL_MAX_VARYING_FLOATS,
GL_MAX_VERTEX_ATTRIBS,
GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS,
GL_MAX_VERTEX_UNIFORM_COMPONENTS,
GL_MAX_VIEWPORT_DIMS,
GL_STEREO,
GL_MAX_SAMPLES,
GL_SAMPLES,
};
const char* names[] = {
"GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS",
"GL_MAX_CUBE_MAP_TEXTURE_SIZE",
"GL_MAX_DRAW_BUFFERS",
"GL_MAX_FRAGMENT_UNIFORM_COMPONENTS",
"GL_MAX_TEXTURE_IMAGE_UNITS",
"GL_MAX_TEXTURE_SIZE",
"GL_MAX_VARYING_FLOATS",
"GL_MAX_VERTEX_ATTRIBS",
"GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS",
"GL_MAX_VERTEX_UNIFORM_COMPONENTS",
"GL_MAX_VIEWPORT_DIMS",
"GL_STEREO",
"GL_MAX_SAMPLES",
"GL_SAMPLES",
};
gl_log("GL Context Params:\n");
// integers - only works if the order is 0-10 integer return types
for (int i = 0; i < 10; i++) {
int v = 0;
glGetIntegerv (params[i], &v);
gl_log("%s %i\n", names[i], v);
}
// others
int v[2];
v[0] = v[1] = 0;
glGetIntegerv(params[10], v);
gl_log("%s %i %i\n", names[10], v[0], v[1]);
glGetIntegerv(params[12], v);
gl_log("%s %i %i\n", names[12], v[0], v[1]);
glGetIntegerv(params[13], v);
gl_log("%s %i\n", names[13], v[0]);
unsigned char s = 0;
glGetBooleanv(params[11], &s);
gl_log("%s %u\n", names[11], (unsigned int)s);
gl_log("-----------------------------\n");
}

12
src/logger/gl_log.h Normal file
View File

@@ -0,0 +1,12 @@
//
// Created by Tristan on 11/26/2025.
//
#ifndef GL_LOG_H
#define GL_LOG_H
bool restart_gl_log();
bool gl_log(const char *message, ...);
bool gl_log_err(const char *message, ...);
void log_gl_params();
#endif //GL_LOG_H

168
src/logger/log.c Normal file
View File

@@ -0,0 +1,168 @@
/*
* Copyright (c) 2020 rxi
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
#include "log.h"
#define MAX_CALLBACKS 32
typedef struct {
log_LogFn fn;
void *udata;
int level;
} Callback;
static struct {
void *udata;
log_LockFn lock;
int level;
bool quiet;
Callback callbacks[MAX_CALLBACKS];
} L;
static const char *level_strings[] = {
"TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"
};
#ifdef LOG_USE_COLOR
static const char *level_colors[] = {
"\x1b[94m", "\x1b[36m", "\x1b[32m", "\x1b[33m", "\x1b[31m", "\x1b[35m"
};
#endif
static void stdout_callback(log_Event *ev) {
char buf[16];
buf[strftime(buf, sizeof(buf), "%H:%M:%S", ev->time)] = '\0';
#ifdef LOG_USE_COLOR
fprintf(
ev->udata, "%s %s%-5s\x1b[0m \x1b[90m%s:%d:\x1b[0m ",
buf, level_colors[ev->level], level_strings[ev->level],
ev->file, ev->line);
#else
fprintf(
ev->udata, "%s %-5s %s:%d: ",
buf, level_strings[ev->level], ev->file, ev->line);
#endif
vfprintf(ev->udata, ev->fmt, ev->ap);
fprintf(ev->udata, "\n");
fflush(ev->udata);
}
static void file_callback(log_Event *ev) {
char buf[64];
buf[strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", ev->time)] = '\0';
fprintf(
ev->udata, "%s %-5s %s:%d: ",
buf, level_strings[ev->level], ev->file, ev->line);
vfprintf(ev->udata, ev->fmt, ev->ap);
fprintf(ev->udata, "\n");
fflush(ev->udata);
}
static void lock(void) {
if (L.lock) { L.lock(true, L.udata); }
}
static void unlock(void) {
if (L.lock) { L.lock(false, L.udata); }
}
const char* log_level_string(int level) {
return level_strings[level];
}
void log_set_lock(log_LockFn fn, void *udata) {
L.lock = fn;
L.udata = udata;
}
void log_set_level(int level) {
L.level = level;
}
void log_set_quiet(bool enable) {
L.quiet = enable;
}
int log_add_callback(log_LogFn fn, void *udata, int level) {
for (int i = 0; i < MAX_CALLBACKS; i++) {
if (!L.callbacks[i].fn) {
L.callbacks[i] = (Callback) { fn, udata, level };
return 0;
}
}
return -1;
}
int log_add_fp(FILE *fp, int level) {
return log_add_callback(file_callback, fp, level);
}
static void init_event(log_Event *ev, void *udata) {
if (!ev->time) {
time_t t = time(NULL);
ev->time = localtime(&t);
}
ev->udata = udata;
}
void log_log(int level, const char *file, int line, const char *fmt, ...) {
log_Event ev = {
.fmt = fmt,
.file = file,
.line = line,
.level = level,
};
lock();
if (!L.quiet && level >= L.level) {
init_event(&ev, stderr);
va_start(ev.ap, fmt);
stdout_callback(&ev);
va_end(ev.ap);
}
for (int i = 0; i < MAX_CALLBACKS && L.callbacks[i].fn; i++) {
Callback *cb = &L.callbacks[i];
if (level >= cb->level) {
init_event(&ev, cb->udata);
va_start(ev.ap, fmt);
cb->fn(&ev);
va_end(ev.ap);
}
}
unlock();
}

49
src/logger/log.h Normal file
View File

@@ -0,0 +1,49 @@
/**
* Copyright (c) 2020 rxi
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the MIT license. See `log.c` for details.
*/
#ifndef LOG_H
#define LOG_H
#include <stdio.h>
#include <stdarg.h>
#include <stdbool.h>
#include <time.h>
#define LOG_VERSION "0.1.0"
typedef struct {
va_list ap;
const char *fmt;
const char *file;
struct tm *time;
void *udata;
int line;
int level;
} log_Event;
typedef void (*log_LogFn)(log_Event *ev);
typedef void (*log_LockFn)(bool lock, void *udata);
enum { LOG_TRACE, LOG_DEBUG, LOG_INFO, LOG_WARN, LOG_ERROR, LOG_FATAL };
#define log_trace(...) log_log(LOG_TRACE, __FILE__, __LINE__, __VA_ARGS__)
#define log_debug(...) log_log(LOG_DEBUG, __FILE__, __LINE__, __VA_ARGS__)
#define log_info(...) log_log(LOG_INFO, __FILE__, __LINE__, __VA_ARGS__)
#define log_warn(...) log_log(LOG_WARN, __FILE__, __LINE__, __VA_ARGS__)
#define log_error(...) log_log(LOG_ERROR, __FILE__, __LINE__, __VA_ARGS__)
#define log_fatal(...) log_log(LOG_FATAL, __FILE__, __LINE__, __VA_ARGS__)
const char* log_level_string(int level);
void log_set_lock(log_LockFn fn, void *udata);
void log_set_level(int level);
void log_set_quiet(bool enable);
int log_add_callback(log_LogFn fn, void *udata, int level);
int log_add_fp(FILE *fp, int level);
void log_log(int level, const char *file, int line, const char *fmt, ...);
#endif

1051
src/main.c Normal file

File diff suppressed because it is too large Load Diff

5
src/mpq/mpq.c Normal file
View File

@@ -0,0 +1,5 @@
//
// Created by Tristan on 11/12/2025.
//
#include "mpq.h"

49
src/mpq/mpq.h Normal file
View File

@@ -0,0 +1,49 @@
//
// Created by Tristan on 11/12/2025.
//
#ifndef MPQ_H
#define MPQ_H
#include <StormLib.h>
/*
typedef struct {
HANDLE backup_enUS_MPQ;
HANDLE base_enUS_MPQ;
HANDLE expansion_locale_enUS_MPQ;
HANDLE expansion_speech_enUS_MPQ;
HANDLE lichking_locale_enUS_MPQ;
HANDLE lichking_speech_enUS_MPQ;
HANDLE locale_enUS_MPQ;
HANDLE patch_enUS_2_MPQ;
HANDLE patch_enUS_3_MPQ;
HANDLE speech_enUS_MPQ;
} LocaleArchives;
typedef struct {
HANDLE common_MPQ;
HANDLE common_2_MPQ;
HANDLE expansion_MPQ;
HANDLE lichking_MPQ;
HANDLE patch_MPQ;
HANDLE patch_2_MPQ;
HANDLE patch_3_MPQ;
HANDLE patch_4_MPQ;
LocaleArchives;
} ArchiveManager; */
typedef struct {
HANDLE *archives; // array of handles pointing to archives
size_t count; // num of handles
size_t capacity; // size of array
char root_path[MAX_PATH]; // might need this for reconstruction of filepaths
} ArchiveManager;
typedef struct {
char path[MAX_PATH];
int load_order_score;
} ArchiveEntry;
#endif //MPQ_H

216
src/renderer/matrix.c Normal file
View File

@@ -0,0 +1,216 @@
//
// Created by Tristan on 12/10/2025.
//
#include "matrix.h"
#include <math.h>
mat4_t mat4_identity(void) {
mat4_t m = {
{
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f
}
};
return m;
}
mat4_t mat4_make_scale(GLfloat sx, GLfloat sy, GLfloat sz) {
mat4_t m = mat4_identity();
m.m[0] = sx;
m.m[5] = sy;
m.m[10] = sz;
return m;
};
mat4_t mat4_make_rotation_x(GLfloat angle) {
GLfloat c = cos(angle);
GLfloat s = sin(angle);
mat4_t m = mat4_identity();
m.m[5] = c;
m.m[6] = -s;
m.m[9] = s;
m.m[10] = c;
return m;
}
mat4_t mat4_make_rotation_y(GLfloat angle) {
GLfloat c = cos(angle);
GLfloat s = sin(angle);
mat4_t m = mat4_identity();
m.m[0] = c;
m.m[2] = s;
m.m[8] = -s;
m.m[10] = c;
return m;
}
mat4_t mat4_make_rotation_z(GLfloat angle) {
GLfloat c = cos(angle);
GLfloat s = sin(angle);
mat4_t m = mat4_identity();
m.m[0] = c;
m.m[1] = -s;
m.m[4] = s;
m.m[5] = c;
return m;
}
mat4_t mat4_make_translation(GLfloat tx, GLfloat ty, GLfloat tz) {
mat4_t m = mat4_identity();
m.m[12] = tx;
m.m[13] = ty;
m.m[14] = tz;
return m;
}
mat4_t mat4_mul_mat4(mat4_t a, mat4_t b) {
mat4_t m;
for (int j = 0; j < 4; j++) { // cols
// m.m[j*4 + i]
for (int i = 0; i < 4; i++) { // rows
GLfloat sum = 0.0f;
for (int k = 0; k < 4; k++) {
GLfloat element_a = a.m[k * 4 + i];
GLfloat element_b = b.m[j * 4 + k];
sum += element_a * element_b;
}
m.m[j * 4 + i] = sum;
}
}
return m;
}
mat4_t mat4_make_perspective(float fov_rad, float aspect, float znear, float zfar) {
/*
Sx, 0.0f, 0.0f, 0.0f,
0.0f, Sy, 0.0f, 0.0f,
0.0f, 0.0f, Sz, -1.0f,
0.0f, 0.0f, Pz, 0.0f
*/
float range = tan(fov_rad * 0.5f) * znear;
float Sy = znear / range;
float Sx = Sy / aspect;
float Sz = -(zfar + znear) / (zfar - znear);
float Pz = -(2.0f * zfar * znear) / (zfar - znear);
mat4_t m = { 0 }; // Initialize all to 0.0f
// Diagonal scaling
m.m[0] = Sx;
m.m[5] = Sy;
m.m[10] = Sz;
// perspective divide (w-axis)
m.m[11] = -1.0f;
// depth translation (Pz)
m.m[14] = Pz;
return m;
}
mat4_t mat4_make_quaternion(const GLfloat* q) {
mat4_t m;
float w = q[0];
float x = q[1];
float y = q[2];
float z = q[3];
float x2 = x * x;
float y2 = y * y;
float z2 = z * z;
// col 1
m.m[0] = 1.0f - (2.0f * y2) - (2.0f * z2);
m.m[1] = (2.0f * x * y) + (2.0f * w * z);
m.m[2] = (2 * x * z) - (2 * w * y);
m.m[3] = 0.0f;
// col 2
m.m[4] = (2.0f * x * y) - (2.0f * w * z);
m.m[5] = 1.0f - (2 * x2) - (2 * z2);
m.m[6] = (2.0f * y * z) + (2.0f * w * x);
m.m[7] = 0.0f;
// col 3
m.m[8] = (2.0f * x * z) + (2.0f * w * y);
m.m[9] = (2.0f * y * z) - (2.0f * w * x);
m.m[10] = 1.0f - (2.0f * x2) - (2.0 * y2);
m.m[11] = 0.0f;
// col 4
m.m[12] = 0.0f;
m.m[13] = 0.0f;
m.m[14] = 0.0f;
m.m[15] = 1.0f;
return m;
}
mat4_t mat4_look_at(float eyex, float eyey, float eyez,
float targetx, float targety, float targetz,
float upx, float upy, float upz) {
// calculate the forward vector (direction from Eye to Target)
float forward_x = targetx - eyex;
float forward_y = targety - eyey;
float forward_z = targetz - eyez;
// normalize forward vector
float reciprocal_length_forward = 1.0f / sqrt(forward_x * forward_x + forward_y * forward_y + forward_z * forward_z);
forward_x *= reciprocal_length_forward;
forward_y *= reciprocal_length_forward;
forward_z *= reciprocal_length_forward;
// calculate right vector (Forward x Up)
// use the "world up" passed in (usually 0, 1, 0) to determine "right"
float rightx = forward_z * upy - forward_y * upz;
float righty = forward_x * upz - forward_z * upx;
float rightz = forward_y * upx - forward_x * upy;
// normalize right vector
float reciprocal_length_right = 1.0f / sqrt(rightx * rightx + righty * righty + rightz * rightz);
rightx *= reciprocal_length_right;
righty *= reciprocal_length_right;
rightz *= reciprocal_length_right;
// re-calculate Up Vector (Right x Forward)
// calculate the "True Up" perpendicular to the new Forward and Right vector
upx = righty * forward_z - rightz * forward_y;
upy = rightz * forward_x - rightx * forward_z;
upz = rightx * forward_y - righty * forward_x;
// create the matrix
mat4_t m = mat4_identity();
// row 0 (right vector)
m.m[0] = rightx;
m.m[4] = righty;
m.m[8] = rightz;
// row 1 (up vector)
m.m[1] = upx;
m.m[5] = upy;
m.m[9] = upz;
// row 2 (forward vector inverted for openGL camera)
// invert Forward since we're looking down the negative Z axis with openGL
m.m[2] = -forward_x;
m.m[6] = -forward_y;
m.m[10] = -forward_z;
// Dot product of rotation and negative eye position
m.m[12] = -(rightx * eyex + righty * eyey + rightz * eyez);
m.m[13] = -(upx * eyex + upy * eyey + upz * eyez);
m.m[14] = -(-forward_x * eyex - forward_y * eyey - forward_z * eyez);
// row 3 (0, 0, 0, 1)
return m;
}

33
src/renderer/matrix.h Normal file
View File

@@ -0,0 +1,33 @@
#ifndef MATRIX_H
#define MATRIX_H
#include "glad/glad.h"
#define M_PI 3.14159265358979323846
#define ONE_DEG_IN_RAD (2.0 * M_PI) / 360.0 // 0.017444444
#ifndef TO_RAD
#define TO_RAD(deg) ((deg) * 3.14159265358979323846)
#endif
typedef struct {
GLfloat m[16];
} mat4_t;
typedef struct {
float q[4];
} versor_t;
mat4_t mat4_make_rotation_x(GLfloat m);
mat4_t mat4_make_rotation_y(GLfloat m);
mat4_t mat4_make_rotation_z(GLfloat m);
mat4_t mat4_make_scale(GLfloat sx, GLfloat sy, GLfloat sz);
mat4_t mat4_make_translation(GLfloat tx, GLfloat ty, GLfloat tz);
mat4_t mat4_make_quaternion(const GLfloat* q, GLfloat* m);
mat4_t mat4_make_perspective(float fov_rad, float aspect, float znear, float zfar);
mat4_t mat4_mul_mat4(mat4_t a, mat4_t b);
mat4_t mat4_identity(void);
mat4_t mat4_look_at(float eyex, float eyey, float eyez,
float targetx, float targety, float targetz,
float upx, float upy, float upz);
#endif //MATRIX_H

173
src/renderer/mesh.c Normal file
View File

@@ -0,0 +1,173 @@
//
// Created by Tristan on 11/24/2025.
//
#include "mesh.h"
#include "../logger/log.h"
GroupMesh create_mesh_from_group(const WMOGroupData* group, const WMORootData* root_data) {
GroupMesh mesh = {0};
if (!group->movt_data_ptr || !group->movi_data_ptr) {
log_warn("Group has vertex flag but no MOVT/MOVI data");
return mesh;
}
size_t num_vertices = group->movt_size / sizeof(C3Vector);
size_t num_indices = group->movi_size / sizeof(uint16_t);
Vertex* vertices = (Vertex*)malloc(num_vertices * sizeof(Vertex));
if (!vertices) {
log_error("Failed to allocate memory for mesh generation");
return mesh;
}
const C3Vector* positions = (const C3Vector*)group->movt_data_ptr;
const C3Vector* normals = (const C3Vector*)group->monr_data_ptr;
const C2Vector* texCoords = (const C2Vector*)group->motv_data_ptr;
GLfloat offset_x = group->header.boundingBox.min[0];
GLfloat offset_y = group->header.boundingBox.min[1];
GLfloat offset_z = group->header.boundingBox.min[2];
for (size_t i = 0; i < num_vertices; i++) {
float wx = positions[i].x + offset_x;
float wy = positions[i].y + offset_y;
float wz = positions[i].z + offset_z;
vertices[i].position.x = wx;
vertices[i].position.y = wy;
vertices[i].position.z = wz;
if (normals) {
vertices[i].normal.x = normals[i].x;
vertices[i].normal.y = normals[i].y;
vertices[i].normal.z = normals[i].z;
} else {
vertices[i].normal = (C3Vector){0.0f, 1.0f, 0.0f};
}
if (texCoords) {
vertices[i].texCoord.x = texCoords[i].x;
vertices[i].texCoord.y = texCoords[i].y;
} else {
vertices[i].texCoord = (C2Vector){0.0f, 0.0f};
}
}
if (num_vertices > 0) {
float minX = vertices[0].position.x, maxX = vertices[0].position.x;
float minY = vertices[0].position.y, maxY = vertices[0].position.y;
float minZ = vertices[0].position.z, maxZ = vertices[0].position.z;
for (size_t k = 0; k < num_vertices; k++) {
if (vertices[k].position.x < minX) minX = vertices[k].position.x;
if (vertices[k].position.x > maxX) maxX = vertices[k].position.x;
if (vertices[k].position.y < minY) minY = vertices[k].position.y;
if (vertices[k].position.y > maxY) maxY = vertices[k].position.y;
if (vertices[k].position.z < minZ) minZ = vertices[k].position.z;
if (vertices[k].position.z > maxZ) maxZ = vertices[k].position.z;
}
log_info("MESH DEBUG: Group has %zu vertices", num_vertices);
// This is the critical line. It tells us where the mesh actually IS.
log_info("BOUNDS: X[%.1f to %.1f] Y[%.1f to %.1f] Z[%.1f to %.1f]",
minX, maxX, minY, maxY, minZ, maxZ);
}
if (group->moba_data_ptr && group->moba_size > 0) {
const SMOBatch* wmo_batches = (const SMOBatch*)group->moba_data_ptr;
size_t num_batches = group->moba_size / sizeof(SMOBatch);
mesh.batches = (RenderBatch*)malloc(num_batches * sizeof(RenderBatch));
mesh.batchCount = num_batches;
for (size_t i = 0; i < num_batches; i++) {
mesh.batches[i].indexOffset = wmo_batches[i].startIndex;
mesh.batches[i].indexCount = wmo_batches[i].count;
// lookup texture id
uint8_t mat_id = wmo_batches[i].material_id;
if (root_data && root_data->material_textures) {
mesh.batches[i].textureID = root_data->material_textures[mat_id];
} else {
mesh.batches[i].textureID = 0;
}
}
} else {
// Fallback if none
mesh.batchCount = 1;
mesh.batches = (RenderBatch*)malloc(sizeof(RenderBatch));
mesh.batches[0].indexOffset = 0;
mesh.batches[0].indexCount = num_indices;
mesh.batches[0].textureID = 0;
}
// create buffers
glGenVertexArrays(1, &mesh.VAO);
glGenBuffers(1, &mesh.VBO);
glGenBuffers(1, &mesh.EBO);
glBindVertexArray(mesh.VAO);
// vbo
glBindBuffer(GL_ARRAY_BUFFER, mesh.VBO);
glBufferData(GL_ARRAY_BUFFER, num_vertices * sizeof(Vertex), vertices, GL_STATIC_DRAW);
// ebo
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, group->movi_size, group->movi_data_ptr, GL_STATIC_DRAW);
// attributes
// 0: Position
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
glEnableVertexAttribArray(0);
// 1: Normal
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)sizeof(C3Vector));
glEnableVertexAttribArray(1);
// 2: TexCoord
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)(2 * sizeof(C3Vector)));
glEnableVertexAttribArray(2);
glBindVertexArray(0);
free(vertices);
return mesh;
}
void draw_group_mesh(GroupMesh mesh) {
if (mesh.VAO == 0)
return;
glBindVertexArray(mesh.VAO);
for (size_t i = 0; i < mesh.batchCount; i++) {
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, mesh.batches[i].textureID);
// The last argument is the byte offset into the EBO
// multiply indexOffset * sizeof(uint16_t) because OpenGL expects bytes
void* offset = (void*)(uintptr_t)(mesh.batches[i].indexOffset * sizeof(uint16_t));
glDrawElements(GL_TRIANGLES, (GLsizei)mesh.batches[i].indexCount, GL_UNSIGNED_SHORT, offset);
}
glBindVertexArray(0);
}
void free_group_mesh(GroupMesh *mesh) {
if (mesh->VAO) glDeleteVertexArrays(1, &mesh->VAO);
if (mesh->VBO) glDeleteBuffers(1, &mesh->VBO);
if (mesh->EBO) glDeleteBuffers(1, &mesh->EBO);
if (mesh->batches) {
free(mesh->batches);
mesh->batches = NULL;
}
mesh->VAO = 0;
mesh->batchCount = 0;
}

35
src/renderer/mesh.h Normal file
View File

@@ -0,0 +1,35 @@
//
// Created by Tristan on 11/24/2025.
//
#ifndef MESH_H
#define MESH_H
#include <stdint.h>
#include "glad/glad.h"
#include "../wmo/wmo_structs.h"
typedef struct {
C3Vector position;
C3Vector normal;
C2Vector texCoord;
} Vertex;
typedef struct {
uint32_t indexOffset; // where in the EBO this batch starts
uint32_t indexCount; // how many indices to draw
GLuint textureID; // the texture to bind to
} RenderBatch;
typedef struct {
GLuint VAO, VBO, EBO;
RenderBatch* batches;
size_t batchCount;
} GroupMesh;
GroupMesh create_mesh_from_group(const WMOGroupData* group, const WMORootData* root_data);
void draw_group_mesh(GroupMesh mesh);
void free_group_mesh(GroupMesh *mesh);
#endif //MESH_H

136
src/renderer/shader.c Normal file
View File

@@ -0,0 +1,136 @@
//
// Created by Tristan on 11/26/2025.
//
#include "shader.h"
#include "../util.h"
#include "../logger/log.h"
#include "../logger/gl_log.h"
// creates a shader program from a vertex and fragment shader
// vertex_shader_str - a null terminated string of text containing a vertex shader
// fragment_shader_str - a null terminated string of text containing a fragment shader
// returns a new, valid shader program handle, or 0 if there was an issue
// assert on NULL parameters
GLuint create_shader_program_from_strings(const char *vertex_shader_str, const char *fragment_shader_str) {
assert(vertex_shader_str && fragment_shader_str);
GLuint shader_program = glCreateProgram();
GLuint vertex_shader_handle = glCreateShader(GL_VERTEX_SHADER);
GLuint fragment_shader_handle = glCreateShader(GL_FRAGMENT_SHADER);
// compile shader and check for errors
glShaderSource(vertex_shader_handle, 1, &vertex_shader_str, NULL);
glCompileShader(vertex_shader_handle);
int lparams = -1;
glGetShaderiv(vertex_shader_handle, GL_COMPILE_STATUS, &lparams);
if (GL_TRUE != lparams) {
log_error("ERROR: vertex shader index %u did not compile", vertex_shader_handle);
const int max_length = 2048;
int actual_length = 0;
char slog[2048];
glGetShaderInfoLog(vertex_shader_handle, max_length, &actual_length, slog);
log_error("shader info log for GL index %u:\n%s", vertex_shader_handle, slog);
glDeleteShader(vertex_shader_handle);
glDeleteShader(fragment_shader_handle);
glDeleteProgram(shader_program);
return 0;
}
// compile shader and check for errors
glShaderSource(fragment_shader_handle, 1, &fragment_shader_str, NULL);
glCompileShader(fragment_shader_handle);
lparams = -1;
glGetShaderiv(fragment_shader_handle, GL_COMPILE_STATUS, &lparams);
if (GL_TRUE != lparams) {
log_error("ERROR: fragment shader index %u did not compile", fragment_shader_handle);
const int max_length = 2048;
int actual_length = 0;
char slog[2048];
glGetShaderInfoLog(fragment_shader_handle, max_length, &actual_length, slog);
gl_log_err(slog);
glDeleteShader(vertex_shader_handle);
glDeleteShader(fragment_shader_handle);
glDeleteProgram(shader_program);
return 0;
}
glAttachShader(shader_program, fragment_shader_handle);
glAttachShader(shader_program, vertex_shader_handle);
// link program and check for errors
glLinkProgram(shader_program);
glDeleteShader(vertex_shader_handle);
glDeleteShader(fragment_shader_handle);
lparams = -1;
glGetProgramiv(shader_program, GL_LINK_STATUS, &lparams);
if (GL_TRUE != lparams) {
log_error("ERROR: could not link shader program GL index %u", shader_program);
const int max_length = 2048;
int actual_length = 0;
char plog[2048];
glGetProgramInfoLog(shader_program, max_length, &actual_length, plog);
log_error("program info log for GL index %u:\n%s", shader_program, plog);
glDeleteProgram(shader_program);
return 0;
}
return shader_program;
}
GLuint create_shader_program_from_files(const char *vertex_shader_filename, const char *fragment_shader_filename) {
assert(vertex_shader_filename && fragment_shader_filename);
log_info("loading shader from files `%s` and `%s`", vertex_shader_filename, fragment_shader_filename);
char vs_shader_str[MAX_SHADER_SZ];
char fs_shader_str[MAX_SHADER_SZ];
vs_shader_str[0] = fs_shader_str[0] = '\0';
// read the vertex shader file into a buffer
FILE *fp = fopen(vertex_shader_filename, "r");
if (!fp) {
log_error("ERROR: could not open vertex shader file `%s`", vertex_shader_filename);
return 0;
}
size_t count = fread(vs_shader_str, 1, MAX_SHADER_SZ - 1, fp);
assert(count < MAX_SHADER_SZ - 1); // file too long otherwise
vs_shader_str[count] = '\0';
fclose(fp);
// read fragment shader file into a buffer
fp = fopen(fragment_shader_filename, "r");
if (!fp) {
log_error("ERROR: could not open fragment shader file `%s`", fragment_shader_filename);
return 0;
}
count = fread(fs_shader_str, 1, MAX_SHADER_SZ - 1, fp);
assert(count < MAX_SHADER_SZ - 1); // file too long otherwise
fs_shader_str[count] = '\0';
fclose(fp);
return create_shader_program_from_strings(vs_shader_str, fs_shader_str);
}
void shader_reload(GLuint *program, const char *vertex_shader_filename, const char *fragment_shader_filename) {
assert(program && vertex_shader_filename && fragment_shader_filename);
GLuint reloaded_program = create_shader_program_from_files(vertex_shader_filename, fragment_shader_filename);
if (reloaded_program) {
glDeleteProgram(*program);
*program = reloaded_program;
}
}

14
src/renderer/shader.h Normal file
View File

@@ -0,0 +1,14 @@
//
// Created by Tristan on 11/26/2025.
//
#ifndef SHADER_H
#define SHADER_H
#include "glad/glad.h"
#define MAX_SHADER_SZ 100000
void shader_reload(GLuint *program, const char *vertex_shader_filename, const char *fragment_shader_filename);
GLuint create_shader_program_from_strings(const char *vertex_shader_str, const char *fragment_shader_str);
GLuint create_shader_program_from_files(const char *vertex_shader_filename, const char *fragment_shader_filename);
#endif //SHADER_H

76
src/renderer/texture.c Normal file
View File

@@ -0,0 +1,76 @@
//
// Created by Tristan on 1/4/2026.
//
#include "texture.h"
#include <string.h>
#include "../logger/log.h"
GLuint texture_load_from_blp_memory(const uint8_t* file_data, size_t file_size) {
if (!file_data || file_size < sizeof(BLPHeader)) {
log_error("BLP texture data is empty or too small");
return 0;
}
const BLPHeader* h = (const BLPHeader*)file_data;
if (memcmp(h->magic, "BLP2", 4) != 0) {
log_error("Invalid BLP Magic. Expected BLP2, got %s", h->magic);
return 0;
}
if (h->compression != 2) {
log_error("Unsupported BLP compression type: %d", h->compression);
return 0;
}
GLenum glFormat = 0;
switch (h->pixelFormat) {
case 0: // DXT1
glFormat = (h->alphaBits > 0) ? GL_COMPRESSED_RGBA_S3TC_DXT1_EXT : GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
break;
case 1: // DXT3
glFormat = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
break;
case 7:
glFormat = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
break;
default:
log_error("Unsupported DXT pixelFormat: %d", h->pixelFormat);
return 0;
}
GLuint textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
// WMO textures standard settings
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
uint32_t width = h->width;
uint32_t height = h->height;
int blockSize = (glFormat == GL_COMPRESSED_RGB_S3TC_DXT1_EXT || glFormat == GL_COMPRESSED_RGBA_S3TC_DXT1_EXT) ? 8 : 16;
for (int i = 0; i < 16; i++) {
if (h->mipOffset[i] == 0 || (width == 0 && height == 0)) break;
const uint8_t* dataPtr = file_data + h->mipOffset[i];
uint32_t mipSize = ((width + 3) / 4) * ((height + 3) / 4) * blockSize;
if (mipSize == 0)
mipSize = blockSize;
glCompressedTexImage2D(GL_TEXTURE_2D, i, glFormat, width, height, 0, mipSize, dataPtr);
width /= 2;
height /= 2;
if (width == 0)
width = 1;
if (height == 0)
height = 1;
}
return textureID;
}

42
src/renderer/texture.h Normal file
View File

@@ -0,0 +1,42 @@
//
// Created by Tristan on 1/4/2026.
//
#ifndef TEXTURE_H
#define TEXTURE_H
#include <glad/glad.h>
#include <stdint.h>
#include <stdlib.h>
#ifndef GL_COMPRESSED_RGB_S3TC_DXT1_EXT
#define GL_COMPRESSED_RGB_S3TC_DXT1_EXT 0x83F0
#endif
#ifndef GL_COMPRESSED_RGBA_S3TC_DXT1_EXT
#define GL_COMPRESSED_RGBA_S3TC_DXT1_EXT 0x83F1
#endif
#ifndef GL_COMPRESSED_RGBA_S3TC_DXT3_EXT
#define GL_COMPRESSED_RGBA_S3TC_DXT3_EXT 0x83F2
#endif
#ifndef GL_COMPRESSED_RGBA_S3TC_DXT5_EXT
#define GL_COMPRESSED_RGBA_S3TC_DXT5_EXT 0x83F3
#endif
// BLP2 Header def
typedef struct {
char magic[4]; // "BLP2"
uint32_t version; // always 1
uint8_t compression; // 1=Raw/Paletted, 2=DXT
uint8_t alphaBits; // 0, 1, 8
uint8_t pixelFormat; // 0=DXT1, 1=DXT3, 7=DXT5
uint8_t mipFlags;
uint32_t width;
uint32_t height;
uint32_t mipOffset[16];
uint32_t mipSize[16];
} BLPHeader;
// Loads a BLP file from memory directly to the GPU
GLuint texture_load_from_blp_memory(const uint8_t* file_data, size_t file_size);
#endif //TEXTURE_H

116
src/renderer/vector.c Normal file
View File

@@ -0,0 +1,116 @@
//
// Created by Tristan on 1/4/2026.
//
#include <math.h>
#include "vector.h"
float vec3_length(vec3_t v) {
return sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
}
// Addition
vec3_t vec3_add(vec3_t a, vec3_t b) {
vec3_t result = {
.x = a.x + b.x,
.y = a.y + b.y,
.z = a.z + b.z
};
return result;
}
// Subtraction
vec3_t vec3_sub(vec3_t a, vec3_t b) {
vec3_t result = {
.x = a.x - b.x,
.y = a.y - b.y,
.z = a.z - b.z
};
return result;
}
// Multiplication
vec3_t vec3_mul(vec3_t v, float factor) {
vec3_t result = {
.x = v.x * factor,
.y = v.y * factor,
.z = v.z * factor
};
return result;
}
// Division
vec3_t vec3_div(vec3_t v, float factor) {
vec3_t result = {
.x = v.x / factor,
.y = v.y / factor,
.z = v.z / factor
};
return result;
}
// Cross Product - helps to find the perpendicular vector between two vectors
vec3_t vec3_cross(vec3_t a, vec3_t b) {
vec3_t result = {
.x = a.y * b.z - a.z * b.y,
.y = a.z * b.x - a.x * b.z,
.z = a.x * b.y - a.y * b.x
};
return result;
}
// Dot Product - helps to find how aligned two vectors are to each other
float vec3_dot(vec3_t a, vec3_t b) {
return (a.x * b.x) + (a.y * b.y) + (a.z * b.z);
}
// Normalize a vec3_t vector
void vec3_normalize(vec3_t* v) {
float length = sqrt((v->x * v->x) + (v->y * v->y) + (v->z * v->z));
// avoid dividing by 0
if (length < 0.00001f) {
v->x = 0;
v->y = 0;
v->z = 0;
return;
}
v->x /= length;
v->y /= length;
v->z /= length;
}
vec3_t vec3_rotate_x(vec3_t v, float angle) {
vec3_t rotated_vector = {
.x = v.x,
.y = v.y * cos(angle) - v.z * sin(angle),
.z = v.y * sin(angle) + v.z * cos(angle)
};
return rotated_vector;
}
vec3_t vec3_rotate_y(vec3_t v, float angle) {
vec3_t rotated_vector = {
.x = v.x * cos(angle) - v.z * sin(angle),
.y = v.y,
.z = v.x * sin(angle) + v.z * cos(angle)
};
return rotated_vector;
}
vec3_t vec3_rotate_z(vec3_t v, float angle) {
vec3_t rotated_vector = {
.x = v.x * cos(angle) - v.y * sin(angle),
.y = v.x * sin(angle) + v.y * cos(angle),
.z = v.z
};
return rotated_vector;
}

42
src/renderer/vector.h Normal file
View File

@@ -0,0 +1,42 @@
//
// Created by Tristan on 1/4/2026.
//
#ifndef VECTOR_H
#define VECTOR_H
typedef struct {
float x;
float y;
} vec2_t;
typedef struct {
float x;
float y;
float z;
} vec3_t;
typedef struct {
float x, y, z, w;
} vec4_t;
////////////////////////////////////////////////////////////////////////////////
// Vector 3D Functions
////////////////////////////////////////////////////////////////////////////////
float vec3_length(vec3_t v);
vec3_t vec3_add(vec3_t a, vec3_t b);
vec3_t vec3_sub(vec3_t a, vec3_t b);
vec3_t vec3_mul(vec3_t v, float factor);
vec3_t vec3_div(vec3_t v, float factor);
vec3_t vec3_cross(vec3_t a, vec3_t b);
vec3_t vec3_rotate_x(vec3_t v, float angle);
vec3_t vec3_rotate_y(vec3_t v, float angle);
vec3_t vec3_rotate_z(vec3_t v, float angle);
vec3_t vec3_cross(vec3_t a, vec3_t b);
float vec3_dot(vec3_t a, vec3_t b);
void vec3_normalize(vec3_t* v);
#endif //VECTOR_H

803
src/util.c Normal file
View File

@@ -0,0 +1,803 @@
//
// Created by Tristan on 11/6/2025.
//
#include "util.h"
#include "logger/log.h"
#include "wmo/wmo.h"
#include "mpq/mpq.h"
WMORootData out_wmo_data;
char *get_file_contents(const char *filename) {
FILE *fp;
long length;
char *buffer = NULL;
fp = fopen(filename, "rb");
if (!fp) {
log_error("Could not open file '%s'. Reason: %s", filename, strerror(errno));
}
fseek(fp, 0, SEEK_END);
length = ftell(fp);
fseek(fp, 0, SEEK_SET);
buffer = (char *)malloc(length + 1);
if (!buffer) {
log_error("Memory allocation failed for file '%s'.", filename);
fclose(fp);
return NULL;
}
fread(buffer, 1, length, fp);
buffer[length] ='\0';
fclose(fp);
return buffer;
}
int load_wmo_file(const char *filename, FILE **file_ptr_out) {
*file_ptr_out = fopen(filename, "rb");
if (*file_ptr_out == NULL) {
log_error("Error opening WMO file %s: %s", filename, GetLastError());
return (int)GetLastError();
}
return 0;
}
int create_dir_recursive(const char *full_path) {
char temp_path[FILENAME_MAX];
log_info("Full Path in craete_dir_recursive == '%s", full_path);
strncpy(temp_path, full_path, FILENAME_MAX - 1);
temp_path[FILENAME_MAX - 1] = '\0';
char *p = temp_path;
// Check for "C:\" or "/" prefix and move the pointer past
if (p[0] == PATH_SEPARATOR || p[0] == '/')
p++;
#ifdef _WIN32
if (p[1] == ':')
p += 2; // skips (C:)
#endif
while (*p != '\0') {
if (*p == PATH_SEPARATOR || *p == '/') {
// replace with null terminator to isolate
char separator = *p;
*p = '\0';
// Attempt to create dir
if (MAKE_DIR(temp_path) != 0) {
if (errno != EEXIST) {
DWORD dwError = GetLastError();
log_error("Failed to create directory '%s'. error: %s", temp_path, dwError);
return -1;
}
}
// Restore separator character
*p = separator;
}
p++; // adv to next char
}
if (MAKE_DIR(temp_path) != 0) {
if (errno != EEXIST) {
log_error("Failed to create final directory '%s'. Error: %lu", temp_path, GetLastError());
return -1;
}
}
return 0;
}
// TODO: might not need this honestly
/*
WMOData load_mver_data() {
WMOChunkHeader mver_header;
WMOData result = {NULL, 0};
DWORD bytesRead = 0;
// magic;
// size;
// version;
// return;
}*/
WMOData load_wmo_data(ArchiveManager *archives, const char *wmoFileName) {
HANDLE hFile = get_file_in_archives(archives, wmoFileName);
WMOData result = {NULL, 0};
DWORD bytesRead = 0;
if (!hFile) {
log_error("Failed to open WMO file '%s' inside MPQ. Error: %lu", wmoFileName, GetLastError());
return result;
}
result.size = get_file_size_in_mpq(hFile, wmoFileName);
if (result.size == SFILE_INVALID_SIZE) {
SFileCloseFile(hFile);
return result;
}
result.data = (char *)malloc(result.size);
if (result.data == NULL) {
log_error("Memory allocation failed for WMO file '%s'.\n", wmoFileName);
SFileCloseFile(hFile);
return result;
}
// TODO: make into helper function
if (!SFileReadFile(hFile, result.data, result.size, &bytesRead, NULL)) {
log_error("Failed to read WMO data for '%s'. Error: %lu", wmoFileName, GetLastError());
free(result.data);
result.data = NULL;
result.size = 0;
} else if (bytesRead != result.size) {
log_error("Read size mismatch for WMO file '%s'. Expected %lu, got %lu.",
wmoFileName, result.size, bytesRead);
free(result.data);
result.data = NULL;
result.size = 0;
}
SFileCloseFile(hFile);
log_info("Successfully loaded WMO file '%s' (%lu bytes) into memory.", wmoFileName, result.size);
return result;
}
WMOData load_wmo_data_from_file(FILE **file_ptr_in) {
//HANDLE hFile = get_file_in_mpq(hMPQ, wmoFileName);
WMOData result = {NULL, 0};
//DWORD bytesRead = 0;
if (!*file_ptr_in) {
log_error("Failed to open WMO file. Error: %lu", GetLastError());
return result;
}
fseek(*file_ptr_in, 0, SEEK_END);
long file_size = ftell(*file_ptr_in);
result.size = file_size;
if (result.size == -1L) {
log_error("Failed to determine WMO size.");
fclose(*file_ptr_in);
*file_ptr_in = NULL;
return result;
}
result.data = (char *)malloc(result.size);
if (result.data == NULL) {
log_error("Memory allocation failed for WMO file.\n");
fclose(*file_ptr_in);
*file_ptr_in = NULL;
return result;
}
fseek(*file_ptr_in, 0, SEEK_SET);
size_t items_read = fread(result.data, 1, result.size, *file_ptr_in);
if (items_read != result.size) {
log_error("Error reading WMO file data. Expected %ld bytes, read %zu bytes.\n", result.size, items_read);
free(result.data);
result.data = NULL;
result.size = 0;
fclose(*file_ptr_in);
*file_ptr_in = NULL;
return result;
}
/*
// TODO: make into helper function
if (!SFileReadFile(hFile, result.data, result.size, &bytesRead, NULL)) {
log_error("Failed to read WMO data for '%s'. Error: %lu", wmoFileName, GetLastError());
free(result.data);
result.data = NULL;
result.size = 0;
} else if (bytesRead != result.size) {
log_error("Read size mismatch for WMO file '%s'. Expected %lu, got %lu.",
wmoFileName, result.size, bytesRead);
free(result.data);
result.data = NULL;
result.size = 0;
}
SFileCloseFile(hFile);*/
fclose(*file_ptr_in);
*file_ptr_in = NULL;
log_info("Successfully loaded WMO file (%lu bytes) into memory.", result.size);
return result;
}
size_t get_wmo_base_name(char *dest, size_t dest_size, const char *full_path) {
size_t len = strlen(full_path);
if (len >= dest_size) {
//TODO: handle error
return 0;
}
strncpy (dest, full_path, len);
dest[len] = '\0';
if (len > 4 && strcmp(dest + len - 4, ".wmo") == 0) {
dest[len - 4] = '\0';
return len -4;
}
return len;
}
void parse_wmo_chunks(ArchiveManager *archives, const char *wmo_buffer, DWORD total_size, WMORootData *out_wmo_data,
const char *wmo_file_path) {
const char *current_ptr = wmo_buffer;
const char *end_ptr = wmo_buffer + total_size;
// WMORootData out_wmo_data; not sure if should be defined here or outside of scope at top
// initialize struct passed into function
memset(out_wmo_data, 0, sizeof(WMORootData));
log_info("Starting WMO chunk traversal...");
while (current_ptr < end_ptr) {
// must be enough room for a header (8 bytes)
if (current_ptr + sizeof(WMOChunkHeader) > end_ptr) {
log_warn("End of file reached unexpectedly.");
break;
}
const WMOChunkHeader *header = (const WMOChunkHeader *)current_ptr;
log_info("Found chunk: %c%c%c%c, Size: %lu bytes",
(char)(header->chunk_name >> 24),
(char)(header->chunk_name >> 16),
(char)(header->chunk_name >> 8),
(char)(header->chunk_name),
header->chunk_size);
const char *data_start = current_ptr + sizeof(WMOChunkHeader); // sizeof(WMOChunkHeader);
switch (header->chunk_name) {
case MVER:
out_wmo_data->mver_data_ptr = data_start;
out_wmo_data->mver_size = header->chunk_size;
log_info(" -> MVER Chunk found at offset %td.", out_wmo_data->mver_data_ptr - wmo_buffer);
break;
case MOHD:
out_wmo_data->mohd_data_ptr = data_start;
out_wmo_data->mohd_size = header->chunk_size;
log_info(" -> MOHD Chunk found at offset %td.", out_wmo_data->mohd_data_ptr - wmo_buffer);
parse_mohd_chunk(out_wmo_data);
break;
case MOGN:
out_wmo_data->mogn_data_ptr = data_start;
out_wmo_data->mogn_size = header->chunk_size;
log_info(" -> MOGN Chunk found at offset %td.", out_wmo_data->mogn_data_ptr - wmo_buffer);
break;
case MOTX:
out_wmo_data->motx_data_ptr = data_start;
out_wmo_data->motx_size = header->chunk_size;
log_info(" -> MOTX Chunk found at offset %td.", out_wmo_data->motx_data_ptr - wmo_buffer);
break;
case MOMT:
out_wmo_data->momt_data_ptr = data_start;
out_wmo_data->momt_size = header->chunk_size;
log_info(" -> MOMT Chunk found at offset %td", out_wmo_data->momt_data_ptr - wmo_buffer);
break;
case MOGI:
out_wmo_data->mogi_data_ptr = data_start;
out_wmo_data->mogi_size = header->chunk_size;
log_info(" -> MOGI Chunk found at offset %td", out_wmo_data->mogi_data_ptr - wmo_buffer);
break;
case MOSB:
out_wmo_data->mosb_data_ptr = data_start;
out_wmo_data->mosb_size = header->chunk_size;
log_info(" -> MOSB Chunk found at offset %td", out_wmo_data->mosb_data_ptr - wmo_buffer);
get_mosb_skybox(out_wmo_data);
break;
case MOPV:
out_wmo_data->mopv_data_ptr = data_start;
out_wmo_data->mopv_size = header->chunk_size;
log_info(" -> MOPV Chunk found at offset %td", out_wmo_data->mopv_data_ptr - wmo_buffer);
parse_mopv_chunk(out_wmo_data);
break;
case MOPT:
out_wmo_data->mopt_data_ptr = data_start;
out_wmo_data->mopt_size = header->chunk_size;
log_info(" -> MOPT Chunk found at offset %td", out_wmo_data->mopt_data_ptr - wmo_buffer);
parse_mopt_chunk(out_wmo_data);
break;
case MOPR:
out_wmo_data->mopr_data_ptr = data_start;
out_wmo_data->mopr_size = header->chunk_size;
log_info(" -> MOPR Chunk found at offset %td", out_wmo_data->mopr_data_ptr - wmo_buffer);
parse_mopr_chunk(out_wmo_data);
break;
case MOVV:
out_wmo_data->movv_data_ptr = data_start;
out_wmo_data->movv_size = header->chunk_size;
log_info(" -> MOVV Chunk found at offset %td", out_wmo_data->movv_data_ptr - wmo_buffer);
break;
case MOVB:
out_wmo_data->movb_data_ptr = data_start;
out_wmo_data->movb_size = header->chunk_size;
log_info(" -> MOVB Chunk found at offset %td", out_wmo_data->movb_data_ptr - wmo_buffer);
break;
case MOLT:
out_wmo_data->molt_data_ptr = data_start;
out_wmo_data->molt_size = header->chunk_size;
log_info(" -> MOLT Chunk found at offset %td", out_wmo_data->molt_data_ptr - wmo_buffer);
break;
case MODS:
out_wmo_data->mods_data_ptr = data_start;
out_wmo_data->mods_size = header->chunk_size;
log_info(" -> MODS Chunk found at offset %td", out_wmo_data->mods_data_ptr - wmo_buffer);
break;
case MODN:
out_wmo_data->modn_data_ptr = data_start;
out_wmo_data->modn_size = header->chunk_size;
log_info(" -> MODN Chunk found at offset %td", out_wmo_data->modn_data_ptr - wmo_buffer);
break;
case MODD:
out_wmo_data->modd_data_ptr = data_start;
out_wmo_data->modd_size = header->chunk_size;
log_info(" -> MODD Chunk found at offset %td", out_wmo_data->modd_data_ptr - wmo_buffer);
break;
case MFOG:
out_wmo_data->mfog_data_ptr = data_start;
out_wmo_data->mfog_size = header->chunk_size;
log_info(" -> MFOG Chunk found at offset %td", out_wmo_data->mfog_data_ptr - wmo_buffer);
break;
default:
log_info("TODO! %c%c%c%c",
(char)(header->chunk_name >> 24),
(char)(header->chunk_name >> 16),
(char)(header->chunk_name >> 8),
(char)(header->chunk_name));
break;
}
current_ptr = data_start + header->chunk_size;
}
// TODO - actually setup proper loading of chunks before trying to parse NULL info LOL
get_wmo_group_names(archives, out_wmo_data, wmo_file_path);
}
// TODO: need to pass a proper handle to the file here, not just the array of handles
// currently we just pass the whole iteration and hopefully one of them contains (slow!!!)
BOOL extract_blp_file(HANDLE hMPQ, const char *mpqFilePath, const char *localOutPath) {
HANDLE hFile = NULL;
if (hMPQ == NULL || hMPQ == INVALID_HANDLE_VALUE) {
return FALSE; // Skip invalid handles
}
if (!SFileOpenFileEx(hMPQ, mpqFilePath, 0, &hFile)) {
log_error("Could not open file '%s' in MPQ, continuing...", hFile);
return FALSE;
}
// if (!SFileFindFirstFile(hCurrentArchive, mpqFilePath, ))
if (SFileExtractFile(hMPQ, mpqFilePath, localOutPath, SFILE_OPEN_FROM_MPQ)) {
log_info("Sucessfully extracted '%s' from archive to '%s'", mpqFilePath, localOutPath);
return TRUE;
}
if (GetLastError() != ERROR_FILE_NOT_FOUND) {
log_warn("Extraction attempt from archive failed with unexpected error (%lu) for file '%s'. Continuing...",
GetLastError(), mpqFilePath);
}
log_error("Failed to extract file '%s'. File not found in any loaded archive.", mpqFilePath);
return FALSE;
}
void parse_momt_and_extract_textures(ArchiveManager *archives, const WMORootData *wmo_data) {
log_info("Sizeof(SMOMaterial) is '%lu'", sizeof(SMOMaterial));
if (wmo_data->momt_size < sizeof(SMOMaterial)) {
log_error("MOMT chunk size (%lu) is too small to contain a single material!");
return;
}
size_t material_count = wmo_data->momt_size / sizeof(SMOMaterial);
const SMOMaterial *materials = (const SMOMaterial *)wmo_data->momt_data_ptr;
log_info("Parsing %zu materials from MOMT chunk...", material_count);
for (size_t i = 0; i < material_count; i++) {
uint32_t offset = materials[i].texture_name_offset;
// Skip materials that dont reference a texture
if (offset == 0) {
log_info("Material [%zu]: No texture referenced (Offset 0)\n", i);
continue;
}
if (offset >= wmo_data->motx_size) {
log_error("Material [%zu]: Invalid texture offset %lu (outside MOTX boundaries).\n", i, offset);
continue;
}
const char *mpq_blp_path = wmo_data->motx_data_ptr + offset; // offset was throwing us off by 4 bytes it looked like
log_info("Material [%zu] references texture at offset %lu: %s\n", i, offset, mpq_blp_path);
size_t file_len = strlen(mpq_blp_path);
char *local_path_buffer = (char *)malloc(file_len + 1);
if (local_path_buffer == NULL) {
log_error("Memory allocation failed for output path.\n");
continue;
}
// use strncpy!!!
strcpy(local_path_buffer, mpq_blp_path);
char *last_separator = NULL;
// Find the last path separator ('\' or '/')
char *last_sep_1 = strrchr(local_path_buffer, '\\');
char *last_sep_2 = strrchr(local_path_buffer, '/');
// Use the one that is further into the string
last_separator = (last_sep_1 > last_sep_2) ? last_sep_1 : last_sep_2;
// If a directory structure exists (e.g., not just "file.blp")
if (last_separator != NULL) {
char original_separator = *last_separator;
// Temporarily null-terminate to isolate the directory part
*last_separator = '\0';
// Create the directory structure (e.g., "Textures\Doodad")
if (create_dir_recursive(local_path_buffer) == 0) {
// Restore the original path string
*last_separator = original_separator;
HANDLE hCurrentArchive = NULL;
for (size_t i = 0; i < archives->count; i++) {
hCurrentArchive = archives->archives[i];
if (hCurrentArchive == NULL || hCurrentArchive == INVALID_HANDLE_VALUE) {
continue; // Skip invalid handles
}
// so we don't try to extract multiple archives where the file clearly doesnt exist!
// TODO: I should check other for loops and see if they're needlessly sending data to be processed
// when files don't exist!
if (SFileVerifyFile(hCurrentArchive, mpq_blp_path, 0) != 0) {
continue;
}
//extract_blp_file(hCurrentArchive, mpq_blp_path, local_path_buffer);
}
// Extract using the full path
}
} else {
// File is in the root; extract directly
HANDLE hCurrentArchive = NULL;
for (size_t i = 0; i < archives->count; i++) {
hCurrentArchive = archives->archives[i];
if (hCurrentArchive == NULL || hCurrentArchive == INVALID_HANDLE_VALUE) {
continue; // Skip invalid handles
}
//extract_blp_file(hCurrentArchive, mpq_blp_path, local_path_buffer);
}
}
// Cleanup the dynamically allocated path buffer
free(local_path_buffer);
}
}
/*
HANDLE get_file_in_mpq(ArchiveManager *archives, const char *file_in_mpq_name) {
HANDLE hFileInArchive = NULL;
// DWORD dwError = 0;
if (archives->count == 0 || archives->archives[0] == NULL) {
log_error("No archives are mounted. Cannot search for file '%s'", file_in_mpq_name);
return NULL;
}
if (!SFileOpenFileEx(archives->archives[12], file_in_mpq_name, SFILE_OPEN_FROM_MPQ, &hFileInArchive)) {
log_error("Failed to open file '%s' inside MPQ. Error: %lu", file_in_mpq_name, GetLastError());
return NULL;
}
return hFileInArchive;
}*/
uint32_t get_file_size_in_mpq(HANDLE hFileInArchive, const char *file_in_mpq_name) {
DWORD dwFileSize = 0;
if (SFileGetFileInfo(hFileInArchive, SFileInfoFileSize, &dwFileSize,
sizeof(dwFileSize), NULL)) {
log_info("File '%s' size: %lu bytes (0x%lX) or %.6lf megabytes ", file_in_mpq_name, dwFileSize, dwFileSize,
(float)dwFileSize / 1000000);
} else {
log_error("Failed to get size for file '%s'. Error: %lu", file_in_mpq_name, GetLastError());
}
// SFileCloseFile(hFileInArchive);
return dwFileSize;
}
HANDLE get_file_in_archives(ArchiveManager *archives, const char *file_in_mpq_name) {
HANDLE hFile = NULL;
for (size_t i = 0; i < archives->count; i++) {
HANDLE hCurrentArchive = archives->archives[i];
if (hCurrentArchive == NULL || hCurrentArchive == INVALID_HANDLE_VALUE) {
continue;
}
if (SFileOpenFileEx (hCurrentArchive, file_in_mpq_name, SFILE_OPEN_FROM_MPQ, &hFile)) {
// TODO: figure out stormlib hierachy linking, as this won't always return the newest file, just
// a file found...
return hFile;
}
}
log_error("Failed to open file '%s' in ANY mounted archive. Last Error: %lu", file_in_mpq_name, GetLastError());
return NULL;
}
// pass by pointer
void get_mpq_file_path(char *buffer, size_t buffer_size) {
printf("Enter a file path (full or current dir): ");
if (fgets(buffer, buffer_size, stdin) == NULL){
log_error("Error: Could not read from buffer for mpq_file_path()");
return;
}
// convert '\n\r' to null terminating byte '\0'
buffer[strcspn(buffer, "\n\r")] = '\0';
printf("Opening MPQ at %s\n", buffer);
}
// pass by pointer
void get_wmo_file_path(char *buffer, size_t buffer_size) {
printf("Enter a file path (full or current dir): ");
if (fgets(buffer, buffer_size, stdin) == NULL){
log_error("Error: Could not read from buffer for wmo_file_path()");
return;
}
// convert '\n\r' to null terminating byte '\0'
buffer[strcspn(buffer, "\n\r")] = '\0';
printf("Opening WMO at %s\n", buffer);
}
void get_data_dir(char *buffer, size_t buffer_size) {
printf("Enter your WoW 3.3.5a Data Directory (ex. World of Warcraft 3.3.5a\\Data): ");
if (fgets(buffer, buffer_size, stdin) == NULL) {
log_error("Could not read from buffer for wmo_file_path()");
return;
}
// convert '\n\r' to null terminating byte '\0'
buffer[strcspn(buffer, "\n\r")] = '\0';
}
void init_archive_manager(ArchiveManager* manager, const char* root_path) {
manager->archives = (HANDLE*)malloc(INIT_CAPACITY * sizeof(HANDLE));
if (manager->archives == NULL) {
log_error("Failed to allocate memory for ArchiveManager handles");
return;
}
manager->count = 0;
manager->capacity = INIT_CAPACITY;
if (root_path != NULL) {
strncpy(manager->root_path, root_path, MAX_PATH - 1);
manager->root_path[MAX_PATH - 1] = '\0';
} else {
manager->root_path[0] = '\0';
}
log_info("ArchiveManager initialized with capacity %zu", manager->capacity);
}
void init_path_list(PathList *list) {
list->entries = (ArchiveEntry*)malloc(INIT_CAPACITY * sizeof(ArchiveEntry));
list->count = 0;
list->capacity = INIT_CAPACITY;
if (list->entries == NULL) {
log_error("Cannot allocate memory for paths in PathList");
}
}
void expand_path_list(PathList* list) {
list->capacity *= 2;
list->entries = (ArchiveEntry*)realloc(list->entries, list->capacity * sizeof(ArchiveEntry));
if (list->entries == NULL) {
log_error("Cannot reallocate memory for paths in PathList");
}
}
void recursively_scan_directory(const char *current_dir, PathList* list) {
char search_path[MAX_PATH];
WIN32_FIND_DATA find_data;
HANDLE hFind;
snprintf(search_path, MAX_PATH, "%s\\*.*", current_dir);
hFind = FindFirstFile(search_path, &find_data);
if (hFind == INVALID_HANDLE_VALUE) {
log_error("Invalid Handle: %lu", GetLastError());
return;
}
do {
if (strcmp(find_data.cFileName, ".") == 0 || strcmp(find_data.cFileName, "..") == 0) {
continue;
}
char full_path[MAX_PATH];
snprintf(full_path, MAX_PATH, "%s\\%s", current_dir, find_data.cFileName);
if (find_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
recursively_scan_directory(full_path, list);
} else {
if (strstr(find_data.cFileName, ".MPQ") ||
strstr(find_data.cFileName, ".IDX") ||
strstr(find_data.cFileName, ".CASC")) {
// store the path
if (list->count >= list->capacity) {
expand_path_list(list);
}
ArchiveEntry *new_entry = &list->entries[list->count];
snprintf(new_entry->path, MAX_PATH, "%s\\%s", current_dir, find_data.cFileName);
new_entry->load_order_score = assign_score(find_data.cFileName);
list->count++;
/*
// allocate memory for the string
list->entries[list->count] = (char*)malloc(strlen(full_path) + 1);
if (list->entries[list->count] != NULL) {
strcpy(list->entries[list->count], full_path);
list->count++;
// log_info("Found Archive: %s", full_path);
}*/
}
}
} while (FindNextFile(hFind, &find_data) != 0);
FindClose(hFind);
}
void free_path_list(PathList* list) {
if (list == NULL)
return;
/*
for (int i = 0; i < list->count; i++) {
if (list->entries[i] != NULL) {
free(list->entries[i]);
}
}*/
if (list->entries != NULL) {
free(list->entries);
}
}
int assign_score(const char *filename) {
// TODO: Confirm loading order, I believe locales should be last! check titi discord photo
if (strstr(filename, "base-enUS.MPQ") != 0) {
return 10;
}
if (strstr(filename, "patch-enUS.MPQ") != 0) {
return 20;
}
if (strstr(filename, "patch.MPQ") != 0) {
return 30;
}
if (strstr(filename, "patch-enUS-2.MPQ") != 0) {
return 40;
}
if (strstr(filename, "patch-enUS-3.MPQ") != 0) {
return 50;
}
if (strstr(filename, "patch-2.MPQ") != 0) {
return 60;
}
if (strstr(filename, "patch-3.MPQ") != 0) {
return 70;
}
if (strstr(filename, "alternate.MPQ") != 0) {
return 80;
}
if (strstr(filename, "expansion.MPQ") != 0) {
return 90;
}
if (strstr(filename, "lichking.MPQ") != 0) {
return 100;
}
if (strstr(filename, "common.MPQ") != 0) {
return 110;
}
if (strstr(filename, "common-2.MPQ") != 0) {
return 120;
}
if (strstr(filename, "locale-enUS.MPQ") != 0) {
return 130;
}
if (strstr(filename, "speech-enUS.MPQ") != 0) {
return 140;
}
if (strstr(filename, "expansion-locale-enUS.MPQ") != 0) {
return 150;
}
if (strstr(filename, "lichking-locale-enUS.MPQ") != 0) {
return 160;
}
if (strstr(filename, "expansion-speech-enUS.MPQ") != 0) {
return 170;
}
if (strstr(filename, "lichking-speech-enUS.MPQ") != 0) {
return 180;
}
/*
if (strstr(filename, "patch-")) {
// get patch number
const char *num_str = strstr(filename, "patch-") + 6; // adjusts pointer past '-'
int patch_num = atoi(num_str); // TODO; use strtol() instead for error caching?
return 300 + (patch_num * 100);
}*/
// anything else not recognized gets the lowest score
return 1;
}
int archive_comparator(const void *a, const void *b) {
const ArchiveEntry *entryA = (const ArchiveEntry *)a;
const ArchiveEntry *entryB = (const ArchiveEntry *)b;
// lowest score (older) to come first, newest overwrites
if (entryA->load_order_score < entryB->load_order_score) {
return -1;
}
if (entryA->load_order_score > entryB->load_order_score) {
return 1;
}
// use lexicographical comparison of the full path if scores are equal
return strcmp(entryA->path, entryB->path);
}
void expand_archive_manager(ArchiveManager *manager) {
manager->capacity *= 2;
manager->archives = (HANDLE*)realloc(manager->archives, manager->capacity * sizeof(HANDLE));
if (manager->archives == NULL) {
log_error("Failed to reallocate ArchiveManager handles");
return;
}
}

111
src/util.h Normal file
View File

@@ -0,0 +1,111 @@
//
// Created by Tristan on 11/6/2025.
//
#ifndef UTIL_H
#define UTIL_H
#include <stdint.h>
#include "StormLib.h"
#include "mpq/mpq.h"
#include "wmo/wmo_structs.h"
#ifdef _WIN32
#include <direct.h> // _mkdir
//#define dwError GetLastError()
#define MAKE_DIR(path) _mkdir(path)
#else
#include <sys/stat.h>
#include <errno.h>
#define dwError errno
#define MAKE_DIR(path) mkdir(path, 0777)
#endif
#define DEV_MODE
#define PATH_SEPARATOR '\\'
// Root Chunks
#define MVER 0x4d564552
#define MOTX 0x4d4f5458
#define MOMT 0x4d4f4d54
#define MOHD 0x4d4f4844
#define MOGN 0x4d4f474e
#define MOGI 0x4d4f4749
#define MOSB 0x4d4f5342
#define MOPV 0x4d4f5056
#define MOPT 0x4d4f5054
#define MOPR 0x4d4f5052
#define MOVV 0x4d4f5656
#define MOVB 0x4d4f5642
#define MOLT 0x4d4f4c54
#define MODS 0x4d4f4453
#define MODN 0x4d4f444e
#define MODD 0x4d4f4444
#define MFOG 0x4d464f47
// Group Chunks
#define MOGP 0x4d4f4750
#define MOGX 0x4d4f4758
#define MOPY 0x4d4f5059
#define MPY2 0x4d505932
#define MOVI 0x4d4f5649
#define MOVT 0x4d4f5654
#define MONR 0x4d4f4e52
#define MOTV 0x4d4f5456
#define MOBA 0x4d4f4241
#define MOQG 0x4d4f4747
#define MOLR 0x4d4f4c52
#define MODR 0x4d4f4452
#define MOBN 0x4d4f424e
#define MOBR 0x4d4f4252
#define MOCV 0x4d4f4356
#define MOC2 0x4d4f4332
#define MLIQ 0x4d4c4951
#define MORI 0x4d4f5249
#define INIT_CAPACITY 19
typedef struct {
uint32_t chunk_name; // FourCC
uint32_t chunk_size; // in bytes
} WMOChunkHeader;
typedef struct {
char *data;
DWORD size;
} WMOData;
typedef struct {
ArchiveEntry* entries;
int count;
int capacity;
} PathList;
WMOData load_wmo_data(ArchiveManager *archives, const char *wmoFileName);
WMOData load_wmo_data_from_file(FILE **file_ptr_in);
// HANDLE get_file_in_mpq(HANDLE hMPQ, const char *file_in_mpq_name);
HANDLE get_file_in_archives(ArchiveManager *archives, const char *file_in_mpq_name);
uint32_t get_file_size_in_mpq(HANDLE hFileInArchive, const char *file_in_mpq_name);
size_t get_wmo_base_name(char *dest, size_t dest_size, const char *full_path);
int load_wmo_file(const char *filename, FILE **file_ptr_out);
void get_mpq_file_path(char *buffer, size_t buffer_size);
void get_wmo_file_path(char *buffer, size_t buffer_size);
void parse_wmo_chunks(ArchiveManager *archives, const char *wmo_buffer, DWORD total_size, WMORootData *out_wmo_data,
const char *wmo_file_path);
void parse_momt_and_extract_textures(ArchiveManager *archives, const WMORootData *wmo_data);
void get_data_dir(char *buffer, size_t buffer_size);
void init_archive_manager (ArchiveManager *manager, const char *root_path);
void init_path_list(PathList *list);
void expand_path_list(PathList* list);
void recursively_scan_directory(const char *current_dir, PathList* list);
void free_path_list(PathList* list);
int assign_score(const char *filename);
int archive_comparator(const void *a, const void *b);
void expand_archive_manager(ArchiveManager *manager);
char *get_file_contents(const char *filename);
#endif //UTIL_H

703
src/wmo/wmo.c Normal file
View File

@@ -0,0 +1,703 @@
//
// Created by Tristan on 11/11/2025.
//
#include <stdint.h>
#include "../util.h"
#include "wmo.h"
#include "../logger/log.h"
#include "../renderer/texture.h"
void parse_mohd_chunk(const WMORootData *wmo_root_data) {
SMOHeader *header = (SMOHeader *)wmo_root_data->mohd_data_ptr;
log_info("\t%d groups found in root file", header->nGroups);
}
void get_wmo_group_names(ArchiveManager *archives, WMORootData *wmo_root_data, const char *wmo_file_path) {
const SMOGroupInfo *groupInfo = (SMOGroupInfo* )wmo_root_data->mogi_data_ptr;
size_t group_count = wmo_root_data->mogi_size / sizeof(SMOGroupInfo);
wmo_root_data->groups = (WMOGroupData *)calloc(group_count, sizeof(WMOGroupData));
if (wmo_root_data->groups == NULL) {
log_error("Failed to allocate memory for WMO groups");
return;
}
wmo_root_data->group_count = group_count;
char wmo_base_name[MAX_PATH];
if (get_wmo_base_name(wmo_base_name, MAX_PATH, wmo_file_path) == 0) {
log_error("Could not determine WMO base path for group naming");
}
char mpq_path_base[MAX_PATH];
strncpy(mpq_path_base, wmo_file_path, MAX_PATH);
size_t path_len = strlen(mpq_path_base);
if (path_len > 4 && strcmp(mpq_path_base + path_len - 4, ".wmo") == 0) {
mpq_path_base[path_len - 4] = '\0'; // strips .wmo
} else {
log_error("WMO file path missing .wmo extension. Cannot construct group file paths");
}
log_info("Parsing %zu groups from MOGI chunk. Base path: %s", group_count, wmo_base_name);
if (archives == NULL) {
log_error("HANDLE is NULL. Cannot verify WMO group file existence");
return;
}
int mpq_check_index = 0;
size_t file_index_counter = 0;
size_t file_index = 0;
for (size_t i = 0; i < group_count; i++) {
int32_t offset = groupInfo[i].name_offset;
// Skip materials that dont reference a group
if (offset == 0) {
log_info("Group [%zu]: No Group referenced (Offset 0)", i);
continue;
}
// -1 means no name wowdev.wiki/WMO#MOGI
if (offset == -1) {
log_info("Group [%zu]: No group name referenced (Offset -1)", i);
}
if (offset >= wmo_root_data->mogi_size && offset != -1) {
log_error("Group [%zu]: Invalid group offset %lu (outside MOGI boundaries).", i, offset);
continue;
}
if (archives != NULL) {
bool file_found = false;
int found_file_index = -1;
while (mpq_check_index < 100) { // init_capacity is hardcoded, should dynamically check loaded mpqs...
// need to pass struct pointer for that tho I think
char file_to_check[MAX_PATH];
snprintf(file_to_check, MAX_PATH, "%s_%03d.wmo", mpq_path_base, mpq_check_index);
HANDLE hFile = get_file_in_archives(archives, file_to_check);
if (hFile != NULL) {
SFileCloseFile(hFile);
found_file_index = mpq_check_index;
file_found = true;
mpq_check_index++;
break;
}
mpq_check_index++;
}
if (!file_found) {
log_error("MOGI Group [%zu] found, but could not find a matching WMO group file in MPQ. Skipping.", i);
continue;
}
char group_file_path[MAX_PATH];
int result = snprintf(group_file_path, MAX_PATH, "%s_%03d.wmo", mpq_path_base, found_file_index);
if (result >= MAX_PATH || result < 0) {
log_error("Group path construction failed or truncated for index %zu", i);
continue;
}
const char *mogn_name = (offset == -1) ? "No name referenced (Offset -1)" : (wmo_root_data->mogn_data_ptr + offset);
log_info("Group [%zu] file path: %s (MOGN: %s) Mapped to actual file index %03d", i, group_file_path, mogn_name, found_file_index);
// if (get_file_in_mpq())
WMOGroupData group_data = parse_wmo_group_file(archives, group_file_path);
wmo_root_data->groups[i] = group_data;
if (wmo_root_data->groups[i].movt_data_ptr != NULL) {
log_info("Successfully loaded group %zu: %lu vertices found!", i, wmo_root_data->groups[i].movt_size / sizeof(C3Vector));
}
}
}
}
void get_mosb_skybox(const WMORootData *wmo_root_data) {
if (wmo_root_data->mosb_data_ptr == NULL || wmo_root_data->mosb_size == 0) {
log_info("MOSB chunk not found in WMO root");
}
const char *skybox_name = wmo_root_data->mosb_data_ptr;
log_info("Parsing MOSB chunk...");
if (*skybox_name != '\0') {
log_info(" Skybox name found in MOSB chunk: %s", skybox_name);
} else {
log_info(" Skybox is not found for WMO. (first byte is 0)");
}
}
void parse_mopv_chunk(const WMORootData *wmo_root_data) {
if (wmo_root_data->mopv_data_ptr == NULL || wmo_root_data->mopv_size == 0) {
log_info("MOPV chunk not found in WMO root");
}
// Portal vertices, one entry is a float[3], usually 4 * 3 * float per portal (actual number of vertices given in portal entry)
// 4 bytes * float(x,y,z) * number of vertices
C3Vector *portalVertexList = (C3Vector *)wmo_root_data->mopv_data_ptr;
size_t portal_vertex_count = wmo_root_data->mopv_size / sizeof(C3Vector);
log_info("Found MOPV data (portals) with %zu vertices.", portal_vertex_count);
for (size_t i = 0; i < portal_vertex_count; i++) {
C3Vector current_vertex = portalVertexList[i];
log_info("Vertex %zu: X=%f, Y=%f, Z=%f", i, current_vertex.x, current_vertex.y, current_vertex.z);
}
}
void parse_mopt_chunk(const WMORootData *wmo_root_data) {
if (wmo_root_data->mopt_data_ptr == NULL || wmo_root_data->mopt_size == 0) {
log_info("MOPT chunk not found in WMO root");
}
SMOPortal *portalList = (SMOPortal *)wmo_root_data->mopt_data_ptr;
size_t portal_list_count = wmo_root_data->mopt_size / sizeof(SMOPortal);
log_info("Found MOPT data (portal lists) with %zu portals", portal_list_count);
for (size_t i = 0; i < portal_list_count; i++) {
SMOPortal portal = portalList[i];
log_info("Portal %zu: startVertex: %u, count: %u, plane: normal(X=%.2f, Y=%.2f, Z=%.2f), distance(%.2f)",
i, portal.startVertex, portal.count, portal.plane.normal.x, portal.plane.normal.y, portal.plane.normal.z,
portal.plane.distance);
}
}
void parse_mopr_chunk(const WMORootData *wmo_root_data) {
if (wmo_root_data->mopr_data_ptr == NULL || wmo_root_data->mopr_size == 0) {
log_info("MOPR chunk not found in WMO root");
return;
}
SMOPortalRef *portalRefList = (SMOPortalRef *)wmo_root_data->mopr_data_ptr;
size_t portal_ref_list_count = wmo_root_data->mopr_size / sizeof(SMOPortalRef);
log_info("Found MOPR data (portal ref lists) with %zu references", portal_ref_list_count);
/* wmo_root_data->portal_references = (SMOPortalRef *)malloc(portal_ref_list_count * sizeof(SMOPortalRef));
if (wmo_root_data->portal_references == NULL) {
log_error("Failed to allocate memory for MOPR references.");
return; // Handle allocation failure
}*/
//wmo_root_data->portal_ref_count = portal_ref_list_count;
//memcpy(wmo_root_data->portal_references, wmo_root_data->mopr_data_ptr, wmo_root_data->mopr_size);
for (size_t i = 0; i < portal_ref_list_count; i++) {
SMOPortalRef portal_ref = portalRefList[i];
log_info("Portal reference %zu: portalIndex: %u, groupIndex: %u, side: %d", i, portal_ref.portalIndex,
portal_ref.groupIndex, portal_ref.side);
}
// TODO - DOES THE CALLER OR THE FUNCTION FREE MALLOC()?
}
WMOGroupData parse_wmo_group_file(ArchiveManager *archives, const char *group_file_path) {
WMOGroupData group_data = {0};
WMOData file_data = load_wmo_data(archives, group_file_path);
if (file_data.data == NULL) {
log_error("Failed to load WMO group file: %s", group_file_path);
return group_data;
}
group_data.group_file_buffer = file_data.data;
group_data.group_file_size = file_data.size;
const char *current_ptr = file_data.data;
const char *end_ptr = file_data.data + file_data.size;
if (current_ptr + sizeof(WMOChunkHeader) > end_ptr) {
log_error("Group file is too small to contain a chunk header");
return group_data;
}
const WMOChunkHeader *mver_header = (const WMOChunkHeader *)current_ptr;
if (mver_header->chunk_name != MVER) {
log_error("Expected MVER chunk as first chunk, found: %c%c%c%c",
(char)(mver_header->chunk_name >> 24), (char)(mver_header->chunk_name >> 16),
(char)(mver_header->chunk_name >> 8), (char)(mver_header->chunk_name));
return group_data;
}
current_ptr += sizeof(WMOChunkHeader) + mver_header->chunk_size;
if (current_ptr + sizeof(WMOChunkHeader) > end_ptr) {
log_error("Group file ended before MOGP chunk was found");
return group_data;
}
const WMOChunkHeader *mogp_header = (const WMOChunkHeader *)current_ptr;
const char *data_start = current_ptr + sizeof(WMOChunkHeader);
if (mogp_header->chunk_name != MOGP) {
log_error("Expected MVER chunk as first chunk, found: %c%c%c%c",
(char)(mogp_header->chunk_name >> 24), (char)(mogp_header->chunk_name >> 16),
(char)(mogp_header->chunk_name >> 8), (char)(mogp_header->chunk_name));
return group_data;
}
log_info("Found MOGP chunk (size: %lu bytes) in group file", mogp_header->chunk_size);
parse_mogp_sub_chunks(data_start, mogp_header->chunk_size, &group_data);
/*
if (file_data.data != NULL) {
free(file_data.data);
}*/
return group_data;
}
void parse_moba_chunk(const WMOGroupData *wmo_group_data) {
if (wmo_group_data->moba_data_ptr == NULL || wmo_group_data->moba_size == 0) {
log_error("Could not find valid MOBA chunk inside WMO Group File");
return;
}
const SMOBatch *batches = (const SMOBatch *)wmo_group_data->moba_data_ptr;
size_t num_batches = wmo_group_data->moba_size / sizeof(SMOBatch);
log_info("Found %zu batches in MOBA chunk (Total bytes: %lu)", num_batches, wmo_group_data->moba_size);
for (size_t i = 0; i < num_batches; i++) {
const SMOBatch *batch = &batches[i];
log_info("Batch: %zu, Material ID: %u, MOVI Start Index: %u, MOVI Index Count: %u, MOVT Vertex Range: %u to %u",
i, batch->material_id, batch->startIndex, batch->count, batch->minIndex, batch->maxIndex);
}
}
void parse_motv_chunk(const WMOGroupData *wmo_group_data) {
if (wmo_group_data->motv_data_ptr == NULL || wmo_group_data->motv_size == 0) {
log_error("Could not find valid MOTV chunk inside WMO Group File");
return;
}
const C2Vector *textureVertexList = (const C2Vector *)wmo_group_data->motv_data_ptr;
size_t num_texture_vertexs = wmo_group_data->motv_size / sizeof(C2Vector);
log_info("Found %zu texture vertexs in MOTV chunk (Total bytes: %lu)", num_texture_vertexs, wmo_group_data->motv_size);
/*
for (size_t i = 0; i < num_texture_vertexs; i++) {
float x = textureVertexList[i].x;
float y = textureVertexList[i].y;
log_info("Texture Vertex %zu: (X: %.4f, Y: %.4f)", i, x, y);
}*/
}
void parse_molr_chunk(const WMOGroupData *wmo_group_data) {
if (wmo_group_data->molr_data_ptr == NULL || wmo_group_data->molr_size == 0) {
log_error("Could not find valid MOLR chunk inside WMO Group File");
return;
}
const uint16_t *overrideLightRefs = (const uint16_t *)wmo_group_data->molr_data_ptr;
size_t num_light_refs = wmo_group_data->molr_size / sizeof(uint16_t);
log_info("Found %zu light references in MOLR chunk (Total bytes: %lu", num_light_refs, wmo_group_data->molr_size);
///*
for (size_t i = 0; i < num_light_refs; i++) {
log_info("Light Reference %zu: %hu", i, overrideLightRefs[i]);
} //*/
}
void parse_monr_chunk(const WMOGroupData *wmo_group_data) {
if (wmo_group_data->monr_data_ptr == NULL || wmo_group_data->monr_size == 0) {
log_error("Could not find valid MONR chunk inside WMO Group File");
return;
}
const C3Vector *normalList = (const C3Vector *)wmo_group_data->monr_data_ptr;
size_t num_normals = wmo_group_data->monr_size / sizeof(C3Vector);
log_info("Found %zu normals in MONR chunk (Total bytes: %lu)", num_normals, wmo_group_data->monr_size);
/*
for (size_t i = 0; i < num_normals; i++) {
float x = normalList[i].x;
float y = normalList[i].y;
float z = normalList[i].z;
log_info("Normal %zu: (X: %.4f, Y: %.4f, Z: %.4f)", i, x, y, z);
}*/
}
void parse_movt_chunk(const WMOGroupData *wmo_group_data) {
if (wmo_group_data->movt_data_ptr == NULL || wmo_group_data->movt_size == 0) {
log_error("Could not find valid MOVT chunk inside WMO Group File");
return;
}
const C3Vector *vertexList = (const C3Vector *)wmo_group_data->movt_data_ptr;
size_t num_vertexes = wmo_group_data->movt_size / sizeof(C3Vector);
log_info("Found %zu vertices in MOVT chunk (Total bytes: %lu)", num_vertexes, wmo_group_data->movt_size);
/*
for (size_t i = 0; i < num_vertexs; i++) {
float x = vertexList[i].x;
float y = vertexList[i].y;
float z = vertexList[i].z;
log_info("Vertex %zu: (X: %.4f, Y: %.4f, Z: %.4f)", i, x, y, z);
}*/
}
void parse_movi_chunk(const WMOGroupData *wmo_group_data) {
if (wmo_group_data->movi_data_ptr == NULL || wmo_group_data->movi_size == 0) {
log_error("Could not find valid MOVI chunk inside WMO Group File");
return;
}
const uint16_t *indices = (const uint16_t *)wmo_group_data->movi_data_ptr;
size_t num_indices = wmo_group_data->movi_size / sizeof(uint16_t);
size_t num_triangles = num_indices / 3;
log_info("Found %zu triangles from %zu indices", num_triangles, num_indices);
/*
for (size_t i = 0; i < num_indices; i += 3) {
uint16_t v1_idx = indices[i + 0];
uint16_t v2_idx = indices[i + 1];
uint16_t v3_idx = indices[i + 2];
log_info("Triangle %zu indices: %u, %u, %u", i/3, v1_idx, v2_idx, v3_idx);
// TODO: export f line for obj file?
}*/
}
void parse_mogp_sub_chunks(const char *mogp_data_buffer, DWORD mogp_data_size, WMOGroupData *out_group_data) {
if (mogp_data_size < sizeof(SMOGroupHeader)) {
log_error("MOGP chunk size is too small for SMOGroupHeader");
return;
}
memcpy(&out_group_data->header, mogp_data_buffer, sizeof(SMOGroupHeader));
log_info("SMOGroupHeader successfully extracted (size: %zu bytes)", sizeof(SMOGroupHeader));
const char *current_ptr = mogp_data_buffer + sizeof(SMOGroupHeader);
const char *end_ptr = mogp_data_buffer + mogp_data_size;
log_info("Starting WMO Group file sub-chunk traversal...");
while (current_ptr < end_ptr) {
if (current_ptr + sizeof(WMOChunkHeader) > end_ptr) {
log_warn("End of MOGP chunk reached unexpectedly");
break;
}
const WMOChunkHeader *header = (const WMOChunkHeader *)current_ptr;
const char *data_start = current_ptr + sizeof(WMOChunkHeader);
switch (header->chunk_name) {
case MOGX: // versions >10.0.0.46181
out_group_data->mogx_data_ptr = data_start;
out_group_data->mogx_size = header->chunk_size;
log_info(" -> MOGX Chunk found at offset %td", out_group_data->mogx_data_ptr - mogp_data_buffer);
break;
case MOPY: // material info for triangles, two bytes per triangle. Size is twice number of triangles in WMO group
out_group_data->mopy_data_ptr = data_start;
out_group_data->mopy_size = header->chunk_size;
log_info(" -> MOPY Chunk found at offset %td", out_group_data->mopy_data_ptr - mogp_data_buffer);
break;
case MPY2: // versions >10.0.0.46181
out_group_data->mpy2_data_ptr = data_start;
out_group_data->mpy2_size = header->chunk_size;
log_info(" -> MPY2 Chunk found at offset %td", out_group_data->mpy2_data_ptr - mogp_data_buffer);
break;
case MOVI: // MapObject Vertex Indices
out_group_data->movi_data_ptr = data_start;
out_group_data->movi_size = header->chunk_size;
log_info(" -> MOVI Chunk found at offset %td", out_group_data->movi_data_ptr - mogp_data_buffer);
parse_movi_chunk(out_group_data);
break;
case MOVT:
out_group_data->movt_data_ptr = data_start;
out_group_data->movt_size = header->chunk_size;
log_info(" -> MOVT Chunk found at offset %td", out_group_data->movt_data_ptr - mogp_data_buffer);
parse_movt_chunk(out_group_data);
break;
case MONR:
out_group_data->monr_data_ptr = data_start;
out_group_data->monr_size = header->chunk_size;
log_info(" -> MONR Chunk found at offset %td", out_group_data->monr_data_ptr - mogp_data_buffer);
parse_monr_chunk(out_group_data);
break;
case MOTV:
out_group_data->motv_data_ptr = data_start;
out_group_data->motv_size = header->chunk_size;
log_info(" -> MOTV Chunk found at offset %td", out_group_data->motv_data_ptr - mogp_data_buffer);
parse_motv_chunk(out_group_data);
break;
case MOBA:
out_group_data->moba_data_ptr = data_start;
out_group_data->moba_size = header->chunk_size;
log_info(" -> MOBA Chunk found at offset %td", out_group_data->moba_data_ptr - mogp_data_buffer);
log_info("Sizeof SMOBatch: %zu", sizeof(SMOBatch));
parse_moba_chunk(out_group_data);
break;
case MOQG: // versions >10.0.0.46181
out_group_data->moqg_data_ptr = data_start;
out_group_data->moqg_size = header->chunk_size;
log_info(" -> MOQG Chunk found at offset %td", out_group_data->moqg_data_ptr - mogp_data_buffer);
break;
case MOLR:
out_group_data->molr_data_ptr = data_start;
out_group_data->molr_size = header->chunk_size;
log_info(" -> MOLR Chunk found at offset %td", out_group_data->molr_data_ptr - mogp_data_buffer);
parse_molr_chunk(out_group_data);
break;
case MODR:
case MOBN:
case MOBR:
case MOCV:
case MOC2:
case MLIQ:
case MORI:
default:
log_info("TODO! %c%c%c%c",
(char)(header->chunk_name >> 24),
(char)(header->chunk_name >> 16),
(char)(header->chunk_name >> 8),
(char)(header->chunk_name));
break;
}
current_ptr = data_start + header->chunk_size;
}
}
void extract_wmo_geometry(const WMORootData *wmo_root_data, FILE *output_file) {
if (wmo_root_data->groups == NULL || wmo_root_data->group_count == 0) {
log_error("No WMO group data found to extract geometry from");
return;
}
if (output_file == NULL) {
log_error("Output file handle is NULL. Cannot log geometry details.");
return;
}
fprintf(output_file, "# WMO Geometry Data Extraction Log\n");
fprintf(output_file, "# Root WMO contains %zu groups\n", wmo_root_data->group_count);
log_info("Starting geometry extraction for %zu WMO groups...", wmo_root_data->group_count);
if (wmo_root_data->mohd_data_ptr == NULL) {
log_error("MOHD data is missing. Cannot calculate global center");
return;
}
const SMOHeader *header = (const SMOHeader *)wmo_root_data->mohd_data_ptr;
float center_x = (header->bounding_box.min[0] + header->bounding_box.max[0]) / 2.0f;
float center_y = (header->bounding_box.min[1] + header->bounding_box.max[1]) / 2.0f;
float center_z = (header->bounding_box.min[2] + header->bounding_box.max[2]) / 2.0f;
log_info("Global WMO Center calculated: (%.2f, %.2f, %.2f)", center_x, center_y, center_z);
size_t cumulative_vertex_count = 0;
for (size_t i = 0; i < wmo_root_data->group_count; i++) {
const WMOGroupData *group = &wmo_root_data->groups[i];
if (group->movt_data_ptr == NULL) {
fprintf(output_file, "g Group_%zu_SKIPPED\n", i);
log_info("Group %zu skipped (no geometry data found)", i);
continue;
}
fprintf(output_file, "\n# -------------------\n");
fprintf(output_file, "g Group_%zu\n", i);
/* -----------------------------------------------------------------
1. SETUP ALL CHUNKS AND OFFSETS
----------------------------------------------------------------- */
size_t num_vertices = group->movt_size / sizeof(C3Vector);
const C3Vector *vertices = (const C3Vector *)group->movt_data_ptr;
// Pointers for new chunks
const C3Vector *normals = (const C3Vector *)group->monr_data_ptr;
const C2Vector *tex_coords = (const C2Vector *)group->motv_data_ptr;
// Offset (already calculated)
float offset_x = group->header.boundingBox.min[0];
float offset_y = group->header.boundingBox.min[1];
float offset_z = group->header.boundingBox.min[2];
fprintf(output_file, "# Found %zu vertices (MOVT), normals (MONR), and texcoords (MOTV)\n", num_vertices);
/* -----------------------------------------------------------------
2. VERTICES, NORMALS, AND TEXCOORDS (MOVT, MONR, MOTV) - ONE LOOP
----------------------------------------------------------------- */
for (size_t j = 0; j < num_vertices; j++) {
// A. Vertices (v) - Apply offset and coordinate swap
float x_wmo = vertices[j].x + offset_x;
float y_wmo = vertices[j].y + offset_y;
float z_wmo = vertices[j].z + offset_z;
// WMO -> OBJ Transformation
float x_obj = x_wmo;
float y_obj = z_wmo;
float z_obj = -y_wmo;
x_obj -= center_x;
y_obj -= center_z;
z_obj -= center_y;
fprintf(output_file, "v %.6f %.6f %.6f\n", x_obj, y_obj, z_obj);
// B. Texture Coordinates (vt) - Apply V-flip
float u_wmo = tex_coords[j].x;
float v_wmo = tex_coords[j].y;
float v_obj = 1.0f - v_wmo; // V-Flip is commonly required
fprintf(output_file, "vt %.6f %.6f\n", u_wmo, v_obj);
// C. Normals (vn) - Apply the SAME coordinate swap (NO offset for normals)
// Normals are unit vectors and should NOT be translated.
float nx_wmo = normals[j].x;
float ny_wmo = normals[j].y;
float nz_wmo = normals[j].z;
float nx_obj = nx_wmo;
float ny_obj = nz_wmo;
float nz_obj = -ny_wmo;
fprintf(output_file, "vn %.6f %.6f %.6f\n", nx_obj, ny_obj, nz_obj);
}
/* -----------------------------------------------------------------
3. FACES (MOVI & MOBA) - Second Loop
----------------------------------------------------------------- */
const uint16_t *all_indices = (const uint16_t *)group->movi_data_ptr;
const SMOBatch *batches = (const SMOBatch *)group->moba_data_ptr;
size_t num_batches = group->moba_size / sizeof(SMOBatch);
fprintf(output_file, "# Found %zu batches (MOBA) for face definitions.\n", num_batches);
for (size_t b = 0; b < num_batches; b++) {
const SMOBatch *batch = &batches[b];
// Output material information (requires MTL file, but helps organize the OBJ)
fprintf(output_file, "\n# Material Group ID: %u\n", batch->material_id);
// In a full export, you would write: fprintf(output_file, "usemtl material_%u\n", batch->material_id);
uint32_t start_index = batch->startIndex;
uint32_t index_count = batch->count;
// Iterate through the indices for this batch, three at a time (triangles)
for (uint32_t k = start_index; k < start_index + index_count; k += 3) {
// Get the local (0-based) vertex indices
uint16_t v1_local = all_indices[k + 0];
uint16_t v2_local = all_indices[k + 1];
uint16_t v3_local = all_indices[k + 2];
// Apply the GLOBAL offset and the 1-based OBJ requirement
size_t v1_obj = (size_t)v1_local + 1 + cumulative_vertex_count;
size_t v2_obj = (size_t)v2_local + 1 + cumulative_vertex_count;
size_t v3_obj = (size_t)v3_local + 1 + cumulative_vertex_count;
// Write face line: f v/vt/vn v/vt/vn v/vt/vn
// Since MOVT, MOTV, and MONR indices are 1:1, all three indices are the same.
fprintf(output_file, "f %zu/%zu/%zu %zu/%zu/%zu %zu/%zu/%zu\n",
v1_obj, v1_obj, v1_obj,
v2_obj, v2_obj, v2_obj,
v3_obj, v3_obj, v3_obj
);
}
}
// ----------------------------------------------------
// C. Update Cumulative Count for the NEXT Group
// ----------------------------------------------------
cumulative_vertex_count += num_vertices;
}
log_info("Geometry data written to output file");
}
// isTransFace: triangles flagged as TRANSITION. They blend lighting from exterior to interior
char smopoly_is_trans_face(SMOPolyFlags flags) {
return flags.f_unk && (flags.f_detail || flags.f_render);
}
// isColor: used to determine if the triangle has color/texture data (not just collision)
char smopoly_is_color(SMOPolyFlags flags) {
return !flags.f_collision;
}
// isRenderFace: triangles that should be visible
char smopoly_is_render_face(SMOPolyFlags flags) {
return flags.f_render && !flags.f_detail;
}
// isCollidable: Triangles that can be physically interacted with
char smopoly_is_collidable(SMOPolyFlags flags) {
return flags.f_collision || smopoly_is_render_face(flags);
}
void wmo_load_textures_to_gpu(ArchiveManager* manager, WMORootData* root) {
if (!root->momt_data_ptr || !root->motx_data_ptr)
// TODO: error logging
return;
SMOMaterial *materials = (SMOMaterial *)root->momt_data_ptr;
int num_materials = root->momt_size / sizeof(SMOMaterial);
// allocate opengl ID array
root->material_textures = (GLuint*)calloc(num_materials, sizeof(GLuint));
log_info("Loading %d textures to GPU...", num_materials);
for (int i = 0; i < num_materials; i++) {
uint32_t name_offset = materials[i].texture_name_offset;
/*
if (name_offset == 0 || name_offset >= root->motx_size) {
log_warn("Material %d has invalid texture offset", i);
root->material_textures[i] = 0;
continue;
}*/
const char *filename = root->motx_data_ptr + name_offset;
HANDLE hFile = get_file_in_archives(manager, filename);
if (hFile) {
DWORD fileSize = SFileGetFileSize(hFile, NULL);
if (fileSize > 0) {
uint8_t* buffer = (uint8_t*)malloc(fileSize);
DWORD bytesRead = 0;
SFileReadFile(hFile, buffer, fileSize, &bytesRead, NULL);
root->material_textures[i] = texture_load_from_blp_memory(buffer, fileSize);
free(buffer);
}
SFileCloseFile(hFile);
} else {
log_warn("Failed to find texture: %s", filename);
root->material_textures[i] = 0;
}
}
}

26
src/wmo/wmo.h Normal file
View File

@@ -0,0 +1,26 @@
//
// Created by Tristan on 11/11/2025.
//
#ifndef WMO_H
#define WMO_H
#include "wmo_structs.h"
void get_wmo_group_names(HANDLE hMPQ, WMORootData *wmo_root_data, const char *wmo_file_path);
void get_mosb_skybox(const WMORootData *wmo_root_data);
void parse_mohd_chunk(const WMORootData *wmo_root_data);
void parse_mopv_chunk(const WMORootData *wmo_root_data);
void parse_mopt_chunk(const WMORootData *wmo_root_data);
void parse_mopr_chunk(const WMORootData *wmo_root_data);
void parse_mogp_sub_chunks(const char *mogp_data_buffer, DWORD mogp_data_size, WMOGroupData *out_group_data);
WMOGroupData parse_wmo_group_file(ArchiveManager *archives, const char *group_file_path);
void extract_wmo_geometry(const WMORootData *wmo_root_data, FILE *output_file);
void wmo_load_textures_to_gpu(ArchiveManager* manager, WMORootData* root);
bool smopoly_is_trans_face(SMOPolyFlags flags);
bool smopoly_is_color(SMOPolyFlags flags);
bool smopoly_is_render_face(SMOPolyFlags flags);
bool smopoly_is_collidable(SMOPolyFlags flags);
#endif //WMO_H

297
src/wmo/wmo_structs.h Normal file
View File

@@ -0,0 +1,297 @@
//
// Created by Tristan on 11/11/2025.
//
#ifndef WMO_STRUCTS_H
#define WMO_STRUCTS_H
#include "StormLib.h"
#include "glad/glad.h"
/* ---------- Enums ---------- */
typedef enum
{
Flag_XAxis = 0x0,
Flag_YAxis = 0x1,
Flag_ZAxis = 0x2,
Flag_AxisMask = 0x3,
Flag_Leaf = 0x4,
Flag_NoChild = 0xFFFF,
} Flags;
/* ---------- Helper Structs ---------- */
typedef struct {
uint16_t flags; // See above enum (Flags). 4: leaf, 0 for YZ-plane, 1 for XZ-plane, 2 for XY-plane
int16_t negChild; // index of bsp child node (right in this array)
int16_t posChild;
uint16_t nFaces; // num of triangle faces in MOBR
uint32_t faceStart; // index of the first triangle index(in MOBR)
float planeDist;
} CAaBspNode;
// unions allow access to a single uint32 or component value
typedef union {
uint32_t raw_value;
struct {
uint8_t a, r, g, b; // TODO: check endianness
} components;
} CArgb;
// Axis-aligned bounding box
typedef struct {
float min[3];
float max[3];
} CAabox;
// TODO: keep C2/C3/C4 vector struct definitions or use the vec2_t/vec3_t/vec4_t structs
// potentially could wrap them in a define and just have C2Vector/C3/C4 mean vec2/vec3/vec4 but thats not very ideal I imagine...
typedef struct {
float x;
float y;
} C2Vector;
typedef struct {
float x;
float y;
float z;
} C3Vector;
typedef struct {
C3Vector normal;
float distance;
} C4Plane;
// MOHD
typedef struct {
uint32_t nTextures; // 0x00: number of textures (BLP files)
uint32_t nGroups; // 0x04: number of WMO groups
uint32_t nPortals; // 0x08: number of portals
uint32_t nLights; // 0x0C: number of lights
uint32_t nModels; // 0x10: number of M2 models imported
uint32_t nDoodadDefs; // 0x14: number of doodads (M2 instances)
uint32_t nDoodadSets; // 0x18: number of doodad sets
CArgb amColor; // 0x1C:
uint32_t wmoID; // 0x20: Foreign key for WMOAreaTableRec::m_WMOID (column 2 in WMOAreaTable.dbc)
CAabox bounding_box; // 0x24:
uint32_t flags; // 0x3C: Combined 16-bit flags field
uint32_t numLOD; // 0x3E:
} SMOHeader;
typedef struct {
uint32_t flags; // 0x00;
CAabox bounding_box; // 0x04;
int32_t name_offset; // 0x1C;
} SMOGroupInfo;
typedef struct {
uint32_t flags; // 0x00: Material Flags
uint32_t shader; // 0x04: Shader Index
uint32_t blendMode; // 0x08: Blending (EGxBlend)
uint32_t texture_name_offset; // 0x0C: Offset into MOTX string block
uint32_t sidnColor; // 0x10: Emissive color (CImVector)
uint32_t frameSidnColor; // 0x14: SIDN emissive color (CImVector)
uint32_t texture_2_offset; // 0x18: Secondary texture offset/ID
uint32_t diffColor; // 0x1C: Diffuse color (CImVector)
uint32_t ground_type; // 0x20: TerrainTypeRec::m_ID
uint32_t texture_3_offset; // 0x24: Tertiary texture offset/ID
uint32_t color_2; // 0x28:
uint32_t flags_2; // 0x2C:
uint32_t runTimeData[4]; // 0x30: 16 bytes of runtime data
} SMOMaterial;
// MOPT
typedef struct {
uint16_t startVertex;
uint16_t count;
C4Plane plane;
} SMOPortal;
// MOPR
typedef struct {
uint16_t portalIndex; // into MOPT
uint16_t groupIndex; // the other one? (MOGI?)
int16_t side; // positive or negative
uint16_t padding; // filler
} SMOPortalRef;
typedef struct {
int16_t x, y, z;
} SInt16Vector;
// MOBA
typedef struct {
SInt16Vector min_bound; // Part of the bounding box
SInt16Vector max_bound; // Part of the bounding box
uint32_t startIndex; // 0x0C: index of first face used in MOVI
uint16_t count; // 0x10: number of MOVI indices used
uint16_t minIndex; // 0x12: index of the first vertex used in MOVT
uint16_t maxIndex; // 0x14: index of the last vertex used (batch includes this one)
uint8_t flag_unk1; // 0x16:
uint8_t material_id; // 0x17 index in MOMT
} SMOBatch;
typedef struct {
uint8_t f_unk : 1; // 0x01: Bit 0
uint8_t f_noCamCollide : 1; // 0x02: Bit 1
uint8_t f_detail : 1; // 0x04: Bit 2
uint8_t f_collision : 1; // 0x08: Bit 3 turn off water rippling effects
uint8_t f_hint : 1; // 0x10: Bit 4
uint8_t f_render : 1; // 0x20: Bit 5
uint8_t f_cullObjects : 1; // 0x40: Bit 6
uint8_t f_collideHit : 1; // 0x80: Bit 7
} SMOPolyFlags;
typedef struct {
SMOPolyFlags flags;
uint8_t material_id;
} SMOPoly;
typedef struct {
uint32_t bspTree : 1; // 0x01: Bit 0 (MOBN and MOBR chunk)
uint32_t lightMap : 1; // 0x02: Bit 1 (MOLM, MOLD)
uint32_t vertexColors : 1; // 0x04: Bit 3 (MOCV)
uint32_t renderExterior : 1; // 0x08: Bit 4 (SMOGroup::EXTERIOR)
uint32_t unused1 : 1; // 0x10: Bit 5
uint32_t unused2 : 1; // 0x20: Bit 6
uint32_t useExteriorLighting : 1; // 0x40: Bit 7
uint32_t unreachable : 1; // 0x80: Bit 8
uint32_t useExteriorSky : 1; // 0x100: Bit 9 (for interiors of city in stratholme_past.wmo)
uint32_t hasLights : 1; // 0x200: Bit 10 (MOLR)
uint32_t hasChunks : 1; // 0x400: Bit 11 (Has MPBV, MPBP, MPBI, MPBG chunks, doesnt actually use tho)
uint32_t hasDoodads : 1; // 0x800: Bit 12
uint32_t hasWater : 1; // 0x1000: Bit 13 (SMOGroup::LIQUIDSURFACE)
uint32_t isIndoor : 1; // 0x2000: Bit 14 (SMOGroup::INTERIOR)
uint32_t unsued3 : 1; // 0x4000: Bit 15
uint32_t queryMountAllowed : 1; // 0x8000: Bit 16 QueryMountAllowed in pre-WOTLK
uint32_t alwaysDraw : 1; // 0x10000: Bit 17 SMOGroup::ALWAYSDRAW -- clear 0x8 after CMapObjGroup::Create() in MOGP and MOGI
uint32_t unsued4 : 1; // 0x20000: Bit 18 Has MORI and MORB chunks
uint32_t showSkybox : 1; // 0x40000: Bit 19 unset automatically if MOSB not present (TODO)
uint32_t isNotWaterButOcean : 1; // 0x80000: Bit 20 LiquidType related see (MLIQ chunk)
uint32_t unk1 : 1; // 0x100000: Bit 21
uint32_t isMountAllowed : 1; // 0x200000: Bit 22 IsMountAllowed
uint32_t unused5 : 1; // 0x400000: Bit 23 Unused
uint32_t hasSecondVertexColors : 1; // 0x01000000: Bit 24 (CVERTS2)
uint32_t hasSecondTexCoords : 1; // 0x02000000: Bit 25 (TVERTS2)
uint32_t isAntiPortal : 1; // 0x04000000: Bit 26 (ANTIPORTAL)
uint32_t disableBatchRender : 1; // 0x08000000: Bit 27 (unk)
uint32_t unused6 : 1; // 0x10000000: Bit 28 (UNUSED: 20740)
uint32_t isExteriorCull : 1; // 0x20000000: Bit 29 (EXTERIOR_CULL)
uint32_t hasThirdTexCoords : 1; // 0x40000000: Bit 30 (TVERTS3)
uint32_t unk3 : 1; // 0x80000000: Bit 31
} SMOGroupFlags;
typedef struct {
uint32_t canCutTerrain : 1; // 0x01: Bit 0
uint32_t unk2 : 1; // 0x02: Bit 1
uint32_t unk4 : 1; // 0x04: Bit 2
uint32_t unk8 : 1; // 0x08: Bit 3
uint32_t unk0x10 : 1; // 0x10: Bit 4
uint32_t unk0x20 : 1; // 0x20: Bit 5
uint32_t padding : 26;
} SMOGroupFlags2;
// TODO: setup flag parsing for flags, as well as for MOGI and WMORootData
// MOGP -- contains all other chunks, variables are a header only, actual chunk size way larger
#pragma pack(push, 1)
typedef struct {
uint32_t groupName; // 0x00: offset into MOGN
uint32_t descriptiveGroupName; // 0x04: offset into MOGN
SMOGroupFlags flags; // 0x08: https://wowdev.wiki/WMO#MOGP_chunk
CAabox boundingBox; // 0x0C: same as above, see group flags (same as in corresponding MOGI entry)
uint16_t portalStart; // 0x24: index into MOPR
uint16_t portalCount; // 0x26: number of MOPR items used after portalStart
uint16_t transBatchCount; // 0x28:
uint16_t intBatchCount; // 0x2A:
uint16_t extBatchCount; // 0x2C:
uint16_t padding_or_data; // 0x2E: wiki isn't sure if is padding or data... TODO
uint8_t fogIds[4]; // 0x30: ids in MFOG
uint32_t groupLiquid; // 0x34: see MLIQ chunk https://wowdev.wiki/WMO#MLIQ_chunk
uint32_t wmoGroupID; // 0x38: Foreign key for WMOAreaTableRec::m_WMOGroupID (column 4 in WMOAreaTable.dbc) https://wowdev.wiki/DB/WMOAreaTable
SMOGroupFlags2 flags2; // 0x3C:
uint32_t unk; // 0x40: Unused 20740?
} SMOGroupHeader;
typedef struct {
uint16_t indices;
} MOVIChunk;
typedef struct {
SMOGroupHeader header;
const char *group_file_buffer;
DWORD group_file_size;
const char *mogx_data_ptr; //
DWORD mogx_size;
const char *mopy_data_ptr; //
DWORD mopy_size;
const char *mpy2_data_ptr; // dragonflight... 10.0.0.46181
DWORD mpy2_size;
const char *movi_data_ptr; // Indices
DWORD movi_size;
const char *movt_data_ptr; // Vertices
DWORD movt_size;
const char *monr_data_ptr; // Vertex normals
DWORD monr_size;
const char *motv_data_ptr; //
DWORD motv_size;
const char *moba_data_ptr; // Batch list (drawing regions)
DWORD moba_size;
const char *moqg_data_ptr; //
DWORD moqg_size;
const char *molr_data_ptr;
DWORD molr_size;
// TODO: chunks dependent on flag, need to add their ptr's
} WMOGroupData;
/* ---------- Root WMO Data Struct ---------- */
typedef struct {
// Material and Texture Data
const char *mver_data_ptr;
DWORD mver_size;
const char *mohd_data_ptr;
DWORD mohd_size;
const char *motx_data_ptr;
DWORD motx_size;
const char *momt_data_ptr;
DWORD momt_size;
const char *mogn_data_ptr;
DWORD mogn_size;
const char *mogi_data_ptr;
DWORD mogi_size;
const char *mosb_data_ptr;
DWORD mosb_size;
const char *mopv_data_ptr;
DWORD mopv_size;
const char *mopt_data_ptr;
DWORD mopt_size;
const char *mopr_data_ptr;
DWORD mopr_size;
// for MOPR
// SMOPortalRef *portal_references;
// size_t portal_ref_count;
const char *movv_data_ptr;
DWORD movv_size;
const char *movb_data_ptr;
DWORD movb_size;
const char *molt_data_ptr;
DWORD molt_size;
const char *mods_data_ptr;
DWORD mods_size;
const char *modn_data_ptr;
DWORD modn_size;
const char *modd_data_ptr;
DWORD modd_size;
const char *mfog_data_ptr;
DWORD mfog_size;
GLuint *material_textures; // Array of OpeNGL texture ids corresponding to MOMT entries
WMOGroupData *groups;
size_t group_count;
} WMORootData;
extern WMORootData out_wmo_data;
#endif //WMO_STRUCTS_H