From 95d14c0deb8d8ab6d0e6edbd83f133fb9e25ae4e Mon Sep 17 00:00:00 2001 From: ccpHenry <153081976+ccpHenry@users.noreply.github.com> Date: Mon, 28 Sep 2026 13:43:25 +0000 Subject: [PATCH 1/6] bindable roots for controller expressions https://fenriscreations.atlassian.net/browse/EO-26922 --- trinity/Controllers/Tr2BindingPoint.cpp | 35 ++++ trinity/Controllers/Tr2BindingPoint.h | 2 + .../Controllers/Tr2ControllerExpression.cpp | 158 +++++++++++++++++- trinity/Controllers/Tr2ControllerExpression.h | 13 ++ 4 files changed, 203 insertions(+), 5 deletions(-) diff --git a/trinity/Controllers/Tr2BindingPoint.cpp b/trinity/Controllers/Tr2BindingPoint.cpp index 413f1751b..889e05c32 100644 --- a/trinity/Controllers/Tr2BindingPoint.cpp +++ b/trinity/Controllers/Tr2BindingPoint.cpp @@ -365,6 +365,10 @@ bool Tr2BindingPoint::GetValue( float& value ) const { return false; } + if( !m_path.empty() && !m_resolvedObject ) + { + return false; + } switch( m_entry->mType ) { case Be::FLOAT: @@ -487,6 +491,37 @@ IRoot* Tr2BindingPoint::GetBoundObject() const return m_object; } +const char* Tr2BindingPoint::MatchPath( const char* path ) +{ + auto end = MatchRoot( path ); + if( end == path ) + { + return path; + } + while( true ) + { + auto next = MatchProperty( end ); + if( next > end + 1 ) + { + end = next; + continue; + } + next = MatchNumericIndex( end ); + if( next != end ) + { + end = next; + continue; + } + next = MatchNameIndex( end ); + if( next != end ) + { + end = next; + continue; + } + return end; + } +} + IRootPtr ResolveObjectPath( const std::string& reference, const std::map& roots ) { diff --git a/trinity/Controllers/Tr2BindingPoint.h b/trinity/Controllers/Tr2BindingPoint.h index 6de329cf8..94c31b16c 100644 --- a/trinity/Controllers/Tr2BindingPoint.h +++ b/trinity/Controllers/Tr2BindingPoint.h @@ -18,6 +18,8 @@ class Tr2BindingPoint IRoot* GetBoundObject() const; + static const char* MatchPath( const char* path ); + std::string m_path; IRootPtr m_object; std::string m_attribute; diff --git a/trinity/Controllers/Tr2ControllerExpression.cpp b/trinity/Controllers/Tr2ControllerExpression.cpp index 95f6af4eb..25287c7e6 100644 --- a/trinity/Controllers/Tr2ControllerExpression.cpp +++ b/trinity/Controllers/Tr2ControllerExpression.cpp @@ -235,6 +235,20 @@ bool IsValidVariableName( const char* name ) return true; } +bool IsIdentifierChar( char ch ) +{ + return ( ch >= 'a' && ch <= 'z' ) || ( ch >= 'A' && ch <= 'Z' ) || ( ch >= '0' && ch <= '9' ) || ch == '_'; +} + +bool IsIdentifier( const char* begin, const char* end ) +{ + if( begin == end || ( *begin >= '0' && *begin <= '9' ) ) + { + return false; + } + return std::all_of( begin, end, IsIdentifierChar ); +} + #ifdef _WIN32 // We really need something like FileTimeToSystemTime in blue and platform-independent @@ -542,34 +556,167 @@ std::string Tr2ControllerExpression::SetExpr( const char* expression, const ITr2 std::string Tr2ControllerExpression::CreateParser( const char* expression, const CcpParser::FunctionView& extraFunctions ) { + std::string rewritten; + auto error = BindReferences( expression, rewritten ); + if( !error.empty() ) + { + ClearReferences(); + return error; + } + CcpParser::Externals externals; - CcpParser::VariableView varViews[] = { m_controller->GetVariableView() }; + CcpParser::VariableView varViews[] = { m_controller->GetVariableView(), m_referenceVariables }; externals.variables = varViews; CcpParser::FunctionView funcViews[2] = { extraFunctions, s_functions }; externals.functions = { funcViews, 2 }; ParserObserver observer; observer.m_variables = varViews[0]; - auto parsed = CcpParser::Parse( expression, externals, m_program, &observer ); + auto parsed = CcpParser::Parse( rewritten.c_str(), externals, m_program, &observer ); if( parsed ) { m_controller->EnsureTempArenaSize( m_program.GetTempArenaSize() ); - m_variableMask = observer.m_maskOverflow || observer.m_hasNonPureFunctions ? 0ull : observer.m_mask; + if( observer.m_maskOverflow || observer.m_hasNonPureFunctions || !m_references.empty() ) + { + m_variableMask = 0; + } + else + { + m_variableMask = observer.m_mask; + } return std::string(); } else { - return ToString( parsed, expression ); + ClearReferences(); + return ToString( parsed, rewritten.c_str() ); } } +std::string Tr2ControllerExpression::BindReferences( const char* expression, std::string& rewritten ) +{ + const auto& roots = m_controller->GetBindingPathRoots(); + rewritten.clear(); + const char* p = expression; + while( *p ) + { + if( *p == '"' ) + { + auto close = strchr( p + 1, '"' ); + auto end = close ? close + 1 : p + strlen( p ); + rewritten.append( p, end ); + p = end; + continue; + } + bool identifierStart = ( ( *p >= 'a' && *p <= 'z' ) || ( *p >= 'A' && *p <= 'Z' ) || *p == '_' ) && ( p == expression || ( !IsIdentifierChar( p[-1] ) && p[-1] != '.' ) ); + if( !identifierStart ) + { + rewritten.push_back( *p++ ); + continue; + } + auto rootEnd = p; + while( IsIdentifierChar( *rootEnd ) ) + { + ++rootEnd; + } + auto pathEnd = Tr2BindingPoint::MatchPath( p ); + auto rootLength = size_t( rootEnd - p ); + auto isRoot = std::any_of( begin( roots ), end( roots ), [&]( const auto& x ) { + return x.first.length() == rootLength && strncmp( x.first.c_str(), p, rootLength ) == 0; + } ); + if( !isRoot || pathEnd == rootEnd ) + { + rewritten.append( p, rootEnd ); + p = rootEnd; + continue; + } + std::string reference( p, pathEnd ); + auto found = std::find( begin( m_referencePaths ), end( m_referencePaths ), reference ); + if( found == end( m_referencePaths ) ) + { + auto error = AddReference( reference ); + if( !error.empty() ) + { + return error; + } + rewritten += m_referenceNames.back(); + } + else + { + rewritten += m_referenceNames[found - begin( m_referencePaths )]; + } + p = pathEnd; + } + + m_referenceVariables.clear(); + for( size_t i = 0; i < m_referenceNames.size(); ++i ) + { + m_referenceVariables.push_back( { m_referenceNames[i].c_str(), REFERENCE_BUFFER_INDEX, CcpParser::OffsetType( i * sizeof( float ) ) } ); + } + m_referenceValues.assign( m_references.size(), 0.f ); + return std::string(); +} + +std::string Tr2ControllerExpression::AddReference( const std::string& reference ) +{ + const auto& roots = m_controller->GetBindingPathRoots(); + auto error = "cannot bind \"" + reference + "\" to a float attribute"; + + auto lastDot = reference.rfind( '.' ); + if( lastDot == std::string::npos || !IsIdentifier( reference.c_str() + lastDot + 1, reference.c_str() + reference.length() ) ) + { + return error; + } + + auto binding = std::make_unique(); + auto prevDot = reference.rfind( '.', lastDot - 1 ); + + if( reference.length() - lastDot == 2 && prevDot != std::string::npos && IsIdentifier( reference.c_str() + prevDot + 1, reference.c_str() + lastDot ) ) + { + binding->m_path = reference.substr( 0, prevDot ); + binding->m_attribute = reference.substr( prevDot + 1 ); + binding->Link( roots ); + } + if( !binding->IsValid() ) + { + binding->m_path = reference.substr( 0, lastDot ); + binding->m_attribute = reference.substr( lastDot + 1 ); + binding->Link( roots ); + } + if( !binding->IsValid() ) + { + return error; + } + + m_referenceNames.push_back( "__ref" + std::to_string( m_references.size() ) ); + m_referencePaths.push_back( reference ); + m_references.push_back( std::move( binding ) ); + return std::string(); +} + +void Tr2ControllerExpression::ClearReferences() +{ + m_referencePaths.clear(); + m_referenceNames.clear(); + m_references.clear(); + m_referenceVariables.clear(); + m_referenceValues.clear(); +} + std::pair Tr2ControllerExpression::Eval( void* extraBuffer ) const { if( !m_controller || !m_program ) { return std::make_pair( false, 0.f ); } + for( size_t i = 0; i < m_references.size(); ++i ) + { + if( !m_references[i]->GetValue( m_referenceValues[i] ) ) + { + m_referenceValues[i] = 0.f; + } + } auto owner = m_controller->GetOwner(); - void* externals[] = { m_controller->GetVariableBuffer(), &owner, (void*)&m_stateMachine, extraBuffer }; + void* externals[] = { m_controller->GetVariableBuffer(), &owner, (void*)&m_stateMachine, extraBuffer, m_referenceValues.data() }; float result = m_program.Eval( externals, m_controller->GetTempArena() ); return std::make_pair( true, result ); } @@ -580,6 +727,7 @@ void Tr2ControllerExpression::Clear() { m_program = CcpParser::Program(); } + ClearReferences(); m_stateMachine = nullptr; m_controller = nullptr; } diff --git a/trinity/Controllers/Tr2ControllerExpression.h b/trinity/Controllers/Tr2ControllerExpression.h index d4a3ee276..4dbd6369a 100644 --- a/trinity/Controllers/Tr2ControllerExpression.h +++ b/trinity/Controllers/Tr2ControllerExpression.h @@ -3,6 +3,7 @@ #pragma once #include +#include "Tr2BindingPoint.h" BLUE_DECLARE( Tr2StateMachine ); @@ -27,12 +28,24 @@ class Tr2ControllerExpression static const uint32_t OWNER_BUFFER_INDEX = 1; static const uint32_t STATE_MACHINE_BUFFER_INDEX = 2; static const uint32_t EXTRA_BUFFER_INDEX = 3; + static const uint32_t REFERENCE_BUFFER_INDEX = 4; private: std::string CreateParser( const char* expression, const CcpParser::FunctionView& extraFunctions ); CcpParser::Program m_program; + // bindable refs + std::string BindReferences( const char* expression, std::string& rewritten ); + std::string AddReference( const std::string& reference ); + void ClearReferences(); + + std::vector m_referencePaths; + std::vector m_referenceNames; + std::vector> m_references; + std::vector m_referenceVariables; + mutable std::vector m_referenceValues; + const Tr2StateMachine* m_stateMachine; const ITr2ActionController* m_controller; uint64_t m_variableMask; From c0144123261390e71e3705c3e8b7a41b638b169a Mon Sep 17 00:00:00 2001 From: ccpHenry <153081976+ccpHenry@users.noreply.github.com> Date: Mon, 28 Sep 2026 14:34:40 +0000 Subject: [PATCH 2/6] small clean up + readability --- trinity/Controllers/Tr2BindingPoint.cpp | 29 ++-- .../Controllers/Tr2ControllerExpression.cpp | 155 ++++++++---------- trinity/Controllers/Tr2ControllerExpression.h | 18 +- 3 files changed, 86 insertions(+), 116 deletions(-) diff --git a/trinity/Controllers/Tr2BindingPoint.cpp b/trinity/Controllers/Tr2BindingPoint.cpp index 889e05c32..99fd47060 100644 --- a/trinity/Controllers/Tr2BindingPoint.cpp +++ b/trinity/Controllers/Tr2BindingPoint.cpp @@ -142,8 +142,8 @@ const char* MatchProperty( const char* path ) { return path; } - ++path; - return MatchRoot( path ); + auto nameEnd = MatchRoot( path + 1 ); + return nameEnd == path + 1 ? path : nameEnd; } const char* MatchNumericIndex( const char* path ) @@ -304,7 +304,7 @@ void Tr2BindingPoint::Unlink() bool Tr2BindingPoint::IsValid() const { - return m_destination != nullptr; + return m_destination != nullptr && ( m_path.empty() || !!m_resolvedObject ); } void Tr2BindingPoint::SetValue( float value ) const @@ -365,10 +365,6 @@ bool Tr2BindingPoint::GetValue( float& value ) const { return false; } - if( !m_path.empty() && !m_resolvedObject ) - { - return false; - } switch( m_entry->mType ) { case Be::FLOAT: @@ -501,24 +497,19 @@ const char* Tr2BindingPoint::MatchPath( const char* path ) while( true ) { auto next = MatchProperty( end ); - if( next > end + 1 ) + if( next == end ) { - end = next; - continue; + next = MatchNumericIndex( end ); } - next = MatchNumericIndex( end ); - if( next != end ) + if( next == end ) { - end = next; - continue; + next = MatchNameIndex( end ); } - next = MatchNameIndex( end ); - if( next != end ) + if( next == end ) { - end = next; - continue; + return end; } - return end; + end = next; } } diff --git a/trinity/Controllers/Tr2ControllerExpression.cpp b/trinity/Controllers/Tr2ControllerExpression.cpp index 25287c7e6..7cfc34b9f 100644 --- a/trinity/Controllers/Tr2ControllerExpression.cpp +++ b/trinity/Controllers/Tr2ControllerExpression.cpp @@ -13,6 +13,7 @@ #include "Tr2ExpressionTermInfo.h" #include "TriSettingsRegistrar.h" #include +#include bool g_controllerFunctionOverrideEnabled = false; @@ -211,42 +212,40 @@ float BoosterIntensity( void* ctx ) return 0.0f; } -bool IsValidVariableName( const char* name ) +bool IsIdentifierStart( char ch ) { - auto isLetter = []( char x ) { - return ( x >= 'a' && x <= 'z' ) || ( x >= 'A' && x <= 'Z' ) || ( x == '_' ); - }; - auto isDigit = []( char x ) { - return x >= '0' && x <= '9'; - }; - if( !isLetter( *name ) ) - { - return false; - } - ++name; - while( *name ) - { - if( !isLetter( *name ) && !isDigit( *name ) ) - { - return false; - } - ++name; - } - return true; + return ( ch >= 'a' && ch <= 'z' ) || ( ch >= 'A' && ch <= 'Z' ) || ch == '_'; } bool IsIdentifierChar( char ch ) { - return ( ch >= 'a' && ch <= 'z' ) || ( ch >= 'A' && ch <= 'Z' ) || ( ch >= '0' && ch <= '9' ) || ch == '_'; + return IsIdentifierStart( ch ) || ( ch >= '0' && ch <= '9' ); } bool IsIdentifier( const char* begin, const char* end ) { - if( begin == end || ( *begin >= '0' && *begin <= '9' ) ) + return begin != end && IsIdentifierStart( *begin ) && std::all_of( begin, end, IsIdentifierChar ); +} + +const char* SkipIdentifier( const char* text ) +{ + while( IsIdentifierChar( *text ) ) { - return false; + ++text; } - return std::all_of( begin, end, IsIdentifierChar ); + return text; +} + +bool IsBindingRoot( const std::vector>& roots, std::string_view name ) +{ + return std::any_of( begin( roots ), end( roots ), [&]( const auto& root ) { + return root.first == name; + } ); +} + +std::string CannotBindError( const std::string& path ) +{ + return "cannot bind \"" + path + "\" to a float attribute"; } #ifdef _WIN32 @@ -575,14 +574,8 @@ std::string Tr2ControllerExpression::CreateParser( const char* expression, const if( parsed ) { m_controller->EnsureTempArenaSize( m_program.GetTempArenaSize() ); - if( observer.m_maskOverflow || observer.m_hasNonPureFunctions || !m_references.empty() ) - { - m_variableMask = 0; - } - else - { - m_variableMask = observer.m_mask; - } + bool maskIsUsable = !observer.m_maskOverflow && !observer.m_hasNonPureFunctions && m_references.empty(); + m_variableMask = maskIsUsable ? observer.m_mask : 0ull; return std::string(); } else @@ -596,107 +589,90 @@ std::string Tr2ControllerExpression::BindReferences( const char* expression, std { const auto& roots = m_controller->GetBindingPathRoots(); rewritten.clear(); - const char* p = expression; - while( *p ) + for( const char* p = expression; *p; ) { if( *p == '"' ) { auto close = strchr( p + 1, '"' ); - auto end = close ? close + 1 : p + strlen( p ); - rewritten.append( p, end ); - p = end; + auto literalEnd = close ? close + 1 : p + strlen( p ); + rewritten.append( p, literalEnd ); + p = literalEnd; continue; } - bool identifierStart = ( ( *p >= 'a' && *p <= 'z' ) || ( *p >= 'A' && *p <= 'Z' ) || *p == '_' ) && ( p == expression || ( !IsIdentifierChar( p[-1] ) && p[-1] != '.' ) ); - if( !identifierStart ) + bool startsToken = p == expression || ( !IsIdentifierChar( p[-1] ) && p[-1] != '.' ); + if( !startsToken || !IsIdentifierStart( *p ) ) { rewritten.push_back( *p++ ); continue; } - auto rootEnd = p; - while( IsIdentifierChar( *rootEnd ) ) - { - ++rootEnd; - } + auto rootEnd = SkipIdentifier( p ); auto pathEnd = Tr2BindingPoint::MatchPath( p ); - auto rootLength = size_t( rootEnd - p ); - auto isRoot = std::any_of( begin( roots ), end( roots ), [&]( const auto& x ) { - return x.first.length() == rootLength && strncmp( x.first.c_str(), p, rootLength ) == 0; - } ); - if( !isRoot || pathEnd == rootEnd ) + if( pathEnd == rootEnd || !IsBindingRoot( roots, std::string_view( p, size_t( rootEnd - p ) ) ) ) { + // variable or function name rewritten.append( p, rootEnd ); p = rootEnd; continue; } - std::string reference( p, pathEnd ); - auto found = std::find( begin( m_referencePaths ), end( m_referencePaths ), reference ); - if( found == end( m_referencePaths ) ) + + std::string path( p, pathEnd ); + auto found = std::find_if( begin( m_references ), end( m_references ), [&]( const Reference& reference ) { + return reference.path == path; + } ); + if( found == end( m_references ) ) { - auto error = AddReference( reference ); + auto error = AddReference( path ); if( !error.empty() ) { return error; } - rewritten += m_referenceNames.back(); - } - else - { - rewritten += m_referenceNames[found - begin( m_referencePaths )]; + found = end( m_references ) - 1; } + rewritten += found->name; p = pathEnd; } m_referenceVariables.clear(); - for( size_t i = 0; i < m_referenceNames.size(); ++i ) + m_referenceVariables.reserve( m_references.size() ); + for( size_t i = 0; i < m_references.size(); ++i ) { - m_referenceVariables.push_back( { m_referenceNames[i].c_str(), REFERENCE_BUFFER_INDEX, CcpParser::OffsetType( i * sizeof( float ) ) } ); + m_referenceVariables.push_back( { m_references[i].name.c_str(), REFERENCE_BUFFER_INDEX, CcpParser::OffsetType( i * sizeof( float ) ) } ); } m_referenceValues.assign( m_references.size(), 0.f ); return std::string(); } -std::string Tr2ControllerExpression::AddReference( const std::string& reference ) +std::string Tr2ControllerExpression::AddReference( const std::string& path ) { const auto& roots = m_controller->GetBindingPathRoots(); - auto error = "cannot bind \"" + reference + "\" to a float attribute"; - - auto lastDot = reference.rfind( '.' ); - if( lastDot == std::string::npos || !IsIdentifier( reference.c_str() + lastDot + 1, reference.c_str() + reference.length() ) ) - { - return error; - } - auto binding = std::make_unique(); - auto prevDot = reference.rfind( '.', lastDot - 1 ); - - if( reference.length() - lastDot == 2 && prevDot != std::string::npos && IsIdentifier( reference.c_str() + prevDot + 1, reference.c_str() + lastDot ) ) - { - binding->m_path = reference.substr( 0, prevDot ); - binding->m_attribute = reference.substr( prevDot + 1 ); + auto linkAt = [&]( size_t dot ) { + binding->m_path = path.substr( 0, dot ); + binding->m_attribute = path.substr( dot + 1 ); binding->Link( roots ); - } - if( !binding->IsValid() ) + return binding->IsValid(); + }; + + auto attributeDot = path.rfind( '.' ); + if( attributeDot == std::string::npos || !IsIdentifier( path.c_str() + attributeDot + 1, path.c_str() + path.length() ) ) { - binding->m_path = reference.substr( 0, lastDot ); - binding->m_attribute = reference.substr( lastDot + 1 ); - binding->Link( roots ); + return CannotBindError( path ); } - if( !binding->IsValid() ) + + // swizzle? f.ex -> "thing.color.r" binds to the attribute "color.r" if there is one + auto swizzleDot = path.length() - attributeDot == 2 ? path.rfind( '.', attributeDot - 1 ) : std::string::npos; + bool hasSwizzle = swizzleDot != std::string::npos && IsIdentifier( path.c_str() + swizzleDot + 1, path.c_str() + attributeDot ); + if( !( hasSwizzle && linkAt( swizzleDot ) ) && !linkAt( attributeDot ) ) { - return error; + return CannotBindError( path ); } - m_referenceNames.push_back( "__ref" + std::to_string( m_references.size() ) ); - m_referencePaths.push_back( reference ); - m_references.push_back( std::move( binding ) ); + m_references.push_back( Reference{ path, "__ref" + std::to_string( m_references.size() ), std::move( binding ) } ); return std::string(); } void Tr2ControllerExpression::ClearReferences() { - m_referencePaths.clear(); - m_referenceNames.clear(); m_references.clear(); m_referenceVariables.clear(); m_referenceValues.clear(); @@ -710,7 +686,7 @@ std::pair Tr2ControllerExpression::Eval( void* extraBuffer ) const } for( size_t i = 0; i < m_references.size(); ++i ) { - if( !m_references[i]->GetValue( m_referenceValues[i] ) ) + if( !m_references[i].binding->GetValue( m_referenceValues[i] ) ) { m_referenceValues[i] = 0.f; } @@ -730,6 +706,7 @@ void Tr2ControllerExpression::Clear() ClearReferences(); m_stateMachine = nullptr; m_controller = nullptr; + m_variableMask = 0; } bool Tr2ControllerExpression::IsExpressionValid() const diff --git a/trinity/Controllers/Tr2ControllerExpression.h b/trinity/Controllers/Tr2ControllerExpression.h index 4dbd6369a..593c7999c 100644 --- a/trinity/Controllers/Tr2ControllerExpression.h +++ b/trinity/Controllers/Tr2ControllerExpression.h @@ -31,18 +31,20 @@ class Tr2ControllerExpression static const uint32_t REFERENCE_BUFFER_INDEX = 4; private: - std::string CreateParser( const char* expression, const CcpParser::FunctionView& extraFunctions ); - - CcpParser::Program m_program; + struct Reference + { + std::string path; + std::string name; + std::unique_ptr binding; + }; - // bindable refs + std::string CreateParser( const char* expression, const CcpParser::FunctionView& extraFunctions ); std::string BindReferences( const char* expression, std::string& rewritten ); - std::string AddReference( const std::string& reference ); + std::string AddReference( const std::string& path ); void ClearReferences(); - std::vector m_referencePaths; - std::vector m_referenceNames; - std::vector> m_references; + CcpParser::Program m_program; + std::vector m_references; std::vector m_referenceVariables; mutable std::vector m_referenceValues; From 757856e6b0372ef172813c30b00da59256e2ae92 Mon Sep 17 00:00:00 2001 From: ccpHenry <153081976+ccpHenry@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:06:45 +0000 Subject: [PATCH 3/6] adding spaceObjectParent as a bindable root and adding expression term info https://fenriscreations.atlassian.net/browse/EO-26922 --- trinity/Controllers/ITr2Controller.h | 2 + trinity/Controllers/Tr2Controller.cpp | 49 ++++++++++- .../Controllers/Tr2ControllerExpression.cpp | 85 ++++++++++++++----- trinity/Controllers/Tr2ControllerExpression.h | 5 +- trinity/Controllers/Tr2TimelineController.cpp | 9 +- 5 files changed, 126 insertions(+), 24 deletions(-) diff --git a/trinity/Controllers/ITr2Controller.h b/trinity/Controllers/ITr2Controller.h index 101b0b247..422a834ed 100644 --- a/trinity/Controllers/ITr2Controller.h +++ b/trinity/Controllers/ITr2Controller.h @@ -53,6 +53,8 @@ BLUE_INTERFACE( ITr2Controller ) : } }; +void UpdateSpaceObjectParentRoot( std::vector> & roots, IRoot * owner ); + // A controller that supports controller actions BLUE_INTERFACE( ITr2ActionController ) : public ITr2Controller diff --git a/trinity/Controllers/Tr2Controller.cpp b/trinity/Controllers/Tr2Controller.cpp index 445937637..74cd64e6f 100644 --- a/trinity/Controllers/Tr2Controller.cpp +++ b/trinity/Controllers/Tr2Controller.cpp @@ -8,6 +8,7 @@ #include "Include/ITr2Updateable.h" #include "../Tr2ExpressionTermInfo.h" #include "ContinueOnMainThread.h" +#include "Eve/SpaceObject/EveSpaceObject2.h" CCP_STATS_DECLARE( controllerUpdateTime, "Trinity/Controllers/UpdateTime", true, CST_TIME, "Cumulative per-frame time for controller update" ); CCP_STATS_DECLARE( controllerUpdateablesTime, "Trinity/Controllers/UpdateablesTime", true, CST_TIME, "Cumulative per-frame time for controller updates tick" ); @@ -18,6 +19,45 @@ CCP_STATS_DECLARE( controllerLinkCount, "Trinity/Controllers/LinkCount", false, CcpMutex g_controllerMutex( "", "g_controllerMutex" ); +namespace +{ +const char* const SPACE_OBJECT_PARENT = "spaceObjectParent"; + +bool TryGetSpaceObjectParent( IRoot* owner, IRoot*& spaceObjectParent ) +{ + if( EveSpaceObjectChildPtr child = BlueCastPtr( owner ) ) + { + auto spaceObject = child->GetOwner(); + spaceObjectParent = spaceObject ? spaceObject->GetRootObject() : nullptr; + return true; + } + if( IEveSpaceObject2Ptr spaceObject = BlueCastPtr( owner ) ) + { + spaceObjectParent = spaceObject->GetRootObject(); + return true; + } + return false; +} +} + +void UpdateSpaceObjectParentRoot( std::vector>& roots, IRoot* owner ) +{ + IRoot* spaceObjectParent = nullptr; + if( !TryGetSpaceObjectParent( owner, spaceObjectParent ) ) + { + return; + } + for( auto& root : roots ) + { + if( root.first == SPACE_OBJECT_PARENT ) + { + root.second = spaceObjectParent; + return; + } + } + roots.push_back( { SPACE_OBJECT_PARENT, spaceObjectParent } ); +} + Tr2Controller::Tr2Controller( IRoot* lockobj ) : PARENTLOCK( m_stateMachines ), PARENTLOCK( m_variables ), @@ -323,6 +363,8 @@ std::optional Tr2Controller::GetFloatVariableByName( const char* name ) c void Tr2Controller::GetExpressionTermInfo( std::vector& out ) const { + out.push_back( Tr2ExpressionTermInfo::Variable( "Bindings", "Owner", "the object this controller is attached to - follow it with a path to a float attribute, e.g. Owner.translation.x" ) ); + out.push_back( Tr2ExpressionTermInfo::Variable( "Bindings", SPACE_OBJECT_PARENT, "the space object this controller's owner belongs to, or the owner itself if it is a space object" ) ); for( auto it = begin( m_variables ); it != end( m_variables ); ++it ) { out.push_back( Tr2ExpressionTermInfo::Variable( "Variables", ( *it )->GetName().c_str(), "controller variable" ) ); @@ -362,16 +404,21 @@ const std::vector>& Tr2Controller::GetBindingPath { if( m_bindingPathRoots.empty() ) { - m_bindingPathRoots.reserve( 1 + m_variables.size() ); + m_bindingPathRoots.reserve( 2 + m_variables.size() ); if( m_owner ) { m_bindingPathRoots.push_back( { "Owner", m_owner } ); } + UpdateSpaceObjectParentRoot( m_bindingPathRoots, m_owner ); for( auto& var : m_variables ) { m_bindingPathRoots.push_back( { var->GetName(), var->GetRawRoot() } ); } } + else + { + UpdateSpaceObjectParentRoot( m_bindingPathRoots, m_owner ); + } return m_bindingPathRoots; } diff --git a/trinity/Controllers/Tr2ControllerExpression.cpp b/trinity/Controllers/Tr2ControllerExpression.cpp index 7cfc34b9f..7f1537fd3 100644 --- a/trinity/Controllers/Tr2ControllerExpression.cpp +++ b/trinity/Controllers/Tr2ControllerExpression.cpp @@ -236,11 +236,46 @@ const char* SkipIdentifier( const char* text ) return text; } -bool IsBindingRoot( const std::vector>& roots, std::string_view name ) +using BindingPathRoots = std::vector>; + +const std::pair* FindBindingRoot( const BindingPathRoots& roots, std::string_view name ) { - return std::any_of( begin( roots ), end( roots ), [&]( const auto& root ) { + auto found = std::find_if( begin( roots ), end( roots ), [&]( const auto& root ) { return root.first == name; } ); + return found == end( roots ) ? nullptr : &*found; +} + +bool HasBindableAttribute( const std::string& path ) +{ + auto dot = path.rfind( '.' ); + return dot != std::string::npos && IsIdentifier( path.c_str() + dot + 1, path.c_str() + path.length() ); +} + +bool HasUnattachedRoot( const BindingPathRoots& roots, const std::string& path ) +{ + auto rootEnd = SkipIdentifier( path.c_str() ); + auto root = FindBindingRoot( roots, std::string_view( path.c_str(), size_t( rootEnd - path.c_str() ) ) ); + return root && !root->second; +} + +bool LinkReference( Tr2BindingPoint& binding, const std::string& path, const BindingPathRoots& roots ) +{ + if( !HasBindableAttribute( path ) ) + { + return false; + } + auto linkAt = [&]( size_t dot ) { + binding.m_path = path.substr( 0, dot ); + binding.m_attribute = path.substr( dot + 1 ); + binding.Link( roots ); + return binding.IsValid(); + }; + + auto attributeDot = path.rfind( '.' ); + auto swizzleDot = path.length() - attributeDot == 2 ? path.rfind( '.', attributeDot - 1 ) : std::string::npos; + bool hasSwizzle = swizzleDot != std::string::npos && IsIdentifier( path.c_str() + swizzleDot + 1, path.c_str() + attributeDot ); + return ( hasSwizzle && linkAt( swizzleDot ) ) || linkAt( attributeDot ); } std::string CannotBindError( const std::string& path ) @@ -531,6 +566,7 @@ struct ParserObserver : public CcpParser::Observer Tr2ControllerExpression::Tr2ControllerExpression() : + m_hasPendingReferences( false ), m_stateMachine( nullptr ), m_controller( nullptr ), m_variableMask( 0 ) @@ -607,7 +643,7 @@ std::string Tr2ControllerExpression::BindReferences( const char* expression, std } auto rootEnd = SkipIdentifier( p ); auto pathEnd = Tr2BindingPoint::MatchPath( p ); - if( pathEnd == rootEnd || !IsBindingRoot( roots, std::string_view( p, size_t( rootEnd - p ) ) ) ) + if( pathEnd == rootEnd || !FindBindingRoot( roots, std::string_view( p, size_t( rootEnd - p ) ) ) ) { // variable or function name rewritten.append( p, rootEnd ); @@ -646,44 +682,51 @@ std::string Tr2ControllerExpression::AddReference( const std::string& path ) { const auto& roots = m_controller->GetBindingPathRoots(); auto binding = std::make_unique(); - auto linkAt = [&]( size_t dot ) { - binding->m_path = path.substr( 0, dot ); - binding->m_attribute = path.substr( dot + 1 ); - binding->Link( roots ); - return binding->IsValid(); - }; - - auto attributeDot = path.rfind( '.' ); - if( attributeDot == std::string::npos || !IsIdentifier( path.c_str() + attributeDot + 1, path.c_str() + path.length() ) ) - { - return CannotBindError( path ); - } - - // swizzle? f.ex -> "thing.color.r" binds to the attribute "color.r" if there is one - auto swizzleDot = path.length() - attributeDot == 2 ? path.rfind( '.', attributeDot - 1 ) : std::string::npos; - bool hasSwizzle = swizzleDot != std::string::npos && IsIdentifier( path.c_str() + swizzleDot + 1, path.c_str() + attributeDot ); - if( !( hasSwizzle && linkAt( swizzleDot ) ) && !linkAt( attributeDot ) ) + bool linked = LinkReference( *binding, path, roots ); + bool pending = !linked && HasUnattachedRoot( roots, path ); + if( !linked && !pending ) { return CannotBindError( path ); } - m_references.push_back( Reference{ path, "__ref" + std::to_string( m_references.size() ), std::move( binding ) } ); + m_references.push_back( Reference{ path, "__ref" + std::to_string( m_references.size() ), std::move( binding ), pending } ); + m_hasPendingReferences = m_hasPendingReferences || pending; return std::string(); } void Tr2ControllerExpression::ClearReferences() { m_references.clear(); + m_hasPendingReferences = false; m_referenceVariables.clear(); m_referenceValues.clear(); } +void Tr2ControllerExpression::ResolvePendingReferences() const +{ + if( !m_hasPendingReferences ) + { + return; + } + const auto& roots = m_controller->GetBindingPathRoots(); + m_hasPendingReferences = false; + for( auto& reference : m_references ) + { + if( reference.pending ) + { + reference.pending = !LinkReference( *reference.binding, reference.path, roots ); + m_hasPendingReferences = m_hasPendingReferences || reference.pending; + } + } +} + std::pair Tr2ControllerExpression::Eval( void* extraBuffer ) const { if( !m_controller || !m_program ) { return std::make_pair( false, 0.f ); } + ResolvePendingReferences(); for( size_t i = 0; i < m_references.size(); ++i ) { if( !m_references[i].binding->GetValue( m_referenceValues[i] ) ) diff --git a/trinity/Controllers/Tr2ControllerExpression.h b/trinity/Controllers/Tr2ControllerExpression.h index 593c7999c..d676c1be4 100644 --- a/trinity/Controllers/Tr2ControllerExpression.h +++ b/trinity/Controllers/Tr2ControllerExpression.h @@ -36,15 +36,18 @@ class Tr2ControllerExpression std::string path; std::string name; std::unique_ptr binding; + bool pending; }; std::string CreateParser( const char* expression, const CcpParser::FunctionView& extraFunctions ); std::string BindReferences( const char* expression, std::string& rewritten ); std::string AddReference( const std::string& path ); void ClearReferences(); + void ResolvePendingReferences() const; CcpParser::Program m_program; - std::vector m_references; + mutable std::vector m_references; + mutable bool m_hasPendingReferences; std::vector m_referenceVariables; mutable std::vector m_referenceValues; diff --git a/trinity/Controllers/Tr2TimelineController.cpp b/trinity/Controllers/Tr2TimelineController.cpp index 838f3006b..ca8cbdc4f 100644 --- a/trinity/Controllers/Tr2TimelineController.cpp +++ b/trinity/Controllers/Tr2TimelineController.cpp @@ -307,16 +307,21 @@ const std::vector>& Tr2TimelineController::GetBin { if( m_bindingPathRoots.empty() ) { - m_bindingPathRoots.reserve( 1 + m_variables.size() ); + m_bindingPathRoots.reserve( 2 + m_variables.size() ); if( m_owner ) { m_bindingPathRoots.push_back( { "Owner", m_owner } ); } + UpdateSpaceObjectParentRoot( m_bindingPathRoots, m_owner ); for( auto& var : m_variables ) { m_bindingPathRoots.push_back( { var->GetName(), var->GetRawRoot() } ); } } + else + { + UpdateSpaceObjectParentRoot( m_bindingPathRoots, m_owner ); + } return m_bindingPathRoots; } @@ -334,6 +339,8 @@ std::optional Tr2TimelineController::GetFloatVariableByName( const char* void Tr2TimelineController::GetExpressionTermInfo( std::vector& out ) const { + out.push_back( Tr2ExpressionTermInfo::Variable( "Bindings", "Owner", "the object this controller is attached to - follow it with a path to a float attribute, e.g. Owner.translation.x" ) ); + out.push_back( Tr2ExpressionTermInfo::Variable( "Bindings", "spaceObjectParent", "the space object this controller's owner belongs to, or the owner itself if it is a space object" ) ); for( auto it = begin( m_variables ); it != end( m_variables ); ++it ) { out.push_back( Tr2ExpressionTermInfo::Variable( "Variables", ( *it )->GetName().c_str(), "controller variable" ) ); From f591f28944d987d525b551071b713696d45a06d7 Mon Sep 17 00:00:00 2001 From: ccpHenry <153081976+ccpHenry@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:20:51 +0000 Subject: [PATCH 4/6] Fix Capitalization to be consistent with the Owner root --- trinity/Controllers/Tr2Controller.cpp | 2 +- trinity/Controllers/Tr2TimelineController.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/trinity/Controllers/Tr2Controller.cpp b/trinity/Controllers/Tr2Controller.cpp index 74cd64e6f..97421bb0e 100644 --- a/trinity/Controllers/Tr2Controller.cpp +++ b/trinity/Controllers/Tr2Controller.cpp @@ -21,7 +21,7 @@ CcpMutex g_controllerMutex( "", "g_controllerMutex" ); namespace { -const char* const SPACE_OBJECT_PARENT = "spaceObjectParent"; +const char* const SPACE_OBJECT_PARENT = "SpaceObjectParent"; bool TryGetSpaceObjectParent( IRoot* owner, IRoot*& spaceObjectParent ) { diff --git a/trinity/Controllers/Tr2TimelineController.cpp b/trinity/Controllers/Tr2TimelineController.cpp index ca8cbdc4f..7a3328928 100644 --- a/trinity/Controllers/Tr2TimelineController.cpp +++ b/trinity/Controllers/Tr2TimelineController.cpp @@ -340,7 +340,7 @@ std::optional Tr2TimelineController::GetFloatVariableByName( const char* void Tr2TimelineController::GetExpressionTermInfo( std::vector& out ) const { out.push_back( Tr2ExpressionTermInfo::Variable( "Bindings", "Owner", "the object this controller is attached to - follow it with a path to a float attribute, e.g. Owner.translation.x" ) ); - out.push_back( Tr2ExpressionTermInfo::Variable( "Bindings", "spaceObjectParent", "the space object this controller's owner belongs to, or the owner itself if it is a space object" ) ); + out.push_back( Tr2ExpressionTermInfo::Variable( "Bindings", "SpaceObjectParent", "the space object this controller's owner belongs to, or the owner itself if it is a space object" ) ); for( auto it = begin( m_variables ); it != end( m_variables ); ++it ) { out.push_back( Tr2ExpressionTermInfo::Variable( "Variables", ( *it )->GetName().c_str(), "controller variable" ) ); From b33a3ba919d5c26eebe4c134209abcd16bb78b70 Mon Sep 17 00:00:00 2001 From: ccpHenry <153081976+ccpHenry@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:46:34 +0000 Subject: [PATCH 5/6] I am sorry mr. Linter --- trinity/Controllers/ITr2Controller.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trinity/Controllers/ITr2Controller.h b/trinity/Controllers/ITr2Controller.h index 422a834ed..85822aedb 100644 --- a/trinity/Controllers/ITr2Controller.h +++ b/trinity/Controllers/ITr2Controller.h @@ -53,7 +53,7 @@ BLUE_INTERFACE( ITr2Controller ) : } }; -void UpdateSpaceObjectParentRoot( std::vector> & roots, IRoot * owner ); +void UpdateSpaceObjectParentRoot( std::vector>& roots, IRoot* owner ); // A controller that supports controller actions BLUE_INTERFACE( ITr2ActionController ) : From 0e4dc496bd4bc0043baed1f9aa4dddf773585f99 Mon Sep 17 00:00:00 2001 From: ccpHenry <153081976+ccpHenry@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:28:45 +0000 Subject: [PATCH 6/6] get rid of undefined behavior to comply with a copilot review This should not affect anything but now the comparator is strictly defined runs the range check first, using std::less, and only subtracts once the pointer is known to be inside the first view None of the other lines do pointer arithmetic, so there's nothing to change. It looks like Copilot grouped them in by mistake? --- .../Controllers/Tr2ControllerExpression.cpp | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/trinity/Controllers/Tr2ControllerExpression.cpp b/trinity/Controllers/Tr2ControllerExpression.cpp index 7f1537fd3..e83874930 100644 --- a/trinity/Controllers/Tr2ControllerExpression.cpp +++ b/trinity/Controllers/Tr2ControllerExpression.cpp @@ -540,17 +540,19 @@ struct ParserObserver : public CcpParser::Observer void OnVariable( const CcpParser::Variable* variable ) override { + std::less less; + if( less( variable, m_variables.data ) || !less( variable, m_variables.data + m_variables.count ) ) + { + return; + } auto offset = variable - m_variables.data; - if( offset >= 0 && offset < ptrdiff_t( m_variables.count ) ) + if( offset >= 64 ) { - if( offset >= 64 ) - { - m_maskOverflow = true; - } - else - { - m_mask |= 1ull << offset; - } + m_maskOverflow = true; + } + else + { + m_mask |= 1ull << offset; } }