#if SSC_ENTITIES using System; using Unity.Entities; using Unity.Transforms; using Unity.Mathematics; using Unity.Collections; using Unity.Jobs; using UnityEngine; using Unity.Burst; // Sci-Fi Ship Controller. Copyright (c) 2018-2023 SCSM Pty Ltd. All rights reserved. namespace SciFiShipController { #region ProjectileComponent [Serializable] public struct Projectile : IComponentData { public float3 velocity; public float damageAmount; public float despawnTime; public float despawnTimer; public byte useGravity; // 0 = no, 1 = yes public float gravity; public float3 gravityDirection; public float speed; public float3 fwdDirection; public float3 upDirection; public int projectilePrefabID; public int effectsObjectPrefabID; public int shieldEffectsObjectPrefabID; public int sourceShipId; public int sourceSquadronId; public int damageTypeInt; public int _tempIdx; } #endregion #region ProjectileSystem /// /// ProjectileSystem is used to create and move projectiles with the /// Data-Orientated Tech Stack (DOTS). It uses C# Jobs, Entities and /// the Burst compiler.The default SimulationSystemGroup in Entities /// 0.0.12 preview 30 runs at end of Update rather than FixedUpdate. /// So disable auto creation and create and update it manually from /// SSCManager. /// U2019.1 - Entities 0.0.12-preview.30 /// U2019.2 - Entities 0.1.1 preview /// U2019.3 - Entities 0.2.0 preview.18 /// U2019.4 - Entities 0.5.2 preview.4 /// U2020.1 - Entities 0.12.0 (untested) /// U2020.3 - Entities 0.51.0 preview.32+ (SSC 1.3.4 and earlier Entities 0.17.0-preview.42) /// [DisableAutoCreation] #if UNITY_2020_3_OR_NEWER public partial class ProjectileSystem : SystemBase #elif UNITY_2020_1_OR_NEWER public class ProjectileSystem : SystemBase #else public class ProjectileSystem : JobComponentSystem #endif { #region Public Properties // For testing only public static int GetTotalProjectiles { get; private set; } #endregion #region Private Variables // EntityQuery aka ComponentGroup pre-ECS 0.27 private static EntityQuery projectileEntityQuery; private static EntityManager entityManager; private static ComponentType compTypeProjectile; private Vector3 pos, velo, frameMovement; private Entity projectileEntity; private int nProjectiles; private NativeArray raycastResults; private NativeArray raycastCommands; private RaycastCommand raycastCommand; private NativeList entitiesToDestroy; private RaycastHit hitInfo; private SSCManager sscManager; #if SSC_PHYSICS private Unity.Physics.Systems.BuildPhysicsWorld buildPhysicsWorld; private NativeArray physicsraycastResults; private int numPhysicsRaycastHits; private int numPhysicsBodies; //private NativeList physicsraycastResultList; #endif #endregion #region Projectile Movement Job [BurstCompile] struct ProjectileMoveJob : IJobChunk { public float deltaTime; #if UNITY_2020_1_OR_NEWER // Renamed from ArchetypeChunkComponentType to ComponentTypeHandle in Entities 0.12.0 public ComponentTypeHandle translationType; public ComponentTypeHandle projectileType; #else public ArchetypeChunkComponentType translationType; public ArchetypeChunkComponentType projectileType; #endif public void Execute(ArchetypeChunk archetypeChunk, int chunkIndex, int firstEntityIndex) { NativeArray chunkTranslations = archetypeChunk.GetNativeArray(translationType); NativeArray chunkProjectiles = archetypeChunk.GetNativeArray(projectileType); for (int i = 0; i < chunkTranslations.Length; i++) { Projectile projectile = chunkProjectiles[i]; Translation position = chunkTranslations[i]; // Use Gravity? if (projectile.useGravity == (byte)1) { projectile.velocity += projectile.gravity * deltaTime * projectile.gravityDirection; } // Update our current position using our velocity and frame time position.Value += projectile.velocity * deltaTime; chunkTranslations[i] = position; // Update the timer so that in the OnUpdate we can destroy the entity when required. projectile.despawnTimer += deltaTime; chunkProjectiles[i] = projectile; // Update our current rotation using our velocity (currently not required) //rotation.Value = quaternion.LookRotationSafe(movement.Velocity, new float3(0f, 1f, 0f)); } } } [BurstCompile] struct ProjectileTelePortJob: IJobChunk { public float3 deltaPosition; #if UNITY_2020_1_OR_NEWER // Renamed from ArchetypeChunkComponentType to ComponentTypeHandle in Entities 0.12.0 public ComponentTypeHandle translationType; #else public ArchetypeChunkComponentType translationType; #endif public void Execute(ArchetypeChunk archetypeChunk, int chunkIndex, int firstEntityIndex) { NativeArray chunkTranslations = archetypeChunk.GetNativeArray(translationType); for (int i = 0; i < chunkTranslations.Length; i++) { Translation translation = chunkTranslations[i]; translation.Value += deltaPosition; chunkTranslations[i] = translation; } } } #endregion #region Physics Raycast Job #if SSC_PHYSICS /// /// Parallel job for casting rays from projectiles using Unity.Physics /// [BurstCompile] struct ProjectilePhysicsRayJob : IJobForEach { public NativeArray raycastResultsInJob; [ReadOnly] public Unity.Physics.CollisionWorld collisionWorldInJob; [ReadOnly] public float deltaTime; public void Execute([ReadOnly] ref Translation position, ref Projectile projectile) { Unity.Physics.RaycastInput raycastInput = new Unity.Physics.RaycastInput { Start = position.Value, End = position.Value + (projectile.velocity * deltaTime), Filter = Unity.Physics.CollisionFilter.Default }; if (collisionWorldInJob.CastRay(raycastInput, out Unity.Physics.RaycastHit hit)) { if (hit.RigidBodyIndex == 0) { hit.Position = new float3(1f, 1f, 1f); } raycastResultsInJob[projectile._tempIdx] = hit; } else { // Dummy hit Unity.Physics.RaycastHit raycastHit = new Unity.Physics.RaycastHit(); raycastHit.RigidBodyIndex = -1; raycastResultsInJob[projectile._tempIdx] = raycastHit; } } } #endif #endregion #region Event Methods protected override void OnCreate() { // Get the current entity manager. If it doesn't exist, create one. entityManager = SSCManager.sscWorld.EntityManager; // Cache the component type to avoid ComponentType.op_Implicit in CreateProjectile(..) compTypeProjectile = typeof(Projectile); // Projectile entity query code // Get all the entities with a Translation and Projectile component projectileEntityQuery = GetEntityQuery(new EntityQueryDesc { All = new ComponentType[] { typeof(Translation), typeof(Rotation), compTypeProjectile } }); // Initialise raycast command raycastCommand = new RaycastCommand(Vector3.zero, Vector3.forward, 1f); // Initialise entities to destroy list entitiesToDestroy = new NativeList(Allocator.Persistent); sscManager = SSCManager.GetOrCreateManager(); #if SSC_PHYSICS if (!DOTSHelper.GetBuildPhysicsWorld(SSCManager.sscWorld, ref buildPhysicsWorld)) { #if UNITY_EDITOR Debug.Log("ERROR: ProjectileSystem.OnCreate() - could not get physicsworld - PLEASE REPORT"); #endif } else { // Create empty resultset. Note the difference between Physics and UnityEngine RaycastHit. //physicsraycastResultList = new NativeList(Allocator.Persistent); } #endif } protected override void OnDestroy() { // Dispose of the entities to destroy native list entitiesToDestroy.Dispose(); #if SSC_PHYSICS if (physicsraycastResults.IsCreated) { physicsraycastResults.Dispose(); } #endif } #endregion #region Update Methods // OnUpdate runs on the main thread. #if UNITY_2020_1_OR_NEWER protected override void OnUpdate() #else protected override JobHandle OnUpdate(JobHandle jobHandle) #endif { //int nProjectiles = 0; nProjectiles = 0; #region Physics Updates #if SSC_PHYSICS // Ensure physics world updates first #if UNITY_2020_1_OR_NEWER // FinalJobHandle deprecated in Unity.Physics 0.4.0-preview.5. Replaced with GetOutputDependency(). this.Dependency = JobHandle.CombineDependencies(this.Dependency, buildPhysicsWorld.GetOutputDependency()); #else jobHandle = JobHandle.CombineDependencies(jobHandle, buildPhysicsWorld.FinalJobHandle); #endif #endif #endregion // Get all of the projectile entities in the scene // wrap in using statement to automatically dispose after use using (NativeArray nativeArray = projectileEntityQuery.ToEntityArray(Allocator.TempJob)) { nProjectiles = nativeArray.Length; // for testing only GetTotalProjectiles = nProjectiles; } // Gather the types of the components that we want to manipulate #if UNITY_2020_1_OR_NEWER // Renamed from ArchetypeChunkComponentType to ComponentTypeHandle in Entities 0.12.0 // Replace GetArchetypeChunkComponentType() with GetComponentTypeHandle() ComponentTypeHandle positionType = GetComponentTypeHandle(); ComponentTypeHandle rotationType = GetComponentTypeHandle(); ComponentTypeHandle projectileType = GetComponentTypeHandle(); #else ArchetypeChunkComponentType positionType = GetArchetypeChunkComponentType(); ArchetypeChunkComponentType rotationType = GetArchetypeChunkComponentType(); ArchetypeChunkComponentType projectileType = GetArchetypeChunkComponentType(); #endif // Get all the chunks (segments of data) that match this query (that have Translation and Projectile components) // The chunks will only contain our projectile entities. NativeArray chunks = projectileEntityQuery.CreateArchetypeChunkArray(Allocator.TempJob); // Set up the command and result buffers raycastCommands = new NativeArray(nProjectiles, Allocator.TempJob); raycastResults = new NativeArray(nProjectiles, Allocator.TempJob); // Get delta time once // This system is updated from FixedUpdate, so use PhysX time rather than ECS timing. float deltaTime = UnityEngine.Time.fixedDeltaTime; //#if UNITY_2019_3_OR_NEWER //float deltaTime = Time.DeltaTime; //#else //float deltaTime = Time.deltaTime; //#endif #region Raycast Job // TODO - investigate using a job to populate the raycastCommands NativeArray // Iterate through all projectile entities int raycastIndex = 0; // Loop through the chunks int chunksLength = chunks == null ? 0 : chunks.Length; for (int chunkIndex = 0; chunkIndex < chunksLength; chunkIndex++) { // Get the current chunk ArchetypeChunk chunk = chunks[chunkIndex]; // Get an array of the position components from the entities (that match the projectileEntityQuery) within this chunk NativeArray positionComponents = chunk.GetNativeArray(positionType); // Get an array of the projectile components from the entities within this chunk NativeArray projectileComponents = chunk.GetNativeArray(projectileType); // Loop through the entities in this chunk int chunkSize = chunk == null ? 0 : chunk.Count; for (int entityIndex = 0; entityIndex < chunkSize; entityIndex++) { // Get the position of this projectile pos = positionComponents[entityIndex].Value; // Get the velocity of this projectile velo = projectileComponents[entityIndex].velocity; // Calculate raycast data raycastCommand.from = pos; raycastCommand.direction = velo; raycastCommand.distance = (velo * deltaTime).magnitude; // Set raycast data raycastCommands[raycastIndex] = raycastCommand; // Used in the Unity.Physics raycast job Projectile _projectile = projectileComponents[entityIndex]; _projectile._tempIdx = raycastIndex; projectileComponents[entityIndex] = _projectile; //Debug.Log("tempidx: " + _projectile._tempIdx + " actual: " + raycastIndex); // Increment the raycast index - we're doing things this way to make sure that // in the second loop the indices all match up correctly raycastIndex++; } } // Schedule the batch of raycasts // TODO - in Entities 12+, not sure if this a parallel job... JobHandle handle = RaycastCommand.ScheduleBatch(raycastCommands, raycastResults, 1, default(JobHandle)); // Wait for the batch processing job to complete handle.Complete(); #endregion #region Unity.Physics Raycast Job #if SSC_PHYSICS physicsraycastResults = new NativeArray(nProjectiles, Allocator.TempJob); if (physicsraycastResults.IsCreated) { #if UNITY_2020_1_OR_NEWER // Check if this is parallel... new ProjectilePhysicsRayJob() { collisionWorldInJob = buildPhysicsWorld.PhysicsWorld.CollisionWorld, raycastResultsInJob = physicsraycastResults, deltaTime = deltaTime }.Schedule(this, this.Dependency).Complete(); #else new ProjectilePhysicsRayJob() { collisionWorldInJob = buildPhysicsWorld.PhysicsWorld.CollisionWorld, raycastResultsInJob = physicsraycastResults, deltaTime = deltaTime }.Schedule(this, jobHandle).Complete(); #endif numPhysicsRaycastHits = physicsraycastResults.Length; numPhysicsBodies = buildPhysicsWorld.PhysicsWorld.CollisionWorld.NumBodies; } else { numPhysicsRaycastHits = 0; numPhysicsBodies = 0; } #endif #endregion #region Process Raycasts for collision and despawn old projectiles // Get an archetype for projectiles #if UNITY_2020_1_OR_NEWER // Renamed from ArchetypeChunkEntityType to EntityTypeHandle in Entities 0.12.0 // Replace GetArchetypeChunkEntityType() with GetEntityTypeHandle() EntityTypeHandle projectileChunkEntityArchetype = GetEntityTypeHandle(); #else ArchetypeChunkEntityType projectileChunkEntityArchetype = GetArchetypeChunkEntityType(); #endif // Iterate through all projectile entities again // Reset raycastIndex raycastIndex = 0; Collider other; for (int chunkIndex = 0; chunkIndex < chunksLength; chunkIndex++) { // Get the current chunk of (projectile) entities ArchetypeChunk chunk = chunks[chunkIndex]; // Get a native array of the entities in this chunk NativeArray chunkEntities = chunk.GetNativeArray(projectileChunkEntityArchetype); // Get arrays of the projectile and rotations components from the entities within this chunk NativeArray projectileComponents = chunk.GetNativeArray(projectileType); NativeArray rotationComponents = chunk.GetNativeArray(rotationType); // Loop through the entities in this chunk int chunkSize = chunk == null ? 0 : chunk.Count; for (int entityIndex = 0; entityIndex < chunkSize; entityIndex++) { Projectile projectile = projectileComponents[entityIndex]; Rotation rotation = rotationComponents[entityIndex]; #region Process Legacy Physics Raycast results hitInfo = raycastResults[raycastIndex]; other = hitInfo.collider; //Translation translation = translationComponents[entityIndex]; // If raycastResults[raycastIndex].collider == null there was no hit if (other != null) { bool isShieldHit = false; ShipControlModule shipControlModule = null; // Do we need to check for ship shield hits? if (projectile.shieldEffectsObjectPrefabID >= 0 && ProjectileModule.CheckShipHit(hitInfo, projectile.damageAmount, (ProjectileModule.DamageType)projectile.damageTypeInt, projectile.sourceShipId, projectile.sourceSquadronId, projectile.projectilePrefabID, out shipControlModule)) { isShieldHit = shipControlModule.shipInstance.HasActiveShield(hitInfo.point); } // No shield effects so perform a regular CheckShipHit else if (projectile.shieldEffectsObjectPrefabID < 0 && ProjectileModule.CheckShipHit(hitInfo, projectile.damageAmount, (ProjectileModule.DamageType)projectile.damageTypeInt, projectile.sourceShipId, projectile.sourceSquadronId, projectile.projectilePrefabID)) { // No need to do anything else here } else { // If it hit an object with a DamageReceiver script attached, take appropriate action like call a custom method ProjectileModule.CheckObjectHit(hitInfo, projectile.damageAmount, (ProjectileModule.DamageType)projectile.damageTypeInt, projectile.sourceShipId, projectile.sourceSquadronId, projectile.projectilePrefabID); } // OLD CODE pre v1.3.5 // Determine if it has hit a ship //if (!ProjectileModule.CheckShipHit(hitInfo, projectile.damageAmount, (ProjectileModule.DamageType)projectile.damageTypeInt, projectile.sourceShipId, projectile.sourceSquadronId, projectile.projectilePrefabID)) //{ // // If it hit an object with a DamageReceiver script attached, take appropriate action like call a custom method // ProjectileModule.CheckObjectHit(hitInfo, projectile.damageAmount, (ProjectileModule.DamageType)projectile.damageTypeInt, projectile.sourceShipId, projectile.sourceSquadronId, projectile.projectilePrefabID); //} // If required, use a shield EffectsObject. if (isShieldHit && projectile.shieldEffectsObjectPrefabID >= 0) { if (sscManager != null) { InstantiateEffectsObjectParameters ieParms = new InstantiateEffectsObjectParameters { effectsObjectPrefabID = projectile.shieldEffectsObjectPrefabID, position = hitInfo.point + (hitInfo.normal * 0.0005f), rotation = Quaternion.LookRotation(-hitInfo.normal), }; // For projectiles we don't need to get the effectsObject key from ieParms. sscManager.InstantiateEffectsObject(ref ieParms); } } else if (!isShieldHit && projectile.effectsObjectPrefabID >= 0 && sscManager != null) { InstantiateEffectsObjectParameters ieParms = new InstantiateEffectsObjectParameters { effectsObjectPrefabID = projectile.effectsObjectPrefabID, position = hitInfo.point, rotation = rotation.Value }; sscManager.InstantiateEffectsObject(ref ieParms); } // Mark this (projectile) entity to be destroyed entitiesToDestroy.Add(chunkEntities[entityIndex]); } // Should this entity be despawned? Use and "else" so we don't try to destroy it twice else { // We "could" create a job with a CommandBuffer which would queue all the DestroyEntity requests and then run // it at the end of the job on the main thread, that just creates more overhead. It "might" be // faster with Burst if there were a zillion projectiles but doing it directly on the main thread // is simplier and probably just as performant. // Is the projectile past it's use-by date? if (projectile.despawnTimer + deltaTime > projectile.despawnTime) { entitiesToDestroy.Add(chunkEntities[entityIndex]); } // If the Entity is not being destroyed, we "could" update the despawnTimer here on the main thread // using the following example code, however, we can do that more efficently in the projectileJob // below. Example code: entityManager.SetComponentData(chunkEntities[entityIndex], projectile); } #endregion #region Process Unity.Physics Raycast results #if SSC_PHYSICS if (numPhysicsRaycastHits > 0) { Unity.Physics.RaycastHit raycastHit = physicsraycastResults[projectile._tempIdx]; if (raycastHit.RigidBodyIndex >= 0 && raycastHit.RigidBodyIndex < numPhysicsBodies) { // If the surface normal vector has a length, we must have hit something if (math.abs(math.lengthsq(raycastHit.SurfaceNormal)) > 0f) { //Debug.Log("[DEBUG] PhysicsHit: " + raycastHit.Position.ToString() + " RigidBodyIndex: " + raycastHit.RigidBodyIndex); if (sscManager != null && projectile.effectsObjectPrefabID >= 0) { InstantiateEffectsObjectParameters ieParms = new InstantiateEffectsObjectParameters { effectsObjectPrefabID = projectile.effectsObjectPrefabID, position = raycastHit.Position, rotation = rotation.Value }; sscManager.InstantiateEffectsObject(ref ieParms); } // Mark this (projectile) entity to be destroyed entitiesToDestroy.Add(chunkEntities[entityIndex]); // Potentially destroy the object that was hit. //entitiesToDestroy.Add(buildPhysicsWorld.PhysicsWorld.CollisionWorld.Bodies[raycastHit.RigidBodyIndex].Entity); } } } #endif #endregion // Increment the raycast index - we're doing things this way to make sure that // in the second loop the indices all match up correctly // TODO: Need to check that this works - it might not work if, for instance, the chunks // get populated in a different order each time raycastIndex++; } } // It is faster to destroy a native array of projectiles, than one at a time entityManager.DestroyEntity(entitiesToDestroy.AsArray()); entitiesToDestroy.Clear(); #endregion // Dispose of the native array if (raycastResults.IsCreated) { raycastResults.Dispose(); } if (raycastCommands.IsCreated) { raycastCommands.Dispose(); } if (chunks.IsCreated) { chunks.Dispose(); } #region Unity.Physics Raycast Cleanup #if SSC_PHYSICS if (physicsraycastResults.IsCreated) { physicsraycastResults.Dispose(); } #endif #endregion #region Parallel Job to move the Projectiles // Create a new IJobChunk projectile move job, passing in the current frame time as // an argument along with the chunk component types. // Notice that we cannot used cached chunk component types. ProjectileMoveJob projectileMoveJob = new ProjectileMoveJob { deltaTime = deltaTime, #if UNITY_2020_1_OR_NEWER // Replace GetArchetypeChunkComponentType() with GetComponentTypeHandle() in Entities 0.12.0 translationType = GetComponentTypeHandle(), projectileType = GetComponentTypeHandle() #else translationType = GetArchetypeChunkComponentType(), projectileType = GetArchetypeChunkComponentType() #endif }; // Schedule the parallel projectile move job #if UNITY_2020_1_OR_NEWER this.Dependency = projectileMoveJob.ScheduleParallel(projectileEntityQuery, this.Dependency); #else return projectileMoveJob.Schedule(projectileEntityQuery, jobHandle); #endif #endregion } #endregion #region Private and Internal Methods /// /// Teleport (move) the projectiles a particular amount on x,y,z axes. /// /// internal void TelePortProjectiles(Vector3 deltaPosition) { // Create a new job ProjectileTelePortJob projectileTelePortJob = new ProjectileTelePortJob { deltaPosition = deltaPosition, #if UNITY_2020_1_OR_NEWER // Replace GetArchetypeChunkComponentType() with GetComponentTypeHandle() in Entities 0.12.0 translationType = GetComponentTypeHandle() #else translationType = GetArchetypeChunkComponentType() #endif }; // Schedule the parallel teleport job #if UNITY_2020_1_OR_NEWER this.Dependency = projectileTelePortJob.ScheduleParallel(projectileEntityQuery, this.Dependency); #else JobHandle jobHandle = projectileTelePortJob.Schedule(projectileEntityQuery); #endif } #endregion #region Public Static Methods /// /// Create a new projectile entity in the scene, based on a prefab that has already been converted /// from a gameobject prefab to an entity. /// Add a Projectile component if it wasn't already attached to the original gameobject prefab. /// Update the array of all projectile entities. /// It "might" be better to pass the ProjectModule instance reference which would simplify /// maintenance. /// NOTE: This method runs on the main thread. /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// public static void CreateProjectile ( Vector3 position, float3 weaponVelocity, float3 startFwdDirection, float3 startUpDirection, float startSpeed, float deltaTime, bool useGravity, float gravity, float3 gravityDirection, float damageAmount, float despawnTime, int projectilePrefabID, int effectsObjectPrefabID, int shieldEffectsObjectPrefabID, int shipId, int squadronId, int damageTypeInt, Entity projectilePrefabEntity ) { // The prefab is converted to an entity when the ProjectileTemplate is created. // Translation, Rotation, and RenderMesh components are automatically added to the input projectPrefabEntity // when it is converted from the gameobject prefab. So, we don't need to add them here. Entity entity = entityManager.Instantiate(projectilePrefabEntity); // Add a Projectile component if it wasn't on the template prefab if (!entityManager.HasComponent(entity, compTypeProjectile)) { entityManager.AddComponent(entity, compTypeProjectile); } //Debug.Log("[DEBUG] has projectilemodule: " + entityManager.HasComponent(entity, typeof(ProjectileModule))); //Debug.Log("[DEBUG] has RenderMesh: " + entityManager.HasComponent(entity, typeof(Unity.Rendering.RenderMesh))); float3 _velocity = startFwdDirection * startSpeed; // Set their position, rotation and projectile properties // Shift the position forward by the weapon velocity, so that projectiles don't ever end up behind the ship entityManager.SetComponentData(entity, new Translation() { Value = (float3)position + (_velocity * deltaTime) }); entityManager.SetComponentData(entity, new Rotation() { Value = quaternion.LookRotation(startFwdDirection, startUpDirection) }); entityManager.SetComponentData(entity, new Projectile() { // Initialise the velocity based on the forwards direction // The forwards direction should have been set correctly prior to enabling the object velocity = _velocity + weaponVelocity, // Store the current speed and forwards direction incase we want to change // these if gravity is being applied to the projectile useGravity = useGravity ? (byte)1 : (byte)0, gravity = gravity, gravityDirection = gravityDirection, damageAmount = damageAmount, despawnTime = despawnTime, despawnTimer = 0f, speed = startSpeed, fwdDirection = startFwdDirection, upDirection = startUpDirection, projectilePrefabID = projectilePrefabID, effectsObjectPrefabID = effectsObjectPrefabID, shieldEffectsObjectPrefabID = shieldEffectsObjectPrefabID, sourceShipId = shipId, sourceSquadronId = squadronId, damageTypeInt = damageTypeInt, _tempIdx = -1 } ); } #endregion } #endregion } #endif