diff --git a/Gems/O3DEReflect/CMakeLists.txt b/Gems/O3DEReflect/CMakeLists.txt new file mode 100644 index 0000000000..9a1f55cffe --- /dev/null +++ b/Gems/O3DEReflect/CMakeLists.txt @@ -0,0 +1,18 @@ +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT + + +o3de_gem_setup("O3DEReflect") + +set(gem_path ${CMAKE_CURRENT_LIST_DIR}) +set(gem_json ${gem_path}/gem.json) +o3de_restricted_path(${gem_json} gem_restricted_path gem_parent_relative_path) + +o3de_pal_dir(pal_dir ${CMAKE_CURRENT_LIST_DIR}/Platform/${PAL_PLATFORM_NAME} "${gem_restricted_path}" "${gem_path}" "${gem_parent_relative_path}") + +ly_add_external_target_path(${CMAKE_CURRENT_LIST_DIR}/3rdParty) + +add_subdirectory(Code) + diff --git a/Gems/O3DEReflect/Code/CMakeLists.txt b/Gems/O3DEReflect/Code/CMakeLists.txt new file mode 100644 index 0000000000..180ddb0f21 --- /dev/null +++ b/Gems/O3DEReflect/Code/CMakeLists.txt @@ -0,0 +1,55 @@ +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT + +# O3DEReflect gem - no platform-specific code needed + +ly_add_target( + NAME O3DEReflect.Static STATIC + NAMESPACE Gem + FILES_CMAKE + o3dereflect_files.cmake + INCLUDE_DIRECTORIES + PUBLIC + Include + Source + BUILD_DEPENDENCIES + PUBLIC + AZ::AzCore + AZ::AzFramework +) + +ly_add_target( + NAME O3DEReflect ${PAL_TRAIT_MONOLITHIC_DRIVEN_MODULE_TYPE} + NAMESPACE Gem + FILES_CMAKE + o3dereflect_shared_files.cmake + INCLUDE_DIRECTORIES + PUBLIC + Include + PRIVATE + Source + BUILD_DEPENDENCIES + PRIVATE + Gem::O3DEReflect.Static +) + +# Inject the gem name into the Module source file +ly_add_source_properties( + SOURCES + Source/O3DEReflectModule.cpp + PROPERTY COMPILE_DEFINITIONS + VALUES + O3DE_GEM_NAME=O3DEReflect + O3DE_GEM_VERSION=1.0.0 +) + +# Load the "Gem::O3DEReflect" module in all types of applications +ly_create_alias(NAME O3DEReflect.Servers NAMESPACE Gem TARGETS Gem::O3DEReflect) +ly_create_alias(NAME O3DEReflect.Unified NAMESPACE Gem TARGETS Gem::O3DEReflect) +ly_create_alias(NAME O3DEReflect.Clients NAMESPACE Gem TARGETS Gem::O3DEReflect) +ly_create_alias(NAME O3DEReflect.Tools NAMESPACE Gem TARGETS Gem::O3DEReflect) +ly_create_alias(NAME O3DEReflect.Builders NAMESPACE Gem TARGETS Gem::O3DEReflect) + + diff --git a/Gems/O3DEReflect/Code/Examples/AIState.O3DEReflect.xml b/Gems/O3DEReflect/Code/Examples/AIState.O3DEReflect.xml new file mode 100644 index 0000000000..3d572e05f6 --- /dev/null +++ b/Gems/O3DEReflect/Code/Examples/AIState.O3DEReflect.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Gems/O3DEReflect/Code/Examples/BeforeAfterComparison.h b/Gems/O3DEReflect/Code/Examples/BeforeAfterComparison.h new file mode 100644 index 0000000000..6495ac0d6a --- /dev/null +++ b/Gems/O3DEReflect/Code/Examples/BeforeAfterComparison.h @@ -0,0 +1,80 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace TraditionalExample +{ + // HEADER FILE - PlayerMovementComponent.h + // ======================================== + + class PlayerMovementComponent + : public AZ::Component + { + public: + AZ_COMPONENT(PlayerMovementComponent, "{A1B2C3D4-E5F6-7890-ABCD-123456789ABC}"); + + static void Reflect(AZ::ReflectContext* context); + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent); + + PlayerMovementComponent() = default; + ~PlayerMovementComponent() override = default; + + // Getters and Setters - MUST WRITE MANUALLY + float GetMoveSpeed() const { return m_moveSpeed; } + void SetMoveSpeed(float value) { m_moveSpeed = value; } + + float GetSprintMultiplier() const { return m_sprintMultiplier; } + void SetSprintMultiplier(float value) { m_sprintMultiplier = value; } + + float GetJumpForce() const { return m_jumpForce; } + void SetJumpForce(float value) { m_jumpForce = value; } + + int GetMaxJumps() const { return m_maxJumps; } + void SetMaxJumps(int value) { m_maxJumps = value; } + + bool GetIsGrounded() const { return m_isGrounded; } + const AZ::Vector3& GetCurrentVelocity() const { return m_currentVelocity; } + + void Jump(); + void Move(const AZ::Vector3& direction, float deltaTime); + void SetSprinting(bool sprinting); + float GetCurrentSpeed() const; + void Teleport(const AZ::Vector3& position); + + protected: + void Init() override; + void Activate() override; + void Deactivate() override; + + private: + // MUST MANUALLY DECLARE ALL MEMBERS + float m_moveSpeed = 5.0f; + float m_sprintMultiplier = 2.0f; + float m_jumpForce = 400.0f; + int m_maxJumps = 2; + float m_gravityMultiplier = 1.0f; + bool m_isGrounded = false; + AZ::Vector3 m_currentVelocity = AZ::Vector3::CreateZero(); + }; + +} // namespace TraditionalExample + + diff --git a/Gems/O3DEReflect/Code/Examples/DamageFlags.O3DEReflect.xml b/Gems/O3DEReflect/Code/Examples/DamageFlags.O3DEReflect.xml new file mode 100644 index 0000000000..70a9b161f7 --- /dev/null +++ b/Gems/O3DEReflect/Code/Examples/DamageFlags.O3DEReflect.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Gems/O3DEReflect/Code/Examples/DamageInfo.O3DEReflect.xml b/Gems/O3DEReflect/Code/Examples/DamageInfo.O3DEReflect.xml new file mode 100644 index 0000000000..5dbe3932d0 --- /dev/null +++ b/Gems/O3DEReflect/Code/Examples/DamageInfo.O3DEReflect.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Gems/O3DEReflect/Code/Examples/PlayerMovementComponent.O3DEReflect.xml b/Gems/O3DEReflect/Code/Examples/PlayerMovementComponent.O3DEReflect.xml new file mode 100644 index 0000000000..fbdd2b7cc6 --- /dev/null +++ b/Gems/O3DEReflect/Code/Examples/PlayerMovementComponent.O3DEReflect.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Gems/O3DEReflect/Code/Examples/V2/AIStateComponent.h b/Gems/O3DEReflect/Code/Examples/V2/AIStateComponent.h new file mode 100644 index 0000000000..137dc67f1d --- /dev/null +++ b/Gems/O3DEReflect/Code/Examples/V2/AIStateComponent.h @@ -0,0 +1,325 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + * V2 Example: AI State Component using header-macro parsing + * + * Demonstrates a more complex component with enums and state management. + */ + +#pragma once + +#include +#include +#include +#include +#include + +// ============================================================================ +// AI State Enum +// ============================================================================ + +O3DE_ENUM(ScriptType, + Category = "AI", + Description = "Possible states for AI behavior" +) +enum class AIState : uint8_t +{ + O3DE_ENUM_VALUE(Idle, DisplayName = "Idle", Description = "AI is standing still, not engaged") + Idle = 0, + + O3DE_ENUM_VALUE(Patrol, DisplayName = "Patrolling", Description = "AI is following a patrol route") + Patrol, + + O3DE_ENUM_VALUE(Alert, DisplayName = "Alert", Description = "AI has detected something suspicious") + Alert, + + O3DE_ENUM_VALUE(Chase, DisplayName = "Chasing", Description = "AI is pursuing a target") + Chase, + + O3DE_ENUM_VALUE(Combat, DisplayName = "In Combat", Description = "AI is actively fighting") + Combat, + + O3DE_ENUM_VALUE(Flee, DisplayName = "Fleeing", Description = "AI is running away") + Flee, + + O3DE_ENUM_VALUE(Dead, DisplayName = "Dead", Description = "AI is dead/disabled") + Dead +}; + +// ============================================================================ +// AI Behavior Flags +// ============================================================================ + +O3DE_ENUM(ScriptType, Flags, + Category = "AI", + Description = "Behavior modifiers for AI" +) +enum class AIBehaviorFlags : uint32_t +{ + O3DE_ENUM_VALUE(None, DisplayName = "None") + None = 0, + + O3DE_ENUM_VALUE(CanPatrol, DisplayName = "Can Patrol") + CanPatrol = 1 << 0, + + O3DE_ENUM_VALUE(CanChase, DisplayName = "Can Chase") + CanChase = 1 << 1, + + O3DE_ENUM_VALUE(CanFlee, DisplayName = "Can Flee") + CanFlee = 1 << 2, + + O3DE_ENUM_VALUE(CanCallForHelp, DisplayName = "Can Call For Help") + CanCallForHelp = 1 << 3, + + O3DE_ENUM_VALUE(Aggressive, DisplayName = "Aggressive (attacks on sight)") + Aggressive = 1 << 4, + + O3DE_ENUM_VALUE(Cowardly, DisplayName = "Cowardly (flees when hurt)") + Cowardly = 1 << 5 +}; + +// ============================================================================ +// Patrol Point Struct +// ============================================================================ + +O3DE_STRUCT(ScriptType, + Category = "AI", + Description = "A waypoint in a patrol route" +) +struct PatrolPoint +{ + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Patrol", + Tooltip = "World position of this patrol point" + ) + AZ::Vector3 position = AZ::Vector3::CreateZero(); + + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Patrol", + Tooltip = "How long to wait at this point (seconds)", + Min = 0.0f, Max = 60.0f + ) + float waitTime = 2.0f; + + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Patrol", + Tooltip = "Should the AI look around at this point?" + ) + bool lookAround = false; +}; + +// ============================================================================ +// AI State Component +// ============================================================================ + +O3DE_CLASS( + Category = "AI/Core", + Description = "Manages AI state machine and behavior", + Icon = "Icons/Components/AI.svg" +) +class AIStateComponent : public AZ::Component +{ + O3DE_GENERATED_BODY() + + O3DE_COMPONENT(AIStateComponent, "{B2C3D4E5-F6A7-8901-BCDE-F23456789ABC}", + ProvidesServices = "AIStateService", + RequiresServices = "TransformService", + IncompatibleServices = "PlayerControllerService" + ) + +public: + // ======================================================================== + // State Properties + // ======================================================================== + + O3DE_PROPERTY(VisibleAnywhere, ScriptReadOnly, + Category = "State", + Tooltip = "Current AI state" + ) + AIState m_currentState = AIState::Idle; + + O3DE_PROPERTY(VisibleAnywhere, ScriptReadOnly, + Category = "State", + Tooltip = "Previous AI state (for transition logic)" + ) + AIState m_previousState = AIState::Idle; + + O3DE_PROPERTY(VisibleAnywhere, ScriptReadOnly, + Category = "State", + Tooltip = "Time spent in current state (seconds)" + ) + float m_timeInState = 0.0f; + + O3DE_PROPERTY(VisibleAnywhere, ScriptReadOnly, + Category = "State", + Tooltip = "Current target entity (if any)" + ) + AZ::EntityId m_currentTarget; + + // ======================================================================== + // Configuration Properties + // ======================================================================== + + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Detection", + Tooltip = "How far the AI can see", + Min = 1.0f, Max = 100.0f, Suffix = "m" + ) + float m_sightRange = 20.0f; + + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Detection", + Tooltip = "Field of view angle (degrees)", + Min = 30.0f, Max = 360.0f, Suffix = "°" + ) + float m_fieldOfView = 120.0f; + + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Detection", + Tooltip = "How far the AI can hear", + Min = 1.0f, Max = 50.0f, Suffix = "m" + ) + float m_hearingRange = 15.0f; + + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Behavior", + Tooltip = "Behavior flags controlling AI capabilities" + ) + AIBehaviorFlags m_behaviorFlags = AIBehaviorFlags::CanPatrol; + + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Combat", + Tooltip = "Preferred combat distance", + Min = 1.0f, Max = 30.0f, Suffix = "m" + ) + float m_preferredCombatRange = 5.0f; + + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Combat", + Tooltip = "Health threshold to trigger flee (0-1)", + Min = 0.0f, Max = 1.0f + ) + float m_fleeHealthThreshold = 0.2f; + + // ======================================================================== + // Patrol Properties + // ======================================================================== + + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Patrol", + Tooltip = "Patrol waypoints" + ) + AZStd::vector m_patrolPoints; + + O3DE_PROPERTY(VisibleAnywhere, ScriptReadOnly, + Category = "Patrol", + Tooltip = "Current patrol point index" + ) + int m_currentPatrolIndex = 0; + + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Patrol", + Tooltip = "Should patrol loop back to start?" + ) + bool m_loopPatrol = true; + + // ======================================================================== + // State Control Functions + // ======================================================================== + + O3DE_FUNCTION(ScriptCallable, + Category = "State", + Tooltip = "Force transition to a new state" + ) + void SetState(AIState newState); + + O3DE_FUNCTION(ScriptPure, ScriptCallable, + Category = "State", + Tooltip = "Check if AI is in a specific state" + ) + bool IsInState(AIState state) const; + + O3DE_FUNCTION(ScriptCallable, + Category = "State", + Tooltip = "Set the current target entity" + ) + void SetTarget(AZ::EntityId target); + + O3DE_FUNCTION(ScriptCallable, + Category = "State", + Tooltip = "Clear the current target" + ) + void ClearTarget(); + + // ======================================================================== + // Detection Functions + // ======================================================================== + + O3DE_FUNCTION(ScriptPure, ScriptCallable, + Category = "Detection", + Tooltip = "Check if AI can see a specific entity" + ) + bool CanSeeEntity(AZ::EntityId entity) const; + + O3DE_FUNCTION(ScriptPure, ScriptCallable, + Category = "Detection", + Tooltip = "Check if AI can hear a position" + ) + bool CanHearPosition(const AZ::Vector3& position, float noiseLevel) const; + + O3DE_FUNCTION(ScriptCallable, + Category = "Detection", + Tooltip = "Scan for nearby threats, returns closest threat" + ) + AZ::EntityId ScanForThreats(); + + // ======================================================================== + // Patrol Functions + // ======================================================================== + + O3DE_FUNCTION(ScriptCallable, + Category = "Patrol", + Tooltip = "Add a patrol point at runtime" + ) + void AddPatrolPoint(const PatrolPoint& point); + + O3DE_FUNCTION(ScriptCallable, + Category = "Patrol", + Tooltip = "Clear all patrol points" + ) + void ClearPatrolPoints(); + + O3DE_FUNCTION(ScriptPure, ScriptCallable, + Category = "Patrol", + Tooltip = "Get the next patrol point position" + ) + AZ::Vector3 GetNextPatrolPosition() const; + + // ======================================================================== + // Debug Functions + // ======================================================================== + + O3DE_FUNCTION(CallInEditor, + Category = "Debug", + Tooltip = "Draw detection ranges in editor" + ) + void DrawDebugVisualization(); + + O3DE_FUNCTION(CallInEditor, + Category = "Debug", + Tooltip = "Reset AI to initial state" + ) + void ResetAI(); + +protected: + void Activate() override; + void Deactivate() override; + +private: + void UpdateStateMachine(float deltaTime); + void OnStateEnter(AIState state); + void OnStateExit(AIState state); +}; diff --git a/Gems/O3DEReflect/Code/Examples/V2/DamageInfo.h b/Gems/O3DEReflect/Code/Examples/V2/DamageInfo.h new file mode 100644 index 0000000000..a62ba458f1 --- /dev/null +++ b/Gems/O3DEReflect/Code/Examples/V2/DamageInfo.h @@ -0,0 +1,198 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + * V2 Example: DamageInfo struct using header-macro parsing + * + * Demonstrates O3DE_STRUCT for data-only structures. + */ + +#pragma once + +#include +#include +#include +#include + +// Forward declare the enum +enum class DamageType : uint8_t; + +// ============================================================================ +// DamageInfo Struct - Data container for damage events +// ============================================================================ + +O3DE_STRUCT(ScriptType, + Category = "Combat", + Description = "Contains all information about a damage event" +) +struct DamageInfo +{ + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Damage", + Tooltip = "Base damage amount before modifiers", + Min = 0.0f + ) + float baseDamage = 0.0f; + + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Damage", + Tooltip = "Damage multiplier (crits, weaknesses, etc.)", + Min = 0.0f, Max = 10.0f + ) + float damageMultiplier = 1.0f; + + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Source", + Tooltip = "Entity that caused the damage" + ) + AZ::EntityId instigator; + + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Source", + Tooltip = "Entity that directly dealt the damage (e.g., projectile)" + ) + AZ::EntityId damageCauser; + + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Location", + Tooltip = "World position where damage was applied" + ) + AZ::Vector3 hitLocation = AZ::Vector3::CreateZero(); + + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Location", + Tooltip = "Direction the damage came from" + ) + AZ::Vector3 hitDirection = AZ::Vector3::CreateZero(); + + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Location", + Tooltip = "Name of the bone/hitbox that was hit" + ) + AZStd::string hitBoneName; + + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Damage", + Tooltip = "Type of damage (for resistance calculations)" + ) + DamageType damageType; + + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Flags", + Tooltip = "Was this a critical hit?" + ) + bool isCriticalHit = false; + + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Flags", + Tooltip = "Should this damage be displayed in UI?" + ) + bool showDamageNumbers = true; + + // Helper methods (exposed to scripting) + O3DE_FUNCTION(ScriptPure, ScriptCallable, + Category = "Damage", + Tooltip = "Calculate final damage after multipliers" + ) + float GetFinalDamage() const + { + return baseDamage * damageMultiplier; + } + + O3DE_FUNCTION(ScriptPure, ScriptCallable, + Category = "Damage", + Tooltip = "Check if damage is lethal (would reduce health to 0)" + ) + bool IsLethal(float currentHealth) const + { + return GetFinalDamage() >= currentHealth; + } +}; + +// ============================================================================ +// DamageType Enum +// ============================================================================ + +O3DE_ENUM(ScriptType, + Category = "Combat", + Description = "Types of damage for resistance calculations" +) +enum class DamageType : uint8_t +{ + O3DE_ENUM_VALUE(Physical, DisplayName = "Physical Damage") + Physical = 0, + + O3DE_ENUM_VALUE(Fire, DisplayName = "Fire Damage") + Fire, + + O3DE_ENUM_VALUE(Ice, DisplayName = "Ice/Cold Damage") + Ice, + + O3DE_ENUM_VALUE(Electric, DisplayName = "Electric/Lightning Damage") + Electric, + + O3DE_ENUM_VALUE(Poison, DisplayName = "Poison/Toxic Damage") + Poison, + + O3DE_ENUM_VALUE(Magic, DisplayName = "Magic/Arcane Damage") + Magic, + + O3DE_ENUM_VALUE(True, DisplayName = "True Damage (ignores resistances)") + True +}; + +// ============================================================================ +// DamageFlags Enum (bitmask) +// ============================================================================ + +O3DE_ENUM(ScriptType, Flags, + Category = "Combat", + Description = "Flags that modify damage behavior" +) +enum class DamageFlags : uint32_t +{ + O3DE_ENUM_VALUE(None, DisplayName = "No Flags") + None = 0, + + O3DE_ENUM_VALUE(IgnoreArmor, DisplayName = "Ignore Armor") + IgnoreArmor = 1 << 0, + + O3DE_ENUM_VALUE(IgnoreShield, DisplayName = "Ignore Shield") + IgnoreShield = 1 << 1, + + O3DE_ENUM_VALUE(CanCrit, DisplayName = "Can Critical Hit") + CanCrit = 1 << 2, + + O3DE_ENUM_VALUE(CanBeBlocked, DisplayName = "Can Be Blocked") + CanBeBlocked = 1 << 3, + + O3DE_ENUM_VALUE(AppliesKnockback, DisplayName = "Applies Knockback") + AppliesKnockback = 1 << 4, + + O3DE_ENUM_VALUE(AppliesStagger, DisplayName = "Applies Stagger") + AppliesStagger = 1 << 5, + + O3DE_ENUM_VALUE(OverTime, DisplayName = "Damage Over Time") + OverTime = 1 << 6, + + O3DE_ENUM_VALUE(AreaOfEffect, DisplayName = "Area of Effect") + AreaOfEffect = 1 << 7 +}; + +// Bitwise operators for DamageFlags +inline DamageFlags operator|(DamageFlags a, DamageFlags b) +{ + return static_cast(static_cast(a) | static_cast(b)); +} + +inline DamageFlags operator&(DamageFlags a, DamageFlags b) +{ + return static_cast(static_cast(a) & static_cast(b)); +} + +inline bool HasFlag(DamageFlags flags, DamageFlags flag) +{ + return (flags & flag) == flag; +} diff --git a/Gems/O3DEReflect/Code/Examples/V2/PlayerMovementComponent.h b/Gems/O3DEReflect/Code/Examples/V2/PlayerMovementComponent.h new file mode 100644 index 0000000000..c73860921e --- /dev/null +++ b/Gems/O3DEReflect/Code/Examples/V2/PlayerMovementComponent.h @@ -0,0 +1,172 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + * V2 Example: PlayerMovementComponent using header-macro parsing + * + * This file demonstrates the simplified reflection syntax. + * The O3DEReflect generator will parse these macros and generate: + * - PlayerMovementComponent.generated.h (included via O3DE_GENERATED_BODY) + * - PlayerMovementComponent.generated.cpp (reflection implementation) + * + * Compare this to the equivalent XML definition in: + * ../PlayerMovementComponent.O3DEReflect.xml + */ + +#pragma once + +#include +#include +#include +#include + + +// ============================================================================ + +O3DE_CLASS( + Category = "Gameplay/Movement", + Description = "Handles player movement with physics-based locomotion" +) +class PlayerMovementComponent : public AZ::Component +{ + // This macro includes the generated declarations and Reflect() signature + O3DE_GENERATED_BODY() + + // Explicit UUID (optional - will auto-generate deterministic UUID if omitted) + O3DE_COMPONENT(PlayerMovementComponent, "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}", + ProvidesServices = "PlayerMovementService", + RequiresServices = "TransformService", + IncompatibleServices = "AIMovementService" + ) + +public: + // ======================================================================== + // Movement Properties + // ======================================================================== + + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Movement|Speed", + Tooltip = "Maximum movement speed in meters per second", + DisplayName = "Max Speed", + Min = 0.0f, Max = 100.0f, UIMax = 50.0f, + Suffix = "m/s" + ) + float m_maxSpeed = 10.0f; + + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Movement|Speed", + Tooltip = "Acceleration rate", + Min = 0.0f, Max = 500.0f + ) + float m_acceleration = 50.0f; + + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Movement|Speed", + Tooltip = "Deceleration rate when not moving", + Min = 0.0f, Max = 500.0f + ) + float m_deceleration = 40.0f; + + // ======================================================================== + // Jump Properties + // ======================================================================== + + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Movement|Jump", + Tooltip = "Initial velocity when jumping", + Min = 0.0f, Max = 50.0f + ) + float m_jumpVelocity = 8.0f; + + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Movement|Jump", + Tooltip = "Number of jumps allowed before landing", + Min = 1, Max = 5 + ) + int m_maxJumps = 2; + + O3DE_PROPERTY(EditAnywhere, ScriptReadOnly, + Category = "Movement|Jump", + Tooltip = "Custom gravity multiplier" + ) + float m_gravityMultiplier = 1.0f; + + // ======================================================================== + // State Properties (runtime, visible but not editable) + // ======================================================================== + + O3DE_PROPERTY(VisibleAnywhere, ScriptReadOnly, + Category = "State", + Tooltip = "Current velocity vector" + ) + AZ::Vector3 m_currentVelocity = AZ::Vector3::CreateZero(); + + O3DE_PROPERTY(VisibleAnywhere, ScriptReadOnly, + Category = "State", + Tooltip = "Is the player currently on the ground?" + ) + bool m_isGrounded = false; + + O3DE_PROPERTY(VisibleAnywhere, ScriptReadOnly, + Category = "State", + Tooltip = "Remaining jumps available" + ) + int m_jumpsRemaining = 2; + + // ======================================================================== + // Exposed Functions + // ======================================================================== + + O3DE_FUNCTION(ScriptCallable, + Category = "Movement", + Tooltip = "Apply movement input direction", + DisplayName = "Move" + ) + void Move(const AZ::Vector3& direction); + + O3DE_FUNCTION(ScriptCallable, + Category = "Movement", + Tooltip = "Attempt to jump, returns true if successful" + ) + bool Jump(); + + O3DE_FUNCTION(ScriptCallable, + Category = "Movement", + Tooltip = "Stop all movement immediately" + ) + void StopMovement(); + + O3DE_FUNCTION(ScriptPure, ScriptCallable, + Category = "State", + Tooltip = "Get current speed (magnitude of velocity)" + ) + float GetCurrentSpeed() const; + + O3DE_FUNCTION(ScriptPure, ScriptCallable, + Category = "State", + Tooltip = "Check if player is moving" + ) + bool IsMoving() const; + + O3DE_FUNCTION(CallInEditor, + Category = "Debug", + Tooltip = "Reset player to spawn position" + ) + void ResetToSpawn(); + +protected: + // Component interface (these are manually implemented) + void Activate() override; + void Deactivate() override; + +private: + // Private implementation details (not reflected) + void UpdateMovement(float deltaTime); + void ApplyGravity(float deltaTime); + AZ::Vector3 m_pendingInput = AZ::Vector3::CreateZero(); +}; + +// In PlayerMovementComponent.cpp, you would add: +// O3DE_IMPLEMENT_REFLECT(PlayerMovementComponent) diff --git a/Gems/O3DEReflect/Code/Include/O3DEReflect/AutoGen/O3DEReflect.xsd b/Gems/O3DEReflect/Code/Include/O3DEReflect/AutoGen/O3DEReflect.xsd new file mode 100644 index 0000000000..04f1f86cca --- /dev/null +++ b/Gems/O3DEReflect/Code/Include/O3DEReflect/AutoGen/O3DEReflect.xsd @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Gems/O3DEReflect/Code/Include/O3DEReflect/AutoGen/O3DEReflectGen.py b/Gems/O3DEReflect/Code/Include/O3DEReflect/AutoGen/O3DEReflectGen.py new file mode 100644 index 0000000000..a6c06da8b5 --- /dev/null +++ b/Gems/O3DEReflect/Code/Include/O3DEReflect/AutoGen/O3DEReflectGen.py @@ -0,0 +1,1203 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT + +""" +O3DEReflect Code Generator + +This module extends the AzAutoGen system to provide simplified reflection +code generation for O3DE components, structs, and enums. + +Usage: + python O3DEReflectGen.py --input MyComponent.O3DEReflect.xml --output-dir ./Generated +""" + +import os +import re +import sys +import json +import argparse +import hashlib +import logging +import glob +from pathlib import Path +from xml.etree import ElementTree as ET + +# Add cmake directory to path for accessing AzAutoGen utilities +script_dir = os.path.dirname(os.path.abspath(__file__)) +cmake_dir = os.path.abspath(os.path.join(script_dir, '..', '..', '..', '..', '..', 'cmake')) +if cmake_dir not in sys.path: + sys.path.insert(0, cmake_dir) + +# Try to find O3DE's Python venv site-packages for Jinja2 +def find_o3de_site_packages(): + """Find O3DE's Python venv site-packages directory.""" + # Common locations for O3DE Python venv + home = os.path.expanduser("~") + patterns = [ + os.path.join(home, ".o3de", "Python", "venv", "*", "lib", "site-packages"), + os.path.join(home, ".o3de", "Python", "venv", "*", "Lib", "site-packages"), # Windows + ] + for pattern in patterns: + matches = glob.glob(pattern) + if matches: + return matches[0] + return None + +# Add O3DE site-packages to path if needed +o3de_site_packages = find_o3de_site_packages() +if o3de_site_packages and o3de_site_packages not in sys.path: + sys.path.insert(0, o3de_site_packages) + +try: + from jinja2 import Environment, FileSystemLoader, select_autoescape + JINJA2_AVAILABLE = True +except ImportError: + JINJA2_AVAILABLE = False + print("Warning: Jinja2 not available. Template rendering will fail.", file=sys.stderr) + if o3de_site_packages: + print(f" Tried O3DE site-packages at: {o3de_site_packages}", file=sys.stderr) + +logging.basicConfig(format='[%(levelname)s] %(name)s: %(message)s') +logger = logging.getLogger('O3DEReflectGen') +logger.setLevel(logging.INFO) + + +def create_hash_guid(string: str) -> str: + """ + Generate a deterministic UUID from a string using MD5 hash. + Matches the CreateHashGuid function in AzAutoGen.py. + """ + hash_obj = hashlib.md5(string.encode('utf-8')) + hash_str = hash_obj.hexdigest() + return "{{{}-{}-{}-{}-{}}}".format( + hash_str[0:8].upper(), + hash_str[8:12].upper(), + hash_str[12:16].upper(), + hash_str[16:20].upper(), + hash_str[20:].upper() + ) + + +def boolean_true(value) -> bool: + """Check if a string value represents true.""" + if isinstance(value, bool): + return value + if isinstance(value, str): + return value.lower().strip() in ('true', '1', 'yes') + return bool(value) + + +def camel_to_human(string: str) -> str: + """Convert camelCase to Human Readable format.""" + if not string: + return string + result = string[0].upper() + for char in string[1:]: + if char.isupper(): + result += ' ' + result += char + return result + + +def sanitize_path(path: str) -> str: + """Normalize path separators.""" + return path.replace('\\', '/') + + +class O3DEReflectParser: + """Parser for O3DEReflect XML files.""" + + def __init__(self, input_file: str): + self.input_file = input_file + self.tree = None + self.root = None + + def parse(self) -> dict: + """Parse the XML file and return a structured dictionary.""" + self.tree = ET.parse(self.input_file) + self.root = self.tree.getroot() + + result = { + 'components': [], + 'structs': [], + 'enums': [], + 'source_file': os.path.basename(self.input_file) + } + + # Handle single element root (Component, Struct, or Enum) + if self.root.tag in ('Component', 'Struct', 'Enum'): + elements = [self.root] + else: + # Handle O3DEReflect wrapper with multiple elements + elements = list(self.root) + + for element in elements: + if element.tag == 'Component': + result['components'].append(self._parse_component(element)) + elif element.tag == 'Struct': + result['structs'].append(self._parse_struct(element)) + elif element.tag == 'Enum': + result['enums'].append(self._parse_enum(element)) + + return result + + def _parse_component(self, elem: ET.Element) -> dict: + """Parse a Component element.""" + name = elem.attrib.get('Name', '') + namespace = elem.attrib.get('Namespace', '') + full_name = f"{namespace}::{name}" if namespace else name + + return { + 'name': name, + 'namespace': namespace, + 'full_name': full_name, + 'uuid': elem.attrib.get('Uuid') or create_hash_guid(full_name), + 'category': elem.attrib.get('Category', 'General'), + 'description': elem.attrib.get('Description', f'{name} component'), + 'display_name': elem.attrib.get('DisplayName', camel_to_human(name)), + 'icon': elem.attrib.get('Icon', ''), + 'hide_in_editor': boolean_true(elem.attrib.get('HideInEditor', 'false')), + 'abstract': boolean_true(elem.attrib.get('Abstract', 'false')), + 'menu_category': elem.attrib.get('AppearsInAddComponentMenu', 'Game'), + 'includes': self._parse_includes(elem), + 'base_classes': self._parse_base_classes(elem), + 'services': self._parse_services(elem), + 'properties': self._parse_properties(elem), + 'functions': self._parse_functions(elem), + } + + def _parse_struct(self, elem: ET.Element) -> dict: + """Parse a Struct element.""" + name = elem.attrib.get('Name', '') + namespace = elem.attrib.get('Namespace', '') + full_name = f"{namespace}::{name}" if namespace else name + + return { + 'name': name, + 'namespace': namespace, + 'full_name': full_name, + 'uuid': elem.attrib.get('Uuid') or create_hash_guid(full_name), + 'category': elem.attrib.get('Category', 'Data'), + 'description': elem.attrib.get('Description', f'{name} data structure'), + 'display_name': elem.attrib.get('DisplayName', camel_to_human(name)), + 'script_type': boolean_true(elem.attrib.get('ScriptType', 'true')), + 'atomic': boolean_true(elem.attrib.get('Atomic', 'false')), + 'includes': self._parse_includes(elem), + 'properties': self._parse_properties(elem), + } + + def _parse_enum(self, elem: ET.Element) -> dict: + """Parse an Enum element.""" + name = elem.attrib.get('Name', '') + namespace = elem.attrib.get('Namespace', '') + full_name = f"{namespace}::{name}" if namespace else name + + return { + 'name': name, + 'namespace': namespace, + 'full_name': full_name, + 'uuid': elem.attrib.get('Uuid') or create_hash_guid(full_name), + 'category': elem.attrib.get('Category', 'Enums'), + 'description': elem.attrib.get('Description', f'{name} enumeration'), + 'display_name': elem.attrib.get('DisplayName', camel_to_human(name)), + 'script_type': boolean_true(elem.attrib.get('ScriptType', 'true')), + 'is_flags': boolean_true(elem.attrib.get('Flags', 'false')), + 'underlying_type': elem.attrib.get('UnderlyingType', 'int32_t'), + 'values': self._parse_enum_values(elem), + } + + def _parse_includes(self, elem: ET.Element) -> list: + """Parse Include elements.""" + return [inc.attrib.get('File', '') for inc in elem.findall('Include')] + + def _parse_base_classes(self, elem: ET.Element) -> list: + """Parse BaseClass elements.""" + result = [] + for base in elem.findall('BaseClass'): + result.append({ + 'name': base.attrib.get('Name', ''), + 'namespace': base.attrib.get('Namespace', ''), + 'include': base.attrib.get('Include', ''), + }) + return result + + def _parse_services(self, elem: ET.Element) -> dict: + """Parse Service elements grouped by type.""" + result = { + 'provides': [], + 'requires': [], + 'incompatible': [], + 'dependent': [], + } + for service in elem.findall('Service'): + service_type = service.attrib.get('Type', '').lower() + service_name = service.attrib.get('Name', '') + if service_type in result: + result[service_type].append(service_name) + return result + + def _parse_properties(self, elem: ET.Element) -> list: + """Parse Property elements.""" + result = [] + for prop in elem.findall('Property'): + prop_name = prop.attrib.get('Name', '') + result.append({ + 'name': prop_name, + 'member_name': prop_name if prop_name.startswith('m_') else f'm_{prop_name}', + 'type': prop.attrib.get('Type', 'float'), + 'default': prop.attrib.get('Default', ''), + 'display_name': prop.attrib.get('DisplayName', camel_to_human(prop_name.lstrip('m_'))), + 'category': prop.attrib.get('Category', ''), + 'tooltip': prop.attrib.get('Tooltip', ''), + 'suffix': prop.attrib.get('Suffix', ''), + # Editor visibility + 'edit_anywhere': boolean_true(prop.attrib.get('EditAnywhere', 'true')), + 'edit_defaults_only': boolean_true(prop.attrib.get('EditDefaultsOnly', 'false')), + 'edit_instance_only': boolean_true(prop.attrib.get('EditInstanceOnly', 'false')), + 'visible_anywhere': boolean_true(prop.attrib.get('VisibleAnywhere', 'false')), + 'read_only': boolean_true(prop.attrib.get('ReadOnly', 'false')), + # Script visibility + 'script_read_write': boolean_true(prop.attrib.get('ScriptReadWrite', 'true')), + 'script_read_only': boolean_true(prop.attrib.get('ScriptReadOnly', 'false')), + 'expose_to_script': boolean_true(prop.attrib.get('ExposeToScript', 'true')), + # Numeric constraints + 'min': prop.attrib.get('Min'), + 'max': prop.attrib.get('Max'), + 'ui_min': prop.attrib.get('UIMin'), + 'ui_max': prop.attrib.get('UIMax'), + 'step': prop.attrib.get('Step'), + # Advanced + 'change_notify': prop.attrib.get('ChangeNotify', ''), + 'visibility': prop.attrib.get('Visibility', ''), + 'order': int(prop.attrib.get('Order', '0')), + }) + return result + + def _parse_functions(self, elem: ET.Element) -> list: + """Parse Function elements.""" + result = [] + for func in elem.findall('Function'): + func_name = func.attrib.get('Name', '') + return_elem = func.find('Return') + params = [] + for param in func.findall('Param'): + params.append({ + 'name': param.attrib.get('Name', ''), + 'type': param.attrib.get('Type', ''), + 'default': param.attrib.get('Default', ''), + 'display_name': param.attrib.get('DisplayName', ''), + 'tooltip': param.attrib.get('Tooltip', ''), + }) + + result.append({ + 'name': func_name, + 'display_name': func.attrib.get('DisplayName', camel_to_human(func_name)), + 'category': func.attrib.get('Category', ''), + 'tooltip': func.attrib.get('Tooltip', ''), + 'return_type': return_elem.attrib.get('Type', 'void') if return_elem is not None else 'void', + 'parameters': params, + # Flags + 'script_callable': boolean_true(func.attrib.get('ScriptCallable', 'true')), + 'script_pure': boolean_true(func.attrib.get('ScriptPure', 'false')), + 'call_in_editor': boolean_true(func.attrib.get('CallInEditor', 'false')), + 'server': boolean_true(func.attrib.get('Server', 'false')), + 'client': boolean_true(func.attrib.get('Client', 'false')), + 'net_multicast': boolean_true(func.attrib.get('NetMulticast', 'false')), + }) + return result + + def _parse_enum_values(self, elem: ET.Element) -> list: + """Parse EnumValue elements.""" + result = [] + for val in elem.findall('EnumValue'): + val_name = val.attrib.get('Name', '') + result.append({ + 'name': val_name, + 'value': val.attrib.get('Value'), + 'display_name': val.attrib.get('DisplayName', camel_to_human(val_name)), + 'tooltip': val.attrib.get('Tooltip', ''), + 'hidden': boolean_true(val.attrib.get('Hidden', 'false')), + }) + return result + + +class O3DEReflectGenerator: + """Code generator using Jinja2 templates.""" + + def __init__(self, template_dir: str, output_dir: str): + self.template_dir = template_dir + self.output_dir = output_dir + + if not JINJA2_AVAILABLE: + raise RuntimeError("Jinja2 is required for code generation") + + self.env = Environment( + loader=FileSystemLoader(template_dir), + autoescape=select_autoescape(['html', 'xml']), + trim_blocks=True, + lstrip_blocks=True, + ) + + # Add custom filters + self.env.filters['booleanTrue'] = boolean_true + self.env.globals['CreateHashGuid'] = create_hash_guid + self.env.globals['CamelToHuman'] = camel_to_human + + def generate(self, parsed_data: dict, source_file: str): + """Generate code from parsed data.""" + os.makedirs(self.output_dir, exist_ok=True) + + file_prefix = os.path.splitext(os.path.basename(source_file))[0] + if file_prefix.endswith('.O3DEReflect'): + file_prefix = file_prefix[:-12] + + # Get user code if available + user_code = parsed_data.get('user_code', {}) + + generated_files = [] + + # Generate component files + for component in parsed_data.get('components', []): + header_file = self._generate_component_header(component, file_prefix, source_file) + source_file_out = self._generate_component_source(component, file_prefix, source_file, user_code) + generated_files.extend([header_file, source_file_out]) + + # Generate struct files + for struct in parsed_data.get('structs', []): + header_file = self._generate_struct_header(struct, file_prefix, source_file) + source_file_out = self._generate_struct_source(struct, file_prefix, source_file) + generated_files.extend([header_file, source_file_out]) + + # Generate enum files + for enum in parsed_data.get('enums', []): + header_file = self._generate_enum_header(enum, file_prefix, source_file) + source_file_out = self._generate_enum_source(enum, file_prefix, source_file) + generated_files.extend([header_file, source_file_out]) + + return generated_files + + def _generate_component_header(self, component: dict, file_prefix: str, source_file: str) -> str: + """Generate component header file.""" + output_file = os.path.join(self.output_dir, f"{component['name']}.AutoReflect.h") + content = self._render_component_header(component, file_prefix, source_file) + self._write_file(output_file, content) + return output_file + + def _generate_component_source(self, component: dict, file_prefix: str, source_file: str, user_code: dict = None) -> str: + """Generate component source file.""" + output_file = os.path.join(self.output_dir, f"{component['name']}.AutoReflect.cpp") + content = self._render_component_source(component, file_prefix, source_file, user_code or {}) + self._write_file(output_file, content) + return output_file + + def _generate_struct_header(self, struct: dict, file_prefix: str, source_file: str) -> str: + """Generate struct header file.""" + output_file = os.path.join(self.output_dir, f"{struct['name']}.AutoReflect.h") + content = self._render_struct_header(struct, file_prefix, source_file) + self._write_file(output_file, content) + return output_file + + def _generate_struct_source(self, struct: dict, file_prefix: str, source_file: str) -> str: + """Generate struct source file.""" + output_file = os.path.join(self.output_dir, f"{struct['name']}.AutoReflect.cpp") + content = self._render_struct_source(struct, file_prefix, source_file) + self._write_file(output_file, content) + return output_file + + def _generate_enum_header(self, enum: dict, file_prefix: str, source_file: str) -> str: + """Generate enum header file.""" + output_file = os.path.join(self.output_dir, f"{enum['name']}.AutoReflect.h") + content = self._render_enum_header(enum, file_prefix, source_file) + self._write_file(output_file, content) + return output_file + + def _generate_enum_source(self, enum: dict, file_prefix: str, source_file: str) -> str: + """Generate enum source file.""" + output_file = os.path.join(self.output_dir, f"{enum['name']}.AutoReflect.cpp") + content = self._render_enum_source(enum, file_prefix, source_file) + self._write_file(output_file, content) + return output_file + + def _render_component_header(self, component: dict, file_prefix: str, source_file: str) -> str: + """Render component header using inline template.""" + return f'''/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + * AUTO-GENERATED FILE - DO NOT EDIT + * Generated by O3DEReflect from {os.path.basename(source_file)} + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +{self._render_includes(component.get('includes', []))} +{self._render_base_class_includes(component.get('base_classes', []))} + +{self._render_namespace_begin(component.get('namespace', ''))} + //! {component.get('description', '')} + class {component['name']} +{self._render_base_class_inheritance(component.get('base_classes', []))} + {{ + public: + AZ_COMPONENT_DECL({component['name']}); + + static void Reflect(AZ::ReflectContext* context); + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent); + + {component['name']}() = default; + ~{component['name']}() override = default; + +{self._render_property_accessors(component['name'], component.get('properties', []))} + protected: + // AZ::Component interface + void Init() override; + void Activate() override; + void Deactivate() override; + +{self._render_function_declarations(component.get('functions', []))} + private: + // Member variables +{self._render_property_members(component.get('properties', []))} + }}; +{self._render_namespace_end(component.get('namespace', ''))} +''' + + def _render_component_source(self, component: dict, file_prefix: str, source_file: str, user_code: dict = None) -> str: + """Render component source using inline template, merging user implementations.""" + user_code = user_code or {} + user_functions = user_code.get('functions', {}) + + # Build base class string, filtering out bus handlers (they shouldn't be in serializeContext) + base_class_parts = [] + for b in component.get('base_classes', []): + full_name = f"{b.get('namespace', '')}::{b['name']}" if b.get('namespace') else b['name'] + # Filter out bus handlers - they aren't serializable base classes + if full_name.endswith('::Handler') or 'BusHandler' in full_name or 'Bus::Handler' in full_name: + continue + base_class_parts.append(full_name) + base_classes_str = ', '.join(base_class_parts) if base_class_parts else 'AZ::Component' + + # Get component name and namespace for function lookup + comp_name = component['name'] + namespace = component.get('namespace', '') + full_class_name = f"{namespace}::{comp_name}" if namespace else comp_name + + return f'''/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + * AUTO-GENERATED FILE - DO NOT EDIT + * Generated by O3DEReflect from {os.path.basename(source_file)} + */ + +#include "{component['name']}.AutoReflect.h" + +#include +#include +#include +#include +#include + +{self._render_namespace_begin(component.get('namespace', ''))} + + AZ_COMPONENT_IMPL({component['name']}, "{component['name']}", "{component['uuid']}"{self._render_base_class_list(component.get('base_classes', []), exclude_bus_handlers=True)}); + + void {component['name']}::Reflect(AZ::ReflectContext* context) + {{ + // SerializeContext + if (auto* serializeContext = azrtti_cast(context)) + {{ + serializeContext->Class<{component['name']}, {base_classes_str}>() + ->Version(1) +{self._render_serialize_fields(component['name'], component.get('properties', []))} + ; + + // EditContext + if (AZ::EditContext* editContext = serializeContext->GetEditContext()) + {{ + editContext->Class<{component['name']}>("{component.get('display_name', component['name'])}", "{component.get('description', '')}") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Category, "{component.get('category', 'General')}") +{self._render_edit_class_attributes(component)} +{self._render_edit_data_elements(component['name'], component.get('properties', []))} + ; + }} + }} + + // BehaviorContext + if (auto* behaviorContext = azrtti_cast(context)) + {{ + behaviorContext->Class<{component['name']}>("{component['name']}") + ->Attribute(AZ::Script::Attributes::Category, "{component.get('category', 'General')}") +{self._render_behavior_properties(component['name'], component.get('properties', []))} +{self._render_behavior_methods(component['name'], component.get('functions', []))} + ; + }} + }} + +{self._render_service_functions(component['name'], component.get('services', {}))} + +{self._render_user_implementations(comp_name, full_class_name, user_functions, component.get('functions', []))} + +{self._render_namespace_end(component.get('namespace', ''))} +''' + + def _render_struct_header(self, struct: dict, file_prefix: str, source_file: str) -> str: + """Render struct header.""" + return f'''/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + * AUTO-GENERATED FILE - DO NOT EDIT + * Generated by O3DEReflect from {os.path.basename(source_file)} + */ + +#pragma once + +#include +#include +#include +#include +{self._render_includes(struct.get('includes', []))} + +{self._render_namespace_begin(struct.get('namespace', ''))} + //! {struct.get('description', '')} + struct {struct['name']} + {{ + AZ_TYPE_INFO({struct['name']}, "{struct['uuid']}"); + AZ_CLASS_ALLOCATOR({struct['name']}, AZ::SystemAllocator); + + static void Reflect(AZ::ReflectContext* context); + + {struct['name']}() = default; + ~{struct['name']}() = default; + + bool operator==(const {struct['name']}& rhs) const; + bool operator!=(const {struct['name']}& rhs) const {{ return !(*this == rhs); }} + + // Member variables +{self._render_struct_members(struct.get('properties', []))} + }}; +{self._render_namespace_end(struct.get('namespace', ''))} +''' + + def _render_struct_source(self, struct: dict, file_prefix: str, source_file: str) -> str: + """Render struct source.""" + return f'''/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + * AUTO-GENERATED FILE - DO NOT EDIT + * Generated by O3DEReflect from {os.path.basename(source_file)} + */ + +#include "{struct['name']}.AutoReflect.h" + +#include +#include +#include +#include + +{self._render_namespace_begin(struct.get('namespace', ''))} + + void {struct['name']}::Reflect(AZ::ReflectContext* context) + {{ + if (auto* serializeContext = azrtti_cast(context)) + {{ + serializeContext->Class<{struct['name']}>() + ->Version(1) +{self._render_serialize_fields(struct['name'], struct.get('properties', []))} + ; + + if (AZ::EditContext* editContext = serializeContext->GetEditContext()) + {{ + editContext->Class<{struct['name']}>("{struct.get('display_name', struct['name'])}", "{struct.get('description', '')}") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Category, "{struct.get('category', 'Data')}") +{self._render_edit_data_elements(struct['name'], struct.get('properties', []))} + ; + }} + }} + +{self._render_struct_behavior_context(struct)} + }} + + bool {struct['name']}::operator==(const {struct['name']}& rhs) const + {{ +{self._render_struct_equality(struct.get('properties', []))} + }} + +{self._render_namespace_end(struct.get('namespace', ''))} +''' + + def _render_enum_header(self, enum: dict, file_prefix: str, source_file: str) -> str: + """Render enum header.""" + return f'''/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + * AUTO-GENERATED FILE - DO NOT EDIT + * Generated by O3DEReflect from {os.path.basename(source_file)} + */ + +#pragma once + +#include +#include + +{self._render_namespace_begin(enum.get('namespace', ''))} + //! {enum.get('description', '')} + enum class {enum['name']} : {enum.get('underlying_type', 'int32_t')} + {{ +{self._render_enum_values(enum)} + }}; + +{self._render_enum_bitwise_operators(enum)} + // Enum reflection helper + class {enum['name']}Reflect + {{ + public: + static void Reflect(AZ::ReflectContext* context); + }}; +{self._render_namespace_end(enum.get('namespace', ''))} +''' + + def _render_enum_source(self, enum: dict, file_prefix: str, source_file: str) -> str: + """Render enum source.""" + return f'''/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + * AUTO-GENERATED FILE - DO NOT EDIT + * Generated by O3DEReflect from {os.path.basename(source_file)} + */ + +#include "{enum['name']}.AutoReflect.h" + +#include +#include + +{self._render_namespace_begin(enum.get('namespace', ''))} + + void {enum['name']}Reflect::Reflect(AZ::ReflectContext* context) + {{ + if (auto* behaviorContext = azrtti_cast(context)) + {{ +{self._render_enum_behavior_values(enum)} + }} + }} + +{self._render_namespace_end(enum.get('namespace', ''))} +''' + + # Helper methods for rendering + def _render_includes(self, includes: list) -> str: + return 'n'.join([f'#include <{inc}>' for inc in includes]) + + def _render_base_class_includes(self, base_classes: list) -> str: + includes = [b['include'] for b in base_classes if b.get('include')] + return '\\n'.join([f'#include <{inc}>' for inc in includes]) + + def _render_base_class_inheritance(self, base_classes: list) -> str: + if not base_classes: + return ' : public AZ::Component' + parts = [] + for b in base_classes: + full_name = f"{b['namespace']}::{b['name']}" if b.get('namespace') else b['name'] + parts.append(f'public {full_name}') + return ' : ' + ', '.join(parts) + + def _render_base_class_list(self, base_classes: list, exclude_bus_handlers: bool = False) -> str: + """Render base class list for macros like AZ_COMPONENT_IMPL. + + Args: + base_classes: List of base class dicts with 'name' and 'namespace' keys + exclude_bus_handlers: If True, filter out bus handlers (end with ::Handler or BusHandler) + """ + if not base_classes: + return '' + parts = [] + for b in base_classes: + full_name = f"{b['namespace']}::{b['name']}" if b.get('namespace') else b['name'] + if exclude_bus_handlers: + # Filter out bus handlers - they typically end with ::Handler or BusHandler + if full_name.endswith('::Handler') or 'BusHandler' in full_name or 'Bus::Handler' in full_name: + continue + parts.append(full_name) + if not parts: + return '' + return ', ' + ', '.join(parts) + + def _render_namespace_begin(self, namespace: str) -> str: + if not namespace: + return '' + return f'namespace {namespace}\\n{{' + + def _render_namespace_end(self, namespace: str) -> str: + if not namespace: + return '' + return f'}} // namespace {namespace}' + + def _render_property_accessors(self, class_name: str, properties: list) -> str: + lines = [' // Getters and Setters'] + for prop in properties: + prop_name = prop['name'] + member_name = prop['member_name'] + prop_type = prop['type'] + clean_name = prop_name[2:] if prop_name.startswith('m_') else prop_name + getter_name = f'Get{clean_name[0].upper()}{clean_name[1:]}' + setter_name = f'Set{clean_name[0].upper()}{clean_name[1:]}' + + lines.append(f' //! Get {camel_to_human(clean_name)}') + lines.append(f' const {prop_type}& {getter_name}() const {{ return {member_name}; }}') + if not prop.get('read_only', False): + lines.append(f' //! Set {camel_to_human(clean_name)}') + lines.append(f' void {setter_name}(const {prop_type}& value) {{ {member_name} = value; }}') + lines.append('') + return '\\n'.join(lines) + + def _render_property_members(self, properties: list) -> str: + lines = [] + for prop in properties: + member_name = prop['member_name'] + prop_type = prop['type'] + default = prop.get('default', '') + if default: + lines.append(f' {prop_type} {member_name} = {default};') + else: + lines.append(f' {prop_type} {member_name}{{}};') + return '\\n'.join(lines) + + def _render_struct_members(self, properties: list) -> str: + lines = [] + for prop in properties: + member_name = prop['member_name'] + prop_type = prop['type'] + default = prop.get('default', '') + if default: + lines.append(f' {prop_type} {member_name} = {default};') + else: + lines.append(f' {prop_type} {member_name}{{}};') + return '\\n'.join(lines) + + def _render_function_declarations(self, functions: list) -> str: + lines = [] + for func in functions: + func_name = func['name'] + return_type = func.get('return_type', 'void') + params = ', '.join([ + f"{p['type']} {p['name']}" + (f" = {p['default']}" if p.get('default') else '') + for p in func.get('parameters', []) + ]) + tooltip = func.get('tooltip', func_name) + lines.append(f' //! {tooltip}') + lines.append(f' {return_type} {func_name}({params});') + lines.append('') + return '\\n'.join(lines) + + def _render_serialize_fields(self, class_name: str, properties: list) -> str: + lines = [] + for prop in properties: + prop_name = prop['name'] + member_name = prop['member_name'] + lines.append(f' ->Field("{prop_name}", &{class_name}::{member_name})') + return '\\n'.join(lines) + + def _render_edit_class_attributes(self, component: dict) -> str: + lines = [] + if component.get('icon'): + lines.append(f' ->Attribute(AZ::Edit::Attributes::Icon, "{component["icon"]}")') + if not component.get('hide_in_editor', False): + menu_cat = component.get('menu_category', 'Game') + lines.append(f' ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("{menu_cat}"))') + lines.append(' ->Attribute(AZ::Edit::Attributes::AutoExpand, true)') + return '\\n'.join(lines) + + def _render_edit_data_elements(self, class_name: str, properties: list) -> str: + lines = [] + current_category = '' + for prop in properties: + prop_name = prop['name'] + member_name = prop['member_name'] + display_name = prop.get('display_name', camel_to_human(prop_name.lstrip('m_'))) + tooltip = prop.get('tooltip', '') + category = prop.get('category', '') + + # Add category group if changed + if category and category != current_category: + lines.append(f' ->ClassElement(AZ::Edit::ClassElements::Group, "{category}")') + lines.append(' ->Attribute(AZ::Edit::Attributes::AutoExpand, true)') + current_category = category + + lines.append(f' ->DataElement(AZ::Edit::UIHandlers::Default, &{class_name}::{member_name}, "{display_name}", "{tooltip}")') + + # Add property attributes + if prop.get('suffix'): + lines.append(f' ->Attribute(AZ::Edit::Attributes::Suffix, "{prop["suffix"]}")') + if prop.get('min') is not None: + lines.append(f' ->Attribute(AZ::Edit::Attributes::Min, {prop["min"]})') + if prop.get('max') is not None: + lines.append(f' ->Attribute(AZ::Edit::Attributes::Max, {prop["max"]})') + if prop.get('read_only'): + lines.append(' ->Attribute(AZ::Edit::Attributes::ReadOnly, true)') + + return '\\n'.join(lines) + + def _render_behavior_properties(self, class_name: str, properties: list) -> str: + lines = [] + for prop in properties: + if not prop.get('expose_to_script', True) and not prop.get('script_read_write', False): + continue + + prop_name = prop['name'] + member_name = prop['member_name'] + prop_type = prop['type'] + clean_name = prop_name[2:] if prop_name.startswith('m_') else prop_name + getter_name = f'Get{clean_name[0].upper()}{clean_name[1:]}' + setter_name = f'Set{clean_name[0].upper()}{clean_name[1:]}' + + if prop.get('script_read_only', False): + lines.append(f' ->Property("{clean_name}",') + lines.append(f' []({class_name}* self) {{ return self->{getter_name}(); }},') + lines.append(' nullptr)') + else: + lines.append(f' ->Property("{clean_name}",') + lines.append(f' []({class_name}* self) {{ return self->{getter_name}(); }},') + lines.append(f' []({class_name}* self, const {prop_type}& value) {{ self->{setter_name}(value); }})') + + return '\\n'.join(lines) + + def _render_behavior_methods(self, class_name: str, functions: list) -> str: + lines = [] + for func in functions: + if not func.get('script_callable', True): + continue + + func_name = func['name'] + category = func.get('category', '') + lines.append(f' ->Method("{func_name}", &{class_name}::{func_name})') + if category: + lines.append(f' ->Attribute(AZ::Script::Attributes::Category, "{category}")') + + return '\\n'.join(lines) + + def _render_service_functions(self, class_name: str, services: dict) -> str: + provides = services.get('provides', []) + incompatible = services.get('incompatible', []) + requires = services.get('requires', []) + dependent = services.get('dependent', []) + + return f''' + void {class_name}::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + {{ +{self._render_service_list('provided', provides)} + }} + + void {class_name}::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + {{ +{self._render_service_list('incompatible', incompatible)} + }} + + void {class_name}::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required) + {{ +{self._render_service_list('required', requires)} + }} + + void {class_name}::GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent) + {{ +{self._render_service_list('dependent', dependent)} + }} +''' + + def _render_service_list(self, var_name: str, services: list) -> str: + if not services: + return f' (void){var_name}; // No services' + lines = [] + for service in services: + lines.append(f' {var_name}.push_back(AZ_CRC_CE("{service}"));') + return '\\n'.join(lines) + + def _render_function_implementations(self, class_name: str, functions: list) -> str: + lines = [] + for func in functions: + func_name = func['name'] + return_type = func.get('return_type', 'void') + params = ', '.join([f"{p['type']} {p['name']}" for p in func.get('parameters', [])]) + + lines.append(f' {return_type} {class_name}::{func_name}({params})') + lines.append(' {') + lines.append(f' // TODO: Implement {func_name}') + if return_type != 'void': + lines.append(' return {};') + lines.append(' }') + lines.append('') + + return '\\n'.join(lines) + + def _render_default_function_implementations(self, class_name: str, functions: list) -> str: + """Render default empty implementations for functions (used when no _Impl.inl exists).""" + lines = [] + for func in functions: + func_name = func['name'] + return_type = func.get('return_type', 'void') + params = ', '.join([f"{p['type']} /*{p['name']}*/" for p in func.get('parameters', [])]) + + lines.append(f' {return_type} {class_name}::{func_name}({params})') + lines.append(' {') + if return_type != 'void': + lines.append(' return {};') + lines.append(' }') + + return '\\n'.join(lines) + + def _render_user_implementations(self, class_name: str, full_class_name: str, + user_functions: dict, declared_functions: list) -> str: + """ + Render function implementations, using user code where available. + + Args: + class_name: Short class name (e.g., "TestReflectionComponent") + full_class_name: Full class name with namespace (e.g., "NewProject::TestReflectionComponent") + user_functions: Dict of user-provided function implementations from .cpp + declared_functions: List of O3DE_FUNCTION declared functions + """ + lines = [] + lines.append(' // ========================================================================') + lines.append(' // Component Lifecycle Functions') + lines.append(' // ========================================================================') + lines.append('') + + # List of standard component functions to look for + lifecycle_functions = ['Init', 'Activate', 'Deactivate'] + + for func_name in lifecycle_functions: + # Try to find user implementation + user_impl = None + for key in [f"{class_name}::{func_name}", f"{full_class_name}::{func_name}"]: + if key in user_functions: + user_impl = user_functions[key] + break + + if user_impl: + # Use user's implementation + lines.append(f" void {class_name}::{func_name}()") + lines.append(' {') + # Indent the user's body + for line in user_impl['body'].split('\\n'): + lines.append(f" {line}" if line.strip() else '') + lines.append(' }') + else: + # Provide empty default + lines.append(f" void {class_name}::{func_name}()") + lines.append(' {') + lines.append(f' // TODO: Implement {func_name}') + lines.append(' }') + lines.append('') + + # Now handle O3DE_FUNCTION declared functions + if declared_functions: + lines.append(' // ========================================================================') + lines.append(' // Custom Functions') + lines.append(' // ========================================================================') + lines.append('') + + for func in declared_functions: + func_name = func['name'] + return_type = func.get('return_type', 'void') + params = ', '.join([f"{p['type']} {p['name']}" for p in func.get('parameters', [])]) + + # Try to find user implementation + user_impl = None + for key in [f"{class_name}::{func_name}", f"{full_class_name}::{func_name}"]: + if key in user_functions: + user_impl = user_functions[key] + break + + const_suffix = ' const' if func.get('is_const', False) else '' + + if user_impl: + # Use user's implementation + lines.append(f" {return_type} {class_name}::{func_name}({params}){const_suffix}") + lines.append(' {') + for line in user_impl['body'].split('\\n'): + lines.append(f" {line}" if line.strip() else '') + lines.append(' }') + else: + # Provide stub default + lines.append(f" {return_type} {class_name}::{func_name}({params}){const_suffix}") + lines.append(' {') + lines.append(f' // TODO: Implement {func_name}') + if return_type != 'void': + lines.append(' return {};') + lines.append(' }') + lines.append('') + + return '\\n'.join(lines) + + def _render_struct_behavior_context(self, struct: dict) -> str: + if not struct.get('script_type', True): + return '' + + struct_name = struct['name'] + category = struct.get('category', 'Data') + properties = struct.get('properties', []) + + lines = [ + ' if (auto* behaviorContext = azrtti_cast(context))', + ' {', + f' behaviorContext->Class<{struct_name}>("{struct_name}")', + f' ->Attribute(AZ::Script::Attributes::Category, "{category}")', + ' ->Constructor()', + ] + + for prop in properties: + if not prop.get('expose_to_script', True): + continue + member_name = prop['member_name'] + clean_name = prop['name'][2:] if prop['name'].startswith('m_') else prop['name'] + lines.append(f' ->Property("{clean_name}", BehaviorValueProperty(&{struct_name}::{member_name}))') + + lines.append(' ;') + lines.append(' }') + + return '\\n'.join(lines) + + def _render_struct_equality(self, properties: list) -> str: + if not properties: + return ' return true;' + + lines = [' return'] + for i, prop in enumerate(properties): + member_name = prop['member_name'] + if i == 0: + lines.append(f' {member_name} == rhs.{member_name}') + else: + lines.append(f' && {member_name} == rhs.{member_name}') + lines.append(' ;') + + return '\\n'.join(lines) + + def _render_enum_values(self, enum: dict) -> str: + lines = [] + is_flags = enum.get('is_flags', False) + counter = 0 + + for val in enum.get('values', []): + val_name = val['name'] + val_num = val.get('value') + + if val_num is not None: + lines.append(f' {val_name} = {val_num},') + elif is_flags: + lines.append(f' {val_name} = 1 << {counter},') + counter += 1 + else: + lines.append(f' {val_name},') + + return '\\n'.join(lines) + + def _render_enum_bitwise_operators(self, enum: dict) -> str: + if not enum.get('is_flags', False): + return '' + return f' AZ_DEFINE_ENUM_BITWISE_OPERATORS({enum["name"]});\\n' + + def _render_enum_behavior_values(self, enum: dict) -> str: + enum_name = enum['name'] + category = enum.get('category', 'Enums') + values = enum.get('values', []) + + if not values: + return ' // No enum values defined' + + # Build the Enum template arguments + value_casts = ',\\n'.join([ + f' static_cast({enum_name}::{v["name"]})' + for v in values + ]) + + lines = [ + ' behaviorContext->Enum<', + value_casts, + f' >("{enum_name}")', + ] + + for val in values: + display_name = val.get('display_name', camel_to_human(val['name'])) + lines.append(f' ->Value("{display_name}", static_cast({enum_name}::{val["name"]}))') + + lines.append(f' ->Attribute(AZ::Script::Attributes::Category, "{category}")') + lines.append(' ;') + + return '\\n'.join(lines) + + def _write_file(self, path: str, content: str): + """Write content to file, creating directories as needed.""" + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, 'w', encoding='utf-8', newline='\\n') as f: + f.write(content) + logger.info(f"Generated: {path}") + + +def main(): + parser = argparse.ArgumentParser(description='O3DEReflect Code Generator') + parser.add_argument('--input', '-i', required=True, help='Input XML file') + parser.add_argument('--output-dir', '-o', required=True, help='Output directory') + parser.add_argument('--template-dir', '-t', help='Template directory (for Jinja2 templates)') + parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output') + parser.add_argument('--dry-run', '-n', action='store_true', help='Parse only, do not generate') + + args = parser.parse_args() + + if args.verbose: + logger.setLevel(logging.DEBUG) + + # Parse input file + logger.info(f"Parsing: {args.input}") + xml_parser = O3DEReflectParser(args.input) + parsed_data = xml_parser.parse() + + if args.verbose: + logger.debug(f"Parsed data: {json.dumps(parsed_data, indent=2, default=str)}") + + if args.dry_run: + logger.info("Dry run - no files generated") + return 0 + + # Generate code + template_dir = args.template_dir or os.path.dirname(os.path.abspath(__file__)) + generator = O3DEReflectGenerator(template_dir, args.output_dir) + generated_files = generator.generate(parsed_data, args.input) + + logger.info(f"Generated {len(generated_files)} files") + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/Gems/O3DEReflect/Code/Include/O3DEReflect/AutoGen/O3DEReflectHeaderParser.py b/Gems/O3DEReflect/Code/Include/O3DEReflect/AutoGen/O3DEReflectHeaderParser.py new file mode 100644 index 0000000000..c7f66a7c39 --- /dev/null +++ b/Gems/O3DEReflect/Code/Include/O3DEReflect/AutoGen/O3DEReflectHeaderParser.py @@ -0,0 +1,911 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT + +""" +O3DEReflect Header Parser (V2) + +This module parses C++ header files for O3DE_* macros and generates reflection code automatically. + +Supports: + O3DE_CLASS(...) - Mark a class for reflection + O3DE_COMPONENT(...) - Component-specific metadata + O3DE_PROPERTY(...) - Mark members for serialization/editor + O3DE_FUNCTION(...) - Expose methods to scripting + O3DE_STRUCT(...) - Mark structs for reflection + O3DE_ENUM(...) - Mark enums for reflection + O3DE_ENUM_VALUE(...) - Enum value metadata + +Usage: + python O3DEReflectHeaderParser.py --input MyComponent.h --output-dir ./Generated +""" + +import os +import re +import sys +import json +import glob +import argparse +import hashlib +import logging +from pathlib import Path +from dataclasses import dataclass, field +from typing import List, Dict, Optional, Any, Tuple + +# Try to find O3DE's Python venv site-packages for Jinja2 +def find_o3de_site_packages(): + """Find O3DE's Python venv site-packages directory.""" + home = os.path.expanduser("~") + patterns = [ + os.path.join(home, ".o3de", "Python", "venv", "*", "lib", "site-packages"), + os.path.join(home, ".o3de", "Python", "venv", "*", "Lib", "site-packages"), # Windows + ] + for pattern in patterns: + matches = glob.glob(pattern) + if matches: + return matches[0] + return None + +# Add O3DE site-packages to path if needed +o3de_site_packages = find_o3de_site_packages() +if o3de_site_packages and o3de_site_packages not in sys.path: + sys.path.insert(0, o3de_site_packages) + +logging.basicConfig(format='[%(levelname)s] %(name)s: %(message)s') +logger = logging.getLogger('O3DEReflectHeaderParser') +logger.setLevel(logging.INFO) + + +def create_hash_guid(string: str) -> str: + """Generate a deterministic UUID from a string using MD5 hash.""" + hash_obj = hashlib.md5(string.encode('utf-8')) + hash_str = hash_obj.hexdigest() + return "{{{}-{}-{}-{}-{}}}".format( + hash_str[0:8].upper(), + hash_str[8:12].upper(), + hash_str[12:16].upper(), + hash_str[16:20].upper(), + hash_str[20:].upper() + ) + + +def camel_to_human(string: str) -> str: + """Convert camelCase to Human Readable format.""" + if not string: + return string + result = string[0].upper() + for char in string[1:]: + if char.isupper(): + result += ' ' + result += char + return result + + +@dataclass +class MacroAttribute: + """Represents a parsed macro attribute (e.g., Category="Movement")""" + name: str + value: Any = None + is_flag: bool = False # True for bare flags like "EditAnywhere" + + +@dataclass +class PropertyInfo: + """Parsed property information""" + name: str + cpp_type: str + default_value: str = "" + attributes: Dict[str, Any] = field(default_factory=dict) + line_number: int = 0 + + +@dataclass +class FunctionInfo: + """Parsed function information""" + name: str + return_type: str = "void" + parameters: List[Dict[str, str]] = field(default_factory=list) + attributes: Dict[str, Any] = field(default_factory=dict) + line_number: int = 0 + + +@dataclass +class EnumValueInfo: + """Parsed enum value information""" + name: str + value: Optional[int] = None + attributes: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class ClassInfo: + """Parsed class/struct information""" + name: str + namespace: str = "" + base_classes: List[str] = field(default_factory=list) + is_component: bool = False + is_struct: bool = False + is_enum: bool = False + uuid: str = "" + attributes: Dict[str, Any] = field(default_factory=dict) + properties: List[PropertyInfo] = field(default_factory=list) + functions: List[FunctionInfo] = field(default_factory=list) + enum_values: List[EnumValueInfo] = field(default_factory=list) + services: Dict[str, List[str]] = field(default_factory=dict) + includes: List[str] = field(default_factory=list) + line_number: int = 0 + + +class O3DEHeaderParser: + """ + Parser for C++ headers containing O3DE_* reflection macros. + + This parser uses regex-based tokenization to extract macro annotations + and their associated C++ declarations. + """ + + # Regex patterns for macro parsing + MACRO_PATTERN = re.compile( + r'O3DE_(CLASS|COMPONENT|PROPERTY|FUNCTION|STRUCT|ENUM|ENUM_VALUE|GENERATED_BODY)\s*\(([^)]*)\)', + re.MULTILINE + ) + + # Pattern to match class/struct declaration after O3DE_CLASS/O3DE_STRUCT + CLASS_DECL_PATTERN = re.compile( + r'(?:class|struct)\s+(?:\[\[.*?\]\]\s*)?(\w+)\s*(?:final\s*)?(?::\s*(.+?))?\s*\{', + re.MULTILINE | re.DOTALL + ) + + # Pattern to match enum declaration after O3DE_ENUM + ENUM_DECL_PATTERN = re.compile( + r'enum\s+(?:class\s+)?(\w+)\s*(?::\s*(\w+))?\s*\{([^}]*)\}', + re.MULTILINE | re.DOTALL + ) + + # Pattern to match member variable declaration after O3DE_PROPERTY + MEMBER_PATTERN = re.compile( + r'^\s*((?:const\s+)?(?:[\w:]+(?:<[^>]+>)?(?:\s*[*&])?)+)\s+(\w+)\s*(?:=\s*([^;]+))?\s*;', + re.MULTILINE + ) + + # Pattern to match function declaration after O3DE_FUNCTION + FUNCTION_PATTERN = re.compile( + r'^\s*((?:virtual\s+)?(?:static\s+)?(?:const\s+)?(?:[\w:]+(?:<[^>]+>)?(?:\s*[*&])?)+)\s+(\w+)\s*\(([^)]*)\)\s*(?:const)?\s*(?:override)?\s*(?:=\s*0)?\s*;', + re.MULTILINE + ) + + # Pattern to parse macro attributes + ATTR_PATTERN = re.compile( + r'(\w+)\s*(?:=\s*(?:"([^"]*)"|([^,\s]+)))?', + re.MULTILINE + ) + + # Pattern to match namespace + NAMESPACE_PATTERN = re.compile( + r'namespace\s+(\w+)\s*\{', + re.MULTILINE + ) + + # Pattern to match includes + INCLUDE_PATTERN = re.compile( + r'#include\s*[<"]([^>"]+)[>"]', + re.MULTILINE + ) + + def __init__(self, file_path: str): + self.file_path = file_path + self.content = "" + self.lines = [] + self.classes: List[ClassInfo] = [] + self.current_namespace = "" + + def parse(self) -> Dict: + """Parse the header file and return structured data.""" + with open(self.file_path, 'r', encoding='utf-8') as f: + self.content = f.read() + self.lines = self.content.split('\n') + + # Extract includes + includes = self.INCLUDE_PATTERN.findall(self.content) + + # Find all namespaces + namespaces = list(self.NAMESPACE_PATTERN.finditer(self.content)) + + # Find all O3DE_* macros + macros = list(self.MACRO_PATTERN.finditer(self.content)) + + # Process macros in order + current_class: Optional[ClassInfo] = None + pending_macro: Optional[Tuple[str, Dict, int]] = None + + for macro in macros: + macro_type = macro.group(1) + macro_args = macro.group(2) + macro_pos = macro.start() + macro_line = self.content[:macro_pos].count('\n') + 1 + + # Determine current namespace + current_ns = "" + for ns_match in namespaces: + if ns_match.start() < macro_pos: + current_ns = ns_match.group(1) + + attributes = self._parse_macro_attributes(macro_args) + + if macro_type == 'CLASS': + # Look for class declaration after macro + remaining = self.content[macro.end():] + class_match = self.CLASS_DECL_PATTERN.search(remaining) + if class_match: + class_name = class_match.group(1) + base_classes_str = class_match.group(2) or "" + base_classes = self._parse_base_classes(base_classes_str) + + full_name = f"{current_ns}::{class_name}" if current_ns else class_name + + current_class = ClassInfo( + name=class_name, + namespace=current_ns, + base_classes=base_classes, + is_component=any('Component' in bc for bc in base_classes), + uuid=attributes.get('Uuid', create_hash_guid(full_name)), + attributes=attributes, + includes=includes.copy(), + line_number=macro_line, + ) + current_class.services = { + 'provides': [], + 'requires': [], + 'incompatible': [], + 'dependent': [], + } + self.classes.append(current_class) + + elif macro_type == 'STRUCT': + # Look for struct declaration after macro + remaining = self.content[macro.end():] + class_match = self.CLASS_DECL_PATTERN.search(remaining) + if class_match: + struct_name = class_match.group(1) + full_name = f"{current_ns}::{struct_name}" if current_ns else struct_name + + current_class = ClassInfo( + name=struct_name, + namespace=current_ns, + is_struct=True, + uuid=attributes.get('Uuid', create_hash_guid(full_name)), + attributes=attributes, + includes=includes.copy(), + line_number=macro_line, + ) + self.classes.append(current_class) + + elif macro_type == 'ENUM': + # Look for enum declaration after macro + remaining = self.content[macro.end():] + enum_match = self.ENUM_DECL_PATTERN.search(remaining) + if enum_match: + enum_name = enum_match.group(1) + underlying_type = enum_match.group(2) or "int32_t" + enum_body = enum_match.group(3) + + full_name = f"{current_ns}::{enum_name}" if current_ns else enum_name + + enum_class = ClassInfo( + name=enum_name, + namespace=current_ns, + is_enum=True, + uuid=attributes.get('Uuid', create_hash_guid(full_name)), + attributes=attributes, + line_number=macro_line, + ) + enum_class.attributes['UnderlyingType'] = underlying_type + + # Parse enum values + enum_class.enum_values = self._parse_enum_values(enum_body) + self.classes.append(enum_class) + + elif macro_type == 'COMPONENT': + # O3DE_COMPONENT adds component-specific info to current class + if current_class and current_class.is_component: + current_class.is_component = True + # Parse services from attributes + if 'ProvidesServices' in attributes: + current_class.services['provides'] = self._parse_service_list(attributes['ProvidesServices']) + if 'RequiresServices' in attributes: + current_class.services['requires'] = self._parse_service_list(attributes['RequiresServices']) + if 'IncompatibleServices' in attributes: + current_class.services['incompatible'] = self._parse_service_list(attributes['IncompatibleServices']) + if 'DependentServices' in attributes: + current_class.services['dependent'] = self._parse_service_list(attributes['DependentServices']) + # Merge other attributes + current_class.attributes.update(attributes) + + elif macro_type == 'PROPERTY': + # Store pending property macro, look for member declaration + pending_macro = ('PROPERTY', attributes, macro.end()) + + elif macro_type == 'FUNCTION': + # Store pending function macro, look for function declaration + pending_macro = ('FUNCTION', attributes, macro.end()) + + elif macro_type == 'GENERATED_BODY': + # Marker for where generated code should be included + if current_class: + current_class.attributes['_generated_body_line'] = macro_line + + # Now do a second pass to find member and function declarations + self._parse_members_and_functions() + + return self._to_dict() + + def _parse_macro_attributes(self, args_str: str) -> Dict[str, Any]: + """Parse macro attributes like Category="Movement", EditAnywhere, Min=0.0""" + attributes = {} + if not args_str.strip(): + return attributes + + # Split by comma, but be careful with nested content + parts = self._split_macro_args(args_str) + + for part in parts: + part = part.strip() + if not part: + continue + + # Check for key=value pattern + if '=' in part: + key, value = part.split('=', 1) + key = key.strip() + value = value.strip().strip('"') + + # Try to convert to appropriate type + if value.lower() in ('true', 'false'): + value = value.lower() == 'true' + else: + try: + if '.' in value: + value = float(value.rstrip('f')) + else: + value = int(value) + except ValueError: + pass # Keep as string + + attributes[key] = value + else: + # Bare flag like "EditAnywhere" + attributes[part] = True + + return attributes + + def _split_macro_args(self, args_str: str) -> List[str]: + """Split macro arguments by comma, respecting nested parentheses and strings.""" + parts = [] + current = "" + depth = 0 + in_string = False + + for char in args_str: + if char == '"' and (not current or current[-1] != '\\'): + in_string = not in_string + elif not in_string: + if char in '([{': + depth += 1 + elif char in ')]}': + depth -= 1 + elif char == ',' and depth == 0: + parts.append(current) + current = "" + continue + current += char + + if current: + parts.append(current) + + return parts + + def _parse_base_classes(self, base_str: str) -> List[str]: + """Parse base class list from inheritance declaration.""" + if not base_str: + return [] + + bases = [] + for part in base_str.split(','): + part = part.strip() + # Remove access specifiers + for spec in ['public', 'protected', 'private', 'virtual']: + part = part.replace(spec, '').strip() + if part: + bases.append(part) + + return bases + + def _parse_service_list(self, services_str: str) -> List[str]: + """Parse comma-separated service list.""" + if isinstance(services_str, list): + return services_str + return [s.strip().strip('"') for s in services_str.split(',') if s.strip()] + + def _parse_enum_values(self, enum_body: str) -> List[EnumValueInfo]: + """Parse enum values from enum body.""" + values = [] + + # Remove comments + enum_body = re.sub(r'//.*$', '', enum_body, flags=re.MULTILINE) + enum_body = re.sub(r'/\*.*?\*/', '', enum_body, flags=re.DOTALL) + + # Split by comma + parts = [p.strip() for p in enum_body.split(',') if p.strip()] + + for part in parts: + # Check for O3DE_ENUM_VALUE macro + ev_match = re.search(r'O3DE_ENUM_VALUE\s*\(([^)]*)\)\s*(\w+)', part) + if ev_match: + attrs = self._parse_macro_attributes(ev_match.group(1)) + name = ev_match.group(2) + else: + # Parse regular enum value + if '=' in part: + name, val = part.split('=', 1) + name = name.strip() + try: + attrs = {'Value': int(val.strip())} + except ValueError: + attrs = {'Value': val.strip()} + else: + name = part.strip() + attrs = {} + + if name: + values.append(EnumValueInfo( + name=name, + value=attrs.get('Value'), + attributes=attrs + )) + + return values + + def _parse_members_and_functions(self): + """Second pass: match O3DE_PROPERTY/FUNCTION macros to declarations.""" + for cls in self.classes: + if cls.is_enum: + continue + + # Find the class body + class_start = self._find_class_body_start(cls.name, cls.line_number) + if class_start < 0: + continue + + class_body = self.content[class_start:] + # Find matching closing brace + brace_depth = 1 + class_end = 0 + for i, char in enumerate(class_body[1:], 1): + if char == '{': + brace_depth += 1 + elif char == '}': + brace_depth -= 1 + if brace_depth == 0: + class_end = i + break + + class_body = class_body[:class_end] + + # Find all O3DE_PROPERTY macros in class body + # Format: O3DE_PROPERTY(member_name, Type = "...", ...) + # Match multiline macros with parenthesis counting + property_pattern = re.compile(r'O3DE_PROPERTY\s*\(', re.MULTILINE) + for match in property_pattern.finditer(class_body): + start = match.end() + depth = 1 + end = start + while end < len(class_body) and depth > 0: + if class_body[end] == '(': + depth += 1 + elif class_body[end] == ')': + depth -= 1 + end += 1 + + if depth == 0: + macro_content = class_body[start:end-1] + # Parse: member_name, attr1 = val1, attr2 = val2 + args = self._split_macro_args(macro_content) + if args: + member_name = args[0].strip() + attr_str = ', '.join(args[1:]) if len(args) > 1 else '' + attrs = self._parse_macro_attributes(attr_str) + + # Get type and default from attributes + prop_type = attrs.get('type', attrs.get('Type', 'AZ::s32')) + default_val = attrs.get('default', attrs.get('Default', '')) + + prop = PropertyInfo( + name=member_name, + cpp_type=prop_type, + default_value=default_val, + attributes=attrs, + ) + cls.properties.append(prop) + + # Find all O3DE_FUNCTION macros in class body + for match in re.finditer(r'O3DE_FUNCTION\s*\(([^)]*)\)\s*\n\s*(.+?);', class_body, re.MULTILINE): + attrs = self._parse_macro_attributes(match.group(1)) + decl = match.group(2).strip() + + # Parse function declaration + func_match = re.match(r'([\w:<>,\s*&]+?)\s+(\w+)\s*\(([^)]*)\)', decl) + if func_match: + params = self._parse_function_params(func_match.group(3)) + func = FunctionInfo( + name=func_match.group(2), + return_type=func_match.group(1).strip(), + parameters=params, + attributes=attrs, + ) + cls.functions.append(func) + + def _find_class_body_start(self, class_name: str, start_line: int) -> int: + """Find the position of the opening brace of a class.""" + # Search from the given line + start_pos = sum(len(line) + 1 for line in self.lines[:start_line-1]) + remaining = self.content[start_pos:] + + # Find class declaration + pattern = re.compile(rf'(?:class|struct)\s+{re.escape(class_name)}\s*(?:final\s*)?(?::[^{{]+)?\s*\{{') + match = pattern.search(remaining) + if match: + return start_pos + match.end() - 1 + return -1 + + def _parse_function_params(self, params_str: str) -> List[Dict[str, str]]: + """Parse function parameters.""" + params = [] + if not params_str.strip(): + return params + + for part in self._split_macro_args(params_str): + part = part.strip() + if not part: + continue + + # Parse "Type name = default" or "Type name" + match = re.match(r'([\w:<>,\s*&]+?)\s+(\w+)\s*(?:=\s*(.+))?$', part) + if match: + params.append({ + 'type': match.group(1).strip(), + 'name': match.group(2), + 'default': match.group(3).strip() if match.group(3) else "", + }) + + return params + + def _to_dict(self) -> Dict: + """Convert parsed data to dictionary format compatible with generator.""" + result = { + 'components': [], + 'structs': [], + 'enums': [], + 'source_file': os.path.basename(self.file_path), + } + + for cls in self.classes: + if cls.is_enum: + enum_data = self._enum_to_dict(cls) + result['enums'].append(enum_data) + elif cls.is_struct: + struct_data = self._struct_to_dict(cls) + result['structs'].append(struct_data) + else: + comp_data = self._component_to_dict(cls) + result['components'].append(comp_data) + + return result + + def _component_to_dict(self, cls: ClassInfo) -> Dict: + """Convert ClassInfo to component dictionary.""" + full_name = f"{cls.namespace}::{cls.name}" if cls.namespace else cls.name + + return { + 'name': cls.name, + 'namespace': cls.namespace, + 'full_name': full_name, + 'uuid': cls.uuid, + 'category': cls.attributes.get('Category', 'General'), + 'description': cls.attributes.get('Description', f'{cls.name} component'), + 'display_name': cls.attributes.get('DisplayName', camel_to_human(cls.name)), + 'icon': cls.attributes.get('Icon', ''), + 'hide_in_editor': cls.attributes.get('HideInEditor', False), + 'abstract': cls.attributes.get('Abstract', False), + 'menu_category': cls.attributes.get('AppearsInAddComponentMenu', 'Game'), + 'includes': cls.includes, + 'base_classes': [{'name': bc.split('::')[-1], 'namespace': '::'.join(bc.split('::')[:-1]) if '::' in bc else '', 'include': ''} for bc in cls.base_classes], + 'services': cls.services, + 'properties': [self._property_to_dict(p) for p in cls.properties], + 'functions': [self._function_to_dict(f) for f in cls.functions], + } + + def _struct_to_dict(self, cls: ClassInfo) -> Dict: + """Convert ClassInfo to struct dictionary.""" + full_name = f"{cls.namespace}::{cls.name}" if cls.namespace else cls.name + + return { + 'name': cls.name, + 'namespace': cls.namespace, + 'full_name': full_name, + 'uuid': cls.uuid, + 'category': cls.attributes.get('Category', 'Data'), + 'description': cls.attributes.get('Description', f'{cls.name} data structure'), + 'display_name': cls.attributes.get('DisplayName', camel_to_human(cls.name)), + 'script_type': cls.attributes.get('ScriptType', True), + 'atomic': cls.attributes.get('Atomic', False), + 'includes': cls.includes, + 'properties': [self._property_to_dict(p) for p in cls.properties], + } + + def _enum_to_dict(self, cls: ClassInfo) -> Dict: + """Convert ClassInfo to enum dictionary.""" + full_name = f"{cls.namespace}::{cls.name}" if cls.namespace else cls.name + + return { + 'name': cls.name, + 'namespace': cls.namespace, + 'full_name': full_name, + 'uuid': cls.uuid, + 'category': cls.attributes.get('Category', 'Enums'), + 'description': cls.attributes.get('Description', f'{cls.name} enumeration'), + 'display_name': cls.attributes.get('DisplayName', camel_to_human(cls.name)), + 'script_type': cls.attributes.get('ScriptType', True), + 'is_flags': cls.attributes.get('Flags', False), + 'underlying_type': cls.attributes.get('UnderlyingType', 'int32_t'), + 'values': [self._enum_value_to_dict(v) for v in cls.enum_values], + } + + def _property_to_dict(self, prop: PropertyInfo) -> Dict: + """Convert PropertyInfo to dictionary.""" + prop_name = prop.name + return { + 'name': prop_name, + 'member_name': prop_name if prop_name.startswith('m_') else f'm_{prop_name}', + 'type': prop.cpp_type, + 'default': prop.default_value, + 'display_name': prop.attributes.get('DisplayName', camel_to_human(prop_name.lstrip('m_'))), + 'category': prop.attributes.get('Category', ''), + 'tooltip': prop.attributes.get('Tooltip', ''), + 'suffix': prop.attributes.get('Suffix', ''), + 'edit_anywhere': prop.attributes.get('EditAnywhere', False), + 'edit_defaults_only': prop.attributes.get('EditDefaultsOnly', False), + 'edit_instance_only': prop.attributes.get('EditInstanceOnly', False), + 'visible_anywhere': prop.attributes.get('VisibleAnywhere', False), + 'read_only': prop.attributes.get('ReadOnly', False), + 'script_read_write': prop.attributes.get('ScriptReadWrite', False), + 'script_read_only': prop.attributes.get('ScriptReadOnly', False), + 'expose_to_script': prop.attributes.get('ExposeToScript', True), + 'min': prop.attributes.get('Min'), + 'max': prop.attributes.get('Max'), + 'ui_min': prop.attributes.get('UIMin'), + 'ui_max': prop.attributes.get('UIMax'), + 'step': prop.attributes.get('Step'), + 'change_notify': prop.attributes.get('ChangeNotify', ''), + 'visibility': prop.attributes.get('Visibility', ''), + 'order': prop.attributes.get('Order', 0), + } + + def _function_to_dict(self, func: FunctionInfo) -> Dict: + """Convert FunctionInfo to dictionary.""" + return { + 'name': func.name, + 'display_name': func.attributes.get('DisplayName', camel_to_human(func.name)), + 'category': func.attributes.get('Category', ''), + 'tooltip': func.attributes.get('Tooltip', ''), + 'return_type': func.return_type, + 'parameters': func.parameters, + 'script_callable': func.attributes.get('ScriptCallable', True), + 'script_pure': func.attributes.get('ScriptPure', False), + 'call_in_editor': func.attributes.get('CallInEditor', False), + 'server': func.attributes.get('Server', False), + 'client': func.attributes.get('Client', False), + 'net_multicast': func.attributes.get('NetMulticast', False), + } + + def _enum_value_to_dict(self, val: EnumValueInfo) -> Dict: + """Convert EnumValueInfo to dictionary.""" + return { + 'name': val.name, + 'value': val.value, + 'display_name': val.attributes.get('DisplayName', camel_to_human(val.name)), + 'tooltip': val.attributes.get('Tooltip', ''), + 'hidden': val.attributes.get('Hidden', False), + } + + +class CppSourceParser: + """ + Parser for C++ source files to extract user function implementations. + Used to merge user code with generated reflection boilerplate. + """ + + def __init__(self, file_path: str): + self.file_path = file_path + self.content = "" + self.functions = {} # Dict of function_signature -> function_body + self.includes = [] + self.extra_code = [] # Any code not recognized as a function implementation + + def parse(self) -> Dict: + """Parse the source file and extract function implementations.""" + if not os.path.exists(self.file_path): + logger.debug(f"Source file not found: {self.file_path}") + return {'functions': {}, 'includes': [], 'extra_code': []} + + with open(self.file_path, 'r', encoding='utf-8', errors='ignore') as f: + self.content = f.read() + + self._extract_includes() + self._extract_functions() + + return { + 'functions': self.functions, + 'includes': self.includes, + 'extra_code': self.extra_code, + 'raw_content': self.content + } + + def _extract_includes(self): + """Extract #include statements.""" + include_pattern = re.compile(r'^\s*#include\s*[<"]([^>"]+)[>"]', re.MULTILINE) + self.includes = include_pattern.findall(self.content) + + def _extract_functions(self): + """Extract function implementations with their bodies.""" + # Pattern to match function definitions: ReturnType ClassName::FunctionName(params) { body } + # This handles multi-line function bodies by tracking brace depth + + # First, remove comments to avoid matching code inside comments + content_no_comments = self._remove_comments(self.content) + + # Pattern for function signature (simplified - handles common cases) + func_pattern = re.compile( + r'(?:^|\n)\s*' # Start of line + r'((?:[\w:*&<>,\s]+)?)' # Return type (optional, can be complex like const Type&) + r'\s+([\w:]+)::(\w+)' # ClassName::FunctionName + r'\s*\(([^)]*)\)' # Parameters + r'\s*(const)?' # Optional const + r'\s*(override)?' # Optional override + r'\s*\{', # Opening brace + re.MULTILINE + ) + + for match in func_pattern.finditer(content_no_comments): + return_type = match.group(1).strip() if match.group(1) else 'void' + class_name = match.group(2) + func_name = match.group(3) + params = match.group(4).strip() + is_const = bool(match.group(5)) + + # Find the matching closing brace + start_pos = match.end() - 1 # Position of opening brace + body_start = match.end() + body_end = self._find_matching_brace(content_no_comments, start_pos) + + if body_end > body_start: + body = content_no_comments[body_start:body_end].strip() + + # Create a key that identifies this function + key = f"{class_name}::{func_name}" + + self.functions[key] = { + 'return_type': return_type, + 'class_name': class_name, + 'func_name': func_name, + 'params': params, + 'is_const': is_const, + 'body': body, + 'full_signature': f"{return_type} {class_name}::{func_name}({params})" + (" const" if is_const else "") + } + + def _remove_comments(self, code: str) -> str: + """Remove C and C++ style comments from code.""" + # Remove single-line comments + code = re.sub(r'//.*?$', '', code, flags=re.MULTILINE) + # Remove multi-line comments + code = re.sub(r'/\*.*?\*/', '', code, flags=re.DOTALL) + return code + + def _find_matching_brace(self, code: str, start_pos: int) -> int: + """Find the position of the closing brace that matches the opening brace at start_pos.""" + if start_pos >= len(code) or code[start_pos] != '{': + return -1 + + depth = 1 + pos = start_pos + 1 + in_string = False + string_char = None + + while pos < len(code) and depth > 0: + char = code[pos] + prev_char = code[pos - 1] if pos > 0 else '' + + # Handle string literals + if char in '"\'': + if not in_string: + in_string = True + string_char = char + elif char == string_char and prev_char != '\\': + in_string = False + elif not in_string: + if char == '{': + depth += 1 + elif char == '}': + depth -= 1 + + pos += 1 + + return pos - 1 if depth == 0 else -1 + + +def parse_source(file_path: str) -> Dict: + """Parse a C++ source file and return extracted function implementations.""" + parser = CppSourceParser(file_path) + return parser.parse() + + +def parse_header(file_path: str) -> Dict: + """Parse a header file and return structured data.""" + parser = O3DEHeaderParser(file_path) + return parser.parse() + + +def main(): + parser = argparse.ArgumentParser(description='O3DEReflect Header Parser') + parser.add_argument('--input', '-i', required=True, help='Input header file') + parser.add_argument('--output-dir', '-o', help='Output directory for generated files') + parser.add_argument('--output-json', '-j', help='Output JSON file with parsed data') + parser.add_argument('--template-dir', '-t', help='Template directory (defaults to script directory)') + parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output') + + args = parser.parse_args() + + if args.verbose: + logger.setLevel(logging.DEBUG) + + # Parse header + logger.info(f"Parsing: {args.input}") + parsed_data = parse_header(args.input) + + # Also parse the corresponding .cpp file if it exists + cpp_file = args.input.replace('.h', '.cpp') + user_code = parse_source(cpp_file) + if user_code['functions']: + logger.info(f"Found user implementations in: {cpp_file}") + parsed_data['user_code'] = user_code + + if args.verbose: + logger.debug(f"Parsed data: {json.dumps(parsed_data, indent=2, default=str)}") + + # Output JSON if requested + if args.output_json: + with open(args.output_json, 'w', encoding='utf-8') as f: + json.dump(parsed_data, f, indent=2, default=str) + logger.info(f"Wrote JSON: {args.output_json}") + + # Generate code if output directory specified + if args.output_dir: + # Import the generator from O3DEReflectGen + script_dir = os.path.dirname(os.path.abspath(__file__)) + template_dir = args.template_dir if args.template_dir else script_dir + sys.path.insert(0, script_dir) + from O3DEReflectGen import O3DEReflectGenerator + + generator = O3DEReflectGenerator(template_dir, args.output_dir) + generated_files = generator.generate(parsed_data, args.input) + logger.info(f"Generated {len(generated_files)} files") + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/Gems/O3DEReflect/Code/Include/O3DEReflect/AutoGen/O3DEReflect_Common.jinja b/Gems/O3DEReflect/Code/Include/O3DEReflect/AutoGen/O3DEReflect_Common.jinja new file mode 100644 index 0000000000..c13445992f --- /dev/null +++ b/Gems/O3DEReflect/Code/Include/O3DEReflect/AutoGen/O3DEReflect_Common.jinja @@ -0,0 +1,98 @@ +{# + Copyright (c) Contributors to the Open 3D Engine Project. + For complete copyright and license terms please see the LICENSE at the root of this distribution. + + SPDX-License-Identifier: Apache-2.0 OR MIT + + O3DEReflect Common Macros - Shared Jinja2 macros for code generation +#} + +{# Convert text to have uppercase first letter #} +{% macro UpperFirst(text) %}{{ text[0] | upper}}{{ text[1:] }}{% endmacro %} + +{# Convert text to have lowercase first letter #} +{% macro LowerFirst(text) %}{{ text[0] | lower}}{{ text[1:] }}{% endmacro %} + +{# Convert camelCase to Human Readable #} +{% macro CamelToHuman(text) %}{{ text[0] | upper }}{% for char in text[1:] %}{% if char.isupper() %} {% endif %}{{ char }}{% endfor %}{% endmacro %} + +{# Generate display name from variable name (strip m_ prefix, convert to human readable) #} +{% macro GenerateDisplayName(name) %}{% if name.startswith('m_') %}{{ CamelToHuman(name[2:]) }}{% else %}{{ CamelToHuman(name) }}{% endif %}{% endmacro %} + +{# Get fully qualified name with namespace #} +{% macro FullyQualifiedName(element) %}{% if element.attrib.get('Namespace') %}{{ element.attrib['Namespace'] }}::{% endif %}{{ element.attrib['Name'] }}{% endmacro %} + +{# Generate member variable name from property name #} +{% macro MemberName(name) %}{% if name.startswith('m_') %}{{ name }}{% else %}m_{{ name }}{% endif %}{% endmacro %} + +{# Generate getter name from property name #} +{% macro GetterName(name) %}{% if name.startswith('m_') %}Get{{ UpperFirst(name[2:]) }}{% else %}Get{{ UpperFirst(name) }}{% endif %}{% endmacro %} + +{# Generate setter name from property name #} +{% macro SetterName(name) %}{% if name.startswith('m_') %}Set{{ UpperFirst(name[2:]) }}{% else %}Set{{ UpperFirst(name) }}{% endif %}{% endmacro %} + +{# Check if property is editable in editor #} +{% macro IsEditable(prop) %}{{ prop.attrib.get('EditAnywhere', 'true')|booleanTrue or prop.attrib.get('EditDefaultsOnly', 'false')|booleanTrue or prop.attrib.get('EditInstanceOnly', 'false')|booleanTrue }}{% endmacro %} + +{# Check if property is visible in editor #} +{% macro IsVisible(prop) %}{{ IsEditable(prop) or prop.attrib.get('VisibleAnywhere', 'false')|booleanTrue }}{% endmacro %} + +{# Check if property is exposed to script #} +{% macro IsScriptExposed(prop) %}{{ prop.attrib.get('ExposeToScript', 'true')|booleanTrue or prop.attrib.get('BlueprintReadWrite', 'false')|booleanTrue or prop.attrib.get('BlueprintReadOnly', 'false')|booleanTrue }}{% endmacro %} + +{# Generate edit context attributes for a property #} +{% macro GenerateEditAttributes(prop) %} +{% set displayName = prop.attrib.get('DisplayName', '') %} +{% set tooltip = prop.attrib.get('Tooltip', '') %} +{% set suffix = prop.attrib.get('Suffix', '') %} +{% if displayName %} + ->Attribute(AZ::Edit::Attributes::NameLabelOverride, "{{ displayName }}") +{% endif %} +{% if tooltip %} + ->Attribute(AZ::Edit::Attributes::DescriptionTextOverride, "{{ tooltip }}") +{% endif %} +{% if suffix %} + ->Attribute(AZ::Edit::Attributes::Suffix, "{{ suffix }}") +{% endif %} +{% if prop.attrib.get('Min') %} + ->Attribute(AZ::Edit::Attributes::Min, {{ prop.attrib['Min'] }}) +{% endif %} +{% if prop.attrib.get('Max') %} + ->Attribute(AZ::Edit::Attributes::Max, {{ prop.attrib['Max'] }}) +{% endif %} +{% if prop.attrib.get('ReadOnly', 'false')|booleanTrue %} + ->Attribute(AZ::Edit::Attributes::ReadOnly, true) +{% endif %} +{% if prop.attrib.get('ChangeNotify') %} + ->Attribute(AZ::Edit::Attributes::ChangeNotify, &{{ prop.attrib['ChangeNotify'] }}) +{% endif %} +{% endmacro %} + +{# Generate property category path #} +{% macro PropertyCategory(prop, defaultCategory) %}{% if prop.attrib.get('Category') %}"{{ prop.attrib['Category'] }}"{% else %}"{{ defaultCategory }}"{% endif %}{% endmacro %} + +{# Iterate over all properties in a component/struct #} +{% macro ParseProperties(element) %} +{% for prop in element.findall('Property') %} +{{ caller(prop) }} +{% endfor %} +{% endmacro %} + +{# Iterate over all functions in a component #} +{% macro ParseFunctions(element) %} +{% for func in element.findall('Function') %} +{{ caller(func) }} +{% endfor %} +{% endmacro %} + +{# Iterate over services of a specific type #} +{% macro ParseServices(element, serviceType) %} +{% for service in element.findall('Service') %} +{% if service.attrib.get('Type') == serviceType %} +{{ caller(service) }} +{% endif %} +{% endfor %} +{% endmacro %} + +{# Generate service CRC #} +{% macro ServiceCRC(serviceName) %}AZ_CRC_CE("{{ serviceName }}"){% endmacro %} diff --git a/Gems/O3DEReflect/Code/Include/O3DEReflect/AutoGen/O3DEReflect_Header.jinja b/Gems/O3DEReflect/Code/Include/O3DEReflect/AutoGen/O3DEReflect_Header.jinja new file mode 100644 index 0000000000..433c2d3e7d --- /dev/null +++ b/Gems/O3DEReflect/Code/Include/O3DEReflect/AutoGen/O3DEReflect_Header.jinja @@ -0,0 +1,192 @@ +{# + Copyright (c) Contributors to the Open 3D Engine Project. + For complete copyright and license terms please see the LICENSE at the root of this distribution. + + SPDX-License-Identifier: Apache-2.0 OR MIT + + O3DEReflect Header Template - Generates component/struct/enum header files +#} +{% import 'O3DEReflect_Common.jinja' as Common %} +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + * AUTO-GENERATED FILE - DO NOT EDIT + * Generated by O3DEReflect from {{ dataFiles[0] }} + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +{% for include in root.findall('Include') %} +#include <{{ include.attrib['File'] }}> +{% endfor %} +{% for baseClass in root.findall('BaseClass') %} +{% if baseClass.attrib.get('Include') %} +#include <{{ baseClass.attrib['Include'] }}> +{% endif %} +{% endfor %} + +{% if root.attrib.get('Namespace') %} +namespace {{ root.attrib['Namespace'] }} +{ +{% endif %} +{% if root.tag == 'Component' %} +{{ ComponentHeader(root) }} +{% elif root.tag == 'Struct' %} +{{ StructHeader(root) }} +{% elif root.tag == 'Enum' %} +{{ EnumHeader(root) }} +{% endif %} +{% if root.attrib.get('Namespace') %} +} // namespace {{ root.attrib['Namespace'] }} +{% endif %} + +{# ============================================================================ + Component Header Generation + ============================================================================ #} +{% macro ComponentHeader(comp) %} +{% set className = comp.attrib['Name'] %} +{% set uuid = comp.attrib.get('Uuid', CreateHashGuid(Common.FullyQualifiedName(comp))) %} +{% set category = comp.attrib.get('Category', 'General') %} +{% set description = comp.attrib.get('Description', className + ' component') %} + //! {{ description }} + class {{ className }} +{% set baseClasses = comp.findall('BaseClass') %} +{% if baseClasses %} + : {% for base in baseClasses %}{% if loop.index > 1 %}, {% endif %}public {% if base.attrib.get('Namespace') %}{{ base.attrib['Namespace'] }}::{% endif %}{{ base.attrib['Name'] }}{% endfor %} + +{% else %} + : public AZ::Component +{% endif %} + { + public: + AZ_COMPONENT_DECL({{ className }}); + + static void Reflect(AZ::ReflectContext* context); + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent); + + {{ className }}() = default; + ~{{ className }}() override = default; + + // Getters and Setters +{% for prop in comp.findall('Property') %} +{% set propName = prop.attrib['Name'] %} +{% set propType = prop.attrib['Type'] %} +{% set memberName = Common.MemberName(propName) %} + //! Get {{ Common.GenerateDisplayName(propName) }} + const {{ propType }}& {{ Common.GetterName(propName) }}() const { return {{ memberName }}; } +{% if not prop.attrib.get('ReadOnly', 'false')|booleanTrue %} + //! Set {{ Common.GenerateDisplayName(propName) }} + void {{ Common.SetterName(propName) }}(const {{ propType }}& value) { {{ memberName }} = value; } +{% endif %} + +{% endfor %} + protected: + // AZ::Component interface + void Init() override; + void Activate() override; + void Deactivate() override; + +{% for func in comp.findall('Function') %} +{% set funcName = func.attrib['Name'] %} +{% set returnElem = func.find('Return') %} +{% set returnType = returnElem.attrib['Type'] if returnElem is not none else 'void' %} +{% set params = func.findall('Param') %} + //! {{ func.attrib.get('Tooltip', funcName) }} + {{ returnType }} {{ funcName }}({% for param in params %}{% if loop.index > 1 %}, {% endif %}{{ param.attrib['Type'] }} {{ param.attrib['Name'] }}{% if param.attrib.get('Default') %} = {{ param.attrib['Default'] }}{% endif %}{% endfor %}); + +{% endfor %} + private: + // Member variables +{% for prop in comp.findall('Property') %} +{% set propName = prop.attrib['Name'] %} +{% set propType = prop.attrib['Type'] %} +{% set propDefault = prop.attrib.get('Default', '') %} +{% set memberName = Common.MemberName(propName) %} + {{ propType }} {{ memberName }}{% if propDefault %} = {{ propDefault }}{% endif %}; +{% endfor %} + }; +{% endmacro %} + +{# ============================================================================ + Struct Header Generation + ============================================================================ #} +{% macro StructHeader(struct) %} +{% set structName = struct.attrib['Name'] %} +{% set uuid = struct.attrib.get('Uuid', CreateHashGuid(Common.FullyQualifiedName(struct))) %} +{% set description = struct.attrib.get('Description', structName + ' data structure') %} + //! {{ description }} + struct {{ structName }} + { + AZ_TYPE_INFO({{ structName }}, "{{ uuid }}"); + AZ_CLASS_ALLOCATOR({{ structName }}, AZ::SystemAllocator); + + static void Reflect(AZ::ReflectContext* context); + + {{ structName }}() = default; + ~{{ structName }}() = default; + + bool operator==(const {{ structName }}& rhs) const; + bool operator!=(const {{ structName }}& rhs) const { return !(*this == rhs); } + + // Member variables +{% for prop in struct.findall('Property') %} +{% set propName = prop.attrib['Name'] %} +{% set propType = prop.attrib['Type'] %} +{% set propDefault = prop.attrib.get('Default', '') %} +{% set memberName = Common.MemberName(propName) %} + {{ propType }} {{ memberName }}{% if propDefault %} = {{ propDefault }}{% endif %}; +{% endfor %} + }; +{% endmacro %} + +{# ============================================================================ + Enum Header Generation + ============================================================================ #} +{% macro EnumHeader(enum) %} +{% set enumName = enum.attrib['Name'] %} +{% set uuid = enum.attrib.get('Uuid', CreateHashGuid(Common.FullyQualifiedName(enum))) %} +{% set underlyingType = enum.attrib.get('UnderlyingType', 'int32_t') %} +{% set description = enum.attrib.get('Description', enumName + ' enumeration') %} +{% set isFlags = enum.attrib.get('Flags', 'false')|booleanTrue %} + //! {{ description }} + enum class {{ enumName }} : {{ underlyingType }} + { +{% set valueCounter = {'count': 0} %} +{% for val in enum.findall('EnumValue') %} +{% set valueName = val.attrib['Name'] %} +{% set valueNum = val.attrib.get('Value') %} +{% if valueNum %} + {{ valueName }} = {{ valueNum }}, +{% elif isFlags %} + {{ valueName }} = 1 << {{ valueCounter['count'] }}, +{% set _ = valueCounter.update({'count': valueCounter['count'] + 1}) %} +{% else %} + {{ valueName }}, +{% endif %} +{% endfor %} + }; + +{% if isFlags %} + AZ_DEFINE_ENUM_BITWISE_OPERATORS({{ enumName }}); +{% endif %} + + // Enum reflection helper + class {{ enumName }}Reflect + { + public: + static void Reflect(AZ::ReflectContext* context); + }; +{% endmacro %} diff --git a/Gems/O3DEReflect/Code/Include/O3DEReflect/AutoGen/O3DEReflect_Source.jinja b/Gems/O3DEReflect/Code/Include/O3DEReflect/AutoGen/O3DEReflect_Source.jinja new file mode 100644 index 0000000000..e9952178ac --- /dev/null +++ b/Gems/O3DEReflect/Code/Include/O3DEReflect/AutoGen/O3DEReflect_Source.jinja @@ -0,0 +1,318 @@ +{# + Copyright (c) Contributors to the Open 3D Engine Project. + For complete copyright and license terms please see the LICENSE at the root of this distribution. + + SPDX-License-Identifier: Apache-2.0 OR MIT + + O3DEReflect Source Template - Generates reflection implementation files +#} +{% import 'O3DEReflect_Common.jinja' as Common %} +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + * AUTO-GENERATED FILE - DO NOT EDIT + * Generated by O3DEReflect from {{ dataFiles[0] }} + */ + +#include "{{ fileprefix }}.AutoReflect.h" + +#include +#include +#include +#include +#include + +{% if root.attrib.get('Namespace') %} +namespace {{ root.attrib['Namespace'] }} +{ +{% endif %} +{% if root.tag == 'Component' %} +{{ ComponentSource(root) }} +{% elif root.tag == 'Struct' %} +{{ StructSource(root) }} +{% elif root.tag == 'Enum' %} +{{ EnumSource(root) }} +{% endif %} +{% if root.attrib.get('Namespace') %} +} // namespace {{ root.attrib['Namespace'] }} +{% endif %} + +{# ============================================================================ + Component Source Generation + ============================================================================ #} +{% macro ComponentSource(comp) %} +{% set className = comp.attrib['Name'] %} +{% set uuid = comp.attrib.get('Uuid', CreateHashGuid(Common.FullyQualifiedName(comp))) %} +{% set category = comp.attrib.get('Category', 'General') %} +{% set description = comp.attrib.get('Description', className + ' component') %} +{% set displayName = comp.attrib.get('DisplayName', Common.CamelToHuman(className)) %} +{% set icon = comp.attrib.get('Icon', '') %} +{% set hideInEditor = comp.attrib.get('HideInEditor', 'false')|booleanTrue %} +{% set menuCategory = comp.attrib.get('AppearsInAddComponentMenu', 'Game') %} + + AZ_COMPONENT_IMPL({{ className }}, "{{ className }}", "{{ uuid }}"{% for base in comp.findall('BaseClass') %}, {% if base.attrib.get('Namespace') %}{{ base.attrib['Namespace'] }}::{% endif %}{{ base.attrib['Name'] }}{% endfor %}); + + void {{ className }}::Reflect(AZ::ReflectContext* context) + { + // SerializeContext + if (auto* serializeContext = azrtti_cast(context)) + { + serializeContext->Class<{{ className }}{% for base in comp.findall('BaseClass') %}, {% if base.attrib.get('Namespace') %}{{ base.attrib['Namespace'] }}::{% endif %}{{ base.attrib['Name'] }}{% endfor %}>() + ->Version(1) +{% for prop in comp.findall('Property') %} +{% set propName = prop.attrib['Name'] %} +{% set memberName = Common.MemberName(propName) %} + ->Field("{{ propName }}", &{{ className }}::{{ memberName }}) +{% endfor %} + ; + + // EditContext + if (AZ::EditContext* editContext = serializeContext->GetEditContext()) + { + editContext->Class<{{ className }}>("{{ displayName }}", "{{ description }}") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Category, "{{ category }}") +{% if icon %} + ->Attribute(AZ::Edit::Attributes::Icon, "{{ icon }}") +{% endif %} +{% if not hideInEditor %} + ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("{{ menuCategory }}")) +{% endif %} + ->Attribute(AZ::Edit::Attributes::AutoExpand, true) +{% for prop in comp.findall('Property') %} +{% set propName = prop.attrib['Name'] %} +{% set propType = prop.attrib['Type'] %} +{% set memberName = Common.MemberName(propName) %} +{% set propDisplayName = prop.attrib.get('DisplayName', Common.GenerateDisplayName(propName)) %} +{% set propTooltip = prop.attrib.get('Tooltip', '') %} +{% set propCategory = prop.attrib.get('Category', '') %} +{% if propCategory %} + ->ClassElement(AZ::Edit::ClassElements::Group, "{{ propCategory }}") + ->Attribute(AZ::Edit::Attributes::AutoExpand, true) +{% endif %} + ->DataElement(AZ::Edit::UIHandlers::Default, &{{ className }}::{{ memberName }}, "{{ propDisplayName }}", "{{ propTooltip }}") +{{ Common.GenerateEditAttributes(prop) }} +{% endfor %} + ; + } + } + + // BehaviorContext + if (auto* behaviorContext = azrtti_cast(context)) + { + behaviorContext->Class<{{ className }}>("{{ className }}") + ->Attribute(AZ::Script::Attributes::Category, "{{ category }}") +{% for prop in comp.findall('Property') %} +{% if prop.attrib.get('ExposeToScript', 'true')|booleanTrue or prop.attrib.get('BlueprintReadWrite', 'false')|booleanTrue %} +{% set propName = prop.attrib['Name'] %} +{% set cleanName = propName[2:] if propName.startswith('m_') else propName %} +{% if prop.attrib.get('BlueprintReadOnly', 'false')|booleanTrue %} + ->Property("{{ cleanName }}", + []({{ className }}* self) { return self->{{ Common.GetterName(propName) }}(); }, + nullptr) +{% else %} + ->Property("{{ cleanName }}", + []({{ className }}* self) { return self->{{ Common.GetterName(propName) }}(); }, + []({{ className }}* self, const {{ prop.attrib['Type'] }}& value) { self->{{ Common.SetterName(propName) }}(value); }) +{% endif %} +{% endif %} +{% endfor %} +{% for func in comp.findall('Function') %} +{% if func.attrib.get('BlueprintCallable', 'true')|booleanTrue %} +{% set funcName = func.attrib['Name'] %} +{% set funcDisplayName = func.attrib.get('DisplayName', funcName) %} +{% set funcCategory = func.attrib.get('Category', category) %} + ->Method("{{ funcName }}", &{{ className }}::{{ funcName }}) + ->Attribute(AZ::Script::Attributes::Category, "{{ funcCategory }}") +{% endif %} +{% endfor %} + ; + } + } + + void {{ className }}::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { +{% for service in comp.findall('Service') %} +{% if service.attrib.get('Type') == 'Provides' %} + provided.push_back({{ Common.ServiceCRC(service.attrib['Name']) }}); +{% endif %} +{% endfor %} + } + + void {{ className }}::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { +{% for service in comp.findall('Service') %} +{% if service.attrib.get('Type') == 'Incompatible' %} + incompatible.push_back({{ Common.ServiceCRC(service.attrib['Name']) }}); +{% endif %} +{% endfor %} + } + + void {{ className }}::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required) + { +{% for service in comp.findall('Service') %} +{% if service.attrib.get('Type') == 'Requires' %} + required.push_back({{ Common.ServiceCRC(service.attrib['Name']) }}); +{% endif %} +{% endfor %} + } + + void {{ className }}::GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent) + { +{% for service in comp.findall('Service') %} +{% if service.attrib.get('Type') == 'Dependent' %} + dependent.push_back({{ Common.ServiceCRC(service.attrib['Name']) }}); +{% endif %} +{% endfor %} + } + + void {{ className }}::Init() + { + // TODO: Initialize member variables if needed + } + + void {{ className }}::Activate() + { + // TODO: Subscribe to buses, start systems + } + + void {{ className }}::Deactivate() + { + // TODO: Unsubscribe from buses, stop systems + } + +{% for func in comp.findall('Function') %} +{% set funcName = func.attrib['Name'] %} +{% set returnElem = func.find('Return') %} +{% set returnType = returnElem.attrib['Type'] if returnElem is not none else 'void' %} +{% set params = func.findall('Param') %} + {{ returnType }} {{ className }}::{{ funcName }}({% for param in params %}{% if loop.index > 1 %}, {% endif %}{{ param.attrib['Type'] }} {{ param.attrib['Name'] }}{% endfor %}) + { + // TODO: Implement {{ funcName }} +{% if returnType != 'void' %} + return {}; +{% endif %} + } + +{% endfor %} +{% endmacro %} + +{# ============================================================================ + Struct Source Generation + ============================================================================ #} +{% macro StructSource(struct) %} +{% set structName = struct.attrib['Name'] %} +{% set uuid = struct.attrib.get('Uuid', CreateHashGuid(Common.FullyQualifiedName(struct))) %} +{% set category = struct.attrib.get('Category', 'Data') %} +{% set description = struct.attrib.get('Description', structName + ' data structure') %} +{% set displayName = struct.attrib.get('DisplayName', Common.CamelToHuman(structName)) %} +{% set blueprintType = struct.attrib.get('BlueprintType', 'true')|booleanTrue %} + + void {{ structName }}::Reflect(AZ::ReflectContext* context) + { + // SerializeContext + if (auto* serializeContext = azrtti_cast(context)) + { + serializeContext->Class<{{ structName }}>() + ->Version(1) +{% for prop in struct.findall('Property') %} +{% set propName = prop.attrib['Name'] %} +{% set memberName = Common.MemberName(propName) %} + ->Field("{{ propName }}", &{{ structName }}::{{ memberName }}) +{% endfor %} + ; + + // EditContext + if (AZ::EditContext* editContext = serializeContext->GetEditContext()) + { + editContext->Class<{{ structName }}>("{{ displayName }}", "{{ description }}") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Category, "{{ category }}") +{% for prop in struct.findall('Property') %} +{% set propName = prop.attrib['Name'] %} +{% set memberName = Common.MemberName(propName) %} +{% set propDisplayName = prop.attrib.get('DisplayName', Common.GenerateDisplayName(propName)) %} +{% set propTooltip = prop.attrib.get('Tooltip', '') %} + ->DataElement(AZ::Edit::UIHandlers::Default, &{{ structName }}::{{ memberName }}, "{{ propDisplayName }}", "{{ propTooltip }}") +{{ Common.GenerateEditAttributes(prop) }} +{% endfor %} + ; + } + } + +{% if blueprintType %} + // BehaviorContext + if (auto* behaviorContext = azrtti_cast(context)) + { + behaviorContext->Class<{{ structName }}>("{{ structName }}") + ->Attribute(AZ::Script::Attributes::Category, "{{ category }}") + ->Constructor() +{% for prop in struct.findall('Property') %} +{% if prop.attrib.get('ExposeToScript', 'true')|booleanTrue %} +{% set propName = prop.attrib['Name'] %} +{% set memberName = Common.MemberName(propName) %} +{% set cleanName = propName[2:] if propName.startswith('m_') else propName %} + ->Property("{{ cleanName }}", BehaviorValueProperty(&{{ structName }}::{{ memberName }})) +{% endif %} +{% endfor %} + ; + } +{% endif %} + } + + bool {{ structName }}::operator==(const {{ structName }}& rhs) const + { + return +{% for prop in struct.findall('Property') %} +{% set propName = prop.attrib['Name'] %} +{% set memberName = Common.MemberName(propName) %} +{% if loop.first %} + {{ memberName }} == rhs.{{ memberName }} +{% else %} + && {{ memberName }} == rhs.{{ memberName }} +{% endif %} +{% endfor %} + ; + } + +{% endmacro %} + +{# ============================================================================ + Enum Source Generation + ============================================================================ #} +{% macro EnumSource(enum) %} +{% set enumName = enum.attrib['Name'] %} +{% set uuid = enum.attrib.get('Uuid', CreateHashGuid(Common.FullyQualifiedName(enum))) %} +{% set category = enum.attrib.get('Category', 'Enums') %} +{% set description = enum.attrib.get('Description', enumName + ' enumeration') %} +{% set blueprintType = enum.attrib.get('BlueprintType', 'true')|booleanTrue %} + + void {{ enumName }}Reflect::Reflect(AZ::ReflectContext* context) + { + // BehaviorContext + if (auto* behaviorContext = azrtti_cast(context)) + { + behaviorContext->Enum< +{% for val in enum.findall('EnumValue') %} +{% set valueName = val.attrib['Name'] %} +{% if not loop.last %} + static_cast({{ enumName }}::{{ valueName }}), +{% else %} + static_cast({{ enumName }}::{{ valueName }}) +{% endif %} +{% endfor %} + >("{{ enumName }}") +{% for val in enum.findall('EnumValue') %} +{% set valueName = val.attrib['Name'] %} +{% set displayName = val.attrib.get('DisplayName', Common.CamelToHuman(valueName)) %} + ->Value("{{ displayName }}", static_cast({{ enumName }}::{{ valueName }})) +{% endfor %} + ->Attribute(AZ::Script::Attributes::Category, "{{ category }}") + ; + } + } + +{% endmacro %} diff --git a/Gems/O3DEReflect/Code/Include/O3DEReflect/O3DEReflectMacros.h b/Gems/O3DEReflect/Code/Include/O3DEReflect/O3DEReflectMacros.h new file mode 100644 index 0000000000..5a336c1fba --- /dev/null +++ b/Gems/O3DEReflect/Code/Include/O3DEReflect/O3DEReflectMacros.h @@ -0,0 +1,267 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + * O3DE Reflect System - Reflection macros for simplified component/struct/enum creation + * + * Header-Macro Parsing + * ======================== + * These macros are designed to be parsed by the O3DEReflect code generator. + * They expand to empty or minimal code at compile time, while providing + * metadata that the generator uses to create reflection code. + * + * Usage: + * O3DE_CLASS(Category="Gameplay", Description="My awesome component") + * class MyComponent : public AZ::Component { + * O3DE_GENERATED_BODY() + * + * O3DE_PROPERTY(EditAnywhere, Category="Movement", Tooltip="Speed in m/s", Min=0.0f, Max=100.0f) + * float m_speed = 10.0f; + * + * O3DE_FUNCTION(ScriptCallable, Category="Actions") + * void DoSomething(); + * }; + */ + +#pragma once + +#include +#include +#include +#include + +// ============================================================================ +// MACRO IMPLEMENTATION STRATEGY +// ============================================================================ +// +// These macros serve dual purposes: +// 1. At compile time: They expand to minimal/empty code +// 2. At parse time: The O3DEReflect generator extracts metadata from them +// +// The generator parses the source file and looks for these macro patterns, +// then generates the appropriate reflection code (.generated.h/.generated.cpp) +// +// ============================================================================ + +// ============================================================================ +// Property Visibility Specifiers +// ============================================================================ +// These are used as bare tokens within O3DE_PROPERTY() +// +// Editor visibility - controls how property appears in Entity Inspector: +// EditAnywhere - Editable in editor on instances and archetypes +// EditDefaultsOnly - Editable only on archetypes/prefabs +// EditInstanceOnly - Editable only on instances +// VisibleAnywhere - Visible but not editable +// VisibleDefaultsOnly - Visible only on defaults +// VisibleInstanceOnly - Visible only on instances +// +// Script visibility - controls script access: +// ScriptReadWrite - Readable and writable from Lua/ScriptCanvas +// ScriptReadOnly - Readable only from Lua/ScriptCanvas +// +// Note: These are parsed as string tokens, not as macro values + +// ============================================================================ +// O3DE_CLASS - Mark a class for reflection +// ============================================================================ +// +// Attributes: +// Category - Category in Add Component menu (e.g., "Gameplay/Movement") +// Description - Tooltip description for the class +// Icon - Path to icon file (e.g., "Icons/Components/MyComponent.svg") +// HideInEditor - If true, component won't appear in Add Component menu +// Abstract - If true, class cannot be instantiated directly +// +// Example: +// O3DE_CLASS(Category="Gameplay", Description="Handles player movement") +// class PlayerMovementComponent : public AZ::Component { ... }; +// +#define O3DE_CLASS(...) \ + /* This macro is parsed by O3DEReflect code generator */ \ + /* Attributes: __VA_ARGS__ */ + +// ============================================================================ +// O3DE_COMPONENT - Declare component with UUID (use instead of AZ_COMPONENT) +// ============================================================================ +// +// Automatically provides: +// - AZ_COMPONENT_DECL macro expansion +// - UUID generation (auto or explicit) +// - Service declarations +// +// Attributes: +// ProvidesServices - Comma-separated service names this component provides +// RequiresServices - Services this component requires +// IncompatibleServices - Services this component is incompatible with +// DependentServices - Services that should activate before this component +// +// Example: +// O3DE_COMPONENT(MyComponent, "{12345678-1234-1234-1234-123456789ABC}", +// ProvidesServices="MyService", +// RequiresServices="TransformService") +// +#define O3DE_COMPONENT(ClassName, ...) \ + AZ_COMPONENT_DECL(ClassName) \ + /* UUID and services parsed by O3DEReflect code generator */ \ + /* Attributes: __VA_ARGS__ */ + +// ============================================================================ +// O3DE_PROPERTY - Mark a member for serialization and editor exposure +// ============================================================================ +// +// Visibility Specifiers: +// EditAnywhere - Editable everywhere in editor +// EditDefaultsOnly - Editable only on prefabs/archetypes +// EditInstanceOnly - Editable only on instances +// VisibleAnywhere - Visible but read-only in editor +// ScriptReadWrite - Exposed to Lua/ScriptCanvas read+write +// ScriptReadOnly - Exposed to Lua/ScriptCanvas read-only +// +// Metadata Attributes: +// Category - Property category in inspector (e.g., "Movement|Speed") +// Tooltip - Hover tooltip text +// DisplayName - Override display name (default: variable name) +// Min - Minimum value for numeric types +// Max - Maximum value for numeric types +// UIMin - UI slider minimum (can differ from Min) +// UIMax - UI slider maximum (can differ from Max) +// Suffix - Unit suffix (e.g., "m/s", "degrees") +// ChangeNotify - Function to call when value changes +// +// Examples: +// O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, Category="Movement", Tooltip="Speed in m/s") +// float m_speed = 10.0f; +// +// O3DE_PROPERTY(EditAnywhere, Category="Combat", Min=0, Max=100) +// int m_health = 100; +// +// O3DE_PROPERTY(VisibleAnywhere, ScriptReadOnly) +// AZ::EntityId m_target; +// +#define O3DE_PROPERTY(...) \ + /* This macro is parsed by O3DEReflect code generator */ \ + /* Attributes: __VA_ARGS__ */ + +// ============================================================================ +// O3DE_FUNCTION - Expose a function to reflection/scripting +// ============================================================================ +// +// Specifiers: +// ScriptCallable - Can be called from Lua/ScriptCanvas +// ScriptPure - Pure function (no side effects, can be cached) +// CallInEditor - Can be invoked from editor buttons +// Server - Only runs on server (multiplayer) +// Client - Only runs on client (multiplayer) +// NetMulticast - Runs on all connected clients +// +// Attributes: +// Category - Category in script node palette +// DisplayName - Override function display name +// Tooltip - Hover tooltip text +// +// Example: +// O3DE_FUNCTION(ScriptCallable, Category="Combat", Tooltip="Deals damage to target") +// void DealDamage(float amount, AZ::EntityId target); +// +#define O3DE_FUNCTION(...) \ + /* This macro is parsed by O3DEReflect code generator */ \ + /* Attributes: __VA_ARGS__ */ + +// ============================================================================ +// O3DE_STRUCT - Mark a standalone struct for reflection +// ============================================================================ +// +// Attributes: +// ScriptType - Expose to Lua/ScriptCanvas +// Atomic - Always serialized as a whole unit +// Category - Category for organization +// Description - Tooltip description +// +// Example: +// O3DE_STRUCT(ScriptType, Category="Data") +// struct DamageInfo +// { +// O3DE_PROPERTY(EditAnywhere) +// float damage = 0.0f; +// +// O3DE_PROPERTY(EditAnywhere) +// AZ::EntityId instigator; +// }; +// +#define O3DE_STRUCT(...) \ + /* This macro is parsed by O3DEReflect code generator */ \ + /* Attributes: __VA_ARGS__ */ + +// ============================================================================ +// O3DE_ENUM - Mark an enum for reflection +// ============================================================================ +// +// Attributes: +// ScriptType - Expose to Lua/ScriptCanvas +// Flags - Treat as bitmask/flags enum +// Category - Category for organization +// Description - Tooltip description +// +// Use O3DE_ENUM_VALUE to provide display names for values: +// +// Example: +// O3DE_ENUM(ScriptType, Category="AI") +// enum class AIState +// { +// O3DE_ENUM_VALUE(Idle, DisplayName="Standing Idle") +// Idle, +// +// O3DE_ENUM_VALUE(Patrol, DisplayName="Patrolling") +// Patrol, +// +// O3DE_ENUM_VALUE(Combat, DisplayName="In Combat") +// Combat +// }; +// +#define O3DE_ENUM(...) \ + /* This macro is parsed by O3DEReflect code generator */ \ + /* Attributes: __VA_ARGS__ */ + +#define O3DE_ENUM_VALUE(Value, ...) \ + /* Attributes for enum value: __VA_ARGS__ */ + +// ============================================================================ +// O3DE_GENERATED_BODY - Include generated code in class body +// ============================================================================ +// +// Place this at the beginning of your class body to include generated code: +// +// Example: +// O3DE_CLASS(Category="Gameplay") +// class MyComponent : public AZ::Component +// { +// O3DE_GENERATED_BODY() +// public: +// // Your code here +// }; +// +// This will be replaced with #include "MyComponent.generated.h" pointing to +// the auto-generated reflection code. +// +#define O3DE_GENERATED_BODY() \ + /* Placeholder for generated includes - replaced during code generation */ + +// ============================================================================ +// O3DE_IMPLEMENT_REFLECT - Generate Reflect() implementation +// ============================================================================ +// +// Place in .cpp file to include generated Reflect() implementation: +// +// Example (in MyComponent.cpp): +// O3DE_IMPLEMENT_REFLECT(MyComponent) +// +// This generates the full Reflect() function with SerializeContext, +// EditContext, and BehaviorContext registrations based on O3DE_PROPERTY +// and O3DE_FUNCTION annotations. +// +#define O3DE_IMPLEMENT_REFLECT(ClassName) \ + /* Placeholder - include generated source file */ \ + /* #include "ClassName.generated.cpp" */ diff --git a/Gems/O3DEReflect/Code/Include/O3DEReflect/O3DEReflectTypes.h b/Gems/O3DEReflect/Code/Include/O3DEReflect/O3DEReflectTypes.h new file mode 100644 index 0000000000..74ba1dadc0 --- /dev/null +++ b/Gems/O3DEReflect/Code/Include/O3DEReflect/O3DEReflectTypes.h @@ -0,0 +1,156 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + * O3DE Reflect System - Common types and utilities + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace O3DEReflect +{ + //! Property visibility flags + enum class PropertyVisibility : AZ::u8 + { + None = 0, + EditAnywhere = 1 << 0, + EditDefaultsOnly = 1 << 1, + EditInstanceOnly = 1 << 2, + VisibleAnywhere = 1 << 3, + VisibleDefaultsOnly = 1 << 4, + VisibleInstanceOnly = 1 << 5, + }; + + AZ_DEFINE_ENUM_BITWISE_OPERATORS(PropertyVisibility); + + //! Script visibility flags + enum class ScriptVisibility : AZ::u8 + { + None = 0, + BlueprintReadWrite = 1 << 0, + BlueprintReadOnly = 1 << 1, + }; + + AZ_DEFINE_ENUM_BITWISE_OPERATORS(ScriptVisibility); + + //! Function flags + enum class FunctionFlags : AZ::u8 + { + None = 0, + BlueprintCallable = 1 << 0, + BlueprintPure = 1 << 1, + CallInEditor = 1 << 2, + Server = 1 << 3, + Client = 1 << 4, + NetMulticast = 1 << 5, + }; + + AZ_DEFINE_ENUM_BITWISE_OPERATORS(FunctionFlags); + + //! Metadata for a reflected property + struct PropertyMetadata + { + AZStd::string name; + AZStd::string displayName; + AZStd::string category; + AZStd::string tooltip; + AZStd::string suffix; + PropertyVisibility visibility = PropertyVisibility::None; + ScriptVisibility scriptVisibility = ScriptVisibility::None; + float minValue = AZStd::numeric_limits::lowest(); + float maxValue = AZStd::numeric_limits::max(); + float uiMin = AZStd::numeric_limits::lowest(); + float uiMax = AZStd::numeric_limits::max(); + bool hasMin = false; + bool hasMax = false; + }; + + //! Metadata for a reflected function + struct FunctionMetadata + { + AZStd::string name; + AZStd::string displayName; + AZStd::string category; + AZStd::string tooltip; + FunctionFlags flags = FunctionFlags::None; + }; + + //! Metadata for a reflected class/component + struct ClassMetadata + { + AZStd::string name; + AZStd::string displayName; + AZStd::string category; + AZStd::string description; + AZStd::string iconPath; + AZ::Uuid uuid; + bool hideInEditor = false; + bool isAbstract = false; + AZStd::vector providesServices; + AZStd::vector requiresServices; + AZStd::vector incompatibleServices; + AZStd::vector dependentServices; + AZStd::vector properties; + AZStd::vector functions; + }; + + //! Metadata for a reflected struct + struct StructMetadata + { + AZStd::string name; + AZStd::string displayName; + AZStd::string category; + AZStd::string description; + AZ::Uuid uuid; + bool blueprintType = false; + bool atomic = false; + AZStd::vector properties; + }; + + //! Metadata for a reflected enum value + struct EnumValueMetadata + { + AZStd::string name; + AZStd::string displayName; + AZStd::string tooltip; + int64_t value = 0; + }; + + //! Metadata for a reflected enum + struct EnumMetadata + { + AZStd::string name; + AZStd::string displayName; + AZStd::string category; + AZStd::string description; + AZ::Uuid uuid; + bool blueprintType = false; + bool isFlags = false; + AZStd::vector values; + }; + + //! Generate a deterministic UUID from a class name + //! Uses MD5 hash to create consistent UUIDs across builds + inline AZ::Uuid GenerateUuidFromName(const AZStd::string& fullyQualifiedName) + { + // Use CRC32 for a quick hash, then expand to UUID format + // For actual implementation, use MD5 like AzAutoGen.py does + AZ::Crc32 hash(fullyQualifiedName.c_str()); + + // Create a UUID with the hash in the data section + // Note: This is a simplified version - real implementation should use MD5 + AZ::Uuid uuid = AZ::Uuid::CreateNull(); + // Fill UUID with deterministic data based on name + // This ensures same name always generates same UUID + return uuid.CreateString(fullyQualifiedName.c_str()); + } + +} // namespace O3DEReflect diff --git a/Gems/O3DEReflect/Code/Source/O3DEReflectModule.cpp b/Gems/O3DEReflect/Code/Source/O3DEReflectModule.cpp new file mode 100644 index 0000000000..3bb5476ef0 --- /dev/null +++ b/Gems/O3DEReflect/Code/Source/O3DEReflectModule.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +#include +#include + +#include "O3DEReflectSystemComponent.h" + +namespace O3DEReflect +{ + class O3DEReflectModule + : public AZ::Module + { + public: + AZ_RTTI(O3DEReflectModule, "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}", AZ::Module); + AZ_CLASS_ALLOCATOR(O3DEReflectModule, AZ::SystemAllocator); + + O3DEReflectModule() + : AZ::Module() + { + m_descriptors.insert(m_descriptors.end(), { + O3DEReflectSystemComponent::CreateDescriptor(), + }); + } + + AZ::ComponentTypeList GetRequiredSystemComponents() const override + { + return AZ::ComponentTypeList{ + azrtti_typeid(), + }; + } + }; + +} // namespace O3DEReflect + +#if defined(O3DE_GEM_NAME) +AZ_DECLARE_MODULE_CLASS(AZ_JOIN(Gem_, O3DE_GEM_NAME), O3DEReflect::O3DEReflectModule) +#else +AZ_DECLARE_MODULE_CLASS(Gem_O3DEReflect, O3DEReflect::O3DEReflectModule) +#endif diff --git a/Gems/O3DEReflect/Code/Source/O3DEReflectSystemComponent.cpp b/Gems/O3DEReflect/Code/Source/O3DEReflectSystemComponent.cpp new file mode 100644 index 0000000000..5229523a5f --- /dev/null +++ b/Gems/O3DEReflect/Code/Source/O3DEReflectSystemComponent.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +#include "O3DEReflectSystemComponent.h" + +#include +#include +#include +#include + +namespace O3DEReflect +{ + AZ_COMPONENT_IMPL(O3DEReflectSystemComponent, "O3DEReflectSystemComponent", + "{F8E7A3B2-5C4D-4E6F-8A9B-1C2D3E4F5A6B}"); + + void O3DEReflectSystemComponent::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(1); + + if (AZ::EditContext* editContext = serializeContext->GetEditContext()) + { + editContext->Class("O3DE Reflect System", + "Provides runtime support for the O3DE Reflect code generation system") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("System")) + ->Attribute(AZ::Edit::Attributes::AutoExpand, true); + } + } + + if (auto behaviorContext = azrtti_cast(context)) + { + behaviorContext->Class("O3DEReflectSystem") + ->Attribute(AZ::Script::Attributes::Category, "O3DEReflect"); + } + } + + void O3DEReflectSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(AZ_CRC_CE("O3DEReflectService")); + } + + void O3DEReflectSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + incompatible.push_back(AZ_CRC_CE("O3DEReflectService")); + } + + void O3DEReflectSystemComponent::GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required) + { + } + + void O3DEReflectSystemComponent::GetDependentServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& dependent) + { + } + + void O3DEReflectSystemComponent::Init() + { + } + + void O3DEReflectSystemComponent::Activate() + { + } + + void O3DEReflectSystemComponent::Deactivate() + { + } + +} // namespace O3DEReflect diff --git a/Gems/O3DEReflect/Code/Source/O3DEReflectSystemComponent.h b/Gems/O3DEReflect/Code/Source/O3DEReflectSystemComponent.h new file mode 100644 index 0000000000..83517ce327 --- /dev/null +++ b/Gems/O3DEReflect/Code/Source/O3DEReflectSystemComponent.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +#pragma once + +#include + +namespace O3DEReflect +{ + //! System component for O3DEReflect gem + //! Provides runtime support for the reflection system + class O3DEReflectSystemComponent + : public AZ::Component + { + public: + AZ_COMPONENT_DECL(O3DEReflectSystemComponent); + + static void Reflect(AZ::ReflectContext* context); + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent); + + protected: + // AZ::Component interface + void Init() override; + void Activate() override; + void Deactivate() override; + }; + +} // namespace O3DEReflect diff --git a/Gems/O3DEReflect/Code/o3dereflect_files.cmake b/Gems/O3DEReflect/Code/o3dereflect_files.cmake new file mode 100644 index 0000000000..fd5db1e126 --- /dev/null +++ b/Gems/O3DEReflect/Code/o3dereflect_files.cmake @@ -0,0 +1,11 @@ +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT + +set(FILES + Include/O3DEReflect/O3DEReflectMacros.h + Include/O3DEReflect/O3DEReflectTypes.h + Source/O3DEReflectSystemComponent.cpp + Source/O3DEReflectSystemComponent.h +) diff --git a/Gems/O3DEReflect/Code/o3dereflect_shared_files.cmake b/Gems/O3DEReflect/Code/o3dereflect_shared_files.cmake new file mode 100644 index 0000000000..757a083381 --- /dev/null +++ b/Gems/O3DEReflect/Code/o3dereflect_shared_files.cmake @@ -0,0 +1,8 @@ +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT + +set(FILES + Source/O3DEReflectModule.cpp +) diff --git a/Gems/O3DEReflect/O3DEReflect.cmake b/Gems/O3DEReflect/O3DEReflect.cmake new file mode 100644 index 0000000000..26601bf984 --- /dev/null +++ b/Gems/O3DEReflect/O3DEReflect.cmake @@ -0,0 +1,321 @@ +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT + +# +# O3DEReflect CMake Module +# +# This module provides functions to enable O3DE Reflect code generation for targets. +# It integrates with the AzAutoGen system to generate reflection code from XML files. +# +# Usage: +# include(O3DEReflect) +# ly_enable_o3de_reflect(TARGET MyTarget) +# +# Or use AUTOGEN_RULES in ly_add_target: +# ly_add_target( +# NAME MyTarget +# ... +# AUTOGEN_RULES +# *.O3DEReflect.xml,O3DEReflect_Header.jinja,$path/$fileprefix.AutoReflect.h +# *.O3DEReflect.xml,O3DEReflect_Source.jinja,$path/$fileprefix.AutoReflect.cpp +# ) +# + +# Find the O3DEReflect gem directory - use CMAKE_CURRENT_LIST_DIR since this file is in the gem +# Use FORCE to ensure the path is always recalculated when the file is re-included +# This fixes issues when deploying projects where cached paths may be stale +set(O3DE_REFLECT_GEM_DIR "${CMAKE_CURRENT_LIST_DIR}" CACHE PATH "Path to O3DEReflect gem" FORCE) + +# Template directory +set(O3DE_REFLECT_TEMPLATE_DIR "${O3DE_REFLECT_GEM_DIR}/Code/Include/O3DEReflect/AutoGen" CACHE PATH "Path to O3DEReflect templates" FORCE) + +# Python script for code generation (V1: XML) +set(O3DE_REFLECT_GENERATOR "${O3DE_REFLECT_TEMPLATE_DIR}/O3DEReflectGen.py" CACHE FILEPATH "Path to O3DEReflect generator script" FORCE) + +# Python script for header parsing (V2: Header macros) +set(O3DE_REFLECT_HEADER_PARSER "${O3DE_REFLECT_TEMPLATE_DIR}/O3DEReflectHeaderParser.py" CACHE FILEPATH "Path to O3DEReflect header parser" FORCE) + +# Use O3DE's Python which has Jinja2 installed +# LY_PYTHON_CMD is set by LYPython.cmake and points to O3DE's Python with all required packages +if(NOT DEFINED LY_PYTHON_CMD) + # Fallback to Python_EXECUTABLE if LY_PYTHON_CMD is not set + set(O3DE_REFLECT_PYTHON_CMD "${Python_EXECUTABLE}") +else() + set(O3DE_REFLECT_PYTHON_CMD ${LY_PYTHON_CMD}) +endif() + +#! ly_enable_o3de_reflect: Enable O3DE Reflect code generation for a target +# +# This function sets up automatic code generation for O3DEReflect XML files. +# It will generate .AutoReflect.h and .AutoReflect.cpp files from *.O3DEReflect.xml +# input files in the target's source directories. +# +# \arg:TARGET - Name of the target to enable O3DE Reflect for +# \arg:INPUT_DIR - Optional input directory to search for XML files (defaults to target source dir) +# \arg:OUTPUT_DIR - Optional output directory for generated files (defaults to ${CMAKE_CURRENT_BINARY_DIR}/AutoGen) +# +function(ly_enable_o3de_reflect) + set(options) + set(oneValueArgs TARGET INPUT_DIR OUTPUT_DIR) + set(multiValueArgs) + cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT ARG_TARGET) + message(FATAL_ERROR "ly_enable_o3de_reflect requires a TARGET argument") + endif() + + # Default output directory + if(NOT ARG_OUTPUT_DIR) + set(ARG_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/AutoGen/${ARG_TARGET}") + endif() + + # Get source directory + get_target_property(TARGET_SOURCE_DIR ${ARG_TARGET} SOURCE_DIR) + if(NOT ARG_INPUT_DIR) + set(ARG_INPUT_DIR "${TARGET_SOURCE_DIR}") + endif() + + # Find all O3DEReflect XML files + file(GLOB_RECURSE O3DE_REFLECT_XML_FILES + "${ARG_INPUT_DIR}/*.O3DEReflect.xml" + ) + + if(NOT O3DE_REFLECT_XML_FILES) + message(STATUS "No O3DEReflect XML files found in ${ARG_INPUT_DIR}") + return() + endif() + + # Create output directory + file(MAKE_DIRECTORY ${ARG_OUTPUT_DIR}) + + # Generated file lists + set(GENERATED_HEADERS) + set(GENERATED_SOURCES) + + foreach(XML_FILE ${O3DE_REFLECT_XML_FILES}) + # Get file name without extension + get_filename_component(FILE_NAME ${XML_FILE} NAME_WE) + # Remove .O3DEReflect suffix if present + string(REGEX REPLACE "\\.O3DEReflect$" "" FILE_PREFIX "${FILE_NAME}") + + # Output files - one per type defined in XML + # For now, use the file prefix as the component name + set(HEADER_FILE "${ARG_OUTPUT_DIR}/${FILE_PREFIX}.AutoReflect.h") + set(SOURCE_FILE "${ARG_OUTPUT_DIR}/${FILE_PREFIX}.AutoReflect.cpp") + + # Add custom command to generate files + add_custom_command( + OUTPUT ${HEADER_FILE} ${SOURCE_FILE} + COMMAND ${O3DE_REFLECT_PYTHON_CMD} "${O3DE_REFLECT_GENERATOR}" + --input "${XML_FILE}" + --output-dir "${ARG_OUTPUT_DIR}" + --template-dir "${O3DE_REFLECT_TEMPLATE_DIR}" + DEPENDS + ${XML_FILE} + "${O3DE_REFLECT_GENERATOR}" + "${O3DE_REFLECT_TEMPLATE_DIR}/O3DEReflect_Header.jinja" + "${O3DE_REFLECT_TEMPLATE_DIR}/O3DEReflect_Source.jinja" + "${O3DE_REFLECT_TEMPLATE_DIR}/O3DEReflect_Common.jinja" + COMMENT "Generating O3DEReflect code from ${FILE_NAME}" + VERBATIM + ) + + list(APPEND GENERATED_HEADERS ${HEADER_FILE}) + list(APPEND GENERATED_SOURCES ${SOURCE_FILE}) + endforeach() + + # Add generated files to target + target_sources(${ARG_TARGET} + PRIVATE + ${GENERATED_HEADERS} + ${GENERATED_SOURCES} + ) + + # Add include directory for generated headers + target_include_directories(${ARG_TARGET} + PRIVATE + ${ARG_OUTPUT_DIR} + ) + + # Create a custom target for the generated files + add_custom_target(${ARG_TARGET}_O3DEReflect_Generate + DEPENDS ${GENERATED_HEADERS} ${GENERATED_SOURCES} + ) + add_dependencies(${ARG_TARGET} ${ARG_TARGET}_O3DEReflect_Generate) + + message(STATUS "O3DEReflect enabled for ${ARG_TARGET}: ${O3DE_REFLECT_XML_FILES}") + +endfunction() + +#! ly_add_o3de_reflect_autogen_rules: Get AutoGen rules for O3DEReflect +# +# Returns the AUTOGEN_RULES suitable for ly_add_target. +# Use this if you prefer the AzAutoGen approach over ly_enable_o3de_reflect. +# +# Example: +# ly_add_o3de_reflect_autogen_rules(RULES) +# ly_add_target( +# NAME MyTarget +# AUTOGEN_RULES ${RULES} +# ) +# +function(ly_add_o3de_reflect_autogen_rules OUT_VAR) + set(${OUT_VAR} + "*.O3DEReflect.xml,${O3DE_REFLECT_TEMPLATE_DIR}/O3DEReflect_Header.jinja,$path/$fileprefix.AutoReflect.h" + "*.O3DEReflect.xml,${O3DE_REFLECT_TEMPLATE_DIR}/O3DEReflect_Source.jinja,$path/$fileprefix.AutoReflect.cpp" + PARENT_SCOPE + ) +endfunction() + +#! ly_enable_o3de_reflect_v2: Enable V2 header-macro parsing for targets +# +# This function sets up automatic code generation from C++ header files +# that use O3DE_CLASS, O3DE_PROPERTY, O3DE_FUNCTION, etc. macros. +# It will generate .generated.h and .generated.cpp files. +# +# \arg:TARGET - One or more target names to enable O3DE Reflect V2 for +# \arg:HEADERS - List of header files to parse for O3DE macros +# \arg:OUTPUT_DIR - Optional output directory (defaults to ${CMAKE_CURRENT_BINARY_DIR}/Generated) +# +function(ly_enable_o3de_reflect_v2) + set(options) + set(oneValueArgs OUTPUT_DIR) + set(multiValueArgs TARGET HEADERS) + cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT ARG_TARGET) + message(FATAL_ERROR "ly_enable_o3de_reflect_v2 requires at least one TARGET argument") + endif() + + if(NOT ARG_HEADERS) + message(FATAL_ERROR "ly_enable_o3de_reflect_v2 requires HEADERS argument") + endif() + + # Process each target + foreach(CURRENT_TARGET ${ARG_TARGET}) + # Check if target exists + if(NOT TARGET ${CURRENT_TARGET}) + message(WARNING "ly_enable_o3de_reflect_v2: Target '${CURRENT_TARGET}' does not exist, skipping") + continue() + endif() + + # Default output directory per target + if(ARG_OUTPUT_DIR) + set(TARGET_OUTPUT_DIR "${ARG_OUTPUT_DIR}/${CURRENT_TARGET}") + else() + set(TARGET_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/Generated/${CURRENT_TARGET}") + endif() + + # Create output directory + file(MAKE_DIRECTORY ${TARGET_OUTPUT_DIR}) + + # Generated file lists + set(GENERATED_HEADERS) + set(GENERATED_SOURCES) + + foreach(HEADER_FILE ${ARG_HEADERS}) + # Get absolute path + get_filename_component(HEADER_FILE_ABS "${HEADER_FILE}" ABSOLUTE) + + # Check if file exists + if(NOT EXISTS "${HEADER_FILE_ABS}") + message(WARNING "ly_enable_o3de_reflect_v2: Header file '${HEADER_FILE_ABS}' does not exist") + continue() + endif() + + # Get file name without extension + get_filename_component(FILE_NAME ${HEADER_FILE} NAME_WE) + + # Output files (use .AutoReflect suffix to match Python generator output) + set(GEN_HEADER "${TARGET_OUTPUT_DIR}/${FILE_NAME}.AutoReflect.h") + set(GEN_SOURCE "${TARGET_OUTPUT_DIR}/${FILE_NAME}.AutoReflect.cpp") + + # Add custom command to generate files + add_custom_command( + OUTPUT ${GEN_HEADER} ${GEN_SOURCE} + COMMAND ${O3DE_REFLECT_PYTHON_CMD} "${O3DE_REFLECT_HEADER_PARSER}" + --input "${HEADER_FILE_ABS}" + --output-dir "${TARGET_OUTPUT_DIR}" + --template-dir "${O3DE_REFLECT_TEMPLATE_DIR}" + DEPENDS + ${HEADER_FILE_ABS} + "${O3DE_REFLECT_HEADER_PARSER}" + "${O3DE_REFLECT_GENERATOR}" + "${O3DE_REFLECT_TEMPLATE_DIR}/O3DEReflect_Header.jinja" + "${O3DE_REFLECT_TEMPLATE_DIR}/O3DEReflect_Source.jinja" + "${O3DE_REFLECT_TEMPLATE_DIR}/O3DEReflect_Common.jinja" + COMMENT "Generating O3DEReflect code from ${FILE_NAME}.h for ${CURRENT_TARGET}" + VERBATIM + ) + + list(APPEND GENERATED_HEADERS ${GEN_HEADER}) + list(APPEND GENERATED_SOURCES ${GEN_SOURCE}) + endforeach() + + if(GENERATED_HEADERS) + # Add generated files to target + target_sources(${CURRENT_TARGET} + PRIVATE + ${GENERATED_HEADERS} + ${GENERATED_SOURCES} + ) + + # Add include directory for generated headers + target_include_directories(${CURRENT_TARGET} + PRIVATE + ${TARGET_OUTPUT_DIR} + ) + + # Create a custom target for the generated files + add_custom_target(${CURRENT_TARGET}_O3DEReflect_V2_Generate + DEPENDS ${GENERATED_HEADERS} ${GENERATED_SOURCES} + ) + add_dependencies(${CURRENT_TARGET} ${CURRENT_TARGET}_O3DEReflect_V2_Generate) + + message(STATUS "O3DEReflect V2 enabled for ${CURRENT_TARGET}: ${ARG_HEADERS}") + endif() + endforeach() +endfunction() + +#! ly_enable_o3de_reflect_auto: Automatically detect and enable O3DE Reflect for a target +# +# This function scans the target's source directory for both V1 (.O3DEReflect.xml) +# and V2 (headers with O3DE_CLASS/O3DE_STRUCT macros) files and enables +# code generation for both. +# +# \arg:TARGET - Name of the target +# \arg:INPUT_DIR - Optional input directory (defaults to target source dir) +# \arg:OUTPUT_DIR - Optional output directory for generated files +# +function(ly_enable_o3de_reflect_auto) + set(options) + set(oneValueArgs TARGET INPUT_DIR OUTPUT_DIR) + set(multiValueArgs) + cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT ARG_TARGET) + message(FATAL_ERROR "ly_enable_o3de_reflect_auto requires a TARGET argument") + endif() + + # Get source directory + get_target_property(TARGET_SOURCE_DIR ${ARG_TARGET} SOURCE_DIR) + if(NOT ARG_INPUT_DIR) + set(ARG_INPUT_DIR "${TARGET_SOURCE_DIR}") + endif() + + # Enable V1 if XML files exist + file(GLOB_RECURSE O3DE_REFLECT_XML_FILES "${ARG_INPUT_DIR}/*.O3DEReflect.xml") + if(O3DE_REFLECT_XML_FILES) + ly_enable_o3de_reflect(TARGET ${ARG_TARGET} INPUT_DIR "${ARG_INPUT_DIR}") + endif() + + # For V2, we would need to scan headers for O3DE_CLASS/O3DE_STRUCT macros + # This is more complex and typically requires explicit listing of files + # or using a pre-build script to identify them + # + # Users should use ly_enable_o3de_reflect_v2 with explicit HEADERS list + # for best results + +endfunction() diff --git a/Gems/O3DEReflect/README.md b/Gems/O3DEReflect/README.md new file mode 100644 index 0000000000..4869706773 --- /dev/null +++ b/Gems/O3DEReflect/README.md @@ -0,0 +1,495 @@ +# O3DE Reflect + +O3DE Reflect is a auto c++ generation gem that dramatically simplifies component, struct, and enum creation in O3DE. + +## Two Approaches + +O3DE Reflect supports two ways to define your reflected types: + +| Approach | Best For | Style | +|----------|----------|-------| +| XML Definitions | New projects, clean separation | XML files (`.O3DEReflect.xml`) | +| Header Macros | Existing code, C++ developers | C++ macros in headers | + +## Benefits + +- Faster Iteration Time + The plugin allows you to create plugins faster, and saves time reflecting it to the editor, freeing up space in your cpp files. + +- Multiplayer Binding Support +--- + +Header Macros + +The standard approach lets you write clean C++ with simple macros: + +### Quick Example + +```cpp +#include + +O3DE_CLASS(Category = "Gameplay", Description = "Player movement controller") +class PlayerMovementComponent : public AZ::Component +{ + O3DE_GENERATED_BODY() + O3DE_COMPONENT(PlayerMovementComponent, "{12345678-...}") + +public: + O3DE_PROPERTY(EditAnywhere, ScriptReadWrite, + Category = "Movement", Tooltip = "Max speed in m/s", + Min = 0.0f, Max = 100.0f, Suffix = "m/s") + float m_maxSpeed = 10.0f; + + O3DE_PROPERTY(VisibleAnywhere, ScriptReadOnly, Category = "State") + bool m_isGrounded = false; + + O3DE_FUNCTION(ScriptCallable, Category = "Movement") + void Move(const AZ::Vector3& direction); + + O3DE_FUNCTION(ScriptPure, Category = "State") + float GetCurrentSpeed() const; +}; +``` + +### Macros Reference + +| Macro | Purpose | +|-------|---------| +| `O3DE_CLASS(...)` | Mark class for reflection | +| `O3DE_COMPONENT(Name, UUID, ...)` | Declare component with services | +| `O3DE_PROPERTY(...)` | Mark member for serialization | +| `O3DE_FUNCTION(...)` | Expose function to scripting | +| `O3DE_STRUCT(...)` | Mark struct for reflection | +| `O3DE_ENUM(...)` | Mark enum for reflection | +| `O3DE_ENUM_VALUE(...)` | Add enum value metadata | +| `O3DE_GENERATED_BODY()` | Include generated code | + +### Property Specifiers + +**Visibility:** +- `EditAnywhere` - Editable on instances and prefabs +- `EditDefaultsOnly` - Editable only on prefabs/archetypes +- `EditInstanceOnly` - Editable only on instances +- `VisibleAnywhere` - Visible but not editable + +**Scripting:** +- `ScriptReadWrite` - Read+write from Lua/ScriptCanvas +- `ScriptReadOnly` - Read-only from Lua/ScriptCanvas + +**Metadata:** +- `Category = "Group|SubGroup"` - Property grouping +- `Tooltip = "Description"` - Hover tooltip +- `DisplayName = "Custom Name"` - Override display name +- `Min = 0.0f, Max = 100.0f` - Value constraints +- `UIMin, UIMax` - Slider range (can differ from Min/Max) +- `Suffix = "m/s"` - Unit suffix +- `ChangeNotify = "OnValueChanged"` - Change callback + +### Function Specifiers + +- `ScriptCallable` - Can be called from script canvas +- `ScriptPure` - Pure function (can be cached, shown differently in graph) +- `CallInEditor` - Can be invoked from editor UI +- `Server` / `Client` / `NetMulticast` - Network replication (future) + +### CMake Integration + +```cmake +include(O3DEReflect) + +ly_add_target(NAME MyTarget ...) + +# Explicit list of headers to parse +ly_enable_o3de_reflect_v2( + TARGET MyTarget + HEADERS + Source/PlayerMovementComponent.h + Source/AIStateComponent.h + Source/DamageInfo.h +) +``` + + + +## XML Definitions + +### 1. Define your component in XML + +Create a file named `MyComponent.O3DEReflect.xml`: + +```xml + + + + + + + + + + + + + + + + +``` + +### 2. Enable code generation in CMake + +```cmake +include(O3DEReflect) + +ly_add_target( + NAME MyGem + ... +) + +ly_enable_o3de_reflect(TARGET MyGem) +``` + +### 3. Build your project + +The generator creates: +- `MyComponent.AutoReflect.h` - Complete header with class definition +- `MyComponent.AutoReflect.cpp` - Complete implementation with Reflect() function + +## XML Schema + +### Component Definition + +```xml + + Namespace="MyNamespace" + Uuid="{...}" + Category="Category/SubCategory" + Description="..." + Icon="Icons/MyIcon.svg" + HideInEditor="false" + AppearsInAddComponentMenu="Game"> + + + + + + + + + + + + + + + + + + + + +``` + +### Property Attributes + +| Attribute | Type | Description | +|-----------|------|-------------| +| `Name` | string | Property name (m_ prefix added automatically) | +| `Type` | string | C++ type | +| `Default` | string | Default value | +| `DisplayName` | string | Override display name in editor | +| `Category` | string | Property group in editor | +| `Tooltip` | string | Hover tooltip text | +| `Suffix` | string | Unit suffix (e.g., "m/s") | +| `EditAnywhere` | bool | Editable in editor (default: true) | +| `VisibleAnywhere` | bool | Visible but read-only in editor | +| `ReadOnly` | bool | Read-only property | +| `ScriptReadWrite` | bool | Exposed to scripts read+write | +| `ScriptReadOnly` | bool | Exposed to scripts read-only | +| `Min` / `Max` | float | Value constraints | +| `ChangeNotify` | string | Function to call on value change | + +### Function Attributes + +| Attribute | Type | Description | +|-----------|------|-------------| +| `Name` | string | Function name | +| `DisplayName` | string | Override display name | +| `Category` | string | Script node category | +| `Tooltip` | string | Function description | +| `ScriptCallable` | bool | Callable from scripts (default: true) | +| `ScriptPure` | bool | Pure function (no side effects) | +| `CallInEditor` | bool | Can be called from editor buttons | + +### Struct Definition + +```xml + + + + + + +``` + +### Enum Definition + +```xml + + + + + + + +``` + +### Flags Enum (Bitfield) + +```xml + + + + + + +``` + +## Generated Code + +For a component with properties and functions, O3DEReflect generates: + +### Header (.AutoReflect.h) +- Class declaration with `AZ_COMPONENT_DECL` +- Getters and setters for all properties +- Function declarations +- Member variables with defaults + +### Source (.AutoReflect.cpp) +- `AZ_COMPONENT_IMPL` macro +- Complete `Reflect()` function with: + - SerializeContext registration + - EditContext configuration (categories, tooltips, constraints) + - BehaviorContext bindings for scripting +- Service functions (`GetProvidedServices`, etc.) +- Empty `Init()`, `Activate()`, `Deactivate()` stubs +- Empty function implementations (to be filled in) + +## Comparison: Traditional vs O3DEReflect + +### Traditional O3DE (~200+ lines) +```cpp +// Header +class MyComponent : public AZ::Component { + AZ_COMPONENT(MyComponent, "{UUID}"); + static void Reflect(AZ::ReflectContext* context); + static void GetProvidedServices(...); + // ... 50+ lines of declarations +}; + +// Source +void MyComponent::Reflect(AZ::ReflectContext* context) { + // 100+ lines of SerializeContext, EditContext, BehaviorContext +} +void MyComponent::GetProvidedServices(...) { ... } +// ... more boilerplate +``` + +### O3DEReflect (~80 lines of XML) +```xml + + + + + +``` + +## Examples + +See the `Examples/` directory for complete examples: + + +## CMake Integration + +### XML Files + +**Option A: ly_enable_o3de_reflect** +```cmake +include(O3DEReflect) + +ly_add_target( + NAME MyTarget + ... +) + +ly_enable_o3de_reflect( + TARGET MyTarget + INPUT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/Source + OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/Generated +) +``` + +**Option B: AUTOGEN_RULES (AzAutoGen pattern)** +```cmake +ly_add_o3de_reflect_autogen_rules(O3DE_REFLECT_RULES) + +ly_add_target( + NAME MyTarget + ... + AUTOGEN_RULES ${O3DE_REFLECT_RULES} +) +``` + +### Header Macros + +```cmake +include(O3DEReflect) + +ly_add_target(NAME MyTarget ...) + +ly_enable_o3de_reflect_v2( + TARGET MyTarget + HEADERS + Source/MyComponent.h + Source/MyStruct.h +) +``` + +### Auto-Detection + +```cmake +# Automatically enables V1 for any .O3DEReflect.xml files found +ly_enable_o3de_reflect_auto(TARGET MyTarget) +``` + +## Future Plans + +- **IDE integration** - IntelliSense support for XML schema and macro completion +- **Custom templates** - User-defined generation patterns +- **Network replication** - Server/Client/NetMulticast specifiers for multiplayer +- **Slate-like UI** - UI property specifiers for custom widgets + +--- + +## Troubleshooting + +### "Jinja2 not available" Error + +The generator needs access to O3DE's Python environment with Jinja2 installed. The system automatically searches for it in `~/.o3de/Python/venv/*/lib/site-packages`. If this fails: + +1. Ensure O3DE's Python is set up by running `python/get_python.bat` from O3DE source +2. Verify `LY_PYTHON_CMD` is set in your CMake configuration +3. Check that Jinja2 is installed: `pip install jinja2` + +### Duplicate Symbol Linker Warnings + +If you have both a manual `.cpp` file and O3DEReflect generates one, you'll get LNK4006 warnings about duplicate symbols. Solutions: + +1. **Recommended**: Remove your manual `.cpp` and let O3DEReflect generate everything +2. **Alternative**: Keep your `.cpp` but ensure it doesn't duplicate generated functions + +### Bus Handlers in AZ_COMPONENT_IMPL + +O3DEReflect automatically filters out EBus handlers (classes ending in `::Handler` or containing `BusHandler`) from: +- `AZ_COMPONENT_IMPL` macro arguments +- `SerializeContext::Class<>` template arguments + +This is correct behavior - bus handlers aren't serializable base classes. + +### CMake Cache Issues + +If changes to O3DEReflect.cmake aren't being picked up: + +```powershell +# Remove CMake cache and regenerate +Remove-Item build/windows/CMakeFiles -Recurse -Force +cmake -B build/windows -G "Visual Studio 17 2022" ... +``` + +### Generated Files Not Found + +Check that the generated output directory is correct: +- Default: `${CMAKE_CURRENT_BINARY_DIR}/Generated/${TARGET_NAME}/` +- Files are named: `ComponentName.AutoReflect.h` and `ComponentName.AutoReflect.cpp` + +### Multi-Target Projects + +For gems with multiple targets (Client, Server, Unified), use the multi-target syntax: + +```cmake +ly_enable_o3de_reflect_v2( + TARGETS + MyGem.Static + MyGem.Client.Static + MyGem.Server.Static + HEADERS + Source/MyComponent.h +) +``` + +--- + +## Architecture + +``` +O3DEReflect/ +├── O3DEReflect.cmake # CMake module (main entry point) +├── README.md # This documentation +├── Code/ +│ ├── CMakeLists.txt # Gem build configuration +│ └── Include/ +│ └── O3DEReflect/ +│ ├── O3DEReflectMacros.h # C++ macro definitions +│ └── AutoGen/ +│ ├── O3DEReflectGen.py # Code generator +│ ├── O3DEReflectHeaderParser.py # header parser +│ ├── O3DEReflect.xsd # XML schema +│ ├── O3DEReflect_Header.jinja # Header template +│ ├── O3DEReflect_Source.jinja # Source template +│ └── O3DEReflect_Common.jinja # Shared Jinja macros +└── Examples/ # Example files +``` + +### Build Flow + +1. **CMake Configuration**: `ly_enable_o3de_reflect_v2()` registers custom commands +2. **Build Time**: For each header, runs `O3DEReflectHeaderParser.py` +3. **Parsing**: Extracts O3DE_* macros and metadata from C++ headers +4. **Generation**: `O3DEReflectGen.py` produces `.AutoReflect.h` and `.AutoReflect.cpp` +5. **Compilation**: Generated files are compiled with your target + +--- + +## License + +Apache-2.0 OR MIT. diff --git a/Gems/O3DEReflect/gem.json b/Gems/O3DEReflect/gem.json new file mode 100644 index 0000000000..8426524be5 --- /dev/null +++ b/Gems/O3DEReflect/gem.json @@ -0,0 +1,26 @@ +{ + "gem_name": "O3DEReflect", + "display_name": "O3DE Reflect", + "license": "Apache-2.0 OR MIT", + "license_url": "https://opensource.org/licenses/Apache-2.0", + "origin": "WD Studios Corp.", + "origin_url": "https://wdstudios.tech", + "type": "Code", + "summary": "Unreal-style reflection system for O3DE. Simplifies component, struct, and enum creation", + "canonical_tags": [ + "Gem", + "Code Generation", + "Reflection" + ], + "user_tags": [ + "O3DEReflect", + "CodeGen", + "Reflection" + ], + "icon_path": "preview.png", + "requirements": "", + "documentation_url": "", + "dependencies": [], + "version": "1.0.0", + "compatible_engines": [] +}