diff --git a/doc/source/eve/modular-assembly.rst b/doc/source/eve/modular-assembly.rst new file mode 100644 index 000000000..2260d8fc0 --- /dev/null +++ b/doc/source/eve/modular-assembly.rst @@ -0,0 +1,108 @@ +:orphan: + +Modular ship assembly +===================== + +A modular space object is a single ``EveSpaceObject2`` assembled at runtime from multiple SOF +hulls ("parts"), created via ``CreateModularObject`` and edited through the transient +``EveModularObjectModifier`` session object. Persistent per-part state lives in +``EveChildPartData``, an effect child on the object itself, so a saved or handed-off object +carries everything needed to reopen an edit session with ``ModifyModularObject``. + +This page only describes the cross-cutting flow that no single file shows. API contracts live +with the API: the headers (``EveModularObjectModifier.h``, ``EveChildPartData.h``, ``EveSOF.h``) and the python docstrings on +``trinity.CreateModularObject`` and the modifier methods. Beyond the usage example below, +values and signatures are deliberately not repeated here. + +Python example +-------------- + +Condensed from ``packages/trinity/tests/test_modular.py``, which exercises the full API and is +the authoritative reference for behavior. The modifier edits the object immediately, but +culling bounds are only committed by ``ApplyBounds``, or by dropping the last reference to the +modifier, which the example relies on. + +.. code-block:: python + + import trinity + + IDENTITY_ROT = (0, 0, 0, 1) + UNIT_SCALE = (1, 1, 1) + + sof = trinity.EveSOF() + sof.dataMgr.LoadData('res:/dx9/model/spaceobjectfactory/data.red') + + # Create an empty modular object. The faction/race arguments seed the + # defaults used when AddHull is passed empty strings. + ship, modifier = trinity.CreateModularObject(sof, 'somefaction', 'somerace') + + core = modifier.AddHull('some_hull', '', '', (0, 0, 0), IDENTITY_ROT, UNIT_SCALE) + wing = modifier.AddHull('other_hull', 'somefaction', 'somerace', + (30, 0, 0), IDENTITY_ROT, UNIT_SCALE) + if wing == trinity.GetInvalidPartTag(): + raise RuntimeError('hull failed to build') + + # Non-SOF parts come from a space object child resource. + beacon = modifier.AddChild('res:/model/somechild.red', (0, 50, 0), IDENTITY_ROT, UNIT_SCALE) + + modifier.SetTransform(wing, (-30, 0, 0), IDENTITY_ROT, UNIT_SCALE) + modifier.Remove(beacon) # KeyError on an unknown or already-removed tag + + del modifier # last reference dropped: bounding sphere and shape ellipsoid commit here + + # Part tags stay valid across sessions: EveChildPartData persists them on the + # object, so a saved/reloaded object reopens the same way. + modifier = trinity.ModifyModularObject(ship, sof) + assert modifier.GetPosition(wing) == (-30, 0, 0) + del modifier + +Part-tag propagation +-------------------- + +A part tag (``EveSpaceObjectChild::PartTag``, sentinels documented in ``EveSpaceObjectChild.h`` +and ``EveModularObjectModifier.h``) identifies everything belonging to one part. It flows: + +1. **Allocation**: ``EveModularObjectModifier::AllocatePartId`` (``EveModularObjectModifier.cpp``) takes + the max over ``EveChildPartData::GetUnusedPartID`` and the tags of existing effect children. +2. **SOF build**: ``EveSOF::BuildChild`` (``EveSOF.cpp``) stamps the tag on every container and + child it creates; nested layout placements flow it through ``EveSOF::SetupLayout`` / + ``EveSOF::CreatePlacement``. +3. **Locators**: ``EveSOF::SetupLocatorSets`` stamps ``partTag`` on each generated locator + (``EveSOFDataMgr::LocatorDirectionData`` converts to ``Locator`` preserving it), then merges + into the object via ``EveSpaceObject2::MergeToLocatorSet``. The merged view built by + ``EveSpaceObject2::EnsureChildLocatorMerged`` preserves per-locator tags. +4. **Mesh instances**: instanced meshes are shared across parts, so the tag is per *instance*, + not per child. Each ``EveChildInstancedMeshes::Mesh`` carries a ``partTags`` vector parallel + to the instance data (written in ``AddMesh``, consumed by ``RemoveInstancesByPartTag``). The + child's own ``m_partTag`` is meaningless for instanced meshes. +5. **Effect children**: ``EveSpaceObjectChild::SetPartTag`` propagates through container + overrides (``EveChildContainer::SetPartTag`` etc.), and ``EveSpaceObjectChild::RegisterChild`` + copies the parent's tag onto newly attached children. + +Locator lifecycle during editing +-------------------------------- + +- **AddHull**: locators from every hull merge into the object's sets *by set name*, with no + renaming or prefixing (``EveSpaceObject2::MergeToLocatorSet`` appends to an existing same-named + set). Parts are distinguishable within a set only by ``partTag``. +- **Remove**: locators are stripped from every set by exact ``partTag`` match, mesh instances via + ``RemoveInstancesByPartTag``, effect children by tag; accumulated impact damage is cleared. +- **SetTransform**: locators of the part are re-derived in place (position through + inverse-old-transform then new-transform, direction and scale by delta), and the part's stored + bounding sphere is re-transformed the same way. See + ``EveModularObjectModifier::SetTransform`` (``EveModularObjectModifier.cpp``). +- **Damage locators / impact overlay**: the impact overlay allocates per-damage-locator slots, so + its count must track the merged ``DAMAGE_LOCATOR_SET_NAME`` locator set. ``UpdateImpactOverlayLocatorCount`` + re-syncs it after AddHull/Remove; a stale count would index locators that no longer exist. +- Any structural edit calls ``EveSpaceObject2::InvalidateMergedLocators`` so the merged view is + rebuilt lazily. + +Gotchas +------- + +- A modular object with zero parts (or before ``ApplyBounds``/modifier destruction ever ran) has + a zero-radius bounding sphere: ``EveSpaceObject2::UpdateVisibility`` skips the mesh-visibility + test and ``EveSpaceObject2::IsVisible`` culls it at any distance, so it never renders. +- Culling volumes are only pushed to the object by ``EveModularObjectModifier::ApplyBounds`` (the + destructor calls it too). Editing without applying leaves the object rendering with stale + bounds. diff --git a/trinity/CMakeLists.txt b/trinity/CMakeLists.txt index cbcebdd1b..b5e0b65b3 100644 --- a/trinity/CMakeLists.txt +++ b/trinity/CMakeLists.txt @@ -549,6 +549,9 @@ set(_SOURCES Eve/SpaceObject/Children/EveChildMesh.cpp Eve/SpaceObject/Children/EveChildMesh.h Eve/SpaceObject/Children/EveChildMesh_Blue.cpp + Eve/SpaceObject/Children/EveChildPartData.cpp + Eve/SpaceObject/Children/EveChildPartData.h + Eve/SpaceObject/Children/EveChildPartData_Blue.cpp Eve/SpaceObject/Children/EveChildParticleSphere.cpp Eve/SpaceObject/Children/EveChildParticleSphere.h Eve/SpaceObject/Children/EveChildParticleSphere_Blue.cpp @@ -575,6 +578,9 @@ set(_SOURCES Eve/SpaceObject/Children/EveCloudEditableVolume.cpp Eve/SpaceObject/Children/EveCloudEditableVolume.h Eve/SpaceObject/Children/EveCloudEditableVolume_Blue.cpp + Eve/SpaceObject/Children/EveModularObjectModifier.cpp + Eve/SpaceObject/Children/EveModularObjectModifier.h + Eve/SpaceObject/Children/EveModularObjectModifier_Blue.cpp Eve/SpaceObject/Children/EveSpaceObjectChild.cpp Eve/SpaceObject/Children/EveSpaceObjectChild.h Eve/SpaceObject/Children/EveSpaceObjectChild_Blue.cpp diff --git a/trinity/Eve/EveInstancedMeshManager.h b/trinity/Eve/EveInstancedMeshManager.h index 3b4209c66..e6485245f 100644 --- a/trinity/Eve/EveInstancedMeshManager.h +++ b/trinity/Eve/EveInstancedMeshManager.h @@ -46,15 +46,32 @@ class EveInstancedMeshManager DataHandle& operator=( const DataHandle& ) = delete; DataHandle( DataHandle&& other ) noexcept { + owner = other.owner; + index = other.index; if( owner ) { - owner->ReplaceHandle( this, &other ); + owner->ReplaceHandle( &other, this ); } - owner = other.owner; - index = other.index; other.owner = nullptr; other.index = InvalidIndex; } + DataHandle& operator=( DataHandle&& other ) noexcept + { + if( this != &other ) + { + CCP_ASSERT( !*this ); + owner = other.owner; + index = other.index; + if( owner ) + { + owner->ReplaceHandle( &other, this ); + } + other.owner = nullptr; + other.index = InvalidIndex; + } + return *this; + } + operator bool() const { diff --git a/trinity/Eve/SpaceObject/Children/EveChildInstancedMeshes.cpp b/trinity/Eve/SpaceObject/Children/EveChildInstancedMeshes.cpp index 193f27751..b29193781 100644 --- a/trinity/Eve/SpaceObject/Children/EveChildInstancedMeshes.cpp +++ b/trinity/Eve/SpaceObject/Children/EveChildInstancedMeshes.cpp @@ -399,13 +399,80 @@ void EveChildInstancedMeshes::AddMesh( const Matrix* instanceTransforms, size_t count, const BlueSharedString& sofHullName, - const BlueSharedString& sofLocatorSetName ) + const BlueSharedString& sofLocatorSetName, + EveSpaceObjectChild::PartTag partTag ) { if( areaCount == 0 || count == 0 ) { return; } + for( auto& mesh : m_meshes ) + { + if( mesh.geometryPath != geometryPath || mesh.meshIndex != meshIndex ) + { + continue; + } + if( mesh.flags.GetCastsShadow() != castsShadow || mesh.reflectionMode != reflectionMode ) + { + continue; + } + if( mesh.areas.size() != areaCount ) + { + continue; + } + bool areasEqual = true; + for( size_t i = 0; i < areaCount; ++i ) + { + if( strcmp( mesh.areas[i].effect->GetEffectPathName(), areas[i].effect->GetEffectPathName() ) != 0 || mesh.areas[i].batchType != areas[i].batchType || mesh.areas[i].areaIndex != areas[i].areaIndex || mesh.areas[i].areaCount != areas[i].areaCount ) + { + areasEqual = false; + break; + } + if( mesh.areas[i].effectHash != areas[i].effect->GetHashValue() ) + { + areasEqual = false; + break; + } + } + if( !areasEqual ) + { + continue; + } + if( !( mesh.sofHullName == sofHullName && mesh.sofLocatorSetName == sofLocatorSetName ) ) + { + continue; + } + const size_t existingCount = mesh.instances.size(); + mesh.instances.reserve( existingCount + count ); + mesh.partTags.reserve( mesh.partTags.size() + count ); + for( size_t i = 0; i < count; ++i ) + { + EveInstancedMeshManager::StaticPerInstanceData instanceData; + auto& mat = instanceTransforms[i]; + instanceData.worldTransform[0] = Vector4( mat._11, mat._21, mat._31, mat._41 ); + instanceData.worldTransform[1] = Vector4( mat._12, mat._22, mat._32, mat._42 ); + instanceData.worldTransform[2] = Vector4( mat._13, mat._23, mat._33, mat._43 ); + instanceData.sphereIndex = static_cast( existingCount + i ); + mesh.instances.push_back( instanceData ); + mesh.partTags.push_back( partTag ); + } + mesh.instanceSpheres.resize( mesh.instances.size() ); + if( mesh.sphereHandle ) + { + mesh.sphereHandle.owner->RemoveBoundingSphereGroup( mesh.sphereHandle ); + } + for( auto& area : mesh.areas ) + { + if( area.meshGroupHandle ) + { + area.meshGroupHandle.owner->RemoveMeshGroup( area.meshGroupHandle ); + } + } + m_allRegistered = false; + return; + } + Mesh& mesh = m_meshes.emplace_back(); mesh.geometryPath = geometryPath; mesh.reflectionMode = reflectionMode; @@ -420,6 +487,7 @@ void EveChildInstancedMeshes::AddMesh( a.effectHash = a.effect ? a.effect->GetHashValue() : 0; } mesh.instances.reserve( count ); + mesh.partTags.reserve( count ); for( size_t i = 0; i < count; ++i ) { EveInstancedMeshManager::StaticPerInstanceData instanceData; @@ -429,6 +497,7 @@ void EveChildInstancedMeshes::AddMesh( instanceData.worldTransform[2] = Vector4( mat._13, mat._23, mat._33, mat._43 ); instanceData.sphereIndex = static_cast( i ); mesh.instances.push_back( instanceData ); + mesh.partTags.push_back( partTag ); } mesh.instanceSpheres.resize( count ); BeResMan->GetResource( mesh.geometryPath, "", mesh.geometry ); @@ -458,6 +527,72 @@ void EveChildInstancedMeshes::AddMesh( m_allRegistered = false; } +void EveChildInstancedMeshes::RemoveInstancesByPartTag( EveSpaceObjectChild::PartTag partTag ) +{ + for( size_t i = 0; i < m_meshes.size(); ++i ) + { + auto& mesh = m_meshes[i]; + + auto newEnd = std::remove_if( begin( mesh.instances ), end( mesh.instances ), [&]( const EveInstancedMeshManager::StaticPerInstanceData& instance ) { + return mesh.partTags[&instance - mesh.instances.data()] == partTag; + } ); + bool removed = newEnd != end( mesh.instances ); + if( !removed ) + { + continue; + } + if( newEnd == begin( mesh.instances ) ) + { + if( mesh.sphereHandle ) + { + mesh.sphereHandle.owner->RemoveBoundingSphereGroup( mesh.sphereHandle ); + } + for( auto& area : mesh.areas ) + { + if( area.meshGroupHandle ) + { + area.meshGroupHandle.owner->RemoveMeshGroup( area.meshGroupHandle ); + } + } + TriGeometryResPtr geometry = mesh.geometry; + std::swap( mesh, m_meshes.back() ); + auto seenMesh = find_if( begin( m_meshes ), end( m_meshes ) - 1, [&]( const Mesh& m ) { return m.geometry == geometry; } ); + if( geometry && seenMesh == end( m_meshes ) - 1 ) + { + geometry->RemoveNotifyTarget( this ); + } + m_meshes.pop_back(); + --i; + continue; + } + mesh.instances.erase( newEnd, end( mesh.instances ) ); + auto newTagEnd = std::remove_if( begin( mesh.partTags ), end( mesh.partTags ), [&]( uint32_t tag ) { + return tag == partTag; + } ); + mesh.partTags.erase( newTagEnd, end( mesh.partTags ) ); + for( auto& instance : mesh.instances ) + { + instance.sphereIndex = static_cast( &instance - mesh.instances.data() ); + } + if( removed ) + { + m_allRegistered = false; + mesh.instanceSpheres.resize( mesh.instances.size() ); + if( mesh.sphereHandle ) + { + mesh.sphereHandle.owner->RemoveBoundingSphereGroup( mesh.sphereHandle ); + } + for( auto& area : mesh.areas ) + { + if( area.meshGroupHandle ) + { + area.meshGroupHandle.owner->RemoveMeshGroup( area.meshGroupHandle ); + } + } + } + } +} + void EveChildInstancedMeshes::ReleaseCachedData( BlueAsyncRes* p ) { } diff --git a/trinity/Eve/SpaceObject/Children/EveChildInstancedMeshes.h b/trinity/Eve/SpaceObject/Children/EveChildInstancedMeshes.h index 270d24687..cfcff75fa 100644 --- a/trinity/Eve/SpaceObject/Children/EveChildInstancedMeshes.h +++ b/trinity/Eve/SpaceObject/Children/EveChildInstancedMeshes.h @@ -90,7 +90,10 @@ BLUE_CLASS( EveChildInstancedMeshes ) : const Matrix* instanceTransforms, size_t count, const BlueSharedString& sofHullName, - const BlueSharedString& sofLocatorSetName ); + const BlueSharedString& sofLocatorSetName, + EveSpaceObjectChild::PartTag partTag = EveSpaceObjectChild::NO_PART_TAG ); + + void RemoveInstancesByPartTag( EveSpaceObjectChild::PartTag partTag ); BluePy GetSofSourceLocator( uint32_t areaId ) const; uint32_t GetMeshCount() const; @@ -134,6 +137,7 @@ BLUE_CLASS( EveChildInstancedMeshes ) : std::vector instances; std::vector instanceSpheres; + std::vector partTags; EveInstancedMeshManager::BoundingSphereHandle sphereHandle; diff --git a/trinity/Eve/SpaceObject/Children/EveChildPartData.cpp b/trinity/Eve/SpaceObject/Children/EveChildPartData.cpp new file mode 100644 index 000000000..bf237679c --- /dev/null +++ b/trinity/Eve/SpaceObject/Children/EveChildPartData.cpp @@ -0,0 +1,17 @@ +// Copyright © 2026 CCP ehf. + +#include "StdAfx.h" +#include "EveChildPartData.h" +#include + + +EveChildPartData::EveChildPartData( IRoot* ) +{ +} + +EveSpaceObjectChild::PartTag EveChildPartData::GetUnusedPartID() const +{ + return std::accumulate( m_parts.begin(), m_parts.end(), 1u, []( EveSpaceObjectChild::PartTag maxId, const PartData& part ) { + return std::max( maxId, part.partId + 1 ); + } ); +} \ No newline at end of file diff --git a/trinity/Eve/SpaceObject/Children/EveChildPartData.h b/trinity/Eve/SpaceObject/Children/EveChildPartData.h new file mode 100644 index 000000000..d55d20284 --- /dev/null +++ b/trinity/Eve/SpaceObject/Children/EveChildPartData.h @@ -0,0 +1,37 @@ +// Copyright © 2026 CCP ehf. + +#pragma once + +#include "EveSpaceObjectChild.h" + + +BLUE_CLASS_IMPL( EveChildPartData ); +/** + * @brief Persistent state of a modular space object: seed faction/race and per-part transforms and bounds. + * Stored as an effect child so the state travels with the object. All editing goes through EveModularObjectModifier. + */ +class EveChildPartData : public EveSpaceObjectChild +{ +public: + EveChildPartData( IRoot* lockobj = nullptr ); + + EXPOSE_TO_BLUE(); + + PartTag GetUnusedPartID() const; + + /// @brief Fallbacks for EveModularObjectModifier::AddHull when it is called with an empty faction or race name. + std::string m_faction; + std::string m_race; + + struct PartData + { + PartTag partId; + Vector3 position; + Quaternion rotation; + Vector3 scale; + CcpMath::Sphere boundingSphere; ///< In the modular object's local space. + }; + std::vector m_parts; +}; + +TYPEDEF_BLUECLASS( EveChildPartData ); \ No newline at end of file diff --git a/trinity/Eve/SpaceObject/Children/EveChildPartData_Blue.cpp b/trinity/Eve/SpaceObject/Children/EveChildPartData_Blue.cpp new file mode 100644 index 000000000..7fbf367f8 --- /dev/null +++ b/trinity/Eve/SpaceObject/Children/EveChildPartData_Blue.cpp @@ -0,0 +1,15 @@ +// Copyright © 2026 CCP ehf. + +#include "StdAfx.h" +#include "EveChildPartData.h" + + +BLUE_DEFINE( EveChildPartData ); + +const Be::ClassInfo* EveChildPartData::ExposeToBlue() +{ + EXPOSURE_BEGIN( EveChildPartData, "Persistent state of a modular space object (per-part transforms and bounds). Edit through EveModularObjectModifier" ) + MAP_INTERFACE( EveSpaceObjectChild ); + MAP_INTERFACE( IEveSpaceObjectChild ) + EXPOSURE_END() +} diff --git a/trinity/Eve/SpaceObject/Children/EveModularObjectModifier.cpp b/trinity/Eve/SpaceObject/Children/EveModularObjectModifier.cpp new file mode 100644 index 000000000..3a86747f2 --- /dev/null +++ b/trinity/Eve/SpaceObject/Children/EveModularObjectModifier.cpp @@ -0,0 +1,299 @@ +// Copyright © 2026 CCP ehf. + +#include "StdAfx.h" +#include "EveModularObjectModifier.h" +#include "IEveEffectChildrenOwner.h" +#include "../EveStation2.h" +#include "../Attachments/EveImpactOverlay.h" +#include "EveChildInstancedMeshes.h" +#include "EveChildContainer.h" +#include + + +void EveModularObjectModifier::Create( SpaceObjectType* object, EveSOF* sof ) +{ + m_object = object; + m_sof = sof; + for( auto& child : object->GetEffectChildren() ) + { + if( EveChildPartDataPtr partData = BlueCastPtr( child ) ) + { + m_data = partData; + break; + } + } + if( !m_data ) + { + m_data.CreateInstance(); + object->AddToEffectChildrenList( m_data ); + } + for( auto& child : object->GetEffectChildren() ) + { + if( EveChildInstancedMeshesPtr instancedMeshes = BlueCastPtr( child ) ) + { + m_instancedMeshes = instancedMeshes; + break; + } + } +} + +EveModularObjectModifier::~EveModularObjectModifier() +{ + ApplyBounds(); +} + +void EveModularObjectModifier::ApplyBounds() +{ + if( m_object ) + { + std::vector orderedParts; + orderedParts.reserve( m_data->m_parts.size() ); + for( const auto& part : m_data->m_parts ) + { + orderedParts.push_back( &part ); + } + std::sort( orderedParts.begin(), orderedParts.end(), []( const auto* a, const auto* b ) { + return a->boundingSphere.radius > b->boundingSphere.radius; + } ); + + CcpMath::Sphere bounds; + CcpMath::AxisAlignedBox box; + for( const auto* part : orderedParts ) + { + bounds.Include( part->boundingSphere ); + box.IncludeSphere( part->boundingSphere ); + } + m_object->SetBoundingSphereInformation( bounds ); + m_object->SetShapeEllipsoid( m_data->m_parts.empty() ? CcpMath::AxisAlignedEllipsoid{} : CcpMath::AxisAlignedEllipsoid{ box, true } ); + } +} + +void EveModularObjectModifier::UpdateImpactOverlayLocatorCount() const +{ + if( EveImpactOverlayPtr overlay = m_object->GetImpactOverlay() ) + { + auto locators = m_object->GetLocatorsForSet( DAMAGE_LOCATOR_SET_NAME ); + overlay->SetDamageLocatorCount( locators ? uint32_t( locators->size() ) : 0 ); + } +} + +EveSpaceObjectChild::PartTag EveModularObjectModifier::AllocatePartId() const +{ + auto id = m_data->GetUnusedPartID(); + for( auto& child : m_object->GetEffectChildren() ) + { + if( child->GetPartTag() != EveSpaceObjectChild::NO_PART_TAG ) + { + id = std::max( id, child->GetPartTag() + 1 ); + } + } + return id; +} + +EveSpaceObjectChild::PartTag EveModularObjectModifier::AddHull( const char* hullName, const char* factionName, const char* raceName, const Vector3& position, const Quaternion& rotation, const Vector3& scale ) +{ + auto id = AllocatePartId(); + auto size = m_object->GetEffectChildren().size(); + auto dna = std::string( hullName ) + ":" + ( factionName[0] ? factionName : m_data->m_faction.c_str() ) + ":" + ( raceName[0] ? raceName : m_data->m_race.c_str() ); + if( !m_sof->BuildChild( m_object, dna.c_str(), id, TransformationMatrix( scale, rotation, position ) ) ) + { + return INVALID_PART_TAG; + } + + if( !m_instancedMeshes ) + { + for( size_t i = size; i < m_object->GetEffectChildren().size(); ++i ) + { + if( EveChildInstancedMeshesPtr instancedMesh = BlueCastPtr( m_object->GetEffectChildren()[i] ) ) + { + m_instancedMeshes = instancedMesh; + break; + } + } + } + + // SOF will reset the bounding sphere of the object to the one of the part + // Store the part bounding sphere and recalculate the bounding sphere of the modular object after adding all the parts + CcpMath::Sphere sphere{ m_object->GetBoundingSphereCenter(), m_object->GetBoundingSphereRadius() }; + + auto part = EveChildPartData::PartData{ id, position, rotation, scale, sphere }; + m_data->m_parts.emplace_back( part ); + m_object->InvalidateMergedLocators( LocatorInvalidationReason::StructureChanged ); + UpdateImpactOverlayLocatorCount(); + return id; +} + +EveSpaceObjectChild::PartTag EveModularObjectModifier::AddChild( const char* resPath, const Vector3& position, const Quaternion& rotation, const Vector3& scale ) +{ + if( auto child = BeResMan->LoadObject( resPath ) ) + { + child->Setup( &scale, &rotation, &position, Tr2Lod::TR2_LOD_LOW ); + m_object->AddToEffectChildrenList( child ); + auto id = AllocatePartId(); + child->SetPartTag( id ); + m_data->m_parts.emplace_back( EveChildPartData::PartData{ id, position, rotation, scale } ); + m_object->InvalidateMergedLocators( LocatorInvalidationReason::StructureChanged ); + return id; + } + return INVALID_PART_TAG; +} + +BlueStdResult EveModularObjectModifier::Remove( EveSpaceObjectChild::PartTag partId ) +{ + auto found = std::find_if( m_data->m_parts.begin(), m_data->m_parts.end(), [partId]( const EveChildPartData::PartData& part ) { + return part.partId == partId; + } ); + if( found == m_data->m_parts.end() ) + { + return BlueStdResultType::BLUE_STD_RESULT_KEY_ERROR; + } + + for( size_t i = 0; i < m_object->GetEffectChildren().size(); ) + { + auto child = m_object->GetEffectChildren()[i]; + if( child->GetPartTag() == partId ) + { + m_object->RemoveFromEffectChildrenList( child ); + continue; + } + ++i; + } + + for( auto& set : m_object->GetLocatorSets() ) + { + auto& locators = *set->GetLocators(); + auto removed = std::remove_if( locators.begin(), locators.end(), [partId]( const auto& locator ) { + return locator.partTag == partId; + } ); + locators.Resize( std::distance( locators.begin(), removed ) ); + } + + if( m_instancedMeshes ) + { + m_instancedMeshes->RemoveInstancesByPartTag( partId ); + } + m_data->m_parts.erase( found ); + m_object->InvalidateMergedLocators( LocatorInvalidationReason::StructureChanged ); + m_object->ClearImpactDamage(); + UpdateImpactOverlayLocatorCount(); + return BlueStdResultType::BLUE_STD_RESULT_OK; +} + +BlueStdResult EveModularObjectModifier::SetTransform( EveSpaceObjectChild::PartTag partId, const Vector3& position, const Quaternion& rotation, Vector3 scale ) +{ + auto found = std::find_if( m_data->m_parts.begin(), m_data->m_parts.end(), [partId]( const EveChildPartData::PartData& part ) { + return part.partId == partId; + } ); + if( found == m_data->m_parts.end() ) + { + return BlueStdResultType::BLUE_STD_RESULT_KEY_ERROR; + } + + cmf::Transform oldTransform{ found->position, found->rotation, found->scale }; + cmf::Transform newTransform{ position, rotation, scale }; + auto invOldTransform = cmf::Inverse( oldTransform ); + + for( auto& set : m_object->GetLocatorSets() ) + { + auto& locators = *set->GetLocators(); + for( auto& locator : locators ) + { + if( locator.partTag == partId ) + { + locator.scale.x = scale.x / found->scale.x; + locator.scale.y = scale.y / found->scale.y; + locator.scale.z = scale.z / found->scale.z; + locator.direction = invOldTransform.rotation * rotation; + locator.position = cmf::TransformPoint( cmf::TransformPoint( locator.position, invOldTransform ), newTransform ); + } + } + } + + found->boundingSphere.center = cmf::TransformPoint( cmf::TransformPoint( found->boundingSphere.center, invOldTransform ), newTransform ); + found->boundingSphere.radius *= std::max( { scale.x, scale.y, scale.z } ) / std::max( { found->scale.x, found->scale.y, found->scale.z } ); + + found->position = position; + found->rotation = rotation; + found->scale = scale; + + for( auto& child : m_object->GetEffectChildren() ) + { + if( child->GetPartTag() == partId ) + { + child->Setup( &scale, &rotation, &position, Tr2Lod::TR2_LOD_LOW ); + } + } + m_object->InvalidateMergedLocators( LocatorInvalidationReason::PartMoved ); + return BlueStdResultType::BLUE_STD_RESULT_OK; +} + +BlueStdResult EveModularObjectModifier::GetPosition( EveSpaceObjectChild::PartTag partId, Vector3& position ) const +{ + auto found = std::find_if( m_data->m_parts.begin(), m_data->m_parts.end(), [partId]( const EveChildPartData::PartData& part ) { + return part.partId == partId; + } ); + if( found == m_data->m_parts.end() ) + { + return BlueStdResultType::BLUE_STD_RESULT_KEY_ERROR; + } + position = found->position; + return BlueStdResultType::BLUE_STD_RESULT_OK; +} + +BlueStdResult EveModularObjectModifier::GetRotation( EveSpaceObjectChild::PartTag partId, Quaternion& rotation ) const +{ + auto found = std::find_if( m_data->m_parts.begin(), m_data->m_parts.end(), [partId]( const EveChildPartData::PartData& part ) { + return part.partId == partId; + } ); + if( found == m_data->m_parts.end() ) + { + return BlueStdResultType::BLUE_STD_RESULT_KEY_ERROR; + } + rotation = found->rotation; + return BlueStdResultType::BLUE_STD_RESULT_OK; +} + +BlueStdResult EveModularObjectModifier::GetScale( EveSpaceObjectChild::PartTag partId, Vector3& scale ) const +{ + auto found = std::find_if( m_data->m_parts.begin(), m_data->m_parts.end(), [partId]( const EveChildPartData::PartData& part ) { + return part.partId == partId; + } ); + if( found == m_data->m_parts.end() ) + { + return BlueStdResultType::BLUE_STD_RESULT_KEY_ERROR; + } + scale = found->scale; + return BlueStdResultType::BLUE_STD_RESULT_OK; +} + + +std::pair CreateModularObject( EveSOF* sof, const char* factionName, const char* raceName ) +{ + EveStation2Ptr object; + object.CreateInstance(); + object->Initialize(); + + EveChildPartDataPtr partData; + partData.CreateInstance(); + partData->m_faction = factionName; + partData->m_race = raceName; + + object->AddToEffectChildrenList( partData ); + + EveModularObjectModifierPtr modifier; + modifier.CreateInstance(); + modifier->Create( object, sof ); + return { IEveSpaceObject2Ptr( object ), modifier }; +} + +EveModularObjectModifierPtr ModifyModularObject( EveModularObjectModifier::SpaceObjectType* object, EveSOF* sof ) +{ + EveModularObjectModifierPtr modifier; + modifier.CreateInstance(); + modifier->Create( object, sof ); + return modifier; +} +EveSpaceObjectChild::PartTag GetInvalidPartTag() +{ + return EveModularObjectModifier::INVALID_PART_TAG; +} \ No newline at end of file diff --git a/trinity/Eve/SpaceObject/Children/EveModularObjectModifier.h b/trinity/Eve/SpaceObject/Children/EveModularObjectModifier.h new file mode 100644 index 000000000..53bdc6fc7 --- /dev/null +++ b/trinity/Eve/SpaceObject/Children/EveModularObjectModifier.h @@ -0,0 +1,77 @@ +// Copyright © 2026 CCP ehf. + +#pragma once + +#include "EveChildPartData.h" +#include "../../SpaceObjectFactory/EveSOF.h" + + +BLUE_CLASS_IMPL( EveModularObjectModifier ); +/** + * @brief Transient edit session for a modular space object. Reads and writes the object's EveChildPartData and + * holds no persistent state itself. Object-level culling volumes only update on ApplyBounds(), which the + * destructor also calls as a fallback. + */ +class EveModularObjectModifier : public IRoot +{ +public: + using SpaceObjectType = EveSpaceObject2; + + EXPOSE_TO_BLUE(); + + void Create( SpaceObjectType* object, EveSOF* sof ); + ~EveModularObjectModifier(); + + /// @brief Returned by AddHull/AddChild when the part could not be built. Never a valid tag. + static constexpr EveSpaceObjectChild::PartTag INVALID_PART_TAG = 0xFFFFFFFF; + + /** + * @brief Builds a SOF hull as a new part at the given transform. Empty factionName/raceName fall back to + * the seed faction/race stored in EveChildPartData (set by CreateModularObject). + * @return The new part's tag, or INVALID_PART_TAG if the hull could not be built. + */ + EveSpaceObjectChild::PartTag AddHull( const char* hullName, const char* factionName, const char* raceName, const Vector3& position, const Quaternion& rotation, const Vector3& scale ); + + /** + * @brief Loads a space object child resource and adds it as a new part at the given transform. + * @return The new part's tag, or INVALID_PART_TAG if the resource could not be loaded. + */ + EveSpaceObjectChild::PartTag AddChild( const char* resPath, const Vector3& position, const Quaternion& rotation, const Vector3& scale ); + + BlueStdResult Remove( EveSpaceObjectChild::PartTag partId ); + + /** + * @brief Recomputes the object's bounding sphere and shape ellipsoid from the current parts. + * Cheap; call after a batch of edits to keep culling volumes in sync. The destructor also calls it. + */ + void ApplyBounds(); + + BlueStdResult SetTransform( EveSpaceObjectChild::PartTag partId, const Vector3& position, const Quaternion& rotation, Vector3 scale ); + BlueStdResult GetPosition( EveSpaceObjectChild::PartTag partId, Vector3& position ) const; + BlueStdResult GetRotation( EveSpaceObjectChild::PartTag partId, Quaternion& rotation ) const; + BlueStdResult GetScale( EveSpaceObjectChild::PartTag partId, Vector3& scale ) const; + +private: + EveChildPartData::PartData* FindPartData( EveSpaceObjectChild::PartTag partId ) const; + EveSpaceObjectChild::PartTag AllocatePartId() const; + void UpdateImpactOverlayLocatorCount() const; + + BluePtr m_object; + EveChildPartDataPtr m_data; + EveChildInstancedMeshesPtr m_instancedMeshes; + EveSOFPtr m_sof; +}; + +TYPEDEF_BLUECLASS( EveModularObjectModifier ); + +/** + * @brief Creates an empty modular space object together with an open edit session for it. + * factionName/raceName seed the AddHull fallbacks. + */ +std::pair CreateModularObject( EveSOF* sof, const char* factionName, const char* raceName ); + +/// @brief Opens an edit session on an existing modular space object. +EveModularObjectModifierPtr ModifyModularObject( EveModularObjectModifier::SpaceObjectType* object, EveSOF* sof ); + +/// @brief Wrapper for EveModularObjectModifier::INVALID_PART_TAG because blue doesn't support exporting const variables. +EveSpaceObjectChild::PartTag GetInvalidPartTag(); \ No newline at end of file diff --git a/trinity/Eve/SpaceObject/Children/EveModularObjectModifier_Blue.cpp b/trinity/Eve/SpaceObject/Children/EveModularObjectModifier_Blue.cpp new file mode 100644 index 000000000..bdf5c2535 --- /dev/null +++ b/trinity/Eve/SpaceObject/Children/EveModularObjectModifier_Blue.cpp @@ -0,0 +1,33 @@ +// Copyright © 2026 CCP ehf. + +#include "StdAfx.h" +#include "EveModularObjectModifier.h" + + +BLUE_DEFINE_NONEXPOSED( EveModularObjectModifier ); + +const Be::ClassInfo* EveModularObjectModifier::ExposeToBlue() +{ + EXPOSURE_BEGIN( EveModularObjectModifier, "Edit session for a modular space object, from CreateModularObject or ModifyModularObject. Culling bounds update on ApplyBounds; dropping the last reference also applies them" ) + MAP_METHOD_AND_WRAP( "AddHull", AddHull, "Builds a SOF hull as a new part at the given transform. Empty faction/race fall back to the seeds given to CreateModularObject. Returns the new part tag, or GetInvalidPartTag() on failure" ); + MAP_METHOD_AND_WRAP( "AddChild", AddChild, "Loads a space object child resource and adds it as a new part at the given transform. Returns the new part tag, or GetInvalidPartTag() on failure" ) + MAP_METHOD_AND_WRAP( "Remove", Remove, "Removes a part: its effect children, mesh instances and locators. Clears accumulated impact damage. Key error if the part tag is unknown" ) + MAP_METHOD_AND_WRAP( "ApplyBounds", ApplyBounds, "Recomputes the object's bounding sphere and shape ellipsoid from the current parts. Call after a batch of edits to keep culling in sync" ) + MAP_METHOD_AND_WRAP( "SetTransform", SetTransform, "Moves a part: re-derives its effect children, locators and bounding sphere from the new transform. Key error if the part tag is unknown" ) + MAP_METHOD_AND_WRAP( "GetPosition", GetPosition, "Returns the part's position. Key error if the part tag is unknown" ) + MAP_METHOD_AND_WRAP( "GetRotation", GetRotation, "Returns the part's rotation. Key error if the part tag is unknown" ) + MAP_METHOD_AND_WRAP( "GetScale", GetScale, "Returns the part's scale. Key error if the part tag is unknown" ) + + EXPOSURE_END() +} + +MAP_FUNCTION_AND_WRAP( "CreateModularObject", CreateModularObject, "Creates an empty modular space object and an edit session for it. Returns (object, modifier).\n" + "\n" + " obj, mod = trinity.CreateModularObject(sof, 'faction', 'race')\n" + " tag = mod.AddHull('hullname', '', '', pos, rot, scale) # '' -> seed faction/race\n" + " mod.SetTransform(tag, pos2, rot2, scale2)\n" + " mod.ApplyBounds() # apply culling bounds after a batch of edits\n" + " del mod # end the session (also applies bounds)" ); +MAP_FUNCTION_AND_WRAP( "ModifyModularObject", ModifyModularObject, "Opens an edit session on an existing modular space object" ); + +MAP_FUNCTION_AND_WRAP( "GetInvalidPartTag", GetInvalidPartTag, "Gets the INVALID_PART_TAG constant" ); \ No newline at end of file diff --git a/trinity/Eve/SpaceObject/EveSpaceObject2.cpp b/trinity/Eve/SpaceObject/EveSpaceObject2.cpp index 6ba4e3f15..3eeb3592f 100644 --- a/trinity/Eve/SpaceObject/EveSpaceObject2.cpp +++ b/trinity/Eve/SpaceObject/EveSpaceObject2.cpp @@ -1135,6 +1135,11 @@ void EveSpaceObject2::GetBatches( ITriRenderBatchAccumulator* batches, TriBatchT { if( !m_mesh ) { + // meshless objects (modular ships) still render their impact effects + if( m_impactOverlay ) + { + m_impactOverlay->GetBatches( batches, batchType, perObjectData, m_meshScreenSize ); + } return; } @@ -1712,11 +1717,11 @@ void EveSpaceObject2::UpdateVisibility( const EveUpdateContext& updateContext, c } } + m_meshScreenSize = frustum.GetPixelSizeAccrossEst( m_boundingSphereWorldCenter, m_boundingSphereWorldRadius ) * invLodFactor; + m_meshScreenSize = m_allowLodSelection ? m_meshScreenSize : std::numeric_limits::max(); + if( m_mesh ) { - m_meshScreenSize = frustum.GetPixelSizeAccrossEst( m_boundingSphereWorldCenter, m_boundingSphereWorldRadius ) * invLodFactor; - m_meshScreenSize = m_allowLodSelection ? m_meshScreenSize : std::numeric_limits::max(); - m_mesh->UseWithScreenSize( m_meshScreenSize, m_boundingSphereWorldRadius ); if( updateContext.m_raytracingEnabled ) @@ -1930,6 +1935,7 @@ void EveSpaceObject2::EnsureChildLocatorMerged() const Locator transformedLocator; transformedLocator.boneIndex = -1; Decompose( transformedLocator.scale, transformedLocator.direction, transformedLocator.position, transform ); + transformedLocator.partTag = locator->partTag; ( *mergedLocatorSet )->Append( &transformedLocator, 1 ); } @@ -3450,6 +3456,11 @@ void EveSpaceObject2::SetImpactOverlay( EveImpactOverlayPtr overlay ) m_impactOverlay = overlay; } +EveImpactOverlayPtr EveSpaceObject2::GetImpactOverlay() const +{ + return m_impactOverlay; +} + // -------------------------------------------------------------------------------- // Description: // Set the impact damage state: how many percent are gone? @@ -3806,10 +3817,13 @@ void EveSpaceObject2::EstimatePixelDiameter( const TriFrustum& frustum ) // estimate the pixel diameter using the local bounding box, // as the bounding sphere may not pepresent the mesh bounding sphere, // but rather the bounding sphere of the object and it's EveChildMesh attachments - if( m_mesh ) + if( !m_mesh ) { - m_mesh->GetBoundingBox( m_localAabbMin, m_localAabbMax ); + // meshless objects (modular ships) have no local box; size by the bounding sphere + m_estimatedPixelDiameter = frustum.GetPixelSizeAccrossEst( m_boundingSphereWorldCenter, m_boundingSphereWorldRadius ); + return; } + m_mesh->GetBoundingBox( m_localAabbMin, m_localAabbMax ); Vector4 sphere; BoundingSphereFromBox( sphere, m_localAabbMin, m_localAabbMax, &m_worldTransform ); m_estimatedPixelDiameter = frustum.GetPixelSizeAccross( sphere.GetXYZ(), sphere.w ); diff --git a/trinity/Eve/SpaceObject/EveSpaceObject2.h b/trinity/Eve/SpaceObject/EveSpaceObject2.h index cc0bece49..2d57f2381 100644 --- a/trinity/Eve/SpaceObject/EveSpaceObject2.h +++ b/trinity/Eve/SpaceObject/EveSpaceObject2.h @@ -409,6 +409,10 @@ BLUE_CLASS( EveSpaceObject2 ) : EveSpaceObjectChildPtr GetEffectChildByName( const char* name ) const; void AddToEffectChildrenList( EveSpaceObjectChild * child ); void RemoveFromEffectChildrenList( EveSpaceObjectChild * child ); + PEveSpaceObjectChildVector& GetEffectChildren() + { + return m_effectChildren; + } ///////////////////////////////////////////////////////////////////////////////////// // ITr2ControllerOwner @@ -480,6 +484,11 @@ BLUE_CLASS( EveSpaceObject2 ) : void MergeToLocatorSet( const EveLocatorSets& locatorSet ); void RunDamageLocatorFilter(); + PEveLocatorSetsVector& GetLocatorSets() + { + return m_locatorSets; + } + // clear stuff void ClearLocatorSets(); @@ -518,6 +527,7 @@ BLUE_CLASS( EveSpaceObject2 ) : // access to impacts void SetImpactOverlay( EveImpactOverlayPtr overlay ); + EveImpactOverlayPtr GetImpactOverlay() const; void SetImpactDamageState( float shield, float armor, float hull, bool doCreateArmorImpacts ); void SetImpactAnimation( const std::string& name, bool enable, float duration ); void ClearImpactDamage(); @@ -558,6 +568,9 @@ BLUE_CLASS( EveSpaceObject2 ) : void SetMute( bool isMute ); + float GetBoundingSphereRadius() const; + Vector3 GetBoundingSphereCenter() const; + protected: // Activation-Strength float m_activationStrength; @@ -655,9 +668,6 @@ BLUE_CLASS( EveSpaceObject2 ) : bool m_allAreasCastShadow; void CacheAllAreasCastShadow(); - float GetBoundingSphereRadius() const; - Vector3 GetBoundingSphereCenter() const; - Vector4 CalculateSkinnedBoundingSphere(); std::pair CalculateSkinnedBoundingBoxFromTransform( const Matrix& transform ); diff --git a/trinity/Eve/SpaceObject/Utils/EveLocatorSets.cpp b/trinity/Eve/SpaceObject/Utils/EveLocatorSets.cpp index d59caa871..33be1c04f 100644 --- a/trinity/Eve/SpaceObject/Utils/EveLocatorSets.cpp +++ b/trinity/Eve/SpaceObject/Utils/EveLocatorSets.cpp @@ -3,12 +3,15 @@ #include "StdAfx.h" #include "EveLocatorSets.h" +static_assert( sizeof( EveSpaceObjectChild::PartTag ) == sizeof( uint32_t ), "Size mismatch for PartTag: need to update LocatorStructureDef" ); + // locator item definition static BlueStructureDefinition LocatorStructureDef[] = { { "position", Be::FLOAT32_3, 0 }, { "direction", Be::FLOAT32_4, 12 }, { "scale", Be::FLOAT32_3, 28 }, { "boneIndex", Be::INT32_1, 40 }, + { "partTag", Be::UINT32_1, 44 }, { 0 } }; @@ -78,6 +81,11 @@ const LocatorStructureList* EveLocatorSets::GetLocators() const return &m_locators; } +LocatorStructureList* EveLocatorSets::GetLocators() +{ + return &m_locators; +} + const char* EveLocatorSets::GetName() const { return m_name.c_str(); diff --git a/trinity/Eve/SpaceObject/Utils/EveLocatorSets.h b/trinity/Eve/SpaceObject/Utils/EveLocatorSets.h index e844ece6b..c53d7b4fa 100644 --- a/trinity/Eve/SpaceObject/Utils/EveLocatorSets.h +++ b/trinity/Eve/SpaceObject/Utils/EveLocatorSets.h @@ -4,6 +4,8 @@ #ifndef EveLocatorSets_H #define EveLocatorSets_H +#include "../Children/EveSpaceObjectChild.h" + // decalre structured list here struct Locator { @@ -11,6 +13,7 @@ struct Locator Quaternion direction; Vector3 scale; int boneIndex; + EveSpaceObjectChild::PartTag partTag = EveSpaceObjectChild::NO_PART_TAG; ///< Part of a modular object this locator belongs to; NO_PART_TAG when not part-scoped. }; BLUE_DECLARE_STRUCTURE_LIST( Locator ); @@ -37,6 +40,7 @@ BLUE_CLASS( EveLocatorSets ) : bool HasName( const char* name ) const; bool HasName( const BlueSharedString& name ) const; const LocatorStructureList* GetLocators() const; + LocatorStructureList* GetLocators(); const char* GetName() const; void SetName( BlueSharedString name ); diff --git a/trinity/Eve/SpaceObjectFactory/EveSOF.cpp b/trinity/Eve/SpaceObjectFactory/EveSOF.cpp index 4e060394c..6fec973fa 100644 --- a/trinity/Eve/SpaceObjectFactory/EveSOF.cpp +++ b/trinity/Eve/SpaceObjectFactory/EveSOF.cpp @@ -224,7 +224,7 @@ IRootPtr EveSOF::BuildFromDNA( const char* dnaString ) extensionContainer->SetIsPlacementRoot( true ); EveChildInstancedMeshesPtr sharedMeshes; - CreatePlacement( newObj, sharedMeshes, dna, dna, fakePlacement, std::vector( 1, center ), centerOffset, extensionContainer, partTag ); + CreatePlacement( newObj, sharedMeshes, dna, dna, fakePlacement, std::vector( 1, center ), centerOffset, extensionContainer, partTag, true ); newObj->AddToEffectChildrenList( extensionContainer ); // create an empty mesh... @@ -278,7 +278,7 @@ IRootPtr EveSOF::BuildFromDNA( const char* dnaString ) layoutContainer->SetOrigin( EveSpaceObjectChild::SOF ); layoutContainer->SetIsPlacementRoot( true ); layoutContainer->SetAlwaysOn( true ); - SetupLayout( newObj, layoutContainer, sharedMeshes, dna, centerOffset, partTag ); + SetupLayout( newObj, layoutContainer, sharedMeshes, dna, centerOffset, partTag, true ); if( layoutContainer->m_objects.size() != 0 ) { @@ -306,8 +306,202 @@ IRootPtr EveSOF::BuildFromDNA( const char* dnaString ) return newObj->GetRawRoot(); } +bool EveSOF::BuildChild( EveSpaceObject2* newObj, const char* dnaString, uint32_t partTag, const Matrix& transform ) +{ + std::string s = "BuildChild "; + s += std::string( dnaString ); + CCP_STATS_ZONE( s.c_str() ); + + EveSOFDNAPtr dna = CreateDna( dnaString ); + if( dna == nullptr ) + { + return false; + } + dna->SetParentBoundingSphere( {} ); + dna->SetParentShapeEllipsoidInfo( {} ); + + EveChildInstancedMeshesPtr sharedMeshes; + for( auto& child : newObj->GetEffectChildren() ) + { + if( EveChildInstancedMeshesPtr instancedMeshes = BlueCastPtr( child ) ) + { + sharedMeshes = instancedMeshes; + break; + } + } + + const bool hasChildEffects = ( !dna->GetHullChildSets().empty() && dna->UsingSof6() ) || ( !dna->GetHullChildren().empty() && !dna->UsingSof6() ); + const bool hasControllers = !dna->GetHullControllers().empty(); + const bool hasAnimation = dna->IsHullAnimated(); + const bool hasEmitters = !dna->GetHullSoundEmitters().empty(); + const bool hasLayouts = dna->GetLayoutCount() > 0; + bool hasAttachments = false; + for( size_t hullIdx = 0; hullIdx < dna->GetMultiHullCount(); ++hullIdx ) + { + if( !dna->GetHullSpriteSets( hullIdx ).empty() || + !dna->GetHullSpotlightSets( hullIdx ).empty() || + !dna->GetHullPlaneSets( hullIdx ).empty() || + !dna->GetHullSpriteLineSets( hullIdx ).empty() || + !dna->GetHullHazeSets( hullIdx ).empty() || + !dna->GetHullBanners( hullIdx ).empty() || + !dna->GetHullBannerSets( hullIdx ).empty() || + !dna->GetHullLightSets( hullIdx ).empty() ) + { + hasAttachments = true; + break; + } + } + + std::vector placementOffsets = { transform }; + + const bool needsPlacementContainer = hasControllers || hasAnimation || hasEmitters || hasChildEffects || hasLayouts || hasAttachments; + const uint32_t buildFlags = !hasAnimation ? EveSOFDataHullBuildFilter::INSTANCED_PLACEMENT : EveSOFDataHullBuildFilter::NON_INSTANCED_PLACEMENT; + + Quaternion rotation; + Vector3 translation; + Vector3 scale; + Decompose( scale, rotation, translation, transform ); + + EveChildContainerPtr placementContainer; + if( needsPlacementContainer ) + { + placementContainer.CreateInstance(); + placementContainer->SetName( dna->GetHullNames()[0].c_str() ); + placementContainer->SetPartTag( partTag ); + placementContainer->SetupWithStaticTransform( &scale, &rotation, &translation, Tr2Lod::TR2_LOD_LOW ); + newObj->AddToEffectChildrenList( placementContainer ); + } + + if( hasAnimation ) + { + // create the child normally + // create the non instanced extension mesh + EveChildMeshPtr child; + child.CreateInstance(); + auto mesh = CreateMesh( dna ); + child->SetMesh( mesh ); + child->SetReflectionMode( dna->GetReflectionMode() ); + child->SetCastShadow( dna->CastShadow() ); + child->SetMinScreenSize( MIN_MESH_SCREEN_SIZE ); + child->SetName( dna->GetHullNames()[0].c_str() ); + if( !placementContainer ) + { + child->SetupWithStaticTransform( &scale, &rotation, &translation, Tr2Lod::TR2_LOD_LOW ); + } + child->SetPartTag( partTag ); + + if( m_editorMode ) + { + IWeakObjectPtr weak = BlueCastPtr( child ); + BeObjectMetadata->Set( weak, "SofDna", dna->GetDnaString() ); + } + Tr2GrannyAnimationPtr animationPtr; + animationPtr.CreateInstance(); + child->SetAnimationController( animationPtr ); + // This will set the child as the animation owner of the parent, don't think this will be a problem... + placementContainer->SetAnimationOwner( child ); + + SetupDecalSets( BlueCastPtr( child->GetRawRoot() ), dna ); + SetupAttachments( BlueCastPtr( child->GetRawRoot() ), dna, { IdentityMatrix() }, buildFlags ); + placementContainer->AddToEffectChildrenList( child ); + } + else + { + if( !sharedMeshes ) + { + sharedMeshes.CreateInstance(); + sharedMeshes->SetName( "SharedInstancedMeshes" ); + sharedMeshes->SetOrigin( EveSpaceObjectChild::SOF ); + newObj->AddToEffectChildrenList( sharedMeshes ); + } + + TriBatchType types[] = { + TRIBATCHTYPE_OPAQUE, TRIBATCHTYPE_DECAL, TRIBATCHTYPE_TRANSPARENT, TRIBATCHTYPE_ADDITIVE, TRIBATCHTYPE_DISTORTION + }; + std::vector areas; + for( auto type : types ) + { + CTr2MeshAreaVector meshAreas; + // We are purposely ignoring multi-hull logic assuming shared instanced meshes are single hull only + FillMeshAreaVector( &meshAreas, type, dna, 0, 0 ); + for( auto area : meshAreas ) + { + auto effect = area->GetMaterialInterface(); + effect->SetOption( BlueSharedString( "SPACE_OBJECT_INSTANCED_ATTACHMENT" ), BlueSharedString( "SOIA_SHARED" ) ); + areas.push_back( EveChildInstancedMeshes::MeshArea{ effect, type == TRIBATCHTYPE_DECAL ? TRIBATCHTYPE_OPAQUE : type, uint32_t( area->GetIndex() ), uint32_t( area->GetCount() ) } ); + } + } + sharedMeshes->AddMesh( + dna->GetHullGeometryResPath().c_str(), + dna->CastShadow(), + dna->GetReflectionMode(), + 0, + areas.data(), + areas.size(), + placementOffsets.data(), + 1, + m_editorMode ? BlueSharedString( dna->GetHullNames()[0].c_str() ) : BlueSharedString(), + BlueSharedString(), + partTag ); + + SetupAttachments( BlueCastPtr( placementContainer ), dna, placementOffsets, buildFlags ); + } + + CcpMath::Sphere instanceSphere( dna->GetHullBoundingSphere() ); + instanceSphere.Transform( transform ); + + // update the bounding sphere of the parent + newObj->SetBoundingSphereInformation( instanceSphere ); + + // update the shield ellipsoid of the parent + { + CcpMath::AxisAlignedBox instanceBox( instanceSphere ); + if( dna->GetHullShapeEllipsoid() ) + { + instanceBox = CcpMath::AxisAlignedBox( dna->GetHullShapeEllipsoid() ); + instanceBox.Transform( transform ); + } + + // include the instance box in the ellipsoid + CcpMath::AxisAlignedEllipsoid updatedEllipsoid; + updatedEllipsoid.IncludeBox( instanceBox ); + newObj->SetShapeEllipsoid( updatedEllipsoid ); + dna->SetParentShapeEllipsoidInfo( updatedEllipsoid ); + } + + if( hasControllers ) + { + // Controllers! + SetupControllers( BlueCastPtr( placementContainer->GetRawRoot() ), dna, buildFlags ); + } + + // And last but not least! AUDIO! + SetupAudio( BlueCastPtr( placementContainer ), dna, transform ); + + + // Old style instanced meshes are not supported here + if( hasChildEffects ) + { + SetupEffects( newObj, (IEveEffectChildrenOwnerPtr)placementContainer, dna, placementOffsets, buildFlags ); + } + + if( !newObj->GetImpactOverlay() ) + { + SetupImpactEffects( newObj, dna ); + } + SetupLocatorSets( newObj, dna, placementOffsets, partTag ); + // setup nested layout + int layoutPartTag = static_cast( partTag ); + SetupLayout( newObj, placementContainer, sharedMeshes, dna, placementOffsets, layoutPartTag, false ); + return true; +} + void EveSOF::SetupAttachments( IEveSpaceObjectAttachmentOwnerPtr newObj, const EveSOFDNAPtr dna, const std::vector& offsets, uint32_t buildFlags ) const { + if( !newObj ) + { + return; + } // Add all the fluff! SetupSpriteSets( newObj, dna, offsets, buildFlags ); SetupSpotlightSets( newObj, dna, offsets, buildFlags ); @@ -2984,7 +3178,7 @@ void EveSOF::SetupLocators( EveSpaceObject2Ptr obj, const EveSOFDNAPtr dna ) con // Description: // add the hull locator sets to the new ship // -------------------------------------------------------------------------------- -void EveSOF::SetupLocatorSets( EveSpaceObject2Ptr obj, const EveSOFDNAPtr dna, const std::vector& offsets ) +void EveSOF::SetupLocatorSets( EveSpaceObject2Ptr obj, const EveSOFDNAPtr dna, const std::vector& offsets, EveSpaceObjectChild::PartTag partTag ) { CCP_STATS_ZONE( __FUNCTION__ ); @@ -3010,10 +3204,11 @@ void EveSOF::SetupLocatorSets( EveSpaceObject2Ptr obj, const EveSOFDNAPtr dna, c distributedLocators.reserve( offsets.size() * locators->size() ); for( auto& offset : offsets ) { - std::transform( ( *locators ).begin(), ( *locators ).end(), std::back_inserter( distributedLocators ), [offset, hullOffset]( EveSOFDataMgr::LocatorDirectionData d ) -> EveSOFDataMgr::LocatorDirectionData { + std::transform( ( *locators ).begin(), ( *locators ).end(), std::back_inserter( distributedLocators ), [offset, hullOffset, partTag]( EveSOFDataMgr::LocatorDirectionData d ) -> EveSOFDataMgr::LocatorDirectionData { Matrix m = TransformationMatrix( Vector3( 1.0, 1.0, 1.0 ), d.rotation, d.position + hullOffset ) * offset; Vector3 tmp; Decompose( tmp, d.rotation, d.position, m ); + d.partTag = partTag; return d; } ); } @@ -3082,7 +3277,7 @@ std::vector EveSOF::BuildHullLocalLocatorSets( const EveSOFDN return result; } -void EveSOF::SetupLayout( EveSpaceObject2Ptr obj, EveChildContainerPtr layoutContainer, EveChildInstancedMeshesPtr& sharedMeshes, const EveSOFDNAPtr dna, const std::vector& offsets, int& partTag, uint32_t seedOverwrite ) +void EveSOF::SetupLayout( EveSpaceObject2Ptr obj, EveChildContainerPtr layoutContainer, EveChildInstancedMeshesPtr& sharedMeshes, const EveSOFDNAPtr dna, const std::vector& offsets, int& partTag, bool perPlacementTags, uint32_t seedOverwrite ) { CCP_STATS_ZONE( __FUNCTION__ ); @@ -3136,7 +3331,7 @@ void EveSOF::SetupLayout( EveSpaceObject2Ptr obj, EveChildContainerPtr layoutCon // Go over all the placements (each layout can have multiple mesh attachments) for( auto placement : layout->placements ) { - ProcessPlacementDistributionOrGroup( placement, obj, sharedMeshes, dna, locatorSets, layoutIdx, placementIdx, offsets, layoutContainer, partTag ); + ProcessPlacementDistributionOrGroup( placement, obj, sharedMeshes, dna, locatorSets, layoutIdx, placementIdx, offsets, layoutContainer, partTag, perPlacementTags ); } if( layout->scrambleSeed ) @@ -3155,7 +3350,8 @@ void EveSOF::ProcessPlacementDistributionOrGroup( EveSOFDataMgr::ExtensionPlacem size_t& placementIdx, const std::vector& offsets, EveChildContainerPtr layoutContainer, - int& partTag ) + int& partTag, + bool perPlacementTags ) { if( placement.isAGroup ) { @@ -3174,7 +3370,7 @@ void EveSOF::ProcessPlacementDistributionOrGroup( EveSOFDataMgr::ExtensionPlacem // Go over all the placements (each layout can have multiple mesh attachments) for( auto& placement : placement.placements ) { - ProcessPlacementDistributionOrGroup( placement, obj, sharedMeshes, dna, managedLocatorSets, layoutIdx, placementIdx, offsets, layoutContainer, partTag ); + ProcessPlacementDistributionOrGroup( placement, obj, sharedMeshes, dna, managedLocatorSets, layoutIdx, placementIdx, offsets, layoutContainer, partTag, perPlacementTags ); } return; } @@ -3278,12 +3474,12 @@ void EveSOF::ProcessPlacementDistributionOrGroup( EveSOFDataMgr::ExtensionPlacem for( auto& locator : locators ) { singleLocator[0] = locator; - CreatePlacement( obj, sharedMeshes, placementDna, dna, placement, singleLocator, offsets, layoutContainer, partTag ); + CreatePlacement( obj, sharedMeshes, placementDna, dna, placement, singleLocator, offsets, layoutContainer, partTag, perPlacementTags ); } } else { - CreatePlacement( obj, sharedMeshes, placementDna, dna, placement, locators, offsets, layoutContainer, partTag ); + CreatePlacement( obj, sharedMeshes, placementDna, dna, placement, locators, offsets, layoutContainer, partTag, perPlacementTags ); } } @@ -3509,7 +3705,8 @@ void EveSOF::CreatePlacement( const std::vector& locators, const std::vector& nestedOffsets, EveChildContainerPtr layoutContainer, - int& partTag ) + int& partTag, + bool perPlacementTags ) { Matrix placementOffset = TranslationMatrix( placement.offset ); @@ -3610,7 +3807,7 @@ void EveSOF::CreatePlacement( child->SetName( "Hull" ); child->SetupWithStaticTransform( &randomScale, &rotation, &translation, Tr2Lod::TR2_LOD_LOW ); child->SetOwnedLocatorSets( childLocatorSets ); - child->SetPartTag( partTag++ ); + child->SetPartTag( perPlacementTags ? partTag++ : partTag ); if( m_editorMode ) { @@ -3700,7 +3897,7 @@ void EveSOF::CreatePlacement( for( auto type : types ) { CTr2MeshAreaVector meshAreas; - // We are purpusely ignoring multi-hull logic assuming shared instanced meshes are single hull only + // We are purposely ignoring multi-hull logic assuming shared instanced meshes are single hull only FillMeshAreaVector( &meshAreas, type, extensionDna, 0, 0 ); for( auto area : meshAreas ) { @@ -3719,7 +3916,8 @@ void EveSOF::CreatePlacement( placementOffsets.data(), placementOffsets.size(), m_editorMode ? BlueSharedString( parentDna->GetHullNames()[0].c_str() ) : BlueSharedString(), - m_editorMode ? placement.locatorSetName : BlueSharedString() ); + m_editorMode ? placement.locatorSetName : BlueSharedString(), + static_cast( perPlacementTags ? partTag++ : partTag ) ); } else { @@ -3806,7 +4004,7 @@ void EveSOF::CreatePlacement( SetupLocatorSets( parent, extensionDna, placementOffsets ); } // setup nested layout - SetupLayout( parent, layoutContainer, sharedMeshes, extensionDna, placementOffsets, partTag ); + SetupLayout( parent, layoutContainer, sharedMeshes, extensionDna, placementOffsets, partTag, perPlacementTags ); CCP_LOGNOTICE( "Creating %s extensions on %zu places", placement.isInstanced ? " instanced" : "", locators.size() ); } diff --git a/trinity/Eve/SpaceObjectFactory/EveSOF.h b/trinity/Eve/SpaceObjectFactory/EveSOF.h index a4d9f802e..9941fc708 100644 --- a/trinity/Eve/SpaceObjectFactory/EveSOF.h +++ b/trinity/Eve/SpaceObjectFactory/EveSOF.h @@ -32,8 +32,8 @@ BLUE_DECLARE( EveChildInstancedMeshes ); // SeeAlso: // EveBoosterSet2 // -------------------------------------------------------------------------------- -BLUE_CLASS( EveSOF ) : - public IRoot +BLUE_CLASS_IMPL( EveSOF ) +class EveSOF : public IRoot { public: EXPOSE_TO_BLUE(); @@ -45,13 +45,19 @@ BLUE_CLASS( EveSOF ) : IRootPtr Build( const char* hullName, const char* factionName, const char* raceName ); // build a spaceship from a dns string and return a EveShip2 object IRootPtr BuildFromDNA( const char* dnaString ); + /** + * @brief Builds a hull ("hull:faction:race" DNA) directly onto an existing space object as one part of a + * modular object, stamping partTag on every child, locator and mesh instance it creates. + * @return False if the DNA did not resolve to a buildable hull. + */ + bool BuildChild( EveSpaceObject2* owner, const char* dnaString, uint32_t partTag, const Matrix& transform ); // validate a dna string (slow!) bool ValidateDNA( const char* dnaString ); // change the material of a turret with SOF data - void SetupTurretMaterialFromDNA( EveTurretSet * turretSet, const char* dnaString ); - void SetupTurretMaterialFromFaction( EveTurretSet * turretSet, const char* factionName ); + void SetupTurretMaterialFromDNA( EveTurretSet* turretSet, const char* dnaString ); + void SetupTurretMaterialFromFaction( EveTurretSet* turretSet, const char* factionName ); bool LoadData( const char* filePath ); @@ -104,11 +110,11 @@ BLUE_CLASS( EveSOF ) : void SetupDecalSets( IEveSpaceObjectDecalOwnerPtr obj, const EveSOFDNAPtr dna ) const; void SetupModelCurves( EveSpaceObject2Ptr obj, const EveSOFDNAPtr dna ) const; void SetupLocators( EveSpaceObject2Ptr obj, const EveSOFDNAPtr dna ) const; - void SetupLocatorSets( EveSpaceObject2Ptr obj, const EveSOFDNAPtr dna, const std::vector& offsets ); std::vector BuildHullLocalLocatorSets( const EveSOFDNAPtr dna ) const; + void SetupLocatorSets( EveSpaceObject2Ptr obj, const EveSOFDNAPtr dna, const std::vector& offsets, EveSpaceObjectChild::PartTag partTag = EveSpaceObjectChild::NO_PART_TAG ); void SetupImpactEffects( EveSpaceObject2Ptr obj, const EveSOFDNAPtr dna ) const; void SetupLights( ITr2LightOwnerPtr obj, const EveSOFDNAPtr dna, const std::vector& offsets ) const; - void SetupLayout( EveSpaceObject2Ptr obj, EveChildContainerPtr layoutContainer, EveChildInstancedMeshesPtr & sharedMeshes, const EveSOFDNAPtr dna, const std::vector& offsets, int& partTag, uint32_t seedOverwrite = 0 ); + void SetupLayout( EveSpaceObject2Ptr obj, EveChildContainerPtr layoutContainer, EveChildInstancedMeshesPtr& sharedMeshes, const EveSOFDNAPtr dna, const std::vector& offsets, int& partTag, bool perPlacementTags, uint32_t seedOverwrite = 0 ); Tr2MeshPtr CreateMesh( const EveSOFDNAPtr dna ) const; @@ -117,14 +123,15 @@ BLUE_CLASS( EveSOF ) : void CreatePlacement( EveSpaceObject2Ptr parent, - EveChildInstancedMeshesPtr & sharedMeshes, + EveChildInstancedMeshesPtr& sharedMeshes, EveSOFDNAPtr extensionDna, const EveSOFDNAPtr& parentDna, EveSOFDataMgr::ExtensionPlacementData& placement, const std::vector& locators, const std::vector& nestedOffsets, EveChildContainerPtr layoutContainer, - int& partTag ); + int& partTag, + bool perPlacementTags ); void SetupCustomMask( EveSpaceObject2Ptr obj, const EveSOFDNAPtr dna ) const; @@ -133,14 +140,14 @@ BLUE_CLASS( EveSOF ) : Tr2EffectPtr CreateBoosterEffect( const EveSOFDataMgr::RaceBoosterData* rdata, const BlueSharedString& lodOption ) const; - bool ProcessLayoutDistributionConditions( EveSOFDataMgr::ExtensionPlacementData & placement, const EveSOFDNAPtr dna ); - void ProcessLayoutDistributionDistribute( EveSOFDataMgr::ExtensionPlacementDistribution & distributionData, const EveSOFDNAPtr dna, std::vector& placementSet, std::vector& managedLocatorSet ); - void ProcessPlacementDistributionOrGroup( EveSOFDataMgr::ExtensionPlacementData & distributionData, EveSpaceObject2Ptr obj, EveChildInstancedMeshesPtr & sharedMeshes, const EveSOFDNAPtr dna, std::map>& managedLocatorSet, size_t& layoutIdx, size_t& placementIdx, const std::vector& offsets, EveChildContainerPtr childContainer, int& partTag ); + bool ProcessLayoutDistributionConditions( EveSOFDataMgr::ExtensionPlacementData& placement, const EveSOFDNAPtr dna ); + void ProcessLayoutDistributionDistribute( EveSOFDataMgr::ExtensionPlacementDistribution& distributionData, const EveSOFDNAPtr dna, std::vector& placementSet, std::vector& managedLocatorSet ); + void ProcessPlacementDistributionOrGroup( EveSOFDataMgr::ExtensionPlacementData& distributionData, EveSpaceObject2Ptr obj, EveChildInstancedMeshesPtr& sharedMeshes, const EveSOFDNAPtr dna, std::map>& managedLocatorSet, size_t& layoutIdx, size_t& placementIdx, const std::vector& offsets, EveChildContainerPtr childContainer, int& partTag, bool perPlacementTags ); // helper functions - size_t FillMeshAreaVector( Tr2MeshAreaVector * meshAreaVector, TriBatchType areaType, const EveSOFDNAPtr dna, size_t hullIdx, size_t meshIndexOffset ) const; - bool GenerateLodResourcePaths( std::string & mediumResPath, std::string & lowResPath, std::string & ultraResPath, const char* resPath, const char* usage ) const; - void GenerateDepthFromAreaVector( Tr2MeshBase * mesh, const Tr2MeshAreaVector* meshAreaVector, const EveSOFDNAPtr dna ) const; + size_t FillMeshAreaVector( Tr2MeshAreaVector* meshAreaVector, TriBatchType areaType, const EveSOFDNAPtr dna, size_t hullIdx, size_t meshIndexOffset ) const; + bool GenerateLodResourcePaths( std::string& mediumResPath, std::string& lowResPath, std::string& ultraResPath, const char* resPath, const char* usage ) const; + void GenerateDepthFromAreaVector( Tr2MeshBase* mesh, const Tr2MeshAreaVector* meshAreaVector, const EveSOFDNAPtr dna ) const; void CreatePointLightData( const Vector3& pos, const float scale, const Color& color, const EveSOFDataMgr::PointLightAttachment* lightData ) const; void CreateTexturedPointLightData( const Vector3& pos, const float scale, const std::string& texturePath, const EveSOFDataMgr::PointLightAttachment* lightData ) const; diff --git a/trinity/Eve/SpaceObjectFactory/EveSOFDataMgr.h b/trinity/Eve/SpaceObjectFactory/EveSOFDataMgr.h index cadfd89e9..788ea8591 100644 --- a/trinity/Eve/SpaceObjectFactory/EveSOFDataMgr.h +++ b/trinity/Eve/SpaceObjectFactory/EveSOFDataMgr.h @@ -38,10 +38,11 @@ BLUE_CLASS( EveSOFDataMgr ) : Vector3 scaling; int32_t boneIndex; int32_t uniqueID; + EveSpaceObjectChild::PartTag partTag = EveSpaceObjectChild::NO_PART_TAG; ///< Part of a modular object this locator belongs to; NO_PART_TAG when not part-scoped. operator Locator() const { - return Locator{ position, rotation, scaling, boneIndex }; + return Locator{ position, rotation, scaling, boneIndex, partTag }; }; };