Skip to content
5 changes: 5 additions & 0 deletions Core/GameEngine/Include/Common/GameDefines.h
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@
#define PRIORITIZE_TEXTURES_BY_SIZE (1)
#endif

// Enable drawing particles as a terrain-conforming overlay.
#ifndef ENABLE_TERRAIN_CONFORMING_PARTICLES
#define ENABLE_TERRAIN_CONFORMING_PARTICLES (1)
#endif

// Enable obsolete code. This mainly refers to code that existed in Generals but was removed in GeneralsMD.
// Disable and remove this when Generals and GeneralsMD are merged.
#if RTS_GENERALS
Expand Down
6 changes: 5 additions & 1 deletion Core/GameEngine/Include/GameClient/ParticleSys.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#pragma once

#include "Common/AsciiString.h"
#include "Common/GameDefines.h"
#include "Common/GameMemory.h"
#include "Common/GameType.h"
#include "Common/Snapshot.h"
Expand Down Expand Up @@ -436,6 +437,7 @@ class ParticleSystemInfo : public Snapshot
{
PARTICLE_ALIGNMENT_BILLBOARD = 0,
PARTICLE_ALIGNMENT_XYPLANAR,
PARTICLE_ALIGNMENT_CONFORMING,

@qodo-free-for-open-source-projects qodo-free-for-open-source-projects Bot Sep 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

7. Editing effects removes terrain alignment 🐞 Bug ⚙ Maintainability


DebugWindowDialog::getSwitchFromSystem recognizes only XY-planar alignment and
updateSwitchToSystem maps an unchecked alignment control to billboard mode. Once the new
conforming enum value is loaded into the Particle Editor, editing any switches-dialog setting writes
that unchecked state back and replaces conforming alignment with billboard.
Agent Prompt
## Issue description
The Particle Editor represents alignment as a boolean XY-plane checkbox. It cannot represent the new conforming enum value and writes the unchecked value back as billboard alignment during unrelated switch edits.

## Fix Focus Areas
- Core/GameEngine/Include/GameClient/ParticleSys.h[437-443]
- Core/Tools/ParticleEditor/ParticleEditorDialog.cpp[1085-1114]
- Core/Tools/ParticleEditor/CSwitchesDialog.cpp[74-127]

## Recommended Fix
Make the editor expose all alignment enum values, preferably with a three-value control, and write the selected enum value directly. At minimum, preserve conforming alignment when the legacy XY-plane switch is not changed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ParticleEditor support for conforming particles should be handled in a later PR

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dismissed — deferred.

PARTICLE_ALIGNMENT_TYPE_COUNT
};
ParticleAlignmentType m_particleAlignment; ///< align particles toward the camera or with the XY plane.
Expand Down Expand Up @@ -504,7 +506,7 @@ static_assert(ARRAY_SIZE(ParticlePriorityNames) == NUM_PARTICLE_PRIORITIES + 1,

static const char *const GroundAlignmentTypeNames[] =
{
"No", "Yes", nullptr
"No", "Yes", "Conforming", nullptr
};
static_assert(ARRAY_SIZE(GroundAlignmentTypeNames) == ParticleSystemInfo::PARTICLE_ALIGNMENT_TYPE_COUNT + 1, "Incorrect array size");

Expand Down Expand Up @@ -627,6 +629,8 @@ class ParticleSystem : public MemoryPoolObject,
Bool isUsingVolumeParticles() const { return m_particleType == VOLUME_PARTICLE; }
UnsignedInt getVolumeParticleDepth() const { return m_volumeParticleDepth; }

ParticleAlignmentType getParticleAlignment() const { return m_particleAlignment; }
Bool isFieldParticle() const { return m_particleAlignment == PARTICLE_ALIGNMENT_XYPLANAR || m_particleAlignment == PARTICLE_ALIGNMENT_CONFORMING; }
Bool shouldBillboard() const { return m_particleAlignment == PARTICLE_ALIGNMENT_BILLBOARD; }

ParticleShaderType getShaderType() const { return m_shaderType; }
Expand Down
8 changes: 7 additions & 1 deletion Core/GameEngine/Source/GameClient/System/ParticleSys.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1766,7 +1766,8 @@ Particle *ParticleSystem::createParticle( const ParticleInfo *info,
TheGameLODManager->isParticleSkipped()) )
return nullptr;

if ( getParticleCount() > 0 && priority == AREA_EFFECT && !shouldBillboard() && TheParticleSystemManager->getFieldParticleCount() > (UnsignedInt)TheGlobalData->m_maxFieldParticleCount )
if ( getParticleCount() > 0 && priority == AREA_EFFECT && isFieldParticle() &&
TheParticleSystemManager->getFieldParticleCount() > (UnsignedInt)TheGlobalData->m_maxFieldParticleCount )
return nullptr;

