Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f5e9b36
Release the RTPC emitter reference when the bind action stops
phevosccp Aug 20, 2026
aa133e6
Add name and enabled to IEveVolume
phevosccp Aug 20, 2026
6940c42
Add EveTriggerVolume: a standalone spatial trigger space object
phevosccp Aug 20, 2026
1c8b356
Fire a Python callback on trigger enter/exit transitions
phevosccp Aug 20, 2026
064ac92
Subtract exclusion volumes from the trigger region
phevosccp Aug 20, 2026
e29e84d
Add debug rendering and forceTriggered for Graphite
phevosccp Aug 20, 2026
2c5450c
Support dungeon placement: external parameters and ball curves
phevosccp Aug 20, 2026
7d9718a
- use BlueScriptCallback instead of PyObject
phevosccp Aug 21, 2026
feffadd
code clean up and simplifications
phevosccp Aug 21, 2026
df7dad7
override methods
phevosccp Aug 21, 2026
c6768cd
delete not used param
phevosccp Aug 21, 2026
8bfa77a
- remove debug parameter
phevosccp Aug 21, 2026
3611e35
remove not used methods
phevosccp Aug 21, 2026
041aeb9
remove GetEffectiveName
phevosccp Aug 21, 2026
161d4ab
Merge branch 'main' into trigger-volume-clean
phevosccp Aug 24, 2026
b8b6050
Merge branch 'main' into trigger-volume-clean
phevosccp Aug 26, 2026
a017e1a
requested PR review changes
phevosccp Aug 27, 2026
94c6fec
Merge branch 'main' into trigger-volume-clean
phevosccp Aug 27, 2026
1f977ba
Merge branch 'trigger-volume-clean' of https://github.com/carbonengin…
phevosccp Aug 27, 2026
3051c85
remove unrelated change from PR
phevosccp Aug 31, 2026
d6680e2
Merge branch 'main' into trigger-volume-clean
phevosccp Aug 31, 2026
5ab5838
Merge branch 'main' into trigger-volume-clean
phevosccp Aug 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions trinity/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,9 @@ set(_SOURCES
Eve/AudioGameObject.cpp
Eve/AudioGameObject.h
Eve/AudioGameObject_Blue.cpp
Eve/EveTriggerVolume.cpp
Eve/EveTriggerVolume.h
Eve/EveTriggerVolume_Blue.cpp
Eve/EveEffectRoot2.cpp
Eve/EveEffectRoot2.h
Eve/EveEffectRoot2_Blue.cpp
Expand Down
259 changes: 259 additions & 0 deletions trinity/Eve/EveTriggerVolume.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
// Copyright © 2026 CCP ehf.

#include "StdAfx.h"
#include "EveTriggerVolume.h"

EveTriggerVolume::EveTriggerVolume( IRoot* lockobj ) :
PARENTLOCK( m_volumes ),
PARENTLOCK( m_exclusionVolumes ),
PARENTLOCK( m_externalParameters ),
m_worldTransform( IdentityMatrix() ),
m_enterThreshold( 0.5f ),
m_isInside( false ),
m_currentIntensity( 0.0f )
{
}

EveTriggerVolume::~EveTriggerVolume()
{
}

void EveTriggerVolume::RebuildBoundingSphere()
{
CCP_STATS_ZONE( __FUNCTION__ );

m_boundingSphere = CcpMath::Sphere();

for( const auto& volume : m_volumes )
{
auto volumeSphere = volume->GetBoundingSphere();

if( !volumeSphere.IsInitialized() )
{
continue;
}

if( !m_boundingSphere.IsInitialized() || volumeSphere.IsSphereInside( m_boundingSphere ) )
{
m_boundingSphere = volumeSphere;
continue;
}

if( m_boundingSphere.IsSphereInside( volumeSphere ) )
{
continue;
}

Vector3 delta = volumeSphere.center - m_boundingSphere.center;
float deltaLen = Length( delta );

m_boundingSphere.center += 0.5f * ( 1.f + ( volumeSphere.radius - m_boundingSphere.radius ) / deltaLen ) * delta;
m_boundingSphere.radius = 0.5f * ( m_boundingSphere.radius + volumeSphere.radius + deltaLen );
}
}

void EveTriggerVolume::SetCallback( const BlueScriptCallback& callback )
{
m_callback = callback;
}

void EveTriggerVolume::InvokeCallback( bool entered )
{
BlueScriptCallback callback = m_callback;
if( !callback )
{
return;
}

callback.CallVoid( m_name.c_str(), entered ).ReportException();
}

void EveTriggerVolume::UpdateWorldTransform( Be::Time time )
{
Quaternion rotation;
Vector3 translation;

if( m_ballPosition )
{
m_ballPosition->Update( &translation, time );
}
else
{
translation = Vector3( 0.0f, 0.0f, 0.0f );
}

if( m_ballRotation )
{
m_ballRotation->Update( &rotation, time );
}
else
{
rotation = Quaternion( 0.0f, 0.0f, 0.0f, 1.0f );
}

m_worldTransform = RotationMatrix( rotation ) * TranslationMatrix( translation );
}

// IEveSpaceObject2
void EveTriggerVolume::UpdateSyncronous( const EveUpdateContext& updateContext )
{
CCP_STATS_ZONE( __FUNCTION__ );

UpdateWorldTransform( updateContext.GetTime() );

RebuildBoundingSphere();

UpdateTriggerState( updateContext );
}

float EveTriggerVolume::GetMaxIntensity( const PIEveVolumeVector& volumes, const Vector3& position )
{
float intensity = 0.0f;
for( const auto& volume : volumes )
{
intensity = std::max( intensity, volume->GetIntensity( position ) );
if( intensity == 1.0f )
{
// early exit
break;
}
}
return intensity;
}

void EveTriggerVolume::UpdateTriggerState( const EveUpdateContext& updateContext )
{
m_currentIntensity = 0.0f;

bool inside = false;
if( m_trackedPosition && !m_volumes.empty() )
{
Vector3 trackedPosition;
m_trackedPosition->Update( &trackedPosition, updateContext.GetTime() );

Matrix inverseWorldTransform = Inverse( m_worldTransform );
Vector3 positionInObjectSpace = Transform( trackedPosition, inverseWorldTransform ).GetXYZ();

// check first if the tracked position is within the bounding sphere
if( m_boundingSphere.IsPointInside( positionInObjectSpace ) )
{
m_currentIntensity = GetMaxIntensity( m_volumes, positionInObjectSpace );

if( m_currentIntensity != 0.0f )
{
// check if the tracked position is within an exclusion volume
float negativeIntensity = GetMaxIntensity( m_exclusionVolumes, positionInObjectSpace );
m_currentIntensity = std::max( 0.0f, m_currentIntensity - negativeIntensity );
}
}

inside = m_currentIntensity >= m_enterThreshold;
}

if( inside != m_isInside )
{
m_isInside = inside;
InvokeCallback( inside );
}
}

void EveTriggerVolume::UpdateAsyncronous( const EveUpdateContext& updateContext )
{
}

void EveTriggerVolume::UpdateVisibility( const EveUpdateContext& updateContext, const Matrix& parentTransform )
{
}

void EveTriggerVolume::GetRenderables( std::vector<ITr2Renderable*>& renderables, Tr2ImpostorManager* impostors )
{
}

bool EveTriggerVolume::GetBoundingSphere( Vector4& sphere, BoundingSphereQuery query ) const
{
Vector3 worldCenter = Transform( m_boundingSphere.center, m_worldTransform ).GetXYZ();
sphere = Vector4( worldCenter.x, worldCenter.y, worldCenter.z, std::max( m_boundingSphere.radius, 1.0f ) );
return true;
}

void EveTriggerVolume::UpdateModelCenterWorldPosition( Vector3& position, Be::Time t )
{
UpdateWorldTransform( t );
GetModelCenterWorldPosition( position );
}

void EveTriggerVolume::GetModelCenterWorldPosition( Vector3& position ) const
{
position = Transform( m_boundingSphere.center, m_worldTransform ).GetXYZ();
}

bool EveTriggerVolume::GetLocalBoundingBox( Vector3& min, Vector3& max )
{
// Fall back to a unit box when no volumes are set up yet, so the object stays pickable in Graphite.
float radius = std::max( m_boundingSphere.radius, 1.0f );
Vector3 extent( radius, radius, radius );

min = m_boundingSphere.center - extent;
max = m_boundingSphere.center + extent;
return true;
}

void EveTriggerVolume::GetLocalToWorldTransform( Matrix& transform ) const
{
transform = m_worldTransform;
}

Vector3 EveTriggerVolume::GetWorldPosition()
{
return m_worldTransform.GetTranslation();
}

Quaternion EveTriggerVolume::GetWorldRotation()
{
return Normalize( RotationQuaternion( m_worldTransform ) );
}

bool EveTriggerVolume::Initialize()
{
UpdateWorldTransform( Be::Time( 0.0 ) );
RebuildBoundingSphere();
return true;
}

void EveTriggerVolume::GetDebugOptions( Tr2DebugRendererOptions& options )
{
options.insert( "Trigger Volumes" );
options.insert( "Trigger Exclusion Volumes" );
options.insert( "Trigger Bounding Sphere" );
}

void EveTriggerVolume::RenderDebugInfo( ITr2DebugRenderer2& renderer )
{
if( renderer.HasOption( GetRawRoot(), "Trigger Volumes" ) )
{
// green when the tracked position is inside, white otherwise
Color color = 0xFFFFFFFF;
if( m_isInside )
{
color = 0xFF33FF33;
}

for( const auto& volume : m_volumes )
{
volume->RenderDebugInfo( renderer, m_worldTransform, color );
}
}

if( renderer.HasOption( GetRawRoot(), "Trigger Exclusion Volumes" ) )
{
for( const auto& volume : m_exclusionVolumes )
{
volume->RenderDebugInfo( renderer, m_worldTransform, 0xFFFF3333 );
}
}

if( renderer.HasOption( GetRawRoot(), "Trigger Bounding Sphere" ) )
{
renderer.DrawSphere( this, TranslationMatrix( m_boundingSphere.center ) * m_worldTransform, m_boundingSphere.radius, 10, Tr2DebugRenderer::Wireframe, 0xff333333 );
}
}
128 changes: 128 additions & 0 deletions trinity/Eve/EveTriggerVolume.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright © 2026 CCP ehf.

#pragma once

#ifndef EveTriggerVolume_h
#define EveTriggerVolume_h

#include "IWorldPosition.h"
#include "IEveSpaceObject2.h"
#include "Tr2DebugRenderer.h"
#include "Eve/Volume/IEveVolume.h"

#ifdef BLUE_USE_LOCAL_ITr2DebugRenderer2
// This is only needed for py2 as the file now belongs in blue.
// Unfortunatly the blue py2 branch cannot be updated at present due to security vulnerability work.
// The file version in the older blue versions had diverged from this one is incompatible.
#include "Include/ITr2DebugRenderer2.h"
#else
#include <ITr2DebugRenderer2.h>
#endif

#include <ITriFunction.h>

BLUE_DECLARE_INTERFACE( IEveVolume );
BLUE_DECLARE_IVECTOR( IEveVolume );
BLUE_DECLARE( Tr2ExternalParameter );
BLUE_DECLARE_VECTOR( Tr2ExternalParameter );
BLUE_DECLARE( EveTriggerVolume );

/**
* @class EveTriggerVolume
* @brief A volume that triggers a Python callback when a tracked position enters or exits it.
*
*/
BLUE_CLASS( EveTriggerVolume ) :
public IWorldPosition,
public IEveSpaceObject2,
public IInitialize,
public ITr2DebugRenderable
{
public:
EXPOSE_TO_BLUE();

EveTriggerVolume( IRoot* lockobj = NULL );
~EveTriggerVolume();

/**
* @brief Sets the callable invoked on enter/exit transitions.
*
* @param callback Callable or None.
*/
void SetCallback( const BlueScriptCallback& callback );

// IEveSpaceObject2
void UpdateSyncronous( const EveUpdateContext& updateContext ) override;
void UpdateAsyncronous( const EveUpdateContext& updateContext ) override;
void UpdateVisibility( const EveUpdateContext& updateContext, const Matrix& parentTransform ) override;
void GetRenderables( std::vector<ITr2Renderable*> & renderables, Tr2ImpostorManager * impostors ) override;
bool GetBoundingSphere( Vector4 & sphere, BoundingSphereQuery query = EVE_BOUNDS_NORMAL ) const override;
void UpdateModelCenterWorldPosition( Vector3 & position, Be::Time t ) override;
void GetModelCenterWorldPosition( Vector3 & position ) const override;
bool GetLocalBoundingBox( Vector3 & min, Vector3 & max ) override;
void GetLocalToWorldTransform( Matrix & transform ) const override;

// IWorldPosition
Vector3 GetWorldPosition() override;
Quaternion GetWorldRotation() override;

// IInitialize
bool Initialize() override;

// ITr2DebugRenderable
void GetDebugOptions( Tr2DebugRendererOptions & options ) override;
void RenderDebugInfo( ITr2DebugRenderer2 & renderer ) override;

private:
/**
* @brief Recomputes the broad-phase bounding sphere from the volume list.
*/
void RebuildBoundingSphere();

/**
* @brief Rebuilds the world transform from the position and rotation curves.
*/
void UpdateWorldTransform( Be::Time time );

/**
* @brief Returns the highest intensity any enabled volume in the list gives the position.
* @param volumes The volumes to evaluate.
* @param position The position to evaluate, in object space.
*/
static float GetMaxIntensity( const PIEveVolumeVector& volumes, const Vector3& position );

/**
* @brief Evaluates whether the tracked position is inside the volumes and fires the callback on transitions.
*/
void UpdateTriggerState( const EveUpdateContext& updateContext );

/**
* @brief Invokes the stored callback.
* @param entered True if the tracked position entered the volume, false if it exited.
*/
void InvokeCallback( bool entered );

std::string m_name;
PIEveVolumeVector m_volumes;
PIEveVolumeVector m_exclusionVolumes;
PTr2ExternalParameterVector m_externalParameters;

CcpMath::Sphere m_boundingSphere;

ITriVectorFunctionPtr m_trackedPosition;

ITriVectorFunctionPtr m_ballPosition;
ITriQuaternionFunctionPtr m_ballRotation;

Matrix m_worldTransform;

float m_enterThreshold;
bool m_isInside;
float m_currentIntensity;

BlueScriptCallback m_callback;
};

TYPEDEF_BLUECLASS( EveTriggerVolume );

#endif
Loading