diff --git a/neo/engine/doomdll.vcxproj.user b/neo/engine/doomdll.vcxproj.user
index 27c8058f..0d14ea58 100644
--- a/neo/engine/doomdll.vcxproj.user
+++ b/neo/engine/doomdll.vcxproj.user
@@ -17,7 +17,7 @@
WindowsLocalDebugger
+set r_fullscreen 0 +set r_mode 1
D:\projects\Doom3
- +set r_fullscreen 0 +set r_mode 6|+set r_fullscreen 1 +set r_mode 6|+set r_fullscreen 1 +set r_mode 1|+set r_fullscreen 0 +set r_mode 1|
+ +set r_fullscreen 0 +set r_mode 6|+set r_fullscreen 1 +set r_mode 6|+set r_fullscreen 1 +set r_mode 1|+set r_fullscreen 0 +set r_mode 1 +set fs_game q1base|+set r_fullscreen 0 +set r_mode 1|
D:\projects\Doom3\Quake4.exe
diff --git a/neo/engine/idlib/MapFile.cpp b/neo/engine/idlib/MapFile.cpp
index 21609bbd..8cca1e06 100644
--- a/neo/engine/idlib/MapFile.cpp
+++ b/neo/engine/idlib/MapFile.cpp
@@ -112,6 +112,544 @@ static void ComputeAxisBase(const idVec3& normal, idVec3& texS, idVec3& texT) {
#endif
}
+
+
+/*
+=================
+Quake 1 .map compatibility helpers
+
+Quake 1 and Valve 220 maps use the same outer entity/brush braces as the
+legacy Quake 3 parser, but their texture projection data must be interpreted
+as Quake axial vectors instead of the existing Quake 3 fallback texture matrix.
+The detector below enables this mode only for files that advertise Quake 1 or
+Valve 220 metadata, so the original Quake 3 fallback path remains unchanged.
+=================
+*/
+static bool g_parseQuake1Map = false;
+
+typedef struct quake1MapTextureSizeCacheEntry_s {
+ idStr name;
+ int width;
+ int height;
+} quake1MapTextureSizeCacheEntry_t;
+
+static idList g_quake1MapTextureSizeCache;
+
+/*
+=================
+Quake1MapTokenToFloat
+=================
+*/
+static bool Quake1MapTokenToFloat(const idToken& token, float& value) {
+ char* end;
+ const char* str;
+
+ str = token.c_str();
+ if (!str || !str[0]) {
+ return false;
+ }
+
+ value = (float)strtod(str, &end);
+ return end != str && *end == '\0';
+}
+
+/*
+=================
+Quake1MapReadFloatToken
+
+idLexer may emit a negative number as either one token, "-1", or as a
+punctuation sign followed by a number, "-" "1". Quake .map texture scales
+commonly use negative values, so parse both forms from a token list.
+=================
+*/
+static bool Quake1MapReadFloatToken(const idList& tokens, int& index, float& value) {
+ float parsed;
+ float sign;
+
+ if (index >= tokens.Num()) {
+ return false;
+ }
+
+ sign = 1.0f;
+ if (tokens[index] == "-" || tokens[index] == "+") {
+ if (tokens[index] == "-") {
+ sign = -1.0f;
+ }
+ index++;
+ if (index >= tokens.Num()) {
+ return false;
+ }
+ }
+
+ if (!Quake1MapTokenToFloat(tokens[index], parsed)) {
+ return false;
+ }
+
+ value = sign * parsed;
+ index++;
+ return true;
+}
+
+/*
+=================
+Quake1MapTokensAreFloatSuffix
+=================
+*/
+static bool Quake1MapTokensAreFloatSuffix(const idList& tokens, int first, int minimumFloats) {
+ int index, count;
+ float value;
+
+ index = first;
+ count = 0;
+ while (index < tokens.Num()) {
+ if (!Quake1MapReadFloatToken(tokens, index, value)) {
+ return false;
+ }
+ count++;
+ }
+
+ return count >= minimumFloats;
+}
+
+/*
+=================
+JoinQuake1MapTextureName
+
+Quake texture names may begin with punctuation, for example +0WALL or *WATER.
+Collecting the whole texture-name token run avoids treating those prefixes as
+separate syntax.
+=================
+*/
+static void JoinQuake1MapTextureName(const idList& tokens, int first, int count, idStr& material) {
+ int i;
+
+ material.Clear();
+ for (i = 0; i < count; i++) {
+ material += tokens[first + i].c_str();
+ }
+}
+
+/*
+=================
+FindQuake1MapStandardTextureParms
+
+Classic Quake faces end with:
+ textureName offsetX offsetY rotation scaleX scaleY [optional numeric flags]
+Return the first token of that numeric suffix.
+=================
+*/
+static int FindQuake1MapStandardTextureParms(const idList& tokens) {
+ int i;
+
+ for (i = 1; i < tokens.Num(); i++) {
+ if (Quake1MapTokensAreFloatSuffix(tokens, i, 5)) {
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+/*
+=================
+ParseQuake1MapValve220Vector
+=================
+*/
+static bool ParseQuake1MapValve220Vector(const idList& tokens, int& index, idVec3& axis, float& offset) {
+ float v[4];
+ int i;
+
+ if (index >= tokens.Num() || tokens[index] != "[") {
+ return false;
+ }
+ index++;
+
+ for (i = 0; i < 4; i++) {
+ if (!Quake1MapReadFloatToken(tokens, index, v[i])) {
+ return false;
+ }
+ }
+
+ if (index >= tokens.Num() || tokens[index] != "]") {
+ return false;
+ }
+ index++;
+
+ axis[0] = v[0];
+ axis[1] = v[1];
+ axis[2] = v[2];
+ offset = v[3];
+ return true;
+}
+
+/*
+=================
+Quake1MapTextureAxisFromPlane
+
+This is the historical dominant-axis basis used by Quake-family map compilers
+for the classic texture tuple.
+=================
+*/
+static void Quake1MapTextureAxisFromPlane(const idVec3& normal, idVec3& texS, idVec3& texT) {
+ static const float baseAxis[18][3] = {
+ { 0, 0, 1 }, { 1, 0, 0 }, { 0, -1, 0 },
+ { 0, 0, -1 }, { 1, 0, 0 }, { 0, -1, 0 },
+ { 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, -1 },
+ { -1, 0, 0 }, { 0, 1, 0 }, { 0, 0, -1 },
+ { 0, 1, 0 }, { 1, 0, 0 }, { 0, 0, -1 },
+ { 0, -1, 0 }, { 1, 0, 0 }, { 0, 0, -1 }
+ };
+ int i, bestAxis;
+ float best, dot;
+
+ best = 0.0f;
+ bestAxis = 0;
+ for (i = 0; i < 6; i++) {
+ dot = normal[0] * baseAxis[i * 3][0] + normal[1] * baseAxis[i * 3][1] + normal[2] * baseAxis[i * 3][2];
+ if (dot > best) {
+ best = dot;
+ bestAxis = i;
+ }
+ }
+
+ for (i = 0; i < 3; i++) {
+ texS[i] = baseAxis[bestAxis * 3 + 1][i];
+ texT[i] = baseAxis[bestAxis * 3 + 2][i];
+ }
+}
+
+/*
+=================
+RotateQuake1MapTextureAxes
+=================
+*/
+static void RotateQuake1MapTextureAxes(idVec3 axes[2], float rotate) {
+ int i, sv, tv;
+ float angle, sinv, cosv, ns, nt;
+
+ angle = rotate * (3.14159265358979323846f / 180.0f);
+
+ if (rotate == 0.0f) {
+ sinv = 0.0f;
+ cosv = 1.0f;
+ }
+ else if (rotate == 90.0f) {
+ sinv = 1.0f;
+ cosv = 0.0f;
+ }
+ else if (rotate == 180.0f) {
+ sinv = 0.0f;
+ cosv = -1.0f;
+ }
+ else if (rotate == 270.0f) {
+ sinv = -1.0f;
+ cosv = 0.0f;
+ }
+ else {
+#ifdef QUAKE4
+ sinv = idMath::Sin(angle);
+ cosv = idMath::Cos(angle);
+#else
+ sinv = sin(angle);
+ cosv = cos(angle);
+#endif
+ }
+
+ if (axes[0][0]) {
+ sv = 0;
+ }
+ else if (axes[0][1]) {
+ sv = 1;
+ }
+ else {
+ sv = 2;
+ }
+
+ if (axes[1][0]) {
+ tv = 0;
+ }
+ else if (axes[1][1]) {
+ tv = 1;
+ }
+ else {
+ tv = 2;
+ }
+
+ for (i = 0; i < 2; i++) {
+ ns = cosv * axes[i][sv] - sinv * axes[i][tv];
+ nt = sinv * axes[i][sv] + cosv * axes[i][tv];
+ axes[i][sv] = ns;
+ axes[i][tv] = nt;
+ }
+}
+
+/*
+=================
+ClearQuake1MapTextureSizeCache
+=================
+*/
+static void ClearQuake1MapTextureSizeCache() {
+ g_quake1MapTextureSizeCache.Clear();
+}
+
+/*
+=================
+GetQuake1MapTextureDimensions
+
+Quake texture shifts and scales are stored in texture pixels. Convert them
+to Doom 3 brushDef UV space by normalizing S by the real texture width and T
+by the real texture height. renderSystem->LoadImage accepts names without an
+extension and resolves the actual image file. Missing textures fall back to
+32x32, preserving the old hardcoded behavior for unresolved assets.
+=================
+*/
+static bool GetQuake1MapTextureDimensions(const idStr& materialName, int& width, int& height) {
+ int i;
+ byte* pic;
+ ID_TIME_T timestamp;
+ quake1MapTextureSizeCacheEntry_t entry;
+
+ for (i = 0; i < g_quake1MapTextureSizeCache.Num(); i++) {
+ if (!g_quake1MapTextureSizeCache[i].name.Icmp(materialName)) {
+ width = g_quake1MapTextureSizeCache[i].width;
+ height = g_quake1MapTextureSizeCache[i].height;
+ return width > 0 && height > 0;
+ }
+ }
+
+ width = 0;
+ height = 0;
+ pic = NULL;
+ timestamp = 0;
+
+ if (renderSystem) {
+ renderSystem->LoadImage(materialName.c_str(), &pic, &width, &height, ×tamp, false);
+ }
+
+ if (pic) {
+ Mem_Free(pic);
+ }
+
+ if (width <= 0 || height <= 0) {
+ width = 32;
+ height = 32;
+ }
+
+ entry.name = materialName;
+ entry.width = width;
+ entry.height = height;
+ g_quake1MapTextureSizeCache.Append(entry);
+
+ return width > 0 && height > 0;
+}
+
+/*
+=================
+GetQuake1MapTextureUnitScales
+=================
+*/
+static void GetQuake1MapTextureUnitScales(const idStr& materialName, float& sUnitScale, float& tUnitScale) {
+ int textureWidth, textureHeight;
+
+ GetQuake1MapTextureDimensions(materialName, textureWidth, textureHeight);
+ sUnitScale = 1.0f / (float)textureWidth;
+ tUnitScale = 1.0f / (float)textureHeight;
+}
+
+/*
+=================
+BuildQuake1MapTextureMatrix
+
+Convert Quake world-space texture vectors into the brushDef texture matrix
+basis used by idMapBrushSide::GetTextureVectors. Any component along the
+plane normal is folded into the constant offset because all points on the face
+share the same normal distance.
+=================
+*/
+static void BuildQuake1MapTextureMatrix(const idPlane& plane, const idVec3& origin, const idVec3& sVecIn, float sOffsetIn, const idVec3& tVecIn, float tOffsetIn, idVec3 texMat[2]) {
+ idVec3 normal, texX, texY, sVec, tVec;
+ float sNormal, tNormal, planeDistance;
+ int i;
+
+ normal = plane.Normal();
+ ComputeAxisBase(normal, texX, texY);
+
+ sNormal = sVecIn * normal;
+ tNormal = tVecIn * normal;
+ for (i = 0; i < 3; i++) {
+ sVec[i] = sVecIn[i] - normal[i] * sNormal;
+ tVec[i] = tVecIn[i] - normal[i] * tNormal;
+ }
+
+ planeDistance = -plane[3] + (origin * normal);
+
+ texMat[0][0] = sVec * texX;
+ texMat[0][1] = sVec * texY;
+ texMat[0][2] = sOffsetIn + sNormal * planeDistance;
+ texMat[1][0] = tVec * texX;
+ texMat[1][1] = tVec * texY;
+ texMat[1][2] = tOffsetIn + tNormal * planeDistance;
+}
+
+/*
+=================
+SetQuake1MapMaterial
+=================
+*/
+static void SetQuake1MapMaterial(idStr& out, const idStr& material) {
+ // Keep the old id map convention for legacy texture names.
+ out = "textures/";
+ out += material;
+}
+
+/*
+=================
+ParseQuake1MapTextureInfo
+
+Supports both classic Quake 1 faces:
+ TEXTURE offsetX offsetY rotation scaleX scaleY
+and Valve 220 faces:
+ TEXTURE [ ux uy uz offsetX ] [ vx vy vz offsetY ] rotation scaleX scaleY
+=================
+*/
+static bool ParseQuake1MapTextureInfo(const idList& tokens, const idPlane& plane, const idVec3& origin, idStr& materialOut, idVec3 texMat[2]) {
+ idStr material;
+ int i, j, index, parmIndex;
+ float rotate, scale[2], shift[2], sUnitScale, tUnitScale;
+ idVec3 axes[2];
+
+ if (tokens.Num() < 6) {
+ return false;
+ }
+
+ for (i = 0; i < tokens.Num(); i++) {
+ if (tokens[i] == "[") {
+ JoinQuake1MapTextureName(tokens, 0, i, material);
+ if (material.Length() == 0) {
+ return false;
+ }
+
+ index = i;
+ if (!ParseQuake1MapValve220Vector(tokens, index, axes[0], shift[0]) ||
+ !ParseQuake1MapValve220Vector(tokens, index, axes[1], shift[1]) ||
+ !Quake1MapReadFloatToken(tokens, index, rotate) ||
+ !Quake1MapReadFloatToken(tokens, index, scale[0]) ||
+ !Quake1MapReadFloatToken(tokens, index, scale[1])) {
+ return false;
+ }
+
+ if (scale[0] == 0.0f) {
+ scale[0] = 1.0f;
+ }
+ if (scale[1] == 0.0f) {
+ scale[1] = 1.0f;
+ }
+
+ SetQuake1MapMaterial(materialOut, material);
+ GetQuake1MapTextureUnitScales(materialOut, sUnitScale, tUnitScale);
+
+ // Valve 220 stores the texture axes directly. The rotation token is an
+ // editor compatibility field and should not be applied again here.
+ for (j = 0; j < 3; j++) {
+ axes[0][j] *= sUnitScale / scale[0];
+ axes[1][j] *= tUnitScale / scale[1];
+ }
+ shift[0] *= sUnitScale;
+ shift[1] *= tUnitScale;
+
+ BuildQuake1MapTextureMatrix(plane, origin, axes[0], shift[0], axes[1], shift[1], texMat);
+ return true;
+ }
+ }
+
+ parmIndex = FindQuake1MapStandardTextureParms(tokens);
+ if (parmIndex <= 0) {
+ return false;
+ }
+
+ JoinQuake1MapTextureName(tokens, 0, parmIndex, material);
+ index = parmIndex;
+ if (material.Length() == 0 ||
+ !Quake1MapReadFloatToken(tokens, index, shift[0]) ||
+ !Quake1MapReadFloatToken(tokens, index, shift[1]) ||
+ !Quake1MapReadFloatToken(tokens, index, rotate) ||
+ !Quake1MapReadFloatToken(tokens, index, scale[0]) ||
+ !Quake1MapReadFloatToken(tokens, index, scale[1])) {
+ return false;
+ }
+
+ if (scale[0] == 0.0f) {
+ scale[0] = 1.0f;
+ }
+ if (scale[1] == 0.0f) {
+ scale[1] = 1.0f;
+ }
+
+ SetQuake1MapMaterial(materialOut, material);
+ GetQuake1MapTextureUnitScales(materialOut, sUnitScale, tUnitScale);
+
+ Quake1MapTextureAxisFromPlane(plane.Normal(), axes[0], axes[1]);
+ RotateQuake1MapTextureAxes(axes, rotate);
+ for (i = 0; i < 3; i++) {
+ axes[0][i] *= sUnitScale / scale[0];
+ axes[1][i] *= tUnitScale / scale[1];
+ }
+ shift[0] *= sUnitScale;
+ shift[1] *= tUnitScale;
+
+ BuildQuake1MapTextureMatrix(plane, origin, axes[0], shift[0], axes[1], shift[1], texMat);
+ return true;
+}
+
+/*
+=================
+DetectQuake1MapFile
+
+This deliberately avoids classifying every old brace-only map as Quake 1,
+because that would change existing Quake 3 fallback behavior. Instead it
+requires common Quake 1/Valve220 signals: a wad key, mapversion 220, TrenchBroom
+texture metadata, or Valve 220 bracketed face axes.
+=================
+*/
+static bool DetectQuake1MapFile(const idStr& fullName, bool osPath) {
+#ifdef QUAKE4
+ idAutoPtr lexer(LexerFactory::MakeLexer(LEXFL_NOSTRINGCONCAT | LEXFL_NOSTRINGESCAPECHARS | LEXFL_ALLOWPATHNAMES));
+ Lexer& detectSrc(*lexer);
+#else
+ idLexer detectSrc(LEXFL_NOSTRINGCONCAT | LEXFL_NOSTRINGESCAPECHARS | LEXFL_ALLOWPATHNAMES);
+#endif
+ idToken token;
+ idToken value;
+
+ detectSrc.LoadFile(fullName, osPath);
+ if (!detectSrc.IsLoaded()) {
+ return false;
+ }
+
+ // idTech map files advertise their format with a top-level Version token;
+ // Quake 1 maps do not.
+ if (detectSrc.CheckTokenString("Version")) {
+ return false;
+ }
+
+ while (detectSrc.ReadToken(&token)) {
+ if (token == "[") {
+ return true;
+ }
+ if (!token.Icmp("wad") || !token.Icmp("_tb_textures")) {
+ return true;
+ }
+ if (!token.Icmp("mapversion")) {
+ if (detectSrc.ReadTokenOnLine(&value) && atoi(value.c_str()) == 220) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+
#ifdef QUAKE4
/*
===============
@@ -507,6 +1045,63 @@ idMapBrush* idMapBrush::ParseQ3(Lexer & src, const idVec3 & origin) {
#else
idMapBrush* idMapBrush::ParseQ3(idLexer & src, const idVec3 & origin) {
#endif
+ if (g_parseQuake1Map) {
+ int i;
+ idVec3 planepts[3];
+ idToken token;
+ idList textureTokens;
+ idList sides;
+ idMapBrushSide* side;
+ idDict epairs;
+
+ do {
+ if (src.CheckTokenString("}")) {
+ break;
+ }
+
+ side = new idMapBrushSide();
+ sides.Append(side);
+
+ // read the three point plane definition
+ if (!src.Parse1DMatrix(3, planepts[0].ToFloatPtr()) ||
+ !src.Parse1DMatrix(3, planepts[1].ToFloatPtr()) ||
+ !src.Parse1DMatrix(3, planepts[2].ToFloatPtr())) {
+ src.Error("idMapBrush::ParseQ3: unable to read Quake 1 brush side plane definition");
+ sides.DeleteContents(true);
+ return NULL;
+ }
+
+ planepts[0] -= origin;
+ planepts[1] -= origin;
+ planepts[2] -= origin;
+
+ side->plane.FromPoints(planepts[0], planepts[1], planepts[2]);
+ side->origin = origin;
+
+ // Read the rest of the face line. Quake texture names may begin with
+ // punctuation, so keep the whole line and identify the numeric suffix.
+ textureTokens.Clear();
+ while (src.ReadTokenOnLine(&token)) {
+ textureTokens.Append(token);
+ }
+
+ if (!ParseQuake1MapTextureInfo(textureTokens, side->plane, origin, side->material, side->texMat)) {
+ src.Error("idMapBrush::ParseQ3: unable to read Quake 1 brush side texture info");
+ sides.DeleteContents(true);
+ return NULL;
+ }
+ } while (1);
+
+ idMapBrush* brush = new idMapBrush();
+ for (i = 0; i < sides.Num(); i++) {
+ brush->AddSide(sides[i]);
+ }
+
+ brush->epairs = epairs;
+
+ return brush;
+ }
+
int i, shift[2], rotate;
float scale[2];
idVec3 planepts[3];
@@ -925,6 +1520,8 @@ bool idMapFile::Parse(const char* filename, bool ignoreRegion, bool osPath) {
name.StripFileExtension();
fullName = name;
hasPrimitiveData = false;
+ g_parseQuake1Map = false;
+ ClearQuake1MapTextureSizeCache();
#ifdef QUAKE4
mHasExportEntities = false;
mHasFuncGroups = false;
@@ -953,6 +1550,7 @@ bool idMapFile::Parse(const char* filename, bool ignoreRegion, bool osPath) {
version = OLD_MAP_VERSION;
fileTime = src.GetFileTime();
entities.DeleteContents(true);
+ g_parseQuake1Map = DetectQuake1MapFile(fullName, osPath);
if (src.CheckTokenString("Version")) {
src.ReadTokenOnLine(&token);
@@ -1050,6 +1648,8 @@ bool idMapFile::Parse(const char* filename, bool ignoreRegion, bool osPath) {
#endif
hasPrimitiveData = true;
+ g_parseQuake1Map = false;
+ ClearQuake1MapTextureSizeCache();
return true;
}
@@ -1382,6 +1982,8 @@ bool idMapFile::ParseExport(const char* filename, bool osPath)
numMerged = numNotMerged = numAdded = numRemoved = 0;
mHasExportEntities = false;
+ g_parseQuake1Map = false;
+ ClearQuake1MapTextureSizeCache();
fullName = filename;
fullName.StripFileExtension();
diff --git a/neo/engine/renderer/draw_dx.cpp b/neo/engine/renderer/draw_dx.cpp
index 14b4df25..f64008e7 100644
--- a/neo/engine/renderer/draw_dx.cpp
+++ b/neo/engine/renderer/draw_dx.cpp
@@ -176,7 +176,7 @@ void RB_DXDrawInteractions(void)
const float r = srcLight.shaderParms[SHADERPARM_RED];
const float g = srcLight.shaderParms[SHADERPARM_GREEN];
const float b = srcLight.shaderParms[SHADERPARM_BLUE];
- const float intensity = 4.0f;
+ const float intensity = 6.0f;
glRaytracingLight_t light = {};
bool supported = true;