using System; using System.Numerics; using System.Runtime.CompilerServices; using FFXIVClientStructs.FFXIV.Client.System.Memory; namespace Auspex.Lighting.Internal; internal sealed class NativeLightHandle : IDisposable { private unsafe GameLightData* data; private Vector3 position; private Quaternion rotation; private Vector3 color; private float intensity; private float range; private float lightAngle; private float falloffAngle; private AxLightFlags flags; private AxFalloffType falloffType; private float falloffPower; private float shadowNear; private float shadowFar; private float charaShadowRange; public unsafe static NativeLightHandle Create(Vector3 position, AxLightType lightType, AxLightConfig config) { GameLightData* ptr = IMemorySpace.GetDefaultSpace()->Malloc(8uL); if (LightNativeBridge.Spawn(ptr) == null) { IMemorySpace.Free(ptr, (ulong)sizeof(GameLightData)); throw new InvalidOperationException("Failed to spawn game light"); } LightNativeBridge.Create(ptr); LightNativeBridge.Finalize(ptr); ((long*)ptr)[7] |= 2L; LightRenderData* renderObject = ptr->RenderObject; if (renderObject == null) { IMemorySpace.Free(ptr, (ulong)sizeof(GameLightData)); throw new InvalidOperationException("Game did not create RenderObject for light"); } renderObject->Transform = &ptr->Transform; renderObject->EmissionType = lightType; renderObject->Color = config.Color; renderObject->Intensity = config.Intensity; renderObject->Range = config.Range; renderObject->FalloffType = config.FalloffType; renderObject->Falloff = config.FalloffPower; renderObject->LightAngle = config.LightAngle; renderObject->FalloffAngle = config.FalloffAngle; renderObject->CharacterShadowRange = config.CharaShadowRange; renderObject->ShadowPlaneNear = config.ShadowNear; renderObject->ShadowPlaneFar = config.ShadowFar; renderObject->Flags = config.Flags; LightNativeBridge.UpdateRange(renderObject); Quaternion quaternion = config.Rotation ?? Quaternion.Identity; Unsafe.Write((void*)(&ptr->Transform.Position), position); Unsafe.Write((void*)(&ptr->Transform.Rotation), quaternion); ptr->Scale = Vector3.One; return new NativeLightHandle { data = ptr, position = position, rotation = quaternion, color = config.Color, intensity = config.Intensity, range = config.Range, lightAngle = config.LightAngle, falloffAngle = config.FalloffAngle, flags = config.Flags, falloffType = config.FalloffType, falloffPower = config.FalloffPower, shadowNear = config.ShadowNear, shadowFar = config.ShadowFar, charaShadowRange = config.CharaShadowRange }; } public unsafe void UpdatePosition(Vector3 newPosition) { if (data != null && !(position == newPosition)) { position = newPosition; Unsafe.Write((void*)(&data->Transform.Position), newPosition); } } public unsafe void UpdateRotation(Quaternion newRotation) { if (data != null && !(rotation == newRotation)) { rotation = newRotation; Unsafe.Write((void*)(&data->Transform.Rotation), newRotation); } } public unsafe void UpdateColor(Vector3 newColor) { if (data != null && data->RenderObject != null && !(color == newColor)) { color = newColor; data->RenderObject->Color = newColor; } } public unsafe void UpdateIntensity(float newIntensity) { if (data != null && data->RenderObject != null && intensity != newIntensity) { intensity = newIntensity; data->RenderObject->Intensity = newIntensity; } } public unsafe void UpdateRange(float newRange) { if (data != null && data->RenderObject != null && range != newRange) { range = newRange; data->RenderObject->Range = newRange; LightNativeBridge.UpdateRange(data->RenderObject); } } public unsafe void UpdateFlags(AxLightFlags newFlags) { if (data != null && data->RenderObject != null && flags != newFlags) { flags = newFlags; data->RenderObject->Flags = newFlags; } } public unsafe void UpdateAngles(float newLightAngle, float newFalloffAngle) { if (data != null && data->RenderObject != null && (lightAngle != newLightAngle || falloffAngle != newFalloffAngle)) { lightAngle = newLightAngle; falloffAngle = newFalloffAngle; data->RenderObject->LightAngle = newLightAngle; data->RenderObject->FalloffAngle = newFalloffAngle; } } public unsafe void UpdateFalloff(AxFalloffType newFalloffType, float newFalloffPower) { if (data != null && data->RenderObject != null && (falloffType != newFalloffType || falloffPower != newFalloffPower)) { falloffType = newFalloffType; falloffPower = newFalloffPower; data->RenderObject->FalloffType = newFalloffType; data->RenderObject->Falloff = newFalloffPower; } } public unsafe void UpdateShadow(float newShadowNear, float newShadowFar, float newCharaShadowRange) { if (data != null && data->RenderObject != null && (shadowNear != newShadowNear || shadowFar != newShadowFar || charaShadowRange != newCharaShadowRange)) { shadowNear = newShadowNear; shadowFar = newShadowFar; charaShadowRange = newCharaShadowRange; data->RenderObject->ShadowPlaneNear = newShadowNear; data->RenderObject->ShadowPlaneFar = newShadowFar; data->RenderObject->CharacterShadowRange = newCharaShadowRange; } } internal unsafe void Tick() { if (data != null) { LightNativeBridge.UpdateCulling(data); LightNativeBridge.UpdateMaterial(data); } } public unsafe void Dispose() { if (data != null) { GameLightVTable* virtualTable = data->VirtualTable; if (virtualTable != null) { virtualTable->Cleanup(data); virtualTable->Destructor(data, false); } data = null; GC.SuppressFinalize(this); } } }