// ALWAYS_RENDER particles are exempt from all count limits, and are always created, regardless of LOD issues.
Expand Down Expand Up @@ -2911,6 +2912,11 @@ void ParticleSystemTemplate::validate()
m_particleType = ParticleSystemInfo::SMUDGE;
}
#endif

#if !ENABLE_TERRAIN_CONFORMING_PARTICLES
if (m_particleAlignment == PARTICLE_ALIGNMENT_CONFORMING)
m_particleAlignment = PARTICLE_ALIGNMENT_XYPLANAR;
#endif
}

// ------------------------------------------------------------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions Core/GameEngineDevice/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ set(GAMEENGINEDEVICE_SRC
Include/W3DDevice/GameClient/W3DSnow.h
# Include/W3DDevice/GameClient/W3DStatusCircle.h
Include/W3DDevice/GameClient/W3DTerrainBackground.h
Include/W3DDevice/GameClient/W3DTerrainParticle.h
Include/W3DDevice/GameClient/W3DTerrainTracks.h
Include/W3DDevice/GameClient/W3DTerrainVisual.h
Include/W3DDevice/GameClient/W3DTreeBuffer.h
Expand Down Expand Up @@ -171,6 +172,7 @@ set(GAMEENGINEDEVICE_SRC
Source/W3DDevice/GameClient/W3DSnow.cpp
# Source/W3DDevice/GameClient/W3DStatusCircle.cpp
Source/W3DDevice/GameClient/W3DTerrainBackground.cpp
Source/W3DDevice/GameClient/W3DTerrainParticle.cpp
Source/W3DDevice/GameClient/W3DTerrainTracks.cpp
Source/W3DDevice/GameClient/W3DTerrainVisual.cpp
Source/W3DDevice/GameClient/W3DTreeBuffer.cpp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#pragma once

#include "GameClient/ParticleSys.h"
#include "W3DDevice/GameClient/W3DTerrainParticle.h"
#include "WW3D2/pointgr.h"
#include "WW3D2/streak.h"
#include "WW3D2/rinfo.h"
Expand All @@ -51,20 +52,22 @@ class W3DParticleSystemManager : public ParticleSystemManager

private:
Bool finishedBatch(const ParticleSystem& system, const RefCountPtr<TextureClass>& texture);
void initializeBatch(const ParticleSystem& system, const RefCountPtr<TextureClass>& texture);
void initializeBatch(const ParticleSystem& system, const RefCountPtr<TextureClass>& texture, const AABoxClass& bbox);
void flushParticleBatch(RenderInfoClass& rinfo, UnsignedInt& pointCount);

enum { MAX_POINTS_PER_GROUP = 512 };

RefCountPtr<TextureClass> m_batchTexture; ///< the texture used as the drawing surface for batched particle draws
PointGroupClass *m_pointGroup; ///< the point group that contains all of the particles
StreakLineClass *m_streakLine; ///< the streak class that contains all of the streaks
W3DTerrainParticle *m_terrainParticles; ///< the terrain-conforming particles renderer
ShareBufferClass<Vector3> *m_posBuffer; ///< array of particle positions
ShareBufferClass<Vector4> *m_RGBABuffer; ///< array of particle color and alpha
ShareBufferClass<float> *m_sizeBuffer; ///< array of particle sizes
ShareBufferClass<uint8> *m_angleBuffer; ///< array of particle orientations

ParticleSystemInfo::ParticleShaderType m_batchShaderType;
Bool m_readyToRender; ///< if true, it is OK to render
Bool m_batchBillboard;
ParticleSystemInfo::ParticleAlignmentType m_batchParticleAlignment;
AABoxClass m_batchBoundingBox;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
** Command & Conquer Generals Zero Hour(tm)
** Copyright 2026 TheSuperHackers
**
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation, either version 3 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#pragma once

#include <vector>

#include "Common/MapObject.h"
#include "Lib/BaseType.h"
#include "WW3D2/shader.h"
#include "WWLib/sharebuf.h"
#include "WWMath/vector3.h"
#include "WWMath/vector4.h"

constexpr const UnsignedShort MAX_VERTICES = 32768;
constexpr const UnsignedShort MAX_INDICES = 65535;
constexpr const UnsignedShort INVALID_VERTEX = MAX_VERTICES + 1;
constexpr const Real Z_OFFSET = MAP_HEIGHT_SCALE / 10;

static_assert(MAX_VERTICES < INVALID_VERTEX, "Vertex indices must leave room for the INVALID_VERTEX sentinel value.");

class AABoxClass;
class TextureClass;
class WorldHeightMap;
struct VertexFormatXYZNDUV2;

/**
* Render particles as terrain conforming overlays. For each particle, form an initial region by calculating the bounds
* of its rotated square and intersecting that with the map bounds and visible-terrain bounds. Recursively subdivide this
* region until each sub-region is either a single terrain cell or is on perfectly flat terrain. Flat regions become one
* large quad, while non-flat regions must match the terrain's topology exactly.
*/
class W3DTerrainParticle
{
public:
W3DTerrainParticle();
~W3DTerrainParticle();

void setTexture(TextureClass* texture);
void setShader(ShaderClass shader);
void setArrays(ShareBufferClass<Vector3>* locs,
ShareBufferClass<Vector4>* diffuse = nullptr,
ShareBufferClass<Real>* sizes = nullptr,
ShareBufferClass<UnsignedByte>* orientations = nullptr,
Int activePointCount = -1);
void setBoundingBox(const AABoxClass& worldBoundingBox);
void render();

private:

struct ParticleContext;

void drawRegion(WorldHeightMap& map, const ParticleContext& particle, const IRegion2D& bounds);
void drawQuad(WorldHeightMap& map, const ParticleContext& particle, const IRegion2D& bounds);
UnsignedShort addVertex(WorldHeightMap& map, const ParticleContext& particle, Int x, Int y);
void addTriangle(UnsignedShort a, UnsignedShort b, UnsignedShort c);void resetVertexLookup();
void flushBatch();
IRegion2D calcTerrainBounds(WorldHeightMap& map, const Vector3& loc, Real projectedRadius) const;
void updateSettings();

std::vector<VertexFormatXYZNDUV2> m_vertexData; ///< Vertices of the current batch.
std::vector<UnsignedShort> m_indexData; ///< Indices defining the triangles of the current batch.
std::vector<UnsignedByte> m_outcodes; ///< UV outcodes indexed by batch vertex.
std::vector<UnsignedShort> m_vertexLookup; ///< Map grid location to index in m_vertexData.
UnsignedShort m_numVertices; ///< Number of vertices used in m_vertexData.
UnsignedShort m_numIndices; ///< Number of indices used in m_indexData.

TextureClass* m_texture;
ShaderClass m_shader;

ShareBufferClass<Vector3>* m_pointLoc; ///< World space point locations.
ShareBufferClass<Vector4>* m_pointDiffuse; ///< RGBA values (nullptr if not used).
ShareBufferClass<Real>* m_pointSize; ///< Size override table (nullptr if not used).
ShareBufferClass<UnsignedByte>* m_pointOrientation; ///< Orientation indices (nullptr if not used).
Int m_pointCount; ///< Total point count.
IRegion2D m_terrainInViewBounds; ///< Bounding box of which terrain cell indices are on the screen.

Real m_defaultPointSize; ///< Point size (size array overrides if present).
Vector3 m_defaultPointColor; ///< Point color (color array overrides if present).
Real m_defaultPointAlpha; ///< Point alpha (alpha array overrides if present).
UnsignedByte m_defaultPointOrientation; ///< Point orientation (orientation array overrides if present).
UnsignedInt m_defaultDiffuse; ///< Diffuse built from m_defaultPointColor and m_defaultPointAlpha.
};
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,16 @@ class WorldHeightMap : public RefCountClass,
Int getXExtent() const {return m_width;} ///<number of vertices in x
Int getYExtent() const {return m_height;} ///<number of vertices in y

IRegion2D getLogicalBounds() const
{
IRegion2D bounds;
bounds.lo.x = -m_borderSize;
bounds.lo.y = -m_borderSize;
bounds.hi.x = m_width - m_borderSize;
bounds.hi.y = m_height - m_borderSize;
return bounds;
}

Region2D getDrawRegion2D();

Int getDrawOrgX() {return m_drawOriginX;}
Expand All @@ -268,6 +278,14 @@ class WorldHeightMap : public RefCountClass,
return(0);
};

///Faster version of above function without all the safety checks - For people that do checks externally.
UnsignedByte getQuickHeight(Int xIndex, Int yIndex) const
{
return m_data[yIndex * m_width + xIndex];
}

Bool isTerrainFlat(const IRegion2D& bounds) const;

void getUVForBlend(Int edgeClass, Region2D *range);

DrawArea createDrawArea(Int xOrg, Int yOrg);
Expand Down
Loading