mirror of
https://github.com/jmarshall23/Quake_4_Alpha.git
synced 2026-08-15 15:50:52 +02:00
413 lines
13 KiB
C++
413 lines
13 KiB
C++
/*
|
|
===========================================================================
|
|
|
|
Quake 4 Reconstructed GPL Source Code
|
|
Copyright (C) 2026 Justin Marshall (IceColdDuke).
|
|
|
|
Quake 4 tactical AAS generator reconstructed from the public Quake 4 AAS
|
|
format, the retail Toolsx86.dll behavior, and the Doom 3 AAS compiler.
|
|
|
|
===========================================================================
|
|
*/
|
|
|
|
#include "../idlib/precompiled.h"
|
|
#pragma hdrstop
|
|
|
|
#include "AASFile.h"
|
|
|
|
namespace {
|
|
|
|
const int TACTICAL_CONTENTS = CONTENTS_SOLID | CONTENTS_MONSTERCLIP | CONTENTS_AAS_SOLID;
|
|
const float TACTICAL_WALL_EPSILON = 0.5f;
|
|
const float TACTICAL_FEATURE_INSET = 1.0f;
|
|
const float TACTICAL_LEAN_DISTANCE = 16.0f;
|
|
|
|
struct tacticalCandidate_t {
|
|
idVec3 origin;
|
|
idVec3 normal;
|
|
int height;
|
|
int flags;
|
|
idList<int> areas;
|
|
};
|
|
|
|
class idAASTacticalBuilder {
|
|
public:
|
|
idAASTacticalBuilder( idAASFile *aasFile, idCollisionModel *worldModel ) :
|
|
file( aasFile ), world( worldModel ), settings( aasFile->GetSettings() ) {
|
|
}
|
|
|
|
int Build( const idMapFile &mapFile ) {
|
|
file->ClearTactical();
|
|
for ( int i = 0; i < file->GetNumAreas(); i++ ) {
|
|
aasArea_t &area = file->GetArea( i );
|
|
area.numFeatures = 0;
|
|
area.firstFeature = 0;
|
|
}
|
|
|
|
AddManualFeatures( mapFile );
|
|
AddAutomaticCoverFeatures();
|
|
CommitFeatures();
|
|
return candidates.Num();
|
|
}
|
|
|
|
private:
|
|
bool Trace( const idVec3 &start, const idVec3 &end ) const {
|
|
trace_t trace;
|
|
collisionModelManager->Translation( &trace, start, end, NULL, mat3_identity,
|
|
TACTICAL_CONTENTS, world, vec3_origin, mat3_identity );
|
|
return trace.fraction < 1.0f;
|
|
}
|
|
|
|
bool TraceWall( const idVec3 &origin, const idVec3 &normal, float height ) const {
|
|
idVec3 start = origin;
|
|
start.z += height;
|
|
const float distance = settings.Radius() + 12.0f;
|
|
return Trace( start, start + normal * distance );
|
|
}
|
|
|
|
int AreaForPoint( const idVec3 &point ) const {
|
|
idVec3 raised = point;
|
|
raised.z += 1.0f;
|
|
int areaNum = file->PointAreaNum( raised );
|
|
if ( areaNum > 0 ) {
|
|
return areaNum;
|
|
}
|
|
const idBounds searchBounds( idVec3( -64.0f, -64.0f, -64.0f ), idVec3( 64.0f, 64.0f, 64.0f ) );
|
|
return file->PointReachableAreaNum( point, searchBounds, AREA_REACHABLE_WALK, 0 );
|
|
}
|
|
|
|
bool ValidPackedOrigin( const idVec3 &origin ) const {
|
|
for ( int i = 0; i < 3; i++ ) {
|
|
if ( origin[i] < -32768.0f || origin[i] > 32767.0f ) {
|
|
common->Warning( "[AAST] feature at (%g %g %g) is outside the Quake 4 packed coordinate range",
|
|
origin.x, origin.y, origin.z );
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
int AddCandidate( const idVec3 &inOrigin, const idVec3 &inNormal, int height, int flags, int areaNum ) {
|
|
if ( flags == 0 || !ValidPackedOrigin( inOrigin ) ) {
|
|
return -1;
|
|
}
|
|
|
|
idVec3 normal = inNormal;
|
|
normal.z = 0.0f;
|
|
if ( normal.Normalize() == 0.0f ) {
|
|
normal.Set( 1.0f, 0.0f, 0.0f );
|
|
}
|
|
height = idMath::ClampInt( 0, 255, height );
|
|
|
|
for ( int i = 0; i < candidates.Num(); i++ ) {
|
|
tacticalCandidate_t &candidate = candidates[i];
|
|
if ( ( candidate.origin - inOrigin ).LengthSqr() > Square( 3.0f ) || candidate.normal * normal < 0.98f ) {
|
|
continue;
|
|
}
|
|
candidate.flags |= flags;
|
|
candidate.height = Max( candidate.height, height );
|
|
if ( areaNum > 0 ) {
|
|
candidate.areas.AddUnique( areaNum );
|
|
}
|
|
return i;
|
|
}
|
|
|
|
tacticalCandidate_t candidate;
|
|
candidate.origin = inOrigin;
|
|
candidate.normal = normal;
|
|
candidate.height = height;
|
|
candidate.flags = flags;
|
|
if ( areaNum > 0 ) {
|
|
candidate.areas.Append( areaNum );
|
|
}
|
|
return candidates.Append( candidate );
|
|
}
|
|
|
|
void AddManualFeatures( const idMapFile &mapFile ) {
|
|
for ( int i = 0; i < mapFile.GetNumEntities(); i++ ) {
|
|
const idDict &dict = mapFile.GetEntity( i )->epairs;
|
|
if ( idStr::Icmp( dict.GetString( "classname" ), "info_tactical_feature" ) != 0 ) {
|
|
continue;
|
|
}
|
|
|
|
const idVec3 origin = dict.GetVector( "origin", "0 0 0" );
|
|
const float angle = dict.GetFloat( "angle", "0" );
|
|
const idVec3 normal = idAngles( 0.0f, angle, 0.0f ).ToForward();
|
|
int height = dict.GetInt( "height", "72" );
|
|
int flags = dict.GetInt( "flags", "0" );
|
|
|
|
if ( dict.GetBool( "vantage", "0" ) ) {
|
|
flags |= FEATURE_VANTAGE;
|
|
}
|
|
if ( dict.GetBool( "pinch", "0" ) ) {
|
|
flags |= FEATURE_PINCH;
|
|
}
|
|
if ( dict.GetBool( "cover", "0" ) ) {
|
|
flags |= FEATURE_COVER;
|
|
}
|
|
if ( dict.GetBool( "lean_left", "0" ) ) {
|
|
flags |= FEATURE_COVER | FEATURE_LOOK_LEFT;
|
|
}
|
|
if ( dict.GetBool( "corner_left", "0" ) ) {
|
|
flags |= FEATURE_COVER | FEATURE_LOOK_LEFT | FEATURE_CORNER_LEFT;
|
|
}
|
|
if ( dict.GetBool( "lean_right", "0" ) ) {
|
|
flags |= FEATURE_COVER | FEATURE_LOOK_RIGHT;
|
|
}
|
|
if ( dict.GetBool( "corner_right", "0" ) ) {
|
|
flags |= FEATURE_COVER | FEATURE_LOOK_RIGHT | FEATURE_CORNER_RIGHT;
|
|
}
|
|
if ( height >= 20 && height < 64 ) {
|
|
flags |= FEATURE_COVER | FEATURE_LOOK_OVER;
|
|
}
|
|
|
|
const int areaNum = AreaForPoint( origin );
|
|
if ( areaNum == 0 ) {
|
|
common->Warning( "[AAST] info_tactical_feature at (%g %g %g) is not in an AAS area",
|
|
origin.x, origin.y, origin.z );
|
|
continue;
|
|
}
|
|
AddCandidate( origin, normal, height, flags, areaNum );
|
|
}
|
|
}
|
|
|
|
bool FindSolidWallPlane( int areaNum, const idVec3 &start, const idVec3 &end, idPlane &wallPlane ) const {
|
|
const aasArea_t &area = file->GetArea( areaNum );
|
|
for ( int i = 0; i < area.numFaces; i++ ) {
|
|
const int signedFaceNum = file->GetFaceIndex( area.firstFace + i );
|
|
const aasFace_t &face = file->GetFace( abs( signedFaceNum ) );
|
|
if ( !( face.flags & FACE_SOLID ) ) {
|
|
continue;
|
|
}
|
|
const int orientedPlaneNum = face.planeNum ^ ( signedFaceNum < 0 ? 1 : 0 );
|
|
const idPlane &plane = file->GetPlane( orientedPlaneNum );
|
|
if ( idMath::Fabs( plane.Normal().z ) > 0.25f ) {
|
|
continue;
|
|
}
|
|
if ( idMath::Fabs( plane.Distance( start ) ) <= TACTICAL_WALL_EPSILON &&
|
|
idMath::Fabs( plane.Distance( end ) ) <= TACTICAL_WALL_EPSILON ) {
|
|
wallPlane = plane;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void TestCoverPoint( int areaNum, const idVec3 &point, const idPlane &wallPlane ) {
|
|
idVec3 wallNormal = -wallPlane.Normal();
|
|
wallNormal.z = 0.0f;
|
|
if ( wallNormal.Normalize() == 0.0f ) {
|
|
return;
|
|
}
|
|
|
|
idVec3 origin = point + wallPlane.Normal() * TACTICAL_FEATURE_INSET;
|
|
int coverHeight = 0;
|
|
for ( int height = 20; height <= 72; height += 4 ) {
|
|
if ( TraceWall( origin, wallNormal, static_cast<float>( height ) ) ) {
|
|
coverHeight = height;
|
|
}
|
|
}
|
|
if ( coverHeight < 20 ) {
|
|
return;
|
|
}
|
|
|
|
int flags = FEATURE_COVER;
|
|
if ( coverHeight < 68 ) {
|
|
flags |= FEATURE_LOOK_OVER;
|
|
AddCandidate( origin, wallNormal, coverHeight, flags, areaNum );
|
|
return;
|
|
}
|
|
|
|
coverHeight = 72;
|
|
const idVec3 up( 0.0f, 0.0f, 1.0f );
|
|
const idVec3 right = wallNormal.Cross( up );
|
|
const float lookHeight = static_cast<float>( coverHeight ) - TACTICAL_LEAN_DISTANCE;
|
|
idVec3 lookOrigin = origin;
|
|
lookOrigin.z += lookHeight;
|
|
|
|
const idVec3 leftLook = lookOrigin - right * TACTICAL_LEAN_DISTANCE;
|
|
if ( !Trace( lookOrigin, leftLook ) && !TraceWall( origin - right * TACTICAL_LEAN_DISTANCE, wallNormal, lookHeight ) ) {
|
|
flags |= FEATURE_LOOK_LEFT | FEATURE_CORNER_LEFT;
|
|
}
|
|
|
|
const idVec3 rightLook = lookOrigin + right * TACTICAL_LEAN_DISTANCE;
|
|
if ( !Trace( lookOrigin, rightLook ) && !TraceWall( origin + right * TACTICAL_LEAN_DISTANCE, wallNormal, lookHeight ) ) {
|
|
flags |= FEATURE_LOOK_RIGHT | FEATURE_CORNER_RIGHT;
|
|
}
|
|
|
|
if ( flags & ( FEATURE_LOOK_LEFT | FEATURE_LOOK_RIGHT ) ) {
|
|
AddCandidate( origin, wallNormal, coverHeight, flags, areaNum );
|
|
}
|
|
}
|
|
|
|
void AddAutomaticCoverFeatures() {
|
|
for ( int areaNum = 1; areaNum < file->GetNumAreas(); areaNum++ ) {
|
|
const aasArea_t &area = file->GetArea( areaNum );
|
|
if ( !( area.flags & AREA_FLOOR ) || ( area.flags & AREA_LIQUID ) ) {
|
|
continue;
|
|
}
|
|
|
|
for ( int faceOffset = 0; faceOffset < area.numFaces; faceOffset++ ) {
|
|
const int signedFaceNum = file->GetFaceIndex( area.firstFace + faceOffset );
|
|
const aasFace_t &floorFace = file->GetFace( abs( signedFaceNum ) );
|
|
if ( !( floorFace.flags & FACE_FLOOR ) ) {
|
|
continue;
|
|
}
|
|
|
|
for ( int edgeOffset = 0; edgeOffset < floorFace.numEdges; edgeOffset++ ) {
|
|
const int signedEdgeNum = file->GetEdgeIndex( floorFace.firstEdge + edgeOffset );
|
|
const aasEdge_t &edge = file->GetEdge( abs( signedEdgeNum ) );
|
|
const idVec3 start = file->GetVertex( edge.vertexNum[0] );
|
|
const idVec3 end = file->GetVertex( edge.vertexNum[1] );
|
|
|
|
idPlane wallPlane;
|
|
if ( !FindSolidWallPlane( areaNum, start, end, wallPlane ) ) {
|
|
continue;
|
|
}
|
|
|
|
idVec3 edgeDirection = end - start;
|
|
edgeDirection.z = 0.0f;
|
|
const float edgeLength = edgeDirection.Normalize();
|
|
if ( edgeLength < 4.0f ) {
|
|
continue;
|
|
}
|
|
|
|
const float inset = Min( 8.0f, edgeLength * 0.25f );
|
|
TestCoverPoint( areaNum, start + edgeDirection * inset, wallPlane );
|
|
TestCoverPoint( areaNum, end - edgeDirection * inset, wallPlane );
|
|
|
|
const int segments = Max( 1, idMath::Ftoi( edgeLength / 96.0f ) );
|
|
for ( int sample = 1; sample < segments; sample++ ) {
|
|
TestCoverPoint( areaNum, start + edgeDirection * ( edgeLength * sample / segments ), wallPlane );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static unsigned char PackNormal( float value ) {
|
|
const int packed = idMath::Ftoi( ( value + 1.0f ) * 127.0f + 0.5f );
|
|
return static_cast<unsigned char>( idMath::ClampInt( 0, 254, packed ) );
|
|
}
|
|
|
|
void CommitFeatures() {
|
|
idList<int> featureNumbers;
|
|
featureNumbers.SetNum( candidates.Num() );
|
|
for ( int i = 0; i < candidates.Num(); i++ ) {
|
|
const tacticalCandidate_t &candidate = candidates[i];
|
|
aasFeature_t feature;
|
|
memset( &feature, 0, sizeof( feature ) );
|
|
feature.x = static_cast<short>( idMath::Ftoi( candidate.origin.x ) );
|
|
feature.y = static_cast<short>( idMath::Ftoi( candidate.origin.y ) );
|
|
feature.z = static_cast<short>( idMath::Ftoi( candidate.origin.z ) );
|
|
feature.flags = static_cast<unsigned short>( candidate.flags );
|
|
feature.normalx = PackNormal( candidate.normal.x );
|
|
feature.normaly = PackNormal( candidate.normal.y );
|
|
feature.height = static_cast<unsigned char>( candidate.height );
|
|
featureNumbers[i] = file->AppendFeature( feature );
|
|
}
|
|
|
|
for ( int areaNum = 0; areaNum < file->GetNumAreas(); areaNum++ ) {
|
|
aasArea_t &area = file->GetArea( areaNum );
|
|
area.firstFeature = static_cast<unsigned short>( file->GetNumFeatureIndexes() );
|
|
for ( int candidateNum = 0; candidateNum < candidates.Num(); candidateNum++ ) {
|
|
if ( candidates[candidateNum].areas.FindIndex( areaNum ) < 0 ) {
|
|
continue;
|
|
}
|
|
aasIndex_t featureNum = featureNumbers[candidateNum];
|
|
file->AppendFeatureIndex( featureNum );
|
|
area.numFeatures++;
|
|
}
|
|
}
|
|
}
|
|
|
|
idAASFile *file;
|
|
idCollisionModel *world;
|
|
idAASSettings &settings;
|
|
idList<tacticalCandidate_t> candidates;
|
|
};
|
|
|
|
static idStr TacticalMapName( const char *argument ) {
|
|
idStr mapName = argument;
|
|
mapName.BackSlashesToSlashes();
|
|
mapName.StripFileExtension();
|
|
if ( mapName.Icmpn( "maps/", 5 ) != 0 ) {
|
|
mapName = "maps/" + mapName;
|
|
}
|
|
return mapName;
|
|
}
|
|
|
|
}
|
|
|
|
void RunAASTactical_f( const idCmdArgs &args ) {
|
|
if ( args.Argc() != 2 ) {
|
|
common->Printf( "runAASTactical <mapfile>\n" );
|
|
return;
|
|
}
|
|
|
|
common->ClearWarnings( "generating tactical AAS features" );
|
|
common->SetRefreshOnPrint( true );
|
|
common->Printf( "[AAST] Generating Tactical Features...\n" );
|
|
|
|
const idStr mapName = TacticalMapName( args.Argv( 1 ) );
|
|
idStr mapFileName = mapName;
|
|
mapFileName.SetFileExtension( "map" );
|
|
idMapFile mapFile;
|
|
if ( !mapFile.Parse( mapFileName ) ) {
|
|
common->Warning( "[AAST] couldn't load map file '%s'", mapFileName.c_str() );
|
|
common->SetRefreshOnPrint( false );
|
|
common->PrintWarnings();
|
|
return;
|
|
}
|
|
|
|
cmdSystem->BufferCommandText( CMD_EXEC_NOW, "disconnect" );
|
|
collisionModelManager->LoadMap( &mapFile, false );
|
|
idCollisionModel *world = collisionModelManager->LoadModel( mapFile.GetName(), WORLD_MODEL_NAME );
|
|
if ( world == NULL ) {
|
|
common->Warning( "[AAST] couldn't load collision world for '%s'", mapName.c_str() );
|
|
collisionModelManager->FreeMap( mapFile.GetName() );
|
|
common->SetRefreshOnPrint( false );
|
|
common->PrintWarnings();
|
|
return;
|
|
}
|
|
|
|
int totalFeatures = 0;
|
|
const idDict *types = gameEdit->FindEntityDefDict( "aas_types", false );
|
|
if ( types == NULL ) {
|
|
common->Warning( "[AAST] unable to find entityDef 'aas_types'" );
|
|
} else {
|
|
const idKeyValue *type = types->MatchPrefix( "type" );
|
|
while ( type != NULL ) {
|
|
const idDict *settingsDict = gameEdit->FindEntityDefDict( type->GetValue(), false );
|
|
if ( settingsDict != NULL ) {
|
|
idAASSettings settings;
|
|
settings.FromDict( type->GetValue(), settingsDict );
|
|
if ( settings.generateTacticalFeatures ) {
|
|
idStr aasName = mapName;
|
|
aasName.SetFileExtension( settings.fileExtension );
|
|
idAASFile *file = AASFile->CreateNew();
|
|
if ( file->Load( aasName, mapFile.GetGeometryCRC() ) ) {
|
|
file->SetSettings( settings );
|
|
idAASTacticalBuilder builder( file, world );
|
|
const int count = builder.Build( mapFile );
|
|
if ( file->Write( aasName, mapFile.GetGeometryCRC() ) ) {
|
|
common->Printf( "[AAST] %d Features Generated for %s\n", count, settings.fileExtension.c_str() );
|
|
totalFeatures += count;
|
|
}
|
|
} else {
|
|
common->Warning( "[AAST] couldn't load '%s'; run runAAS first", aasName.c_str() );
|
|
}
|
|
delete file;
|
|
}
|
|
}
|
|
type = types->MatchPrefix( "type", type );
|
|
}
|
|
}
|
|
|
|
collisionModelManager->FreeMap( mapFile.GetName() );
|
|
common->Printf( "[AAST] %d Features Generated\n", totalFeatures );
|
|
common->SetRefreshOnPrint( false );
|
|
common->PrintWarnings();
|
|
}
|
|
|