diff --git a/BusyRabbit.uproject.DotSettings.user b/BusyRabbit.uproject.DotSettings.user
new file mode 100644
index 0000000..9370ce5
--- /dev/null
+++ b/BusyRabbit.uproject.DotSettings.user
@@ -0,0 +1,5 @@
+
+	ForceIncluded
+	ForceIncluded
+	ForceIncluded
+	ForceIncluded
\ No newline at end of file
diff --git a/Config/DefaultEngine.ini b/Config/DefaultEngine.ini
index aec5a8b..cab2e2a 100644
--- a/Config/DefaultEngine.ini
+++ b/Config/DefaultEngine.ini
@@ -20,5 +20,11 @@ GameDefaultMap=/Game/Level/HomeLand.HomeLand
 
 [/Script/Engine.RendererSettings]
 r.Mobile.AntiAliasing=0
-r.AntiAliasingMethod=2
+r.AntiAliasingMethod=3
+r.DefaultFeature.MotionBlur=False
+
+[CoreRedirects]
++PropertyRedirects=(OldName="/Script/BusyRabbit.TerrainTileSetConfig.TerrainTileMapping",NewName="/Script/BusyRabbit.TerrainTileSetConfig.n")
++PropertyRedirects=(OldName="/Script/BusyRabbit.TerrainLayerComponent.TerrainLayers",NewName="/Script/BusyRabbit.TerrainLayerComponent.TerrainMeshes")
++ClassRedirects=(OldName="/Script/BusyRabbit.LevelPlayerController",NewName="/Script/BusyRabbit.LevelPlayerController")
 
diff --git a/Config/DefaultGameplayTags.ini b/Config/DefaultGameplayTags.ini
index a886ef0..e1885d6 100644
--- a/Config/DefaultGameplayTags.ini
+++ b/Config/DefaultGameplayTags.ini
@@ -35,12 +35,14 @@ NetIndexFirstBitSegment=16
 +GameplayTagList=(Tag="Recover.Role.Health",DevComment="回复生命值")
 +GameplayTagList=(Tag="Recover.Role.Hunger",DevComment="恢复饥饿值")
 +GameplayTagList=(Tag="Status.Role.Invincible",DevComment="不掉血标签")
++GameplayTagList=(Tag="Terrain.Desert",DevComment="荒漠地形")
 +GameplayTagList=(Tag="Terrain.Forest",DevComment="森林")
 +GameplayTagList=(Tag="Terrain.Grassland",DevComment="草地")
-+GameplayTagList=(Tag="Terrain.Water",DevComment="水体")
-+GameplayTagList=(Tag="Terrain.Water.Shallow", DevComment="浅水区")
-+GameplayTagList=(Tag="Terrain.Water.Deep", DevComment="深水区")
 +GameplayTagList=(Tag="Terrain.Land",DevComment="土地")
-+GameplayTagList=(Tag="Terrain.Swamp", DevComment="沼泽")
-+GameplayTagList=(Tag="Terrain.Swamp.Land", DevComment="沼泽陆地")
-+GameplayTagList=(Tag="Terrain.Swamp.Water", DevComment="沼泽水域")
++GameplayTagList=(Tag="Terrain.Swamp",DevComment="沼泽")
++GameplayTagList=(Tag="Terrain.Swamp.Land",DevComment="沼泽陆地")
++GameplayTagList=(Tag="Terrain.Swamp.Water",DevComment="沼泽水域")
++GameplayTagList=(Tag="Terrain.Water",DevComment="水体")
++GameplayTagList=(Tag="Terrain.Water.Deep",DevComment="深水区")
++GameplayTagList=(Tag="Terrain.Water.Shallow",DevComment="浅水区")
+
diff --git a/Content/Blueprint/Bp_BusyCharacter.uasset b/Content/Blueprint/Bp_BusyCharacter.uasset
deleted file mode 100644
index 1a3d134..0000000
Binary files a/Content/Blueprint/Bp_BusyCharacter.uasset and /dev/null differ
diff --git a/Content/Blueprint/Bp_BusyRole.uasset b/Content/Blueprint/Bp_BusyRole.uasset
index 371f598..38dcbca 100644
Binary files a/Content/Blueprint/Bp_BusyRole.uasset and b/Content/Blueprint/Bp_BusyRole.uasset differ
diff --git a/Content/Blueprint/Bp_HomelandGameMode.uasset b/Content/Blueprint/Bp_HomelandGameMode.uasset
deleted file mode 100644
index 24e0ca4..0000000
Binary files a/Content/Blueprint/Bp_HomelandGameMode.uasset and /dev/null differ
diff --git a/Content/Blueprint/Level/Actor/Holder.uasset b/Content/Blueprint/Level/Actor/Holder.uasset
new file mode 100644
index 0000000..ed7c905
Binary files /dev/null and b/Content/Blueprint/Level/Actor/Holder.uasset differ
diff --git a/Content/Blueprint/Level/Actor/Role/BP_Rabbit.uasset b/Content/Blueprint/Level/Actor/Role/BP_Rabbit.uasset
new file mode 100644
index 0000000..2de2771
Binary files /dev/null and b/Content/Blueprint/Level/Actor/Role/BP_Rabbit.uasset differ
diff --git a/Content/Blueprint/Level/BP_LevelMap.uasset b/Content/Blueprint/Level/BP_LevelMap.uasset
new file mode 100644
index 0000000..e6e6b2e
Binary files /dev/null and b/Content/Blueprint/Level/BP_LevelMap.uasset differ
diff --git a/Content/Blueprint/Level/GameMode/BP_BusyLevelGameMode.uasset b/Content/Blueprint/Level/GameMode/BP_BusyLevelGameMode.uasset
new file mode 100644
index 0000000..ee2dc77
Binary files /dev/null and b/Content/Blueprint/Level/GameMode/BP_BusyLevelGameMode.uasset differ
diff --git a/Content/Blueprint/Level/GameMode/BP_BusyLevelPlayerController.uasset b/Content/Blueprint/Level/GameMode/BP_BusyLevelPlayerController.uasset
new file mode 100644
index 0000000..6e70b90
Binary files /dev/null and b/Content/Blueprint/Level/GameMode/BP_BusyLevelPlayerController.uasset differ
diff --git a/Content/Blueprint/Level/GameMode/BP_BusyLevelPlayerState.uasset b/Content/Blueprint/Level/GameMode/BP_BusyLevelPlayerState.uasset
new file mode 100644
index 0000000..0e3a53c
Binary files /dev/null and b/Content/Blueprint/Level/GameMode/BP_BusyLevelPlayerState.uasset differ
diff --git a/Content/Data/Asset/Test.uasset b/Content/Data/Asset/Test.uasset
new file mode 100644
index 0000000..ee2a83c
Binary files /dev/null and b/Content/Data/Asset/Test.uasset differ
diff --git a/Content/Data/Input/Level/IA_Move.uasset b/Content/Data/Input/Level/IA_Move.uasset
new file mode 100644
index 0000000..8753fba
Binary files /dev/null and b/Content/Data/Input/Level/IA_Move.uasset differ
diff --git a/Content/Data/Input/Level/IMC_PlayerInputContext.uasset b/Content/Data/Input/Level/IMC_PlayerInputContext.uasset
new file mode 100644
index 0000000..ff99710
Binary files /dev/null and b/Content/Data/Input/Level/IMC_PlayerInputContext.uasset differ
diff --git a/Content/Level/BP_Test.uasset b/Content/Level/BP_Test.uasset
deleted file mode 100644
index c737e3d..0000000
Binary files a/Content/Level/BP_Test.uasset and /dev/null differ
diff --git a/Content/Level/FalconPlain.umap b/Content/Level/FalconPlain.umap
index 27a48ed..a0fcc07 100644
Binary files a/Content/Level/FalconPlain.umap and b/Content/Level/FalconPlain.umap differ
diff --git a/Content/Level/HomeLand.umap b/Content/Level/HomeLand.umap
index e2fdc8e..aaac49d 100644
Binary files a/Content/Level/HomeLand.umap and b/Content/Level/HomeLand.umap differ
diff --git a/Content/Level/NewWorld.umap b/Content/Level/NewWorld.umap
deleted file mode 100644
index bf5375d..0000000
Binary files a/Content/Level/NewWorld.umap and /dev/null differ
diff --git a/Content/Level/tileset.uasset b/Content/Level/tileset.uasset
deleted file mode 100644
index 1b5cb55..0000000
Binary files a/Content/Level/tileset.uasset and /dev/null differ
diff --git a/Content/Lua/GamePlay/Level/BusyLevelLogicSubSystem.lua b/Content/Lua/GamePlay/Level/BusyLevelLogicSubSystem.lua
index 64282bb..5e6abe0 100644
--- a/Content/Lua/GamePlay/Level/BusyLevelLogicSubSystem.lua
+++ b/Content/Lua/GamePlay/Level/BusyLevelLogicSubSystem.lua
@@ -42,11 +42,11 @@ function SubSystem:ReceiveWorldBeginPlay()
     self.generator = CreateItemGenerator(row_data)
 
     -- 创建初始篝火
-    local bonfire = BusyActorManagerSubSystem:SpawnBonfire(row_data.FirstBonfirePosition)
+    -- local bonfire = BusyActorManagerSubSystem:SpawnBonfire(row_data.FirstBonfirePosition)
 
     -- 创建角色
-    local role = BusyActorManagerSubSystem:SpawnRole(bonfire)
-    GameplayStatics.GetPlayerController(self, 0):Possess(role)
+    -- local role = BusyActorManagerSubSystem:SpawnRole(bonfire)
+    -- GameplayStatics.GetPlayerController(self, 0):Possess(role)
 end
 
 function SubSystem:ReceiveSubSystemTick(DeltaTime)
diff --git a/Content/Lua/Level/Actor/BusyPlayerRole.lua b/Content/Lua/Level/Actor/BusyPlayerRole.lua
new file mode 100644
index 0000000..68878ea
--- /dev/null
+++ b/Content/Lua/Level/Actor/BusyPlayerRole.lua
@@ -0,0 +1,11 @@
+local BusyPlayerRole = {}
+
+function BusyPlayerRole:UpdateMoveDirection(InDirection)
+    if(InDirection.Y > 0) then
+        self["SpineAnimationComponent"]:SetSkin("front/move")
+    else
+        self["SpineAnimationComponent"]:SetSkin("back/move")
+    end
+end
+
+return Class(nil, nil, BusyPlayerRole)
diff --git a/Content/Resource/Animation/Item/apple_Flipbook.uasset b/Content/Resource/Animation/Item/apple_Flipbook.uasset
deleted file mode 100644
index 59e58f2..0000000
Binary files a/Content/Resource/Animation/Item/apple_Flipbook.uasset and /dev/null differ
diff --git a/Content/Resource/Animation/Item/macadam_Filpbook.uasset b/Content/Resource/Animation/Item/macadam_Filpbook.uasset
deleted file mode 100644
index 36f0bdb..0000000
Binary files a/Content/Resource/Animation/Item/macadam_Filpbook.uasset and /dev/null differ
diff --git a/Content/Resource/Map/FalconPlain/TerrainGrid/desert-tilemap.uasset b/Content/Resource/Map/FalconPlain/TerrainGrid/desert-tilemap.uasset
new file mode 100644
index 0000000..8f780c4
Binary files /dev/null and b/Content/Resource/Map/FalconPlain/TerrainGrid/desert-tilemap.uasset differ
diff --git a/Content/Resource/Map/FalconPlain/TerrainGrid/desert-tilemap_TileSet.uasset b/Content/Resource/Map/FalconPlain/TerrainGrid/desert-tilemap_TileSet.uasset
new file mode 100644
index 0000000..5063b88
Binary files /dev/null and b/Content/Resource/Map/FalconPlain/TerrainGrid/desert-tilemap_TileSet.uasset differ
diff --git a/Content/Resource/Map/FalconPlain/TerrainGrid/grass-tilemap.uasset b/Content/Resource/Map/FalconPlain/TerrainGrid/grass-tilemap.uasset
new file mode 100644
index 0000000..194cc42
Binary files /dev/null and b/Content/Resource/Map/FalconPlain/TerrainGrid/grass-tilemap.uasset differ
diff --git a/Content/Resource/Map/FalconPlain/TerrainGrid/grass-tilemap_TileSet.uasset b/Content/Resource/Map/FalconPlain/TerrainGrid/grass-tilemap_TileSet.uasset
new file mode 100644
index 0000000..5faf2c3
Binary files /dev/null and b/Content/Resource/Map/FalconPlain/TerrainGrid/grass-tilemap_TileSet.uasset differ
diff --git a/Content/Resource/Map/FalconPlain/TerrainGrid/shallow-water-tilemap.uasset b/Content/Resource/Map/FalconPlain/TerrainGrid/shallow-water-tilemap.uasset
new file mode 100644
index 0000000..0f60a8b
Binary files /dev/null and b/Content/Resource/Map/FalconPlain/TerrainGrid/shallow-water-tilemap.uasset differ
diff --git a/Content/Level/tileset_TileSet.uasset b/Content/Resource/Map/FalconPlain/TerrainGrid/shallow-water-tilemap_TileSet.uasset
similarity index 52%
rename from Content/Level/tileset_TileSet.uasset
rename to Content/Resource/Map/FalconPlain/TerrainGrid/shallow-water-tilemap_TileSet.uasset
index b282d02..b860891 100644
Binary files a/Content/Level/tileset_TileSet.uasset and b/Content/Resource/Map/FalconPlain/TerrainGrid/shallow-water-tilemap_TileSet.uasset differ
diff --git a/Content/Resource/Map/FalconPlain/Test.uasset b/Content/Resource/Map/FalconPlain/Test.uasset
index 6b24590..414b774 100644
Binary files a/Content/Resource/Map/FalconPlain/Test.uasset and b/Content/Resource/Map/FalconPlain/Test.uasset differ
diff --git a/Content/Resource/Map/FalconPlain/Test/NewBlueprint.uasset b/Content/Resource/Map/FalconPlain/Test/NewBlueprint.uasset
new file mode 100644
index 0000000..b0417a3
Binary files /dev/null and b/Content/Resource/Map/FalconPlain/Test/NewBlueprint.uasset differ
diff --git a/Content/Resource/Map/FalconPlain/Test/grass-tilemap1.uasset b/Content/Resource/Map/FalconPlain/Test/grass-tilemap1.uasset
new file mode 100644
index 0000000..564c39d
Binary files /dev/null and b/Content/Resource/Map/FalconPlain/Test/grass-tilemap1.uasset differ
diff --git a/Content/Resource/Map/FalconPlain/Test/swamp-water-tilemap.uasset b/Content/Resource/Map/FalconPlain/Test/swamp-water-tilemap.uasset
new file mode 100644
index 0000000..4dcc775
Binary files /dev/null and b/Content/Resource/Map/FalconPlain/Test/swamp-water-tilemap.uasset differ
diff --git a/Content/Resource/Map/FalconPlain/Test/swamp-water-tilemap_Sprite.uasset b/Content/Resource/Map/FalconPlain/Test/swamp-water-tilemap_Sprite.uasset
new file mode 100644
index 0000000..4478fc2
Binary files /dev/null and b/Content/Resource/Map/FalconPlain/Test/swamp-water-tilemap_Sprite.uasset differ
diff --git a/Content/Resource/Map/FalconPlain/Test/swamp-water-tilemap_TileSet.uasset b/Content/Resource/Map/FalconPlain/Test/swamp-water-tilemap_TileSet.uasset
new file mode 100644
index 0000000..267f6d7
Binary files /dev/null and b/Content/Resource/Map/FalconPlain/Test/swamp-water-tilemap_TileSet.uasset differ
diff --git a/Content/Resource/Map/FalconPlain/deep-water-tilemap.uasset b/Content/Resource/Map/FalconPlain/deep-water-tilemap.uasset
new file mode 100644
index 0000000..d9e8872
Binary files /dev/null and b/Content/Resource/Map/FalconPlain/deep-water-tilemap.uasset differ
diff --git a/Content/Resource/Map/FalconPlain/deep-water-tilemap_TileSet.uasset b/Content/Resource/Map/FalconPlain/deep-water-tilemap_TileSet.uasset
new file mode 100644
index 0000000..dbeb3e4
Binary files /dev/null and b/Content/Resource/Map/FalconPlain/deep-water-tilemap_TileSet.uasset differ
diff --git a/Content/Resource/Map/FalconPlain/grass-tilemap_TileSet.uasset b/Content/Resource/Map/FalconPlain/grass-tilemap_TileSet.uasset
new file mode 100644
index 0000000..82ecf06
Binary files /dev/null and b/Content/Resource/Map/FalconPlain/grass-tilemap_TileSet.uasset differ
diff --git a/Content/Resource/Map/FalconPlain/lack-tilemap.uasset b/Content/Resource/Map/FalconPlain/lack-tilemap.uasset
index 19b0ab0..8bef309 100644
Binary files a/Content/Resource/Map/FalconPlain/lack-tilemap.uasset and b/Content/Resource/Map/FalconPlain/lack-tilemap.uasset differ
diff --git a/Content/Resource/Map/FalconPlain/lack-tilemap_TileSet.uasset b/Content/Resource/Map/FalconPlain/lack-tilemap_TileSet.uasset
index 5768f0e..cc61b67 100644
Binary files a/Content/Resource/Map/FalconPlain/lack-tilemap_TileSet.uasset and b/Content/Resource/Map/FalconPlain/lack-tilemap_TileSet.uasset differ
diff --git a/Content/Resource/Map/FalconPlain/land-tilemap.uasset b/Content/Resource/Map/FalconPlain/land-tilemap.uasset
new file mode 100644
index 0000000..80381a1
Binary files /dev/null and b/Content/Resource/Map/FalconPlain/land-tilemap.uasset differ
diff --git a/Content/Resource/Map/FalconPlain/land-tilemap_TileSet.uasset b/Content/Resource/Map/FalconPlain/land-tilemap_TileSet.uasset
new file mode 100644
index 0000000..76ab4d9
Binary files /dev/null and b/Content/Resource/Map/FalconPlain/land-tilemap_TileSet.uasset differ
diff --git a/Content/Resource/Map/FalconPlain/swamp-land-tilemap.uasset b/Content/Resource/Map/FalconPlain/swamp-land-tilemap.uasset
new file mode 100644
index 0000000..096ba54
Binary files /dev/null and b/Content/Resource/Map/FalconPlain/swamp-land-tilemap.uasset differ
diff --git a/Content/Resource/Map/FalconPlain/swamp-land-tilemap_TileSet.uasset b/Content/Resource/Map/FalconPlain/swamp-land-tilemap_TileSet.uasset
new file mode 100644
index 0000000..3cacf68
Binary files /dev/null and b/Content/Resource/Map/FalconPlain/swamp-land-tilemap_TileSet.uasset differ
diff --git a/Content/Resource/Map/FalconPlain/swamp-water-tilemap.uasset b/Content/Resource/Map/FalconPlain/swamp-water-tilemap.uasset
new file mode 100644
index 0000000..7e8498c
Binary files /dev/null and b/Content/Resource/Map/FalconPlain/swamp-water-tilemap.uasset differ
diff --git a/Content/Resource/Map/FalconPlain/swamp-water-tilemap_TileSet.uasset b/Content/Resource/Map/FalconPlain/swamp-water-tilemap_TileSet.uasset
new file mode 100644
index 0000000..ee185bb
Binary files /dev/null and b/Content/Resource/Map/FalconPlain/swamp-water-tilemap_TileSet.uasset differ
diff --git a/Content/Resource/Spine/Role/Rabbit/Rabbit.uasset b/Content/Resource/Spine/Role/Rabbit/Rabbit.uasset
new file mode 100644
index 0000000..cf2b3ec
Binary files /dev/null and b/Content/Resource/Spine/Role/Rabbit/Rabbit.uasset differ
diff --git a/Content/Resource/Spine/Role/Rabbit/RabbitData.uasset b/Content/Resource/Spine/Role/Rabbit/RabbitData.uasset
new file mode 100644
index 0000000..4eba779
Binary files /dev/null and b/Content/Resource/Spine/Role/Rabbit/RabbitData.uasset differ
diff --git a/Content/Resource/Spine/Role/Rabbit/Textures/Rabbit.uasset b/Content/Resource/Spine/Role/Rabbit/Textures/Rabbit.uasset
new file mode 100644
index 0000000..e0e10be
Binary files /dev/null and b/Content/Resource/Spine/Role/Rabbit/Textures/Rabbit.uasset differ
diff --git a/Content/Resource/Texture/Items/Textures/ItemIcons.uasset b/Content/Resource/Texture/Items/Textures/ItemIcons.uasset
index 6257d81..554fca4 100644
Binary files a/Content/Resource/Texture/Items/Textures/ItemIcons.uasset and b/Content/Resource/Texture/Items/Textures/ItemIcons.uasset differ
diff --git a/Plugins/SpinePlugin/Content/SpineLitNormalMaterial.uasset b/Plugins/SpinePlugin/Content/SpineLitNormalMaterial.uasset
new file mode 100644
index 0000000..b0a92fc
Binary files /dev/null and b/Plugins/SpinePlugin/Content/SpineLitNormalMaterial.uasset differ
diff --git a/Plugins/SpinePlugin/Content/SpineUnlitAdditiveMaterial.uasset b/Plugins/SpinePlugin/Content/SpineUnlitAdditiveMaterial.uasset
new file mode 100644
index 0000000..26d0e83
Binary files /dev/null and b/Plugins/SpinePlugin/Content/SpineUnlitAdditiveMaterial.uasset differ
diff --git a/Plugins/SpinePlugin/Content/SpineUnlitMultiplyMaterial.uasset b/Plugins/SpinePlugin/Content/SpineUnlitMultiplyMaterial.uasset
new file mode 100644
index 0000000..fd3eea9
Binary files /dev/null and b/Plugins/SpinePlugin/Content/SpineUnlitMultiplyMaterial.uasset differ
diff --git a/Plugins/SpinePlugin/Content/SpineUnlitNormalMaterial.uasset b/Plugins/SpinePlugin/Content/SpineUnlitNormalMaterial.uasset
new file mode 100644
index 0000000..bea17a3
Binary files /dev/null and b/Plugins/SpinePlugin/Content/SpineUnlitNormalMaterial.uasset differ
diff --git a/Plugins/SpinePlugin/Content/SpineUnlitScreenMaterial.uasset b/Plugins/SpinePlugin/Content/SpineUnlitScreenMaterial.uasset
new file mode 100644
index 0000000..541f616
Binary files /dev/null and b/Plugins/SpinePlugin/Content/SpineUnlitScreenMaterial.uasset differ
diff --git a/Plugins/SpinePlugin/Content/TestMaterial.uasset b/Plugins/SpinePlugin/Content/TestMaterial.uasset
new file mode 100644
index 0000000..acdf04d
Binary files /dev/null and b/Plugins/SpinePlugin/Content/TestMaterial.uasset differ
diff --git a/Plugins/SpinePlugin/Content/UI_SpineUnlitAdditiveMaterial.uasset b/Plugins/SpinePlugin/Content/UI_SpineUnlitAdditiveMaterial.uasset
new file mode 100644
index 0000000..bdd7406
Binary files /dev/null and b/Plugins/SpinePlugin/Content/UI_SpineUnlitAdditiveMaterial.uasset differ
diff --git a/Plugins/SpinePlugin/Content/UI_SpineUnlitMultiplyMaterial.uasset b/Plugins/SpinePlugin/Content/UI_SpineUnlitMultiplyMaterial.uasset
new file mode 100644
index 0000000..4cb8de5
Binary files /dev/null and b/Plugins/SpinePlugin/Content/UI_SpineUnlitMultiplyMaterial.uasset differ
diff --git a/Plugins/SpinePlugin/Content/UI_SpineUnlitNormalMaterial.uasset b/Plugins/SpinePlugin/Content/UI_SpineUnlitNormalMaterial.uasset
new file mode 100644
index 0000000..c42aa44
Binary files /dev/null and b/Plugins/SpinePlugin/Content/UI_SpineUnlitNormalMaterial.uasset differ
diff --git a/Plugins/SpinePlugin/Content/UI_SpineUnlitScreenMaterial.uasset b/Plugins/SpinePlugin/Content/UI_SpineUnlitScreenMaterial.uasset
new file mode 100644
index 0000000..9547cf6
Binary files /dev/null and b/Plugins/SpinePlugin/Content/UI_SpineUnlitScreenMaterial.uasset differ
diff --git a/Plugins/SpinePlugin/Resources/Icon128.png b/Plugins/SpinePlugin/Resources/Icon128.png
new file mode 100644
index 0000000..a20ae08
Binary files /dev/null and b/Plugins/SpinePlugin/Resources/Icon128.png differ
diff --git a/Plugins/SpinePlugin/Source/SpineEditorPlugin/Private/SpineAtlasImportFactory.cpp b/Plugins/SpinePlugin/Source/SpineEditorPlugin/Private/SpineAtlasImportFactory.cpp
new file mode 100644
index 0000000..d415330
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpineEditorPlugin/Private/SpineAtlasImportFactory.cpp
@@ -0,0 +1,143 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#include "SpineAtlasImportFactory.h"
+#include "AssetToolsModule.h"
+#include "SpineAtlasAsset.h"
+#include "Editor.h"
+
+#define LOCTEXT_NAMESPACE "Spine"
+
+using namespace spine;
+
+USpineAtlasAssetFactory::USpineAtlasAssetFactory(const FObjectInitializer &objectInitializer) : Super(objectInitializer) {
+	bCreateNew = false;
+	bEditAfterNew = true;
+	bEditorImport = true;
+	SupportedClass = USpineAtlasAsset::StaticClass();
+
+	Formats.Add(TEXT("atlas;Spine Atlas file"));
+}
+
+FText USpineAtlasAssetFactory::GetToolTip() const {
+	return LOCTEXT("SpineAtlasAssetFactory", "Animations exported from Spine");
+}
+
+bool USpineAtlasAssetFactory::FactoryCanImport(const FString &Filename) {
+	return true;
+}
+
+UObject *USpineAtlasAssetFactory::FactoryCreateFile(UClass *InClass, UObject *InParent, FName InName, EObjectFlags Flags, const FString &Filename, const TCHAR *Parms, FFeedbackContext *Warn, bool &bOutOperationCanceled) {
+	FString FileExtension = FPaths::GetExtension(Filename);
+	GEditor->GetEditorSubsystem()->BroadcastAssetPreImport(this, InClass, InParent, InName, *FileExtension);
+
+	FString rawString;
+	if (!FFileHelper::LoadFileToString(rawString, *Filename)) {
+		return nullptr;
+	}
+
+	FString currentSourcePath, filenameNoExtension, unusedExtension;
+	const FString longPackagePath = FPackageName::GetLongPackagePath(InParent->GetOutermost()->GetPathName());
+	FPaths::Split(UFactory::GetCurrentFilename(), currentSourcePath, filenameNoExtension, unusedExtension);
+
+	USpineAtlasAsset *asset = NewObject(InParent, InClass, InName, Flags);
+	asset->SetRawData(rawString);
+	asset->SetAtlasFileName(FName(*Filename));
+	LoadAtlas(asset, currentSourcePath, longPackagePath);
+	GEditor->GetEditorSubsystem()->BroadcastAssetPostImport(this, asset);
+	return asset;
+}
+
+bool USpineAtlasAssetFactory::CanReimport(UObject *Obj, TArray &OutFilenames) {
+	USpineAtlasAsset *asset = Cast(Obj);
+	if (!asset) return false;
+
+	FString filename = asset->GetAtlasFileName().ToString();
+	if (!filename.IsEmpty())
+		OutFilenames.Add(filename);
+
+	return true;
+}
+
+void USpineAtlasAssetFactory::SetReimportPaths(UObject *Obj, const TArray &NewReimportPaths) {
+	USpineAtlasAsset *asset = Cast(Obj);
+
+	if (asset && ensure(NewReimportPaths.Num() == 1))
+		asset->SetAtlasFileName(FName(*NewReimportPaths[0]));
+}
+
+EReimportResult::Type USpineAtlasAssetFactory::Reimport(UObject *Obj) {
+	USpineAtlasAsset *asset = Cast(Obj);
+	FString rawString;
+	if (!FFileHelper::LoadFileToString(rawString, *asset->GetAtlasFileName().ToString())) return EReimportResult::Failed;
+	asset->SetRawData(rawString);
+
+	FString currentSourcePath, filenameNoExtension, unusedExtension;
+	const FString longPackagePath = FPackageName::GetLongPackagePath(asset->GetOutermost()->GetPathName());
+	FString currentFileName = asset->GetAtlasFileName().ToString();
+	FPaths::Split(currentFileName, currentSourcePath, filenameNoExtension, unusedExtension);
+
+	LoadAtlas(asset, currentSourcePath, longPackagePath);
+
+	if (Obj->GetOuter()) Obj->GetOuter()->MarkPackageDirty();
+	else
+		Obj->MarkPackageDirty();
+
+	GEditor->GetEditorSubsystem()->BroadcastAssetReimport(asset);
+	return EReimportResult::Succeeded;
+}
+
+UTexture2D *resolveTexture(USpineAtlasAsset *Asset, const FString &PageFileName, const FString &TargetSubPath) {
+	FAssetToolsModule &AssetToolsModule = FModuleManager::GetModuleChecked("AssetTools");
+
+	TArray fileNames;
+	fileNames.Add(PageFileName);
+
+	TArray importedAsset = AssetToolsModule.Get().ImportAssets(fileNames, TargetSubPath);
+	UTexture2D *texture = (importedAsset.Num() > 0) ? Cast(importedAsset[0]) : nullptr;
+
+	return texture;
+}
+
+void USpineAtlasAssetFactory::LoadAtlas(USpineAtlasAsset *Asset, const FString &CurrentSourcePath, const FString &LongPackagePath) {
+	Atlas *atlas = Asset->GetAtlas();
+	Asset->atlasPages.Empty();
+
+	const FString targetTexturePath = LongPackagePath / TEXT("Textures");
+
+	Vector &pages = atlas->getPages();
+	for (size_t i = 0, n = pages.size(); i < n; i++) {
+		AtlasPage *page = pages[i];
+		const FString sourceTextureFilename = FPaths::Combine(*CurrentSourcePath, UTF8_TO_TCHAR(page->name.buffer()));
+		UTexture2D *texture = resolveTexture(Asset, sourceTextureFilename, targetTexturePath);
+		Asset->atlasPages.Add(texture);
+	}
+}
+
+#undef LOCTEXT_NAMESPACE
diff --git a/Plugins/SpinePlugin/Source/SpineEditorPlugin/Private/SpineEditorPlugin.cpp b/Plugins/SpinePlugin/Source/SpineEditorPlugin/Private/SpineEditorPlugin.cpp
new file mode 100644
index 0000000..d912358
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpineEditorPlugin/Private/SpineEditorPlugin.cpp
@@ -0,0 +1,71 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#include "SpineEditorPlugin.h"
+#include "AssetTypeActions_Base.h"
+#include "SpineAtlasAsset.h"
+#include "SpineSkeletonDataAsset.h"
+
+class FSpineAtlasAssetTypeActions : public FAssetTypeActions_Base {
+public:
+	UClass *GetSupportedClass() const override { return USpineAtlasAsset::StaticClass(); };
+	FText GetName() const override { return INVTEXT("Spine atlas asset"); };
+	FColor GetTypeColor() const override { return FColor::Red; };
+	uint32 GetCategories() override { return EAssetTypeCategories::Misc; };
+};
+
+class FSpineSkeletonDataAssetTypeActions : public FAssetTypeActions_Base {
+public:
+	UClass *GetSupportedClass() const override { return USpineSkeletonDataAsset::StaticClass(); };
+	FText GetName() const override { return INVTEXT("Spine data asset"); };
+	FColor GetTypeColor() const override { return FColor::Red; };
+	uint32 GetCategories() override { return EAssetTypeCategories::Misc; };
+};
+
+class FSpineEditorPlugin : public ISpineEditorPlugin {
+	virtual void StartupModule() override;
+	virtual void ShutdownModule() override;
+	TSharedPtr SpineAtlasAssetTypeActions;
+	TSharedPtr SpineSkeletonDataAssetTypeActions;
+};
+
+IMPLEMENT_MODULE(FSpineEditorPlugin, SpineEditorPlugin)
+
+void FSpineEditorPlugin::StartupModule() {
+	SpineAtlasAssetTypeActions = MakeShared();
+	FAssetToolsModule::GetModule().Get().RegisterAssetTypeActions(SpineAtlasAssetTypeActions.ToSharedRef());
+	SpineSkeletonDataAssetTypeActions = MakeShared();
+	FAssetToolsModule::GetModule().Get().RegisterAssetTypeActions(SpineSkeletonDataAssetTypeActions.ToSharedRef());
+}
+
+void FSpineEditorPlugin::ShutdownModule() {
+	if (!FModuleManager::Get().IsModuleLoaded("AssetTools")) return;
+	FAssetToolsModule::GetModule().Get().UnregisterAssetTypeActions(SpineAtlasAssetTypeActions.ToSharedRef());
+	FAssetToolsModule::GetModule().Get().UnregisterAssetTypeActions(SpineSkeletonDataAssetTypeActions.ToSharedRef());
+}
diff --git a/Plugins/SpinePlugin/Source/SpineEditorPlugin/Private/SpineSkeletonImportFactory.cpp b/Plugins/SpinePlugin/Source/SpineEditorPlugin/Private/SpineSkeletonImportFactory.cpp
new file mode 100644
index 0000000..b27dcac
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpineEditorPlugin/Private/SpineSkeletonImportFactory.cpp
@@ -0,0 +1,126 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#include "SpineSkeletonImportFactory.h"
+#include "AssetToolsModule.h"
+#include "Developer/AssetTools/Public/IAssetTools.h"
+#include "SpineSkeletonDataAsset.h"
+#include 
+
+#define LOCTEXT_NAMESPACE "Spine"
+
+USpineSkeletonAssetFactory::USpineSkeletonAssetFactory(const FObjectInitializer &objectInitializer) : Super(objectInitializer) {
+	bCreateNew = false;
+	bEditAfterNew = true;
+	bEditorImport = true;
+	SupportedClass = USpineSkeletonDataAsset::StaticClass();
+
+	Formats.Add(TEXT("json;Spine skeleton file"));
+	Formats.Add(TEXT("skel;Spine skeleton file"));
+}
+
+FText USpineSkeletonAssetFactory::GetToolTip() const {
+	return LOCTEXT("USpineSkeletonAssetFactory", "Animations exported from Spine");
+}
+
+bool USpineSkeletonAssetFactory::FactoryCanImport(const FString &Filename) {
+	if (Filename.Contains(TEXT(".skel"))) return true;
+
+	if (Filename.Contains(TEXT(".json"))) {
+		TArray rawData;
+		if (!FFileHelper::LoadFileToArray(rawData, *Filename, 0)) {
+			return false;
+		}
+		if (rawData.Num() == 0) return false;
+		return strcmp((const char *) rawData.GetData(), "skeleton") > 0 && strcmp((const char *) rawData.GetData(), "spine") > 0;
+	}
+
+	return false;
+}
+
+void LoadAtlas(const FString &Filename, const FString &TargetPath) {
+	FAssetToolsModule &AssetToolsModule = FModuleManager::GetModuleChecked("AssetTools");
+
+	FString skelFile = Filename.Replace(TEXT(".skel"), TEXT(".atlas")).Replace(TEXT(".json"), TEXT(".atlas"));
+	if (!FPaths::FileExists(skelFile)) return;
+
+	TArray fileNames;
+	fileNames.Add(skelFile);
+	AssetToolsModule.Get().ImportAssets(fileNames, TargetPath);
+}
+
+UObject *USpineSkeletonAssetFactory::FactoryCreateFile(UClass *InClass, UObject *InParent, FName InName, EObjectFlags Flags, const FString &Filename, const TCHAR *Parms, FFeedbackContext *Warn, bool &bOutOperationCanceled) {
+	USpineSkeletonDataAsset *asset = NewObject(InParent, InClass, InName, Flags);
+	TArray rawData;
+	if (!FFileHelper::LoadFileToArray(rawData, *Filename, 0)) {
+		return nullptr;
+	}
+	asset->SetSkeletonDataFileName(FName(*Filename));
+	asset->SetRawData(rawData);
+
+	const FString longPackagePath = FPackageName::GetLongPackagePath(asset->GetOutermost()->GetPathName());
+	LoadAtlas(Filename, longPackagePath);
+	return asset;
+}
+
+bool USpineSkeletonAssetFactory::CanReimport(UObject *Obj, TArray &OutFilenames) {
+	USpineSkeletonDataAsset *asset = Cast(Obj);
+	if (!asset) return false;
+
+	FString filename = asset->GetSkeletonDataFileName().ToString();
+	if (!filename.IsEmpty())
+		OutFilenames.Add(filename);
+
+	return true;
+}
+
+void USpineSkeletonAssetFactory::SetReimportPaths(UObject *Obj, const TArray &NewReimportPaths) {
+	USpineSkeletonDataAsset *asset = Cast(Obj);
+
+	if (asset && ensure(NewReimportPaths.Num() == 1))
+		asset->SetSkeletonDataFileName(FName(*NewReimportPaths[0]));
+}
+
+EReimportResult::Type USpineSkeletonAssetFactory::Reimport(UObject *Obj) {
+	USpineSkeletonDataAsset *asset = Cast(Obj);
+	TArray rawData;
+	if (!FFileHelper::LoadFileToArray(rawData, *asset->GetSkeletonDataFileName().ToString(), 0)) return EReimportResult::Failed;
+	asset->SetRawData(rawData);
+
+	const FString longPackagePath = FPackageName::GetLongPackagePath(asset->GetOutermost()->GetPathName());
+	LoadAtlas(*asset->GetSkeletonDataFileName().ToString(), longPackagePath);
+
+	if (Obj->GetOuter()) Obj->GetOuter()->MarkPackageDirty();
+	else
+		Obj->MarkPackageDirty();
+
+	return EReimportResult::Succeeded;
+}
+
+#undef LOCTEXT_NAMESPACE
diff --git a/Plugins/SpinePlugin/Source/SpineEditorPlugin/Public/SpineAtlasImportFactory.h b/Plugins/SpinePlugin/Source/SpineEditorPlugin/Public/SpineAtlasImportFactory.h
new file mode 100644
index 0000000..054ec3b
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpineEditorPlugin/Public/SpineAtlasImportFactory.h
@@ -0,0 +1,52 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#pragma once
+
+// clang-format off
+#include "SpineAtlasAsset.h"
+#include "UnrealEd.h"
+#include "SpineAtlasImportFactory.generated.h"
+// clang-format on
+
+UCLASS()
+class USpineAtlasAssetFactory : public UFactory, public FReimportHandler {
+	GENERATED_UCLASS_BODY()
+
+	virtual FText GetToolTip() const override;
+
+	virtual bool FactoryCanImport(const FString &Filename) override;
+	virtual UObject *FactoryCreateFile(UClass *InClass, UObject *InParent, FName InName, EObjectFlags Flags, const FString &Filename, const TCHAR *Parms, FFeedbackContext *Warn, bool &bOutOperationCanceled) override;
+
+	virtual bool CanReimport(UObject *Obj, TArray &OutFilenames) override;
+	virtual void SetReimportPaths(UObject *Obj, const TArray &NewReimportPaths) override;
+	virtual EReimportResult::Type Reimport(UObject *Obj) override;
+
+	void LoadAtlas(USpineAtlasAsset *Asset, const FString &CurrentSourcePath, const FString &LongPackagePath);
+};
diff --git a/Plugins/SpinePlugin/Source/SpineEditorPlugin/Public/SpineEditorPlugin.h b/Plugins/SpinePlugin/Source/SpineEditorPlugin/Public/SpineEditorPlugin.h
new file mode 100644
index 0000000..86ce55a
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpineEditorPlugin/Public/SpineEditorPlugin.h
@@ -0,0 +1,44 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#pragma once
+
+#include "Modules/ModuleManager.h"
+
+class ISpineEditorPlugin : public IModuleInterface {
+
+public:
+	static inline ISpineEditorPlugin &Get() {
+		return FModuleManager::LoadModuleChecked("SpineEditorPlugin");
+	}
+
+	static inline bool IsAvailable() {
+		return FModuleManager::Get().IsModuleLoaded("SpineEditorPlugin");
+	}
+};
diff --git a/Plugins/SpinePlugin/Source/SpineEditorPlugin/Public/SpineSkeletonImportFactory.h b/Plugins/SpinePlugin/Source/SpineEditorPlugin/Public/SpineSkeletonImportFactory.h
new file mode 100644
index 0000000..652113a
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpineEditorPlugin/Public/SpineSkeletonImportFactory.h
@@ -0,0 +1,49 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#pragma once
+
+// clang-format off
+#include "SpineAtlasAsset.h"
+#include "UnrealEd.h"
+#include "SpineSkeletonImportFactory.generated.h"
+// clang-format on
+
+UCLASS()
+class USpineSkeletonAssetFactory : public UFactory, public FReimportHandler {
+	GENERATED_UCLASS_BODY()
+
+	virtual FText GetToolTip() const override;
+	virtual bool FactoryCanImport(const FString &Filename) override;
+	virtual UObject *FactoryCreateFile(UClass *InClass, UObject *InParent, FName InName, EObjectFlags Flags, const FString &Filename, const TCHAR *Parms, FFeedbackContext *Warn, bool &bOutOperationCanceled) override;
+
+	virtual bool CanReimport(UObject *Obj, TArray &OutFilenames) override;
+	virtual void SetReimportPaths(UObject *Obj, const TArray &NewReimportPaths) override;
+	virtual EReimportResult::Type Reimport(UObject *Obj) override;
+};
diff --git a/Plugins/SpinePlugin/Source/SpineEditorPlugin/SpineEditorPlugin.Build.cs b/Plugins/SpinePlugin/Source/SpineEditorPlugin/SpineEditorPlugin.Build.cs
new file mode 100644
index 0000000..364a720
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpineEditorPlugin/SpineEditorPlugin.Build.cs
@@ -0,0 +1,36 @@
+using System.IO;
+
+namespace UnrealBuildTool.Rules
+{
+	public class SpineEditorPlugin : ModuleRules
+	{
+		public SpineEditorPlugin(ReadOnlyTargetRules target) : base(target)
+		{
+			PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
+
+            PublicIncludePaths.Add(Path.Combine(ModuleDirectory, "Public"));
+			PublicIncludePaths.Add(Path.Combine(ModuleDirectory, "../SpinePlugin/Public/spine-cpp/include"));
+
+			PrivateIncludePaths.Add(Path.Combine(ModuleDirectory, "Private"));
+			PrivateIncludePaths.Add(Path.Combine(ModuleDirectory, "../SpinePlugin/Public/spine-cpp/include"));
+
+            PublicDependencyModuleNames.AddRange(new [] {
+                "Core",
+                "CoreUObject",
+                "Engine",
+                "UnrealEd",
+                "SpinePlugin"
+            });
+
+            PublicIncludePathModuleNames.AddRange(new [] {
+               "AssetTools",
+               "AssetRegistry"
+            });
+
+            DynamicallyLoadedModuleNames.AddRange(new [] {
+               "AssetTools",
+               "AssetRegistry"
+            });
+		}
+	}
+}
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Private/SSpineWidget.cpp b/Plugins/SpinePlugin/Source/SpinePlugin/Private/SSpineWidget.cpp
new file mode 100644
index 0000000..585291c
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Private/SSpineWidget.cpp
@@ -0,0 +1,415 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#include "SSpineWidget.h"
+#include "Framework/Application/SlateApplication.h"
+#include "Materials/MaterialInstanceDynamic.h"
+#include "Materials/MaterialInterface.h"
+#include "Rendering/DrawElements.h"
+#include "Runtime/SlateRHIRenderer/Public/Interfaces/ISlateRHIRendererModule.h"
+#include "Slate/SMeshWidget.h"
+#include "Slate/SlateVectorArtData.h"
+#include "SlateMaterialBrush.h"
+#include "SpineWidget.h"
+#include 
+
+using namespace spine;
+
+static int brushNameId = 0;
+
+// Workaround for https://github.com/EsotericSoftware/spine-runtimes/issues/1458
+// See issue comments for more information.
+struct SpineSlateMaterialBrush : public FSlateBrush {
+	static TArray NamePool;
+	static FCriticalSection NamePoolLock;
+
+	SpineSlateMaterialBrush(class UMaterialInterface &InMaterial, const FVector2D &InImageSize)
+		: FSlateBrush(ESlateBrushDrawType::Image, FName(TEXT("None")), FMargin(0), ESlateBrushTileType::NoTile, ESlateBrushImageType::FullColor, InImageSize, FLinearColor::White, &InMaterial) {
+		// Workaround for https://github.com/EsotericSoftware/spine-runtimes/issues/2006
+		FScopeLock Lock(&NamePoolLock);
+
+		if (NamePool.Num() > 0) {
+			ResourceName = NamePool.Pop(false);
+		} else {
+			static uint32 NextId = 0;
+			FString brushName = TEXT("SpineSlateMatBrush");
+			brushName.AppendInt(NextId++);
+			ResourceName = FName(*brushName);
+		}
+	}
+
+	~SpineSlateMaterialBrush() {
+		FScopeLock Lock(&NamePoolLock);
+		NamePool.Add(ResourceName);
+	}
+};
+
+TArray SpineSlateMaterialBrush::NamePool;
+FCriticalSection SpineSlateMaterialBrush::NamePoolLock;
+
+void SSpineWidget::Construct(const FArguments &args) {
+}
+
+void SSpineWidget::SetData(USpineWidget *Widget) {
+	this->widget = Widget;
+	if (widget && widget->skeleton && widget->Atlas) {
+		Skeleton *skeleton = widget->skeleton;
+		skeleton->setToSetupPose();
+		skeleton->updateWorldTransform(Physics_None);
+		Vector scratchBuffer;
+		float x, y, w, h;
+		skeleton->getBounds(x, y, w, h, scratchBuffer);
+		boundsMin.X = x;
+		boundsMin.Y = y;
+		boundsSize.X = w;
+		boundsSize.Y = h;
+	}
+}
+
+static void setVertex(FSlateVertex *vertex, float x, float y, float u, float v, const FColor &color, const FVector2D &offset) {
+	vertex->Position.X = offset.X + x;
+	vertex->Position.Y = offset.Y + y;
+	vertex->TexCoords[0] = u;
+	vertex->TexCoords[1] = v;
+	vertex->TexCoords[2] = u;
+	vertex->TexCoords[3] = v;
+	vertex->MaterialTexCoords.X = u;
+	vertex->MaterialTexCoords.Y = v;
+	vertex->Color = color;
+	vertex->PixelSize[0] = 1;
+	vertex->PixelSize[1] = 1;
+}
+
+int32 SSpineWidget::OnPaint(const FPaintArgs &Args, const FGeometry &AllottedGeometry, const FSlateRect &MyClippingRect, FSlateWindowElementList &OutDrawElements,
+							int32 LayerId, const FWidgetStyle &InWidgetStyle, bool bParentEnabled) const {
+
+	SSpineWidget *self = (SSpineWidget *) this;
+	UMaterialInstanceDynamic *MatNow = nullptr;
+
+	if (widget && widget->skeleton && widget->Atlas) {
+		widget->skeleton->getColor().set(widget->Color.R, widget->Color.G, widget->Color.B, widget->Color.A);
+
+		if (widget->atlasNormalBlendMaterials.Num() != widget->Atlas->atlasPages.Num()) {
+			widget->atlasNormalBlendMaterials.SetNum(0);
+			widget->pageToNormalBlendMaterial.Empty();
+			widget->atlasAdditiveBlendMaterials.SetNum(0);
+			widget->pageToAdditiveBlendMaterial.Empty();
+			widget->atlasMultiplyBlendMaterials.SetNum(0);
+			widget->pageToMultiplyBlendMaterial.Empty();
+			widget->atlasScreenBlendMaterials.SetNum(0);
+			widget->pageToScreenBlendMaterial.Empty();
+
+			for (int i = 0; i < widget->Atlas->atlasPages.Num(); i++) {
+				AtlasPage *currPage = widget->Atlas->GetAtlas()->getPages()[i];
+
+				UMaterialInstanceDynamic *material = UMaterialInstanceDynamic::Create(widget->NormalBlendMaterial, widget);
+				material->SetTextureParameterValue(widget->TextureParameterName, widget->Atlas->atlasPages[i]);
+				widget->atlasNormalBlendMaterials.Add(material);
+				widget->pageToNormalBlendMaterial.Add(currPage, material);
+
+				material = UMaterialInstanceDynamic::Create(widget->AdditiveBlendMaterial, widget);
+				material->SetTextureParameterValue(widget->TextureParameterName, widget->Atlas->atlasPages[i]);
+				widget->atlasAdditiveBlendMaterials.Add(material);
+				widget->pageToAdditiveBlendMaterial.Add(currPage, material);
+
+				material = UMaterialInstanceDynamic::Create(widget->MultiplyBlendMaterial, widget);
+				material->SetTextureParameterValue(widget->TextureParameterName, widget->Atlas->atlasPages[i]);
+				widget->atlasMultiplyBlendMaterials.Add(material);
+				widget->pageToMultiplyBlendMaterial.Add(currPage, material);
+
+				material = UMaterialInstanceDynamic::Create(widget->ScreenBlendMaterial, widget);
+				material->SetTextureParameterValue(widget->TextureParameterName, widget->Atlas->atlasPages[i]);
+				widget->atlasScreenBlendMaterials.Add(material);
+				widget->pageToScreenBlendMaterial.Add(currPage, material);
+			}
+		} else {
+			widget->pageToNormalBlendMaterial.Empty();
+			widget->pageToAdditiveBlendMaterial.Empty();
+			widget->pageToMultiplyBlendMaterial.Empty();
+			widget->pageToScreenBlendMaterial.Empty();
+
+			for (int i = 0; i < widget->Atlas->atlasPages.Num(); i++) {
+				AtlasPage *currPage = widget->Atlas->GetAtlas()->getPages()[i];
+
+				UTexture2D *texture = widget->Atlas->atlasPages[i];
+				UTexture *oldTexture = nullptr;
+
+				UMaterialInstanceDynamic *current = widget->atlasNormalBlendMaterials[i];
+				if (!current || !current->GetTextureParameterValue(widget->TextureParameterName, oldTexture) || oldTexture != texture) {
+					UMaterialInstanceDynamic *material = UMaterialInstanceDynamic::Create(widget->NormalBlendMaterial, widget);
+					material->SetTextureParameterValue(widget->TextureParameterName, texture);
+					widget->atlasNormalBlendMaterials[i] = material;
+				}
+				widget->pageToNormalBlendMaterial.Add(currPage, widget->atlasNormalBlendMaterials[i]);
+
+				current = widget->atlasAdditiveBlendMaterials[i];
+				if (!current || !current->GetTextureParameterValue(widget->TextureParameterName, oldTexture) || oldTexture != texture) {
+					UMaterialInstanceDynamic *material = UMaterialInstanceDynamic::Create(widget->AdditiveBlendMaterial, widget);
+					material->SetTextureParameterValue(widget->TextureParameterName, texture);
+					widget->atlasAdditiveBlendMaterials[i] = material;
+				}
+				widget->pageToAdditiveBlendMaterial.Add(currPage, widget->atlasAdditiveBlendMaterials[i]);
+
+				current = widget->atlasMultiplyBlendMaterials[i];
+				if (!current || !current->GetTextureParameterValue(widget->TextureParameterName, oldTexture) || oldTexture != texture) {
+					UMaterialInstanceDynamic *material = UMaterialInstanceDynamic::Create(widget->MultiplyBlendMaterial, widget);
+					material->SetTextureParameterValue(widget->TextureParameterName, texture);
+					widget->atlasMultiplyBlendMaterials[i] = material;
+				}
+				widget->pageToMultiplyBlendMaterial.Add(currPage, widget->atlasMultiplyBlendMaterials[i]);
+
+				current = widget->atlasScreenBlendMaterials[i];
+				if (!current || !current->GetTextureParameterValue(widget->TextureParameterName, oldTexture) || oldTexture != texture) {
+					UMaterialInstanceDynamic *material = UMaterialInstanceDynamic::Create(widget->ScreenBlendMaterial, widget);
+					material->SetTextureParameterValue(widget->TextureParameterName, texture);
+					widget->atlasScreenBlendMaterials[i] = material;
+				}
+				widget->pageToScreenBlendMaterial.Add(currPage, widget->atlasScreenBlendMaterials[i]);
+			}
+		}
+
+		self->UpdateMesh(LayerId, OutDrawElements, AllottedGeometry, widget->skeleton);
+	}
+
+	return LayerId;
+}
+
+void SSpineWidget::Flush(int32 LayerId, FSlateWindowElementList &OutDrawElements, const FGeometry &AllottedGeometry, int &Idx, TArray &Vertices, TArray &Indices, TArray &Uvs, TArray &Colors, TArray &Colors2, UMaterialInstanceDynamic *Material) {
+	if (Vertices.Num() == 0) return;
+	SSpineWidget *self = (SSpineWidget *) this;
+
+	const FVector2D widgetSize = AllottedGeometry.GetLocalSize();
+	const FVector2D sizeScale = widgetSize / FVector2D(boundsSize.X, boundsSize.Y);
+	const float setupScale = sizeScale.GetMin();
+
+	for (int i = 0; i < Vertices.Num(); i++) {
+		Vertices[i] = (Vertices[i] + FVector(-boundsMin.X - boundsSize.X / 2, boundsMin.Y + boundsSize.Y / 2, 0)) * setupScale + FVector(widgetSize.X / 2, widgetSize.Y / 2, 0);
+	}
+
+	self->renderData.IndexData.SetNumUninitialized(Indices.Num());
+	SlateIndex *indexData = (SlateIndex *) renderData.IndexData.GetData();
+	for (int i = 0; i < Indices.Num(); i++) {
+		indexData[i] = (SlateIndex) Indices[i];
+	}
+
+	self->renderData.VertexData.SetNumUninitialized(Vertices.Num());
+	FSlateVertex *vertexData = (FSlateVertex *) renderData.VertexData.GetData();
+	FVector2D offset = AllottedGeometry.GetAbsolutePositionAtCoordinates(FVector2D(0.0f, 0.0f));
+	FColor white = FColor(0xffffffff);
+	const FSlateRenderTransform &Transform = AllottedGeometry.GetAccumulatedRenderTransform();
+
+	for (size_t i = 0; i < (size_t) Vertices.Num(); i++) {
+		setVertex(&vertexData[i], 0, 0, Uvs[i].X, Uvs[i].Y, Colors[i], Transform.TransformPoint(FVector2D(Vertices[i])));
+	}
+
+	brush = &widget->Brush;
+	if (Material) {
+		renderData.Brush = MakeShareable(new SpineSlateMaterialBrush(*Material, FVector2D(64, 64)));
+		renderData.RenderingResourceHandle = FSlateApplication::Get().GetRenderer()->GetResourceHandle(*renderData.Brush);
+	}
+
+	if (renderData.RenderingResourceHandle.IsValid()) {
+		FSlateDrawElement::MakeCustomVerts(OutDrawElements, LayerId, renderData.RenderingResourceHandle, renderData.VertexData, renderData.IndexData, nullptr, 0, 0);
+	}
+
+	Vertices.SetNum(0);
+	Indices.SetNum(0);
+	Uvs.SetNum(0);
+	Colors.SetNum(0);
+	Colors2.SetNum(0);
+	Idx++;
+}
+
+FVector2D SSpineWidget::ComputeDesiredSize(float X) const {
+	if (widget && widget->skeleton && widget->Atlas) {
+		return FVector2D(boundsSize.X, boundsSize.Y);
+	} else {
+		return FVector2D(256, 256);
+	}
+}
+
+void SSpineWidget::UpdateMesh(int32 LayerId, FSlateWindowElementList &OutDrawElements, const FGeometry &AllottedGeometry, Skeleton *Skeleton) {
+	TArray vertices;
+	TArray indices;
+	TArray uvs;
+	TArray colors;
+	TArray darkColors;
+
+	int idx = 0;
+	int meshSection = 0;
+	UMaterialInstanceDynamic *lastMaterial = nullptr;
+
+	SkeletonClipping &clipper = widget->clipper;
+	Vector &worldVertices = widget->worldVertices;
+
+	float depthOffset = 0;
+	unsigned short quadIndices[] = {0, 1, 2, 0, 2, 3};
+
+	for (int i = 0; i < (int) Skeleton->getSlots().size(); ++i) {
+		Vector *attachmentVertices = &worldVertices;
+		unsigned short *attachmentIndices = nullptr;
+		int numVertices;
+		int numIndices;
+		AtlasRegion *attachmentAtlasRegion = nullptr;
+		Color attachmentColor;
+		attachmentColor.set(1, 1, 1, 1);
+		float *attachmentUvs = nullptr;
+
+		Slot *slot = Skeleton->getDrawOrder()[i];
+		if (!slot->getBone().isActive()) {
+			clipper.clipEnd(*slot);
+			continue;
+		}
+
+		Attachment *attachment = slot->getAttachment();
+		if (!attachment) {
+			clipper.clipEnd(*slot);
+			continue;
+		}
+		if (!attachment->getRTTI().isExactly(RegionAttachment::rtti) && !attachment->getRTTI().isExactly(MeshAttachment::rtti) && !attachment->getRTTI().isExactly(ClippingAttachment::rtti)) {
+			clipper.clipEnd(*slot);
+			continue;
+		}
+
+		if (attachment->getRTTI().isExactly(RegionAttachment::rtti)) {
+			RegionAttachment *regionAttachment = (RegionAttachment *) attachment;
+			attachmentColor.set(regionAttachment->getColor());
+			attachmentVertices->setSize(8, 0);
+			regionAttachment->computeWorldVertices(*slot, *attachmentVertices, 0, 2);
+			attachmentAtlasRegion = (AtlasRegion *) regionAttachment->getRegion();
+			attachmentIndices = quadIndices;
+			attachmentUvs = regionAttachment->getUVs().buffer();
+			numVertices = 4;
+			numIndices = 6;
+		} else if (attachment->getRTTI().isExactly(MeshAttachment::rtti)) {
+			MeshAttachment *mesh = (MeshAttachment *) attachment;
+			attachmentColor.set(mesh->getColor());
+			attachmentVertices->setSize(mesh->getWorldVerticesLength(), 0);
+			mesh->computeWorldVertices(*slot, 0, mesh->getWorldVerticesLength(), attachmentVertices->buffer(), 0, 2);
+			attachmentAtlasRegion = (AtlasRegion *) mesh->getRegion();
+			attachmentIndices = mesh->getTriangles().buffer();
+			attachmentUvs = mesh->getUVs().buffer();
+			numVertices = mesh->getWorldVerticesLength() >> 1;
+			numIndices = mesh->getTriangles().size();
+		} else /* clipping */ {
+			ClippingAttachment *clip = (ClippingAttachment *) attachment;
+			clipper.clipStart(*slot, clip);
+			continue;
+		}
+
+		// if the user switches the atlas data while not having switched
+		// to the correct skeleton data yet, we won't find any regions.
+		// ignore regions for which we can't find a material
+		UMaterialInstanceDynamic *material = nullptr;
+		switch (slot->getData().getBlendMode()) {
+			case BlendMode_Normal:
+				if (!widget->pageToNormalBlendMaterial.Contains(attachmentAtlasRegion->page)) {
+					clipper.clipEnd(*slot);
+					continue;
+				}
+				material = widget->pageToNormalBlendMaterial[attachmentAtlasRegion->page];
+				break;
+			case BlendMode_Additive:
+				if (!widget->pageToAdditiveBlendMaterial.Contains(attachmentAtlasRegion->page)) {
+					clipper.clipEnd(*slot);
+					continue;
+				}
+				material = widget->pageToAdditiveBlendMaterial[attachmentAtlasRegion->page];
+				break;
+			case BlendMode_Multiply:
+				if (!widget->pageToMultiplyBlendMaterial.Contains(attachmentAtlasRegion->page)) {
+					clipper.clipEnd(*slot);
+					continue;
+				}
+				material = widget->pageToMultiplyBlendMaterial[attachmentAtlasRegion->page];
+				break;
+			case BlendMode_Screen:
+				if (!widget->pageToScreenBlendMaterial.Contains(attachmentAtlasRegion->page)) {
+					clipper.clipEnd(*slot);
+					continue;
+				}
+				material = widget->pageToScreenBlendMaterial[attachmentAtlasRegion->page];
+				break;
+			default:
+				if (!widget->pageToNormalBlendMaterial.Contains(attachmentAtlasRegion->page)) {
+					clipper.clipEnd(*slot);
+					continue;
+				}
+				material = widget->pageToNormalBlendMaterial[attachmentAtlasRegion->page];
+		}
+
+		if (clipper.isClipping()) {
+			clipper.clipTriangles(attachmentVertices->buffer(), attachmentIndices, numIndices, attachmentUvs, 2);
+			attachmentVertices = &clipper.getClippedVertices();
+			numVertices = clipper.getClippedVertices().size() >> 1;
+			attachmentIndices = clipper.getClippedTriangles().buffer();
+			numIndices = clipper.getClippedTriangles().size();
+			attachmentUvs = clipper.getClippedUVs().buffer();
+			if (clipper.getClippedTriangles().size() == 0) {
+				clipper.clipEnd(*slot);
+				continue;
+			}
+		}
+
+		if (lastMaterial != material) {
+			Flush(LayerId, OutDrawElements, AllottedGeometry, meshSection, vertices, indices, uvs, colors, darkColors, lastMaterial);
+			lastMaterial = material;
+			idx = 0;
+		}
+
+		uint8 r = static_cast(Skeleton->getColor().r * slot->getColor().r * attachmentColor.r * 255);
+		uint8 g = static_cast(Skeleton->getColor().g * slot->getColor().g * attachmentColor.g * 255);
+		uint8 b = static_cast(Skeleton->getColor().b * slot->getColor().b * attachmentColor.b * 255);
+		uint8 a = static_cast(Skeleton->getColor().a * slot->getColor().a * attachmentColor.a * 255);
+
+		float dr = slot->hasDarkColor() ? slot->getDarkColor().r : 0.0f;
+		float dg = slot->hasDarkColor() ? slot->getDarkColor().g : 0.0f;
+		float db = slot->hasDarkColor() ? slot->getDarkColor().b : 0.0f;
+
+		float *verticesPtr = attachmentVertices->buffer();
+		for (int j = 0; j < numVertices << 1; j += 2) {
+			colors.Add(FColor(r, g, b, a));
+			darkColors.Add(FVector(dr, dg, db));
+			vertices.Add(FVector(verticesPtr[j], -verticesPtr[j + 1], depthOffset));
+			uvs.Add(FVector2D(attachmentUvs[j], attachmentUvs[j + 1]));
+		}
+
+		for (int j = 0; j < numIndices; j++) {
+			indices.Add(idx + attachmentIndices[j]);
+		}
+
+		idx += numVertices;
+		depthOffset += widget->DepthOffset;
+
+		clipper.clipEnd(*slot);
+	}
+
+	Flush(LayerId, OutDrawElements, AllottedGeometry, meshSection, vertices, indices, uvs, colors, darkColors, lastMaterial);
+	clipper.clipEnd();
+}
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineAtlasAsset.cpp b/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineAtlasAsset.cpp
new file mode 100644
index 0000000..0971e52
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineAtlasAsset.cpp
@@ -0,0 +1,124 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#include "SpineAtlasAsset.h"
+#include "spine/spine.h"
+#include 
+#include 
+
+#include "EditorFramework/AssetImportData.h"
+
+#define LOCTEXT_NAMESPACE "Spine"
+
+using namespace spine;
+
+#if WITH_EDITORONLY_DATA
+
+void USpineAtlasAsset::SetAtlasFileName(const FName &AtlasFileName) {
+	importData->UpdateFilenameOnly(AtlasFileName.ToString());
+	TArray files;
+	importData->ExtractFilenames(files);
+	if (files.Num() > 0)
+		atlasFileName = FName(*files[0]);
+}
+
+void USpineAtlasAsset::PostInitProperties() {
+	if (!HasAnyFlags(RF_ClassDefaultObject))
+		importData = NewObject(this, TEXT("AssetImportData"));
+	Super::PostInitProperties();
+}
+
+void USpineAtlasAsset::Serialize(FArchive &Ar) {
+	Super::Serialize(Ar);
+#if ENGINE_MAJOR_VERSION == 4 && ENGINE_MINOR_VERSION <= 27
+	if (Ar.IsLoading() && Ar.UE4Ver() < VER_UE4_ASSET_IMPORT_DATA_AS_JSON && !importData)
+#else
+	if (Ar.IsLoading() && Ar.UEVer() < VER_UE4_ASSET_IMPORT_DATA_AS_JSON && !importData)
+#endif
+		importData = NewObject(this, TEXT("AssetImportData"));
+}
+
+#endif
+
+FName USpineAtlasAsset::GetAtlasFileName() const {
+#if WITH_EDITORONLY_DATA
+	TArray files;
+	if (importData)
+		importData->ExtractFilenames(files);
+	if (files.Num() > 0)
+		return FName(*files[0]);
+	else
+		return atlasFileName;
+#else
+	return atlasFileName;
+#endif
+}
+
+void USpineAtlasAsset::SetRawData(const FString &RawData) {
+	this->rawData = RawData;
+	if (atlas) {
+		delete atlas;
+		atlas = nullptr;
+	}
+}
+
+void USpineAtlasAsset::BeginDestroy() {
+	if (atlas) {
+		delete atlas;
+		atlas = nullptr;
+	}
+	Super::BeginDestroy();
+}
+
+class UETextureLoader : public TextureLoader {
+	void load(AtlasPage &page, const String &path) {
+		page.texture = (void *) (uintptr_t) page.index;
+	}
+
+	void unload(void *texture) {
+	}
+};
+
+UETextureLoader _spineUETextureLoader;
+
+Atlas *USpineAtlasAsset::GetAtlas() {
+	if (!atlas) {
+		if (atlas) {
+			delete atlas;
+			atlas = nullptr;
+		}
+		std::string t = TCHAR_TO_UTF8(*rawData);
+
+		atlas = new (__FILE__, __LINE__)
+				Atlas(t.c_str(), strlen(t.c_str()), "", &_spineUETextureLoader);
+	}
+	return this->atlas;
+}
+
+#undef LOCTEXT_NAMESPACE
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineBoneDriverComponent.cpp b/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineBoneDriverComponent.cpp
new file mode 100644
index 0000000..830786e
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineBoneDriverComponent.cpp
@@ -0,0 +1,67 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#include "SpineBoneDriverComponent.h"
+#include "SpineSkeletonComponent.h"
+#include "GameFramework/Actor.h"
+
+USpineBoneDriverComponent::USpineBoneDriverComponent() {
+	PrimaryComponentTick.bCanEverTick = true;
+	bTickInEditor = true;
+	bAutoActivate = true;
+}
+
+void USpineBoneDriverComponent::BeginPlay() {
+	Super::BeginPlay();
+}
+
+void USpineBoneDriverComponent::BeforeUpdateWorldTransform(USpineSkeletonComponent *skeleton) {
+	if (skeleton == lastBoundComponent && skeleton) {
+		if (UseComponentTransform) {
+			skeleton->SetBoneWorldPosition(BoneName, GetComponentLocation());
+		} else {
+			AActor *owner = GetOwner();
+			if (owner) skeleton->SetBoneWorldPosition(BoneName, owner->GetActorLocation());
+		}
+	}
+}
+
+void USpineBoneDriverComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) {
+	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
+
+	if (Target) {
+		USpineSkeletonComponent *skeleton = static_cast(Target->GetComponentByClass(USpineSkeletonComponent::StaticClass()));
+		if (skeleton != lastBoundComponent && skeleton) {
+			// if (lastBoundComponent) lastBoundComponent->BeforeUpdateWorldTransform.RemoveAll(this);
+			if (!skeleton->BeforeUpdateWorldTransform.GetAllObjects().Contains(this))
+				skeleton->BeforeUpdateWorldTransform.AddDynamic(this, &USpineBoneDriverComponent::BeforeUpdateWorldTransform);
+			lastBoundComponent = skeleton;
+		}
+	}
+}
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineBoneFollowerComponent.cpp b/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineBoneFollowerComponent.cpp
new file mode 100644
index 0000000..cbbb8de
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineBoneFollowerComponent.cpp
@@ -0,0 +1,65 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#include "SpineBoneFollowerComponent.h"
+#include "SpineSkeletonComponent.h"
+#include "GameFramework/Actor.h"
+
+USpineBoneFollowerComponent::USpineBoneFollowerComponent() {
+	PrimaryComponentTick.bCanEverTick = true;
+	bTickInEditor = true;
+	bAutoActivate = true;
+}
+
+void USpineBoneFollowerComponent::BeginPlay() {
+	Super::BeginPlay();
+}
+
+void USpineBoneFollowerComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) {
+	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
+
+	if (Target) {
+		USpineSkeletonComponent *skeleton = static_cast(Target->GetComponentByClass(USpineSkeletonComponent::StaticClass()));
+		if (skeleton) {
+			FTransform transform = skeleton->GetBoneWorldTransform(BoneName);
+			if (UseComponentTransform) {
+				if (UsePosition) SetWorldLocation(transform.GetLocation());
+				if (UseRotation) SetWorldRotation(transform.GetRotation());
+				if (UseScale) SetWorldScale3D(transform.GetScale3D());
+			} else {
+				AActor *owner = GetOwner();
+				if (owner) {
+					if (UsePosition) owner->SetActorLocation(transform.GetLocation());
+					if (UseRotation) owner->SetActorRotation(transform.GetRotation());
+					if (UseScale) owner->SetActorScale3D(transform.GetScale3D());
+				}
+			}
+		}
+	}
+}
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpinePlugin.cpp b/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpinePlugin.cpp
new file mode 100644
index 0000000..c7d66ec
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpinePlugin.cpp
@@ -0,0 +1,74 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#include "SpinePlugin.h"
+#include "spine/Extension.h"
+
+DEFINE_LOG_CATEGORY(SpineLog);
+
+class FSpinePlugin : public SpinePlugin {
+	virtual void StartupModule() override;
+	virtual void ShutdownModule() override;
+};
+
+IMPLEMENT_MODULE(FSpinePlugin, SpinePlugin)
+
+void FSpinePlugin::StartupModule() {
+}
+
+void FSpinePlugin::ShutdownModule() {}
+
+class Ue4Extension : public spine::DefaultSpineExtension {
+public:
+	Ue4Extension() : spine::DefaultSpineExtension() {}
+
+	virtual ~Ue4Extension() {}
+
+	virtual void *_alloc(size_t size, const char *file, int line) {
+		return FMemory::Malloc(size);
+	}
+
+	virtual void *_calloc(size_t size, const char *file, int line) {
+		void *result = FMemory::Malloc(size);
+		FMemory::Memset(result, 0, size);
+		return result;
+	}
+
+	virtual void *_realloc(void *ptr, size_t size, const char *file, int line) {
+		return FMemory::Realloc(ptr, size);
+	}
+
+	virtual void _free(void *mem, const char *file, int line) {
+		FMemory::Free(mem);
+	}
+};
+
+spine::SpineExtension *spine::getDefaultExtension() {
+	return new Ue4Extension();
+}
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineSkeletonAnimationComponent.cpp b/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineSkeletonAnimationComponent.cpp
new file mode 100644
index 0000000..5d7033c
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineSkeletonAnimationComponent.cpp
@@ -0,0 +1,314 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#include "SpineSkeletonAnimationComponent.h"
+#include "SpineAtlasAsset.h"
+
+#define LOCTEXT_NAMESPACE "Spine"
+
+using namespace spine;
+
+void UTrackEntry::SetTrackEntry(TrackEntry *trackEntry) {
+	if (entry) entry->setRendererObject(nullptr);
+	this->entry = trackEntry;
+	if (entry) entry->setRendererObject((void *) this);
+}
+
+void callback(AnimationState *state, spine::EventType type, TrackEntry *entry, Event *event) {
+	USpineSkeletonAnimationComponent *component = (USpineSkeletonAnimationComponent *) state->getRendererObject();
+
+	if (entry->getRendererObject()) {
+		UTrackEntry *uEntry = (UTrackEntry *) entry->getRendererObject();
+		if (type == EventType_Start) {
+			component->AnimationStart.Broadcast(uEntry);
+			uEntry->AnimationStart.Broadcast(uEntry);
+		} else if (type == EventType_Interrupt) {
+			component->AnimationInterrupt.Broadcast(uEntry);
+			uEntry->AnimationInterrupt.Broadcast(uEntry);
+		} else if (type == EventType_Event) {
+			FSpineEvent evt;
+			evt.SetEvent(event);
+			component->AnimationEvent.Broadcast(uEntry, evt);
+			uEntry->AnimationEvent.Broadcast(uEntry, evt);
+		} else if (type == EventType_Complete) {
+			component->AnimationComplete.Broadcast(uEntry);
+			uEntry->AnimationComplete.Broadcast(uEntry);
+		} else if (type == EventType_End) {
+			component->AnimationEnd.Broadcast(uEntry);
+			uEntry->AnimationEnd.Broadcast(uEntry);
+		} else if (type == EventType_Dispose) {
+			component->AnimationDispose.Broadcast(uEntry);
+			uEntry->AnimationDispose.Broadcast(uEntry);
+			uEntry->SetTrackEntry(nullptr);
+			component->GCTrackEntry(uEntry);
+		}
+	}
+}
+
+USpineSkeletonAnimationComponent::USpineSkeletonAnimationComponent() {
+	PrimaryComponentTick.bCanEverTick = true;
+	bTickInEditor = true;
+	bAutoActivate = true;
+	bAutoPlaying = true;
+	physicsTimeScale = 1;
+}
+
+void USpineSkeletonAnimationComponent::BeginPlay() {
+	Super::BeginPlay();
+	for (UTrackEntry *entry : trackEntries) {
+		if (entry && entry->GetTrackEntry()) {
+			entry->GetTrackEntry()->setRendererObject(nullptr);
+		}
+	}
+	trackEntries.Empty();
+}
+
+void UTrackEntry::BeginDestroy() {
+	Super::BeginDestroy();
+}
+
+void USpineSkeletonAnimationComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) {
+	Super::Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
+
+	InternalTick(DeltaTime, true, TickType == LEVELTICK_ViewportsOnly);
+}
+
+void USpineSkeletonAnimationComponent::InternalTick(float DeltaTime, bool CallDelegates, bool Preview) {
+	CheckState();
+
+	if (state && bAutoPlaying) {
+		if (Preview) {
+			if (lastPreviewAnimation != PreviewAnimation) {
+				if (PreviewAnimation != "") SetAnimation(0, PreviewAnimation, true);
+				else
+					SetEmptyAnimation(0, 0);
+				lastPreviewAnimation = PreviewAnimation;
+			}
+
+			if (lastPreviewSkin != PreviewSkin) {
+				if (PreviewSkin != "") SetSkin(PreviewSkin);
+				else
+					SetSkin("default");
+				lastPreviewSkin = PreviewSkin;
+			}
+		}
+		state->update(DeltaTime);
+		state->apply(*skeleton);
+		if (CallDelegates) BeforeUpdateWorldTransform.Broadcast(this);
+		skeleton->update(physicsTimeScale * DeltaTime);
+		skeleton->updateWorldTransform(Physics_Update);
+		if (CallDelegates) AfterUpdateWorldTransform.Broadcast(this);
+	}
+}
+
+void USpineSkeletonAnimationComponent::CheckState() {
+	bool needsUpdate = lastAtlas != Atlas || lastData != SkeletonData;
+
+	if (!needsUpdate) {
+		// Are we doing a re-import? Then check if the underlying spine-cpp data
+		// has changed.
+		if (lastAtlas && lastAtlas == Atlas && lastData && lastData == SkeletonData) {
+			spine::Atlas *atlas = Atlas->GetAtlas();
+			if (lastSpineAtlas != atlas) {
+				needsUpdate = true;
+			}
+			if (skeleton && skeleton->getData() != SkeletonData->GetSkeletonData(atlas)) {
+				needsUpdate = true;
+			}
+		}
+	}
+
+	if (needsUpdate) {
+		DisposeState();
+
+		if (Atlas && SkeletonData) {
+			spine::SkeletonData *data = SkeletonData->GetSkeletonData(Atlas->GetAtlas());
+			if (data) {
+				skeleton = new (__FILE__, __LINE__) Skeleton(data);
+				AnimationStateData *stateData = SkeletonData->GetAnimationStateData(Atlas->GetAtlas());
+				state = new (__FILE__, __LINE__) AnimationState(stateData);
+				state->setRendererObject((void *) this);
+				state->setListener(callback);
+				trackEntries.Empty();
+			}
+		}
+
+		lastAtlas = Atlas;
+		lastSpineAtlas = Atlas ? Atlas->GetAtlas() : nullptr;
+		lastData = SkeletonData;
+	}
+}
+
+void USpineSkeletonAnimationComponent::DisposeState() {
+	if (state) {
+		delete state;
+		state = nullptr;
+	}
+
+	if (skeleton) {
+		delete skeleton;
+		skeleton = nullptr;
+	}
+
+	trackEntries.Empty();
+}
+
+void USpineSkeletonAnimationComponent::FinishDestroy() {
+	DisposeState();
+	Super::FinishDestroy();
+}
+
+void USpineSkeletonAnimationComponent::SetAutoPlay(bool bInAutoPlays) {
+	bAutoPlaying = bInAutoPlays;
+}
+
+void USpineSkeletonAnimationComponent::SetPlaybackTime(float InPlaybackTime, bool bCallDelegates) {
+	CheckState();
+
+	if (state && state->getCurrent(0)) {
+		spine::Animation *CurrentAnimation = state->getCurrent(0)->getAnimation();
+		const float CurrentTime = state->getCurrent(0)->getTrackTime();
+		InPlaybackTime = FMath::Clamp(InPlaybackTime, 0.0f, CurrentAnimation->getDuration());
+		const float DeltaTime = InPlaybackTime - CurrentTime;
+		state->update(DeltaTime);
+		state->apply(*skeleton);
+
+		//Call delegates and perform the world transform
+		if (bCallDelegates) {
+			BeforeUpdateWorldTransform.Broadcast(this);
+		}
+		skeleton->updateWorldTransform(Physics_Update);
+		if (bCallDelegates) {
+			AfterUpdateWorldTransform.Broadcast(this);
+		}
+	}
+}
+
+void USpineSkeletonAnimationComponent::SetTimeScale(float timeScale) {
+	CheckState();
+	if (state) state->setTimeScale(timeScale);
+}
+
+float USpineSkeletonAnimationComponent::GetTimeScale() {
+	CheckState();
+	if (state) return state->getTimeScale();
+	return 1;
+}
+
+UTrackEntry *USpineSkeletonAnimationComponent::SetAnimation(int trackIndex, FString animationName, bool loop) {
+	CheckState();
+	if (state && skeleton->getData()->findAnimation(TCHAR_TO_UTF8(*animationName))) {
+		state->disableQueue();
+		TrackEntry *entry = state->setAnimation(trackIndex, TCHAR_TO_UTF8(*animationName), loop);
+		state->enableQueue();
+		UTrackEntry *uEntry = NewObject();
+		uEntry->SetTrackEntry(entry);
+		trackEntries.Add(uEntry);
+		return uEntry;
+	} else
+		return NewObject();
+}
+
+UTrackEntry *USpineSkeletonAnimationComponent::AddAnimation(int trackIndex, FString animationName, bool loop, float delay) {
+	CheckState();
+	if (state && skeleton->getData()->findAnimation(TCHAR_TO_UTF8(*animationName))) {
+		state->disableQueue();
+		TrackEntry *entry = state->addAnimation(trackIndex, TCHAR_TO_UTF8(*animationName), loop, delay);
+		state->enableQueue();
+		UTrackEntry *uEntry = NewObject();
+		uEntry->SetTrackEntry(entry);
+		trackEntries.Add(uEntry);
+		return uEntry;
+	} else
+		return NewObject();
+}
+
+UTrackEntry *USpineSkeletonAnimationComponent::SetEmptyAnimation(int trackIndex, float mixDuration) {
+	CheckState();
+	if (state) {
+		TrackEntry *entry = state->setEmptyAnimation(trackIndex, mixDuration);
+		UTrackEntry *uEntry = NewObject();
+		uEntry->SetTrackEntry(entry);
+		trackEntries.Add(uEntry);
+		return uEntry;
+	} else
+		return NewObject();
+}
+
+UTrackEntry *USpineSkeletonAnimationComponent::AddEmptyAnimation(int trackIndex, float mixDuration, float delay) {
+	CheckState();
+	if (state) {
+		TrackEntry *entry = state->addEmptyAnimation(trackIndex, mixDuration, delay);
+		UTrackEntry *uEntry = NewObject();
+		uEntry->SetTrackEntry(entry);
+		trackEntries.Add(uEntry);
+		return uEntry;
+	} else
+		return NewObject();
+}
+
+UTrackEntry *USpineSkeletonAnimationComponent::GetCurrent(int trackIndex) {
+	CheckState();
+	if (state && state->getCurrent(trackIndex)) {
+		TrackEntry *entry = state->getCurrent(trackIndex);
+		if (entry->getRendererObject()) {
+			return (UTrackEntry *) entry->getRendererObject();
+		} else {
+			UTrackEntry *uEntry = NewObject();
+			uEntry->SetTrackEntry(entry);
+			trackEntries.Add(uEntry);
+			return uEntry;
+		}
+	} else
+		return NewObject();
+}
+
+void USpineSkeletonAnimationComponent::ClearTracks() {
+	CheckState();
+	if (state) {
+		state->clearTracks();
+	}
+}
+
+void USpineSkeletonAnimationComponent::ClearTrack(int trackIndex) {
+	CheckState();
+	if (state) {
+		state->clearTrack(trackIndex);
+	}
+}
+
+void USpineSkeletonAnimationComponent::SetPhysicsTimeScale(float scale) {
+	physicsTimeScale = scale;
+}
+
+float USpineSkeletonAnimationComponent::GetPhysicsTimeScale() {
+	return physicsTimeScale;
+}
+
+#undef LOCTEXT_NAMESPACE
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineSkeletonComponent.cpp b/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineSkeletonComponent.cpp
new file mode 100644
index 0000000..513702f
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineSkeletonComponent.cpp
@@ -0,0 +1,376 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#include "SpineSkeletonComponent.h"
+#include "SpineSkeletonRendererComponent.h"
+#include "SpineAtlasAsset.h"
+
+#include "spine/spine.h"
+
+#define LOCTEXT_NAMESPACE "Spine"
+
+using namespace spine;
+
+USpineSkeletonComponent::USpineSkeletonComponent() {
+	PrimaryComponentTick.bCanEverTick = true;
+	bTickInEditor = true;
+	bAutoActivate = true;
+}
+
+bool USpineSkeletonComponent::SetSkins(UPARAM(ref) TArray &SkinNames) {
+	CheckState();
+	if (skeleton) {
+		spine::Skin *newSkin = new spine::Skin("__spine-ue3_custom_skin");
+		for (auto &skinName : SkinNames) {
+			spine::Skin *skin = skeleton->getData()->findSkin(TCHAR_TO_UTF8(*skinName));
+			if (!skin) {
+				delete newSkin;
+				return false;
+			}
+			newSkin->addSkin(skin);
+		}
+		skeleton->setSkin(newSkin);
+		if (customSkin != nullptr) {
+			delete customSkin;
+		}
+		customSkin = newSkin;
+		return true;
+	} else
+		return false;
+}
+
+bool USpineSkeletonComponent::SetSkin(const FString skinName) {
+	CheckState();
+	if (skeleton) {
+		Skin *skin = skeleton->getData()->findSkin(TCHAR_TO_UTF8(*skinName));
+		if (!skin) return false;
+		skeleton->setSkin(skin);
+		return true;
+	} else
+		return false;
+}
+
+void USpineSkeletonComponent::GetSkins(TArray &Skins) {
+	CheckState();
+	if (skeleton) {
+		for (size_t i = 0, n = skeleton->getData()->getSkins().size(); i < n; i++) {
+			Skins.Add(skeleton->getData()->getSkins()[i]->getName().buffer());
+		}
+	}
+}
+
+bool USpineSkeletonComponent::HasSkin(const FString skinName) {
+	CheckState();
+	if (skeleton) {
+		return skeleton->getData()->findSkin(TCHAR_TO_UTF8(*skinName)) != nullptr;
+	}
+	return false;
+}
+
+bool USpineSkeletonComponent::SetAttachment(const FString slotName, const FString attachmentName) {
+	CheckState();
+	if (skeleton) {
+		if (attachmentName.IsEmpty()) {
+			skeleton->setAttachment(TCHAR_TO_UTF8(*slotName), NULL);
+			return true;
+		}
+		if (!skeleton->getAttachment(TCHAR_TO_UTF8(*slotName), TCHAR_TO_UTF8(*attachmentName))) return false;
+		skeleton->setAttachment(TCHAR_TO_UTF8(*slotName), TCHAR_TO_UTF8(*attachmentName));
+		return true;
+	}
+	return false;
+}
+
+FTransform USpineSkeletonComponent::GetBoneWorldTransform(const FString &BoneName) {
+	CheckState();
+	if (skeleton) {
+		Bone *bone = skeleton->findBone(TCHAR_TO_UTF8(*BoneName));
+		if (!bone) return FTransform();
+
+		// Need to fetch the renderer component to get world transform of actor plus
+		// offset by renderer component and its parent component(s). If no renderer
+		// component is found, this components owner's transform is used as a fallback
+		FTransform baseTransform;
+		AActor *owner = GetOwner();
+		if (owner) {
+			USpineSkeletonRendererComponent *rendererComponent = static_cast(owner->GetComponentByClass(USpineSkeletonRendererComponent::StaticClass()));
+			if (rendererComponent) baseTransform = rendererComponent->GetComponentTransform();
+			else
+				baseTransform = owner->GetActorTransform();
+		}
+
+		FVector position(bone->getWorldX(), 0, bone->getWorldY());
+		FMatrix localTransform;
+		localTransform.SetIdentity();
+		localTransform.SetAxis(2, FVector(bone->getA(), 0, bone->getC()));
+		localTransform.SetAxis(0, FVector(bone->getB(), 0, bone->getD()));
+		localTransform.SetOrigin(FVector(bone->getWorldX(), 0, bone->getWorldY()));
+		localTransform = localTransform * baseTransform.ToMatrixWithScale();
+
+		FTransform result;
+		result.SetFromMatrix(localTransform);
+		return result;
+	}
+	return FTransform();
+}
+
+void USpineSkeletonComponent::SetBoneWorldPosition(const FString &BoneName, const FVector &position) {
+	CheckState();
+	if (skeleton) {
+		Bone *bone = skeleton->findBone(TCHAR_TO_UTF8(*BoneName));
+		if (!bone) return;
+
+		// Need to fetch the renderer component to get world transform of actor plus
+		// offset by renderer component and its parent component(s). If no renderer
+		// component is found, this components owner's transform is used as a fallback
+		FTransform baseTransform;
+		AActor *owner = GetOwner();
+		if (owner) {
+			USpineSkeletonRendererComponent *rendererComponent = static_cast(owner->GetComponentByClass(USpineSkeletonRendererComponent::StaticClass()));
+			if (rendererComponent) baseTransform = rendererComponent->GetComponentTransform();
+			else
+				baseTransform = owner->GetActorTransform();
+		}
+
+		baseTransform = baseTransform.Inverse();
+		FVector localPosition = baseTransform.TransformPosition(position);
+		float localX = 0, localY = 0;
+		if (bone->getParent()) {
+			bone->getParent()->worldToLocal(localPosition.X, localPosition.Z, localX, localY);
+		} else {
+			bone->worldToLocal(localPosition.X, localPosition.Z, localX, localY);
+		}
+		bone->setX(localX);
+		bone->setY(localY);
+	}
+}
+
+void USpineSkeletonComponent::UpdateWorldTransform() {
+	CheckState();
+	if (skeleton) {
+		skeleton->updateWorldTransform(Physics_Update);
+	}
+}
+
+void USpineSkeletonComponent::SetToSetupPose() {
+	CheckState();
+	if (skeleton) skeleton->setToSetupPose();
+}
+
+void USpineSkeletonComponent::SetBonesToSetupPose() {
+	CheckState();
+	if (skeleton) skeleton->setBonesToSetupPose();
+}
+
+void USpineSkeletonComponent::SetSlotsToSetupPose() {
+	CheckState();
+	if (skeleton) skeleton->setSlotsToSetupPose();
+}
+
+void USpineSkeletonComponent::SetScaleX(float scaleX) {
+	CheckState();
+	if (skeleton) skeleton->setScaleX(scaleX);
+}
+
+float USpineSkeletonComponent::GetScaleX() {
+	CheckState();
+	if (skeleton) return skeleton->getScaleX();
+	return 1;
+}
+
+void USpineSkeletonComponent::SetScaleY(float scaleY) {
+	CheckState();
+	if (skeleton) skeleton->setScaleY(scaleY);
+}
+
+float USpineSkeletonComponent::GetScaleY() {
+	CheckState();
+	if (skeleton) return skeleton->getScaleY();
+	return 1;
+}
+
+void USpineSkeletonComponent::GetBones(TArray &Bones) {
+	CheckState();
+	if (skeleton) {
+		for (size_t i = 0, n = skeleton->getBones().size(); i < n; i++) {
+			Bones.Add(skeleton->getBones()[i]->getData().getName().buffer());
+		}
+	}
+}
+
+bool USpineSkeletonComponent::HasBone(const FString BoneName) {
+	CheckState();
+	if (skeleton) {
+		return skeleton->getData()->findBone(TCHAR_TO_UTF8(*BoneName)) != nullptr;
+	}
+	return false;
+}
+
+void USpineSkeletonComponent::GetSlots(TArray &Slots) {
+	CheckState();
+	if (skeleton) {
+		for (size_t i = 0, n = skeleton->getSlots().size(); i < n; i++) {
+			Slots.Add(skeleton->getSlots()[i]->getData().getName().buffer());
+		}
+	}
+}
+
+bool USpineSkeletonComponent::HasSlot(const FString SlotName) {
+	CheckState();
+	if (skeleton) {
+		return skeleton->getData()->findSlot(TCHAR_TO_UTF8(*SlotName)) != nullptr;
+	}
+	return false;
+}
+
+void USpineSkeletonComponent::SetSlotColor(const FString SlotName, const FColor color) {
+	CheckState();
+	if (skeleton) {
+		Slot *slot = skeleton->findSlot(TCHAR_TO_UTF8(*SlotName));
+		if (slot) {
+			slot->getColor().set(color.R / 255.f, color.G / 255.f, color.B / 255.f, color.A / 255.f);
+		}
+	}
+}
+
+void USpineSkeletonComponent::GetAnimations(TArray &Animations) {
+	CheckState();
+	if (skeleton) {
+		for (size_t i = 0, n = skeleton->getData()->getAnimations().size(); i < n; i++) {
+			Animations.Add(skeleton->getData()->getAnimations()[i]->getName().buffer());
+		}
+	}
+}
+
+bool USpineSkeletonComponent::HasAnimation(FString AnimationName) {
+	CheckState();
+	if (skeleton) {
+		return skeleton->getData()->findAnimation(TCHAR_TO_UTF8(*AnimationName)) != nullptr;
+	}
+	return false;
+}
+
+float USpineSkeletonComponent::GetAnimationDuration(FString AnimationName) {
+	CheckState();
+	if (skeleton) {
+		Animation *animation = skeleton->getData()->findAnimation(TCHAR_TO_UTF8(*AnimationName));
+		if (animation == nullptr) return 0;
+		else
+			return animation->getDuration();
+	}
+	return 0;
+}
+
+void USpineSkeletonComponent::PhysicsTranslate(float x, float y) {
+	CheckState();
+	if (skeleton) {
+		skeleton->physicsTranslate(x, y);
+	}
+}
+
+void USpineSkeletonComponent::PhysicsRotate(float x, float y, float degrees) {
+	CheckState();
+	if (skeleton) {
+		skeleton->physicsRotate(x, y, degrees);
+	}
+}
+
+void USpineSkeletonComponent::ResetPhysicsConstraints() {
+	CheckState();
+	if (skeleton) {
+		Vector &constraints = skeleton->getPhysicsConstraints();
+		for (int i = 0, n = (int) constraints.size(); i < n; i++) {
+			constraints[i]->reset();
+		}
+	}
+}
+
+void USpineSkeletonComponent::BeginPlay() {
+	Super::BeginPlay();
+}
+
+void USpineSkeletonComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) {
+	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
+	InternalTick(DeltaTime);
+}
+
+void USpineSkeletonComponent::InternalTick(float DeltaTime, bool CallDelegates, bool Preview) {
+	CheckState();
+
+	if (skeleton) {
+		if (CallDelegates) BeforeUpdateWorldTransform.Broadcast(this);
+		skeleton->updateWorldTransform(Physics_Update);
+		if (CallDelegates) AfterUpdateWorldTransform.Broadcast(this);
+	}
+}
+
+void USpineSkeletonComponent::CheckState() {
+	bool needsUpdate = lastAtlas != Atlas || lastData != SkeletonData;
+
+	if (!needsUpdate) {
+		// Are we doing a re-import? Then check if the underlying spine-cpp data
+		// has changed.
+		if (lastAtlas && lastAtlas == Atlas && lastData && lastData == SkeletonData) {
+			spine::Atlas *atlas = Atlas->GetAtlas();
+			if (lastSpineAtlas != atlas) {
+				needsUpdate = true;
+			}
+			if (skeleton && skeleton->getData() != SkeletonData->GetSkeletonData(atlas)) {
+				needsUpdate = true;
+			}
+		}
+	}
+
+	if (needsUpdate) {
+		DisposeState();
+
+		if (Atlas && SkeletonData) {
+			spine::SkeletonData *data = SkeletonData->GetSkeletonData(Atlas->GetAtlas());
+			skeleton = new (__FILE__, __LINE__) Skeleton(data);
+		}
+
+		lastAtlas = Atlas;
+		lastSpineAtlas = Atlas ? Atlas->GetAtlas() : nullptr;
+		lastData = SkeletonData;
+	}
+}
+
+void USpineSkeletonComponent::DisposeState() {
+	if (skeleton) {
+		delete skeleton;
+		skeleton = nullptr;
+	}
+}
+
+void USpineSkeletonComponent::FinishDestroy() {
+	DisposeState();
+	Super::FinishDestroy();
+}
+
+#undef LOCTEXT_NAMESPACE
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineSkeletonDataAsset.cpp b/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineSkeletonDataAsset.cpp
new file mode 100644
index 0000000..002fa04
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineSkeletonDataAsset.cpp
@@ -0,0 +1,404 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#include "SpineSkeletonDataAsset.h"
+#include "EditorFramework/AssetImportData.h"
+#include "Runtime/Core/Public/Misc/MessageDialog.h"
+#include "SpinePlugin.h"
+#include "spine/Version.h"
+#include "spine/spine.h"
+#include 
+
+#define LOCTEXT_NAMESPACE "Spine"
+
+using namespace spine;
+
+FName USpineSkeletonDataAsset::GetSkeletonDataFileName() const {
+#if WITH_EDITORONLY_DATA
+	TArray files;
+	if (importData)
+		importData->ExtractFilenames(files);
+	if (files.Num() > 0)
+		return FName(*files[0]);
+	else
+		return skeletonDataFileName;
+#else
+	return skeletonDataFileName;
+#endif
+}
+
+#if WITH_EDITORONLY_DATA
+
+void USpineSkeletonDataAsset::SetSkeletonDataFileName(
+		const FName &SkeletonDataFileName) {
+	importData->UpdateFilenameOnly(SkeletonDataFileName.ToString());
+	TArray files;
+	importData->ExtractFilenames(files);
+	if (files.Num() > 0)
+		this->skeletonDataFileName = FName(*files[0]);
+}
+
+void USpineSkeletonDataAsset::PostInitProperties() {
+	if (!HasAnyFlags(RF_ClassDefaultObject))
+		importData = NewObject(this, TEXT("AssetImportData"));
+	Super::PostInitProperties();
+}
+
+#if ((ENGINE_MAJOR_VERSION >= 5) && (ENGINE_MINOR_VERSION >= 4))
+void USpineSkeletonDataAsset::GetAssetRegistryTags(FAssetRegistryTagsContext Context) const {
+	if (importData) {
+		Context.AddTag(FAssetRegistryTag(SourceFileTagName(), importData->GetSourceData().ToJson(), FAssetRegistryTag::TT_Hidden));
+	}
+	Super::GetAssetRegistryTags(Context);
+}
+#else
+void USpineSkeletonDataAsset::GetAssetRegistryTags(
+		TArray &OutTags) const {
+	if (importData) {
+		OutTags.Add(FAssetRegistryTag(SourceFileTagName(),
+									  importData->GetSourceData().ToJson(),
+									  FAssetRegistryTag::TT_Hidden));
+	}
+
+	Super::GetAssetRegistryTags(OutTags);
+}
+#endif
+
+void USpineSkeletonDataAsset::Serialize(FArchive &Ar) {
+	Super::Serialize(Ar);
+#if ENGINE_MAJOR_VERSION == 4 && ENGINE_MINOR_VERSION <= 27
+	if (Ar.IsLoading() && Ar.UE4Ver() < VER_UE4_ASSET_IMPORT_DATA_AS_JSON && !importData)
+#else
+	if (Ar.IsLoading() && Ar.UEVer() < VER_UE4_ASSET_IMPORT_DATA_AS_JSON && !importData)
+#endif
+		importData = NewObject(this, TEXT("AssetImportData"));
+	LoadInfo();
+}
+
+#endif
+
+void USpineSkeletonDataAsset::ClearNativeData() {
+	for (auto &pair : atlasToNativeData) {
+		if (pair.Value.skeletonData)
+			delete pair.Value.skeletonData;
+		if (pair.Value.animationStateData)
+			delete pair.Value.animationStateData;
+	}
+	atlasToNativeData.Empty();
+}
+
+void USpineSkeletonDataAsset::BeginDestroy() {
+	ClearNativeData();
+
+	Super::BeginDestroy();
+}
+
+class SP_API NullAttachmentLoader : public AttachmentLoader {
+public:
+	virtual RegionAttachment *newRegionAttachment(Skin &skin, const String &name,
+												  const String &path,
+												  Sequence *sequence) {
+		return new (__FILE__, __LINE__) RegionAttachment(name);
+	}
+
+	virtual MeshAttachment *newMeshAttachment(Skin &skin, const String &name,
+											  const String &path,
+											  Sequence *sequence) {
+		return new (__FILE__, __LINE__) MeshAttachment(name);
+	}
+
+	virtual BoundingBoxAttachment *newBoundingBoxAttachment(Skin &skin,
+															const String &name) {
+		return new (__FILE__, __LINE__) BoundingBoxAttachment(name);
+	}
+
+	virtual PathAttachment *newPathAttachment(Skin &skin, const String &name) {
+		return new (__FILE__, __LINE__) PathAttachment(name);
+	}
+
+	virtual PointAttachment *newPointAttachment(Skin &skin, const String &name) {
+		return new (__FILE__, __LINE__) PointAttachment(name);
+	}
+
+	virtual ClippingAttachment *newClippingAttachment(Skin &skin,
+													  const String &name) {
+		return new (__FILE__, __LINE__) ClippingAttachment(name);
+	}
+
+	virtual void configureAttachment(Attachment *attachment) {}
+};
+
+void USpineSkeletonDataAsset::SetRawData(TArray &Data) {
+	this->rawData.Empty();
+	this->rawData.Append(Data);
+
+	ClearNativeData();
+
+	LoadInfo();
+}
+
+static bool checkVersion(const char *version) {
+	if (!version)
+		return false;
+	char *result = (char *) (strstr(version, SPINE_VERSION_STRING) - version);
+	return result == 0;
+}
+
+static bool checkJson(const char *jsonData) {
+	Json json(jsonData);
+	Json *skeleton = Json::getItem(&json, "skeleton");
+	if (!skeleton)
+		return false;
+	const char *version = Json::getString(skeleton, "spine", 0);
+	if (!version)
+		return false;
+
+	return checkVersion(version);
+}
+
+struct BinaryInput {
+	const unsigned char *cursor;
+	const unsigned char *end;
+};
+
+static unsigned char readByte(BinaryInput *input) { return *input->cursor++; }
+
+static int readVarint(BinaryInput *input, bool optimizePositive) {
+	unsigned char b = readByte(input);
+	int value = b & 0x7F;
+	if (b & 0x80) {
+		b = readByte(input);
+		value |= (b & 0x7F) << 7;
+		if (b & 0x80) {
+			b = readByte(input);
+			value |= (b & 0x7F) << 14;
+			if (b & 0x80) {
+				b = readByte(input);
+				value |= (b & 0x7F) << 21;
+				if (b & 0x80)
+					value |= (readByte(input) & 0x7F) << 28;
+			}
+		}
+	}
+
+	if (!optimizePositive) {
+		value = (((unsigned int) value >> 1) ^ -(value & 1));
+	}
+
+	return value;
+}
+
+static char *readString(BinaryInput *input) {
+	int length = readVarint(input, true);
+	char *string;
+	if (length == 0) {
+		return NULL;
+	}
+	string = SpineExtension::alloc(length, __FILE__, __LINE__);
+	memcpy(string, input->cursor, length - 1);
+	input->cursor += length - 1;
+	string[length - 1] = '\0';
+	return string;
+}
+
+static bool checkBinary(const char *binaryData, int length) {
+	BinaryInput input;
+	input.cursor = (const unsigned char *) binaryData;
+	input.end = (const unsigned char *) binaryData + length;
+	// Skip hash
+	input.cursor += 8;
+	char *version = readString(&input);
+	bool result = checkVersion(version);
+	SpineExtension::free(version, __FILE__, __LINE__);
+	return result;
+}
+
+void USpineSkeletonDataAsset::LoadInfo() {
+#if WITH_EDITORONLY_DATA
+	int dataLen = rawData.Num();
+	if (dataLen == 0)
+		return;
+	NullAttachmentLoader loader;
+	SkeletonData *skeletonData = nullptr;
+	if (skeletonDataFileName.GetPlainNameString().Contains(TEXT(".json"))) {
+		SkeletonJson *json = new (__FILE__, __LINE__) SkeletonJson(&loader);
+		if (checkJson((const char *) rawData.GetData()))
+			skeletonData = json->readSkeletonData((const char *) rawData.GetData());
+		if (!skeletonData) {
+			FMessageDialog::Debugf(FText::FromString(
+					FString("Couldn't load skeleton data and/or atlas. Please ensure the "
+							"version of your exported data matches your runtime "
+							"version.\n\n") +
+					skeletonDataFileName.GetPlainNameString() + FString("\n\n") +
+					UTF8_TO_TCHAR(json->getError().buffer())));
+			UE_LOG(SpineLog, Error, TEXT("Couldn't load skeleton data and atlas: %s"),
+				   UTF8_TO_TCHAR(json->getError().buffer()));
+		}
+		delete json;
+	} else {
+		SkeletonBinary *binary = new (__FILE__, __LINE__) SkeletonBinary(&loader);
+		if (checkBinary((const char *) rawData.GetData(), (int) rawData.Num()))
+			skeletonData = binary->readSkeletonData(
+					(const unsigned char *) rawData.GetData(), (int) rawData.Num());
+		if (!skeletonData) {
+			FMessageDialog::Debugf(FText::FromString(
+					FString("Couldn't load skeleton data and/or atlas. Please ensure the "
+							"version of your exported data matches your runtime "
+							"version.\n\n") +
+					skeletonDataFileName.GetPlainNameString() + FString("\n\n") +
+					UTF8_TO_TCHAR(binary->getError().buffer())));
+			UE_LOG(SpineLog, Error, TEXT("Couldn't load skeleton data and atlas: %s"),
+				   UTF8_TO_TCHAR(binary->getError().buffer()));
+		}
+		delete binary;
+	}
+	if (skeletonData) {
+		Bones.Empty();
+		for (int i = 0; i < skeletonData->getBones().size(); i++)
+			Bones.Add(UTF8_TO_TCHAR(skeletonData->getBones()[i]->getName().buffer()));
+		Skins.Empty();
+		for (int i = 0; i < skeletonData->getSkins().size(); i++)
+			Skins.Add(UTF8_TO_TCHAR(skeletonData->getSkins()[i]->getName().buffer()));
+		Slots.Empty();
+		for (int i = 0; i < skeletonData->getSlots().size(); i++)
+			Slots.Add(UTF8_TO_TCHAR(skeletonData->getSlots()[i]->getName().buffer()));
+		Animations.Empty();
+		for (int i = 0; i < skeletonData->getAnimations().size(); i++)
+			Animations.Add(
+					UTF8_TO_TCHAR(skeletonData->getAnimations()[i]->getName().buffer()));
+		Events.Empty();
+		for (int i = 0; i < skeletonData->getEvents().size(); i++)
+			Events.Add(
+					UTF8_TO_TCHAR(skeletonData->getEvents()[i]->getName().buffer()));
+		delete skeletonData;
+	}
+#endif
+}
+
+SkeletonData *USpineSkeletonDataAsset::GetSkeletonData(Atlas *Atlas) {
+	SkeletonData *skeletonData = nullptr;
+	AnimationStateData *animationStateData = nullptr;
+	if (atlasToNativeData.Contains(Atlas)) {
+		skeletonData = atlasToNativeData[Atlas].skeletonData;
+		animationStateData = atlasToNativeData[Atlas].animationStateData;
+	}
+
+	if (!skeletonData) {
+		int dataLen = rawData.Num();
+		if (skeletonDataFileName.GetPlainNameString().Contains(TEXT(".json"))) {
+			SkeletonJson *json = new (__FILE__, __LINE__) SkeletonJson(Atlas);
+			if (checkJson((const char *) rawData.GetData()))
+				skeletonData = json->readSkeletonData((const char *) rawData.GetData());
+			if (!skeletonData) {
+#if WITH_EDITORONLY_DATA
+				FMessageDialog::Debugf(FText::FromString(
+						FString("Couldn't load skeleton data and/or atlas. Please ensure "
+								"the version of your exported data matches your runtime "
+								"version.\n\n") +
+						skeletonDataFileName.GetPlainNameString() + FString("\n\n") +
+						UTF8_TO_TCHAR(json->getError().buffer())));
+#endif
+				UE_LOG(SpineLog, Error,
+					   TEXT("Couldn't load skeleton data and atlas: %s"),
+					   UTF8_TO_TCHAR(json->getError().buffer()));
+			}
+			delete json;
+		} else {
+			SkeletonBinary *binary = new (__FILE__, __LINE__) SkeletonBinary(Atlas);
+			if (checkBinary((const char *) rawData.GetData(), (int) rawData.Num()))
+				skeletonData = binary->readSkeletonData(
+						(const unsigned char *) rawData.GetData(), (int) rawData.Num());
+			if (!skeletonData) {
+#if WITH_EDITORONLY_DATA
+				FMessageDialog::Debugf(FText::FromString(
+						FString("Couldn't load skeleton data and/or atlas. Please ensure "
+								"the version of your exported data matches your runtime "
+								"version.\n\n") +
+						skeletonDataFileName.GetPlainNameString() + FString("\n\n") +
+						UTF8_TO_TCHAR(binary->getError().buffer())));
+#endif
+				UE_LOG(SpineLog, Error,
+					   TEXT("Couldn't load skeleton data and atlas: %s"),
+					   UTF8_TO_TCHAR(binary->getError().buffer()));
+			}
+			delete binary;
+		}
+
+		if (skeletonData) {
+			animationStateData =
+					new (__FILE__, __LINE__) AnimationStateData(skeletonData);
+			SetMixes(animationStateData);
+			atlasToNativeData.Add(Atlas, {skeletonData, animationStateData});
+		}
+	}
+
+	return skeletonData;
+}
+
+void USpineSkeletonDataAsset::SetMixes(AnimationStateData *animationStateData) {
+	for (auto &data : MixData) {
+		if (!data.From.IsEmpty() && !data.To.IsEmpty()) {
+			std::string fromChar = TCHAR_TO_UTF8(*data.From);
+			std::string toChar = TCHAR_TO_UTF8(*data.To);
+			animationStateData->setMix(fromChar.c_str(), toChar.c_str(), data.Mix);
+		}
+	}
+	animationStateData->setDefaultMix(DefaultMix);
+}
+
+AnimationStateData *
+USpineSkeletonDataAsset::GetAnimationStateData(Atlas *atlas) {
+	if (!atlasToNativeData.Contains(atlas))
+		return nullptr;
+	AnimationStateData *data = atlasToNativeData[atlas].animationStateData;
+	SetMixes(data);
+	return data;
+}
+
+void USpineSkeletonDataAsset::SetMix(const FString &from, const FString &to,
+									 float mix) {
+	FSpineAnimationStateMixData data;
+	data.From = from;
+	data.To = to;
+	data.Mix = mix;
+	this->MixData.Add(data);
+	for (auto &pair : atlasToNativeData) {
+		SetMixes(pair.Value.animationStateData);
+	}
+}
+
+float USpineSkeletonDataAsset::GetMix(const FString &from, const FString &to) {
+	for (auto &data : MixData) {
+		if (data.From.Equals(from) && data.To.Equals(to))
+			return data.Mix;
+	}
+	return 0;
+}
+
+#undef LOCTEXT_NAMESPACE
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineSkeletonRendererComponent.cpp b/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineSkeletonRendererComponent.cpp
new file mode 100644
index 0000000..5f5c18f
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineSkeletonRendererComponent.cpp
@@ -0,0 +1,360 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#include "SpineSkeletonRendererComponent.h"
+
+#include "SpineAtlasAsset.h"
+#include "Materials/MaterialInstanceDynamic.h"
+#include "spine/spine.h"
+#include "UObject/ConstructorHelpers.h"
+#if ENGINE_MAJOR_VERSION >= 5
+#include "PhysicsEngine/BodySetup.h"
+#endif
+
+#define LOCTEXT_NAMESPACE "Spine"
+
+using namespace spine;
+
+USpineSkeletonRendererComponent::USpineSkeletonRendererComponent(const FObjectInitializer &ObjectInitializer)
+	: UProceduralMeshComponent(ObjectInitializer) {
+	PrimaryComponentTick.bCanEverTick = true;
+	bTickInEditor = true;
+	bAutoActivate = true;
+
+	static ConstructorHelpers::FObjectFinder NormalMaterialRef(TEXT("/SpinePlugin/SpineUnlitNormalMaterial"));
+	NormalBlendMaterial = NormalMaterialRef.Object;
+
+	static ConstructorHelpers::FObjectFinder AdditiveMaterialRef(TEXT("/SpinePlugin/SpineUnlitAdditiveMaterial"));
+	AdditiveBlendMaterial = AdditiveMaterialRef.Object;
+
+	static ConstructorHelpers::FObjectFinder MultiplyMaterialRef(TEXT("/SpinePlugin/SpineUnlitMultiplyMaterial"));
+	MultiplyBlendMaterial = MultiplyMaterialRef.Object;
+
+	static ConstructorHelpers::FObjectFinder ScreenMaterialRef(TEXT("/SpinePlugin/SpineUnlitScreenMaterial"));
+	ScreenBlendMaterial = ScreenMaterialRef.Object;
+
+	TextureParameterName = FName(TEXT("SpriteTexture"));
+
+	worldVertices.ensureCapacity(1024 * 2);
+
+	SetTickGroup(TG_EndPhysics);
+}
+
+void USpineSkeletonRendererComponent::FinishDestroy() {
+	Super::FinishDestroy();
+}
+
+void USpineSkeletonRendererComponent::BeginPlay() {
+	Super::BeginPlay();
+}
+
+void USpineSkeletonRendererComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) {
+	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
+
+	AActor *owner = GetOwner();
+	if (owner) {
+		UClass *skeletonClass = USpineSkeletonComponent::StaticClass();
+		USpineSkeletonComponent *skeletonComponent = Cast(owner->GetComponentByClass(skeletonClass));
+
+		UpdateRenderer(skeletonComponent);
+	}
+}
+
+void USpineSkeletonRendererComponent::UpdateRenderer(USpineSkeletonComponent *component) {
+	if (component && !component->IsBeingDestroyed() && component->GetSkeleton() && component->Atlas) {
+		component->GetSkeleton()->getColor().set(Color.R, Color.G, Color.B, Color.A);
+
+		if (atlasNormalBlendMaterials.Num() != component->Atlas->atlasPages.Num()) {
+			atlasNormalBlendMaterials.SetNum(0);
+			atlasAdditiveBlendMaterials.SetNum(0);
+			atlasMultiplyBlendMaterials.SetNum(0);
+			atlasScreenBlendMaterials.SetNum(0);
+
+			for (int i = 0; i < component->Atlas->atlasPages.Num(); i++) {
+				AtlasPage *currPage = component->Atlas->GetAtlas()->getPages()[i];
+
+				UMaterialInstanceDynamic *material = UMaterialInstanceDynamic::Create(NormalBlendMaterial, this);
+				material->SetTextureParameterValue(TextureParameterName, component->Atlas->atlasPages[i]);
+				atlasNormalBlendMaterials.Add(material);
+
+				material = UMaterialInstanceDynamic::Create(AdditiveBlendMaterial, this);
+				material->SetTextureParameterValue(TextureParameterName, component->Atlas->atlasPages[i]);
+				atlasAdditiveBlendMaterials.Add(material);
+
+				material = UMaterialInstanceDynamic::Create(MultiplyBlendMaterial, this);
+				material->SetTextureParameterValue(TextureParameterName, component->Atlas->atlasPages[i]);
+				atlasMultiplyBlendMaterials.Add(material);
+
+				material = UMaterialInstanceDynamic::Create(ScreenBlendMaterial, this);
+				material->SetTextureParameterValue(TextureParameterName, component->Atlas->atlasPages[i]);
+				atlasScreenBlendMaterials.Add(material);
+			}
+		} else {
+			for (int i = 0; i < component->Atlas->atlasPages.Num(); i++) {
+				UTexture2D *texture = component->Atlas->atlasPages[i];
+				UpdateMaterial(texture, atlasNormalBlendMaterials[i], NormalBlendMaterial);
+				UpdateMaterial(texture, atlasAdditiveBlendMaterials[i], AdditiveBlendMaterial);
+				UpdateMaterial(texture, atlasMultiplyBlendMaterials[i], MultiplyBlendMaterial);
+				UpdateMaterial(texture, atlasScreenBlendMaterials[i], ScreenBlendMaterial);
+			}
+		}
+		UpdateMesh(component, component->GetSkeleton());
+	} else {
+		ClearAllMeshSections();
+	}
+}
+
+void USpineSkeletonRendererComponent::UpdateMaterial(UTexture2D *Texture, UMaterialInstanceDynamic *&CurrentInstance, UMaterialInterface *ParentMaterial) {
+
+	UTexture *oldTexture = nullptr;
+	if (!CurrentInstance || !CurrentInstance->GetTextureParameterValue(TextureParameterName, oldTexture) ||
+		oldTexture != Texture || CurrentInstance->Parent != ParentMaterial) {
+
+		UMaterialInstanceDynamic *material = UMaterialInstanceDynamic::Create(ParentMaterial, this);
+		material->SetTextureParameterValue(TextureParameterName, Texture);
+		CurrentInstance = material;
+	}
+}
+
+void USpineSkeletonRendererComponent::Flush(int &Idx, TArray &Vertices, TArray &Indices, TArray &Normals, TArray &Uvs, TArray &Colors, UMaterialInstanceDynamic *Material) {
+	if (Vertices.Num() == 0) return;
+	SetMaterial(Idx, Material);
+
+	bool bShouldCreateCollision = false;
+	if (bCreateCollision) {
+		UWorld *world = GetWorld();
+		if (world && world->IsGameWorld()) {
+			bShouldCreateCollision = true;
+		}
+	}
+
+	GetBodySetup()->bGenerateMirroredCollision = GetComponentScale().X < 0 || GetComponentScale().Y < 0 || GetComponentScale().Z < 0;
+	CreateMeshSection(Idx, Vertices, Indices, Normals, Uvs, Colors, TArray(), bShouldCreateCollision);
+
+	Vertices.SetNum(0);
+	Indices.SetNum(0);
+	Normals.SetNum(0);
+	Uvs.SetNum(0);
+	Colors.SetNum(0);
+	Idx++;
+}
+
+void USpineSkeletonRendererComponent::UpdateMesh(USpineSkeletonComponent *component, Skeleton *Skeleton) {
+	vertices.Empty();
+	indices.Empty();
+	normals.Empty();
+	uvs.Empty();
+	colors.Empty();
+
+	int idx = 0;
+	int meshSection = 0;
+	UMaterialInstanceDynamic *lastMaterial = nullptr;
+
+	ClearAllMeshSections();
+
+	// Early out if skeleton is invisible
+	if (Skeleton->getColor().a == 0) return;
+
+	float depthOffset = 0;
+	unsigned short quadIndices[] = {0, 1, 2, 0, 2, 3};
+
+	for (size_t i = 0; i < Skeleton->getSlots().size(); ++i) {
+		Vector *attachmentVertices = &worldVertices;
+		unsigned short *attachmentIndices = nullptr;
+		int numVertices;
+		int numIndices;
+		AtlasRegion *attachmentAtlasRegion = nullptr;
+		spine::Color attachmentColor;
+		attachmentColor.set(1, 1, 1, 1);
+		float *attachmentUvs = nullptr;
+
+		Slot *slot = Skeleton->getDrawOrder()[i];
+		Attachment *attachment = slot->getAttachment();
+
+		if (slot->getColor().a == 0 || !slot->getBone().isActive()) {
+			clipper.clipEnd(*slot);
+			continue;
+		}
+
+		if (!attachment) {
+			clipper.clipEnd(*slot);
+			continue;
+		}
+		if (!attachment->getRTTI().isExactly(RegionAttachment::rtti) && !attachment->getRTTI().isExactly(MeshAttachment::rtti) && !attachment->getRTTI().isExactly(ClippingAttachment::rtti)) {
+			clipper.clipEnd(*slot);
+			continue;
+		}
+
+		if (attachment->getRTTI().isExactly(RegionAttachment::rtti)) {
+			RegionAttachment *regionAttachment = (RegionAttachment *) attachment;
+
+			// Early out if region is invisible
+			if (regionAttachment->getColor().a == 0) {
+				clipper.clipEnd(*slot);
+				continue;
+			}
+
+			attachmentColor.set(regionAttachment->getColor());
+			attachmentVertices->setSize(8, 0);
+			regionAttachment->computeWorldVertices(*slot, *attachmentVertices, 0, 2);
+			attachmentAtlasRegion = (AtlasRegion *) regionAttachment->getRegion();
+			attachmentIndices = quadIndices;
+			attachmentUvs = regionAttachment->getUVs().buffer();
+			numVertices = 4;
+			numIndices = 6;
+		} else if (attachment->getRTTI().isExactly(MeshAttachment::rtti)) {
+			MeshAttachment *mesh = (MeshAttachment *) attachment;
+
+			// Early out if region is invisible
+			if (mesh->getColor().a == 0) {
+				clipper.clipEnd(*slot);
+				continue;
+			}
+
+			attachmentColor.set(mesh->getColor());
+			attachmentVertices->setSize(mesh->getWorldVerticesLength(), 0);
+			mesh->computeWorldVertices(*slot, 0, mesh->getWorldVerticesLength(), attachmentVertices->buffer(), 0, 2);
+			attachmentAtlasRegion = (AtlasRegion *) mesh->getRegion();
+			attachmentIndices = mesh->getTriangles().buffer();
+			attachmentUvs = mesh->getUVs().buffer();
+			numVertices = mesh->getWorldVerticesLength() >> 1;
+			numIndices = mesh->getTriangles().size();
+		} else /* clipping */ {
+			ClippingAttachment *clip = (ClippingAttachment *) attachment;
+			clipper.clipStart(*slot, clip);
+			continue;
+		}
+
+		if (clipper.isClipping()) {
+			clipper.clipTriangles(attachmentVertices->buffer(), attachmentIndices, numIndices, attachmentUvs, 2);
+			attachmentVertices = &clipper.getClippedVertices();
+			numVertices = clipper.getClippedVertices().size() >> 1;
+			attachmentIndices = clipper.getClippedTriangles().buffer();
+			numIndices = clipper.getClippedTriangles().size();
+			attachmentUvs = clipper.getClippedUVs().buffer();
+			if (clipper.getClippedTriangles().size() == 0) {
+				clipper.clipEnd(*slot);
+				continue;
+			}
+		}
+
+		// if the user switches the atlas data while not having switched
+		// to the correct skeleton data yet, we won't find any regions.
+		// ignore regions for which we can't find a material
+		UMaterialInstanceDynamic *material = nullptr;
+		int foundPageIndex = (int) (intptr_t) attachmentAtlasRegion->rendererObject;
+		if (foundPageIndex == -1) {
+			clipper.clipEnd(*slot);
+			continue;
+		}
+		switch (slot->getData().getBlendMode()) {
+			case BlendMode_Additive:
+				if (foundPageIndex >= atlasAdditiveBlendMaterials.Num()) {
+					clipper.clipEnd(*slot);
+					continue;
+				}
+				material = atlasAdditiveBlendMaterials[foundPageIndex];
+				break;
+			case BlendMode_Multiply:
+				if (foundPageIndex >= atlasMultiplyBlendMaterials.Num()) {
+					clipper.clipEnd(*slot);
+					continue;
+				}
+				material = atlasMultiplyBlendMaterials[foundPageIndex];
+				break;
+			case BlendMode_Screen:
+				if (foundPageIndex >= atlasScreenBlendMaterials.Num()) {
+					clipper.clipEnd(*slot);
+					continue;
+				}
+				material = atlasScreenBlendMaterials[foundPageIndex];
+				break;
+			case BlendMode_Normal:
+			default:
+				if (foundPageIndex >= atlasNormalBlendMaterials.Num()) {
+					clipper.clipEnd(*slot);
+					continue;
+				}
+				material = atlasNormalBlendMaterials[foundPageIndex];
+				break;
+		}
+
+		if (lastMaterial != material) {
+			Flush(meshSection, vertices, indices, normals, uvs, colors, lastMaterial);
+			lastMaterial = material;
+			idx = 0;
+		}
+
+		SetMaterial(meshSection, material);
+
+		uint8 r = static_cast(Skeleton->getColor().r * slot->getColor().r * attachmentColor.r * 255);
+		uint8 g = static_cast(Skeleton->getColor().g * slot->getColor().g * attachmentColor.g * 255);
+		uint8 b = static_cast(Skeleton->getColor().b * slot->getColor().b * attachmentColor.b * 255);
+		uint8 a = static_cast(Skeleton->getColor().a * slot->getColor().a * attachmentColor.a * 255);
+
+		float *verticesPtr = attachmentVertices->buffer();
+		for (int j = 0; j < numVertices << 1; j += 2) {
+			colors.Add(FColor(r, g, b, a));
+			vertices.Add(FVector(verticesPtr[j], depthOffset, verticesPtr[j + 1]));
+			uvs.Add(FVector2D(attachmentUvs[j], attachmentUvs[j + 1]));
+		}
+
+		for (int j = 0; j < numIndices; j++) {
+			indices.Add(idx + attachmentIndices[j]);
+		}
+
+		int numTriangles = indices.Num() / 3;
+		for (int j = 0; j < numTriangles; j++) {
+			const int triangleIndex = j * 3;
+			if (FVector::CrossProduct(
+						vertices[indices[triangleIndex + 2]] - vertices[indices[triangleIndex]],
+						vertices[indices[triangleIndex + 1]] - vertices[indices[triangleIndex]])
+						.Y < 0.f) {
+				const int32 targetVertex = indices[triangleIndex];
+				indices[triangleIndex] = indices[triangleIndex + 2];
+				indices[triangleIndex + 2] = targetVertex;
+			}
+		}
+
+		FVector normal = FVector(0, 1, 0);
+		for (int j = 0; j < numVertices; j++) {
+			normals.Add(normal);
+		}
+
+		idx += numVertices;
+		depthOffset += this->DepthOffset;
+
+		clipper.clipEnd(*slot);
+	}
+
+	Flush(meshSection, vertices, indices, normals, uvs, colors, lastMaterial);
+	clipper.clipEnd();
+}
+
+#undef LOCTEXT_NAMESPACE
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineWidget.cpp b/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineWidget.cpp
new file mode 100644
index 0000000..e7d8118
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Private/SpineWidget.cpp
@@ -0,0 +1,564 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#include "SpineWidget.h"
+#include "SSpineWidget.h"
+#include "SpineSkeletonAnimationComponent.h"
+#include "spine/spine.h"
+
+#define LOCTEXT_NAMESPACE "Spine"
+
+using namespace spine;
+
+void callbackWidget(AnimationState *state, spine::EventType type, TrackEntry *entry, Event *event) {
+	USpineWidget *component = (USpineWidget *) state->getRendererObject();
+
+	if (entry->getRendererObject()) {
+		UTrackEntry *uEntry = (UTrackEntry *) entry->getRendererObject();
+		if (type == EventType_Start) {
+			component->AnimationStart.Broadcast(uEntry);
+			uEntry->AnimationStart.Broadcast(uEntry);
+		} else if (type == EventType_Interrupt) {
+			component->AnimationInterrupt.Broadcast(uEntry);
+			uEntry->AnimationInterrupt.Broadcast(uEntry);
+		} else if (type == EventType_Event) {
+			FSpineEvent evt;
+			evt.SetEvent(event);
+			component->AnimationEvent.Broadcast(uEntry, evt);
+			uEntry->AnimationEvent.Broadcast(uEntry, evt);
+		} else if (type == EventType_Complete) {
+			component->AnimationComplete.Broadcast(uEntry);
+			uEntry->AnimationComplete.Broadcast(uEntry);
+		} else if (type == EventType_End) {
+			component->AnimationEnd.Broadcast(uEntry);
+			uEntry->AnimationEnd.Broadcast(uEntry);
+		} else if (type == EventType_Dispose) {
+			component->AnimationDispose.Broadcast(uEntry);
+			uEntry->AnimationDispose.Broadcast(uEntry);
+			uEntry->SetTrackEntry(nullptr);
+			component->GCTrackEntry(uEntry);
+		}
+	}
+}
+
+USpineWidget::USpineWidget(const FObjectInitializer &ObjectInitializer) : Super(ObjectInitializer) {
+	static ConstructorHelpers::FObjectFinder NormalMaterialRef(TEXT("/SpinePlugin/UI_SpineUnlitNormalMaterial"));
+	NormalBlendMaterial = NormalMaterialRef.Object;
+
+	static ConstructorHelpers::FObjectFinder AdditiveMaterialRef(TEXT("/SpinePlugin/UI_SpineUnlitAdditiveMaterial"));
+	AdditiveBlendMaterial = AdditiveMaterialRef.Object;
+
+	static ConstructorHelpers::FObjectFinder MultiplyMaterialRef(TEXT("/SpinePlugin/UI_SpineUnlitMultiplyMaterial"));
+	MultiplyBlendMaterial = MultiplyMaterialRef.Object;
+
+	static ConstructorHelpers::FObjectFinder ScreenMaterialRef(TEXT("/SpinePlugin/UI_SpineUnlitScreenMaterial"));
+	ScreenBlendMaterial = ScreenMaterialRef.Object;
+
+	TextureParameterName = FName(TEXT("SpriteTexture"));
+
+	physicsTimeScale = 1.0f;
+	worldVertices.ensureCapacity(1024 * 2);
+
+	bAutoPlaying = true;
+}
+
+void USpineWidget::SynchronizeProperties() {
+	Super::SynchronizeProperties();
+
+	if (slateWidget.IsValid()) {
+		CheckState();
+		if (skeleton) {
+			if (!bSkinInitialized) {// blueprint On Initialized may be called beforehand
+				if (InitialSkin != "") SetSkin(InitialSkin);
+#if WITH_EDITOR
+				if (IsDesignTime()) {
+					if (InitialSkin == "") SetSkin("default");
+					bSkinInitialized = false;// allow multiple edits in editor
+				}
+#endif
+			}
+			Tick(0, false);
+			slateWidget->SetData(this);
+		} else {
+			slateWidget->SetData(nullptr);
+		}
+		for (UTrackEntry *entry : trackEntries) {
+			if (entry && entry->GetTrackEntry()) {
+				entry->GetTrackEntry()->setRendererObject(nullptr);
+			}
+		}
+		trackEntries.Empty();
+	}
+}
+
+void USpineWidget::ReleaseSlateResources(bool bReleaseChildren) {
+	Super::ReleaseSlateResources(bReleaseChildren);
+	slateWidget.Reset();
+}
+
+TSharedRef USpineWidget::RebuildWidget() {
+	this->slateWidget = SNew(SSpineWidget);
+	return this->slateWidget.ToSharedRef();
+}
+
+#if WITH_EDITOR
+const FText USpineWidget::GetPaletteCategory() {
+	return LOCTEXT("Spine", "Spine");
+}
+#endif
+
+void USpineWidget::Tick(float DeltaTime, bool CallDelegates) {
+	CheckState();
+
+	if (state && bAutoPlaying) {
+		state->update(DeltaTime);
+		state->apply(*skeleton);
+		if (CallDelegates) BeforeUpdateWorldTransform.Broadcast(this);
+		skeleton->update(physicsTimeScale * DeltaTime);
+		skeleton->updateWorldTransform(Physics_Update);
+		if (CallDelegates) AfterUpdateWorldTransform.Broadcast(this);
+	}
+}
+
+void USpineWidget::CheckState() {
+	bool needsUpdate = lastAtlas != Atlas || lastData != SkeletonData;
+
+	if (!needsUpdate) {
+		// Are we doing a re-import? Then check if the underlying spine-cpp data
+		// has changed.
+		if (lastAtlas && lastAtlas == Atlas && lastData && lastData == SkeletonData) {
+			spine::Atlas *atlas = Atlas->GetAtlas();
+			if (lastSpineAtlas != atlas) {
+				needsUpdate = true;
+			}
+			if (skeleton && skeleton->getData() != SkeletonData->GetSkeletonData(atlas)) {
+				needsUpdate = true;
+			}
+		}
+	}
+
+	if (needsUpdate) {
+		DisposeState();
+
+		if (Atlas && SkeletonData) {
+			spine::SkeletonData *data = SkeletonData->GetSkeletonData(Atlas->GetAtlas());
+			if (data) {
+				skeleton = new (__FILE__, __LINE__) Skeleton(data);
+				AnimationStateData *stateData = SkeletonData->GetAnimationStateData(Atlas->GetAtlas());
+				state = new (__FILE__, __LINE__) AnimationState(stateData);
+				state->setRendererObject((void *) this);
+				state->setListener(callbackWidget);
+				trackEntries.Empty();
+				skeleton->setToSetupPose();
+				skeleton->updateWorldTransform(Physics_Update);
+				slateWidget->SetData(this);
+			}
+		}
+
+		lastAtlas = Atlas;
+		lastSpineAtlas = Atlas ? Atlas->GetAtlas() : nullptr;
+		lastData = SkeletonData;
+	}
+}
+
+void USpineWidget::DisposeState() {
+	if (state) {
+		delete state;
+		state = nullptr;
+	}
+
+	if (skeleton) {
+		delete skeleton;
+		skeleton = nullptr;
+	}
+
+	if (customSkin) {
+		delete customSkin;
+		customSkin = nullptr;
+	}
+
+	trackEntries.Empty();
+}
+
+void USpineWidget::FinishDestroy() {
+	DisposeState();
+	Super::FinishDestroy();
+}
+
+bool USpineWidget::SetSkin(const FString skinName) {
+	CheckState();
+	if (skeleton) {
+		spine::Skin *skin = skeleton->getData()->findSkin(TCHAR_TO_UTF8(*skinName));
+		if (!skin) return false;
+		skeleton->setSkin(skin);
+		bSkinInitialized = true;
+		return true;
+	} else
+		return false;
+}
+
+bool USpineWidget::SetSkins(UPARAM(ref) TArray &SkinNames) {
+	CheckState();
+	if (skeleton) {
+		spine::Skin *newSkin = new spine::Skin("__spine-ue3_custom_skin");
+		for (auto &skinName : SkinNames) {
+			spine::Skin *skin = skeleton->getData()->findSkin(TCHAR_TO_UTF8(*skinName));
+			if (!skin) {
+				delete newSkin;
+				return false;
+			}
+			newSkin->addSkin(skin);
+		}
+		skeleton->setSkin(newSkin);
+		bSkinInitialized = true;
+		if (customSkin != nullptr) {
+			delete customSkin;
+		}
+		customSkin = newSkin;
+		return true;
+	} else
+		return false;
+}
+
+void USpineWidget::GetSkins(TArray &Skins) {
+	CheckState();
+	if (skeleton) {
+		for (size_t i = 0, n = skeleton->getData()->getSkins().size(); i < n; i++) {
+			Skins.Add(skeleton->getData()->getSkins()[i]->getName().buffer());
+		}
+	}
+}
+
+bool USpineWidget::HasSkin(const FString skinName) {
+	CheckState();
+	if (skeleton) {
+		return skeleton->getData()->findSkin(TCHAR_TO_UTF8(*skinName)) != nullptr;
+	}
+	return false;
+}
+
+bool USpineWidget::SetAttachment(const FString slotName, const FString attachmentName) {
+	CheckState();
+	if (skeleton) {
+		if (attachmentName.IsEmpty()) {
+			skeleton->setAttachment(TCHAR_TO_UTF8(*slotName), NULL);
+			return true;
+		}
+		if (!skeleton->getAttachment(TCHAR_TO_UTF8(*slotName), TCHAR_TO_UTF8(*attachmentName))) return false;
+		skeleton->setAttachment(TCHAR_TO_UTF8(*slotName), TCHAR_TO_UTF8(*attachmentName));
+		return true;
+	}
+	return false;
+}
+
+void USpineWidget::UpdateWorldTransform() {
+	CheckState();
+	if (skeleton) {
+		skeleton->updateWorldTransform(Physics_Update);
+	}
+}
+
+void USpineWidget::SetToSetupPose() {
+	CheckState();
+	if (skeleton) skeleton->setToSetupPose();
+}
+
+void USpineWidget::SetBonesToSetupPose() {
+	CheckState();
+	if (skeleton) skeleton->setBonesToSetupPose();
+}
+
+void USpineWidget::SetSlotsToSetupPose() {
+	CheckState();
+	if (skeleton) skeleton->setSlotsToSetupPose();
+}
+
+void USpineWidget::SetScaleX(float scaleX) {
+	CheckState();
+	if (skeleton) skeleton->setScaleX(scaleX);
+}
+
+float USpineWidget::GetScaleX() {
+	CheckState();
+	if (skeleton) return skeleton->getScaleX();
+	return 1;
+}
+
+void USpineWidget::SetScaleY(float scaleY) {
+	CheckState();
+	if (skeleton) skeleton->setScaleY(scaleY);
+}
+
+float USpineWidget::GetScaleY() {
+	CheckState();
+	if (skeleton) return skeleton->getScaleY();
+	return 1;
+}
+
+void USpineWidget::GetBones(TArray &Bones) {
+	CheckState();
+	if (skeleton) {
+		for (size_t i = 0, n = skeleton->getBones().size(); i < n; i++) {
+			Bones.Add(skeleton->getBones()[i]->getData().getName().buffer());
+		}
+	}
+}
+
+bool USpineWidget::HasBone(const FString BoneName) {
+	CheckState();
+	if (skeleton) {
+		return skeleton->getData()->findBone(TCHAR_TO_UTF8(*BoneName)) != nullptr;
+	}
+	return false;
+}
+
+FTransform USpineWidget::GetBoneTransform(const FString &BoneName) {
+	CheckState();
+	if (skeleton) {
+		Bone *bone = skeleton->findBone(TCHAR_TO_UTF8(*BoneName));
+		if (!bone) return FTransform();
+
+		FMatrix localTransform;
+		localTransform.SetIdentity();
+		localTransform.SetAxis(2, FVector(bone->getA(), 0, bone->getC()));
+		localTransform.SetAxis(0, FVector(bone->getB(), 0, bone->getD()));
+		localTransform.SetOrigin(FVector(bone->getWorldX(), 0, bone->getWorldY()));
+
+		FTransform result;
+		result.SetFromMatrix(localTransform);
+		return result;
+	}
+	return FTransform();
+}
+
+void USpineWidget::GetSlots(TArray &Slots) {
+	CheckState();
+	if (skeleton) {
+		for (size_t i = 0, n = skeleton->getSlots().size(); i < n; i++) {
+			Slots.Add(skeleton->getSlots()[i]->getData().getName().buffer());
+		}
+	}
+}
+
+bool USpineWidget::HasSlot(const FString SlotName) {
+	CheckState();
+	if (skeleton) {
+		return skeleton->getData()->findSlot(TCHAR_TO_UTF8(*SlotName)) != nullptr;
+	}
+	return false;
+}
+
+void USpineWidget::SetSlotColor(const FString SlotName, const FColor SlotColor) {
+	CheckState();
+	if (skeleton) {
+		spine::Slot *slot = skeleton->findSlot(TCHAR_TO_UTF8(*SlotName));
+		if (slot) {
+			slot->getColor().set(SlotColor.R / 255.f, SlotColor.G / 255.f, SlotColor.B / 255.f, SlotColor.A / 255.f);
+		}
+	}
+}
+
+void USpineWidget::GetAnimations(TArray &Animations) {
+	CheckState();
+	if (skeleton) {
+		for (size_t i = 0, n = skeleton->getData()->getAnimations().size(); i < n; i++) {
+			Animations.Add(skeleton->getData()->getAnimations()[i]->getName().buffer());
+		}
+	}
+}
+
+bool USpineWidget::HasAnimation(FString AnimationName) {
+	CheckState();
+	if (skeleton) {
+		return skeleton->getData()->findAnimation(TCHAR_TO_UTF8(*AnimationName)) != nullptr;
+	}
+	return false;
+}
+
+float USpineWidget::GetAnimationDuration(FString AnimationName) {
+	CheckState();
+	if (skeleton) {
+		spine::Animation *animation = skeleton->getData()->findAnimation(TCHAR_TO_UTF8(*AnimationName));
+		if (animation == nullptr) return 0;
+		else
+			return animation->getDuration();
+	}
+	return 0;
+}
+
+void USpineWidget::SetAutoPlay(bool bInAutoPlays) {
+	bAutoPlaying = bInAutoPlays;
+}
+
+void USpineWidget::SetPlaybackTime(float InPlaybackTime, bool bCallDelegates) {
+	CheckState();
+
+	if (state && state->getCurrent(0)) {
+		spine::Animation *CurrentAnimation = state->getCurrent(0)->getAnimation();
+		const float CurrentTime = state->getCurrent(0)->getTrackTime();
+		InPlaybackTime = FMath::Clamp(InPlaybackTime, 0.0f, CurrentAnimation->getDuration());
+		const float DeltaTime = InPlaybackTime - CurrentTime;
+		state->update(DeltaTime);
+		state->apply(*skeleton);
+
+		//Call delegates and perform the world transform
+		if (bCallDelegates) {
+			BeforeUpdateWorldTransform.Broadcast(this);
+		}
+		skeleton->updateWorldTransform(Physics_Update);
+		if (bCallDelegates) {
+			AfterUpdateWorldTransform.Broadcast(this);
+		}
+	}
+}
+
+void USpineWidget::SetTimeScale(float timeScale) {
+	CheckState();
+	if (state) state->setTimeScale(timeScale);
+}
+
+float USpineWidget::GetTimeScale() {
+	CheckState();
+	if (state) return state->getTimeScale();
+	return 1;
+}
+
+UTrackEntry *USpineWidget::SetAnimation(int trackIndex, FString animationName, bool loop) {
+	CheckState();
+	if (state && skeleton->getData()->findAnimation(TCHAR_TO_UTF8(*animationName))) {
+		state->disableQueue();
+		TrackEntry *entry = state->setAnimation(trackIndex, TCHAR_TO_UTF8(*animationName), loop);
+		state->enableQueue();
+		UTrackEntry *uEntry = NewObject();
+		uEntry->SetTrackEntry(entry);
+		trackEntries.Add(uEntry);
+		return uEntry;
+	} else
+		return NewObject();
+}
+
+UTrackEntry *USpineWidget::AddAnimation(int trackIndex, FString animationName, bool loop, float delay) {
+	CheckState();
+	if (state && skeleton->getData()->findAnimation(TCHAR_TO_UTF8(*animationName))) {
+		state->disableQueue();
+		TrackEntry *entry = state->addAnimation(trackIndex, TCHAR_TO_UTF8(*animationName), loop, delay);
+		state->enableQueue();
+		UTrackEntry *uEntry = NewObject();
+		uEntry->SetTrackEntry(entry);
+		trackEntries.Add(uEntry);
+		return uEntry;
+	} else
+		return NewObject();
+}
+
+UTrackEntry *USpineWidget::SetEmptyAnimation(int trackIndex, float mixDuration) {
+	CheckState();
+	if (state) {
+		TrackEntry *entry = state->setEmptyAnimation(trackIndex, mixDuration);
+		UTrackEntry *uEntry = NewObject();
+		uEntry->SetTrackEntry(entry);
+		trackEntries.Add(uEntry);
+		return uEntry;
+	} else
+		return NewObject();
+}
+
+UTrackEntry *USpineWidget::AddEmptyAnimation(int trackIndex, float mixDuration, float delay) {
+	CheckState();
+	if (state) {
+		TrackEntry *entry = state->addEmptyAnimation(trackIndex, mixDuration, delay);
+		UTrackEntry *uEntry = NewObject();
+		uEntry->SetTrackEntry(entry);
+		trackEntries.Add(uEntry);
+		return uEntry;
+	} else
+		return NewObject();
+}
+
+UTrackEntry *USpineWidget::GetCurrent(int trackIndex) {
+	CheckState();
+	if (state && state->getCurrent(trackIndex)) {
+		TrackEntry *entry = state->getCurrent(trackIndex);
+		if (entry->getRendererObject()) {
+			return (UTrackEntry *) entry->getRendererObject();
+		} else {
+			UTrackEntry *uEntry = NewObject();
+			uEntry->SetTrackEntry(entry);
+			trackEntries.Add(uEntry);
+			return uEntry;
+		}
+	} else
+		return NewObject();
+}
+
+void USpineWidget::ClearTracks() {
+	CheckState();
+	if (state) {
+		state->clearTracks();
+	}
+}
+
+void USpineWidget::ClearTrack(int trackIndex) {
+	CheckState();
+	if (state) {
+		state->clearTrack(trackIndex);
+	}
+}
+
+void USpineWidget::PhysicsTranslate(float x, float y) {
+	CheckState();
+	if (skeleton) {
+		skeleton->physicsTranslate(x, y);
+	}
+}
+
+void USpineWidget::PhysicsRotate(float x, float y, float degrees) {
+	CheckState();
+	if (skeleton) {
+		skeleton->physicsRotate(x, y, degrees);
+	}
+}
+
+void USpineWidget::ResetPhysicsConstraints() {
+	CheckState();
+	if (skeleton) {
+		Vector &constraints = skeleton->getPhysicsConstraints();
+		for (int i = 0, n = (int) constraints.size(); i < n; i++) {
+			constraints[i]->reset();
+		}
+	}
+}
+
+void USpineWidget::SetPhysicsTimeScale(float scale) {
+	physicsTimeScale = scale;
+}
+
+float USpineWidget::GetPhysicsTimeScale() {
+	return physicsTimeScale;
+}
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Public/SSpineWidget.h b/Plugins/SpinePlugin/Source/SpinePlugin/Public/SSpineWidget.h
new file mode 100644
index 0000000..bfa6eae
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Public/SSpineWidget.h
@@ -0,0 +1,64 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#pragma once
+
+#include "Slate/SMeshWidget.h"
+#include "SlateCore.h"
+#include "SpineAtlasAsset.h"
+#include 
+
+class USpineWidget;
+
+class SSpineWidget : public SMeshWidget {
+
+public:
+	SLATE_BEGIN_ARGS(SSpineWidget) : _MeshData(nullptr) {}
+	SLATE_ARGUMENT(USlateVectorArtData *, MeshData)
+	SLATE_END_ARGS()
+
+	void Construct(const FArguments &Args);
+
+	void SetData(USpineWidget *Widget);
+	FSlateBrush *brush;
+
+protected:
+	virtual int32 OnPaint(const FPaintArgs &Args, const FGeometry &AllottedGeometry, const FSlateRect &MyCullingRect, FSlateWindowElementList &OutDrawElements, int32 LayerId, const FWidgetStyle &InWidgetStyle, bool bParentEnabled) const override;
+
+	void UpdateMesh(int32 LayerId, FSlateWindowElementList &OutDrawElements, const FGeometry &AllottedGeometry, spine::Skeleton *Skeleton);
+
+	void Flush(int32 LayerId, FSlateWindowElementList &OutDrawElements, const FGeometry &AllottedGeometry, int &Idx, TArray &Vertices, TArray &Indices, TArray &Uvs, TArray &Colors, TArray &Colors2, UMaterialInstanceDynamic *Material);
+
+	virtual FVector2D ComputeDesiredSize(float) const override;
+
+	USpineWidget *widget;
+	FRenderData renderData;
+	FVector boundsMin;
+	FVector boundsSize;
+};
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpineAtlasAsset.h b/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpineAtlasAsset.h
new file mode 100644
index 0000000..17789ff
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpineAtlasAsset.h
@@ -0,0 +1,75 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#pragma once
+
+// clang-format off
+#include "Engine/Texture2D.h"
+#include "spine/spine.h"
+#include "SpineAtlasAsset.generated.h"
+// clang-format on
+
+UCLASS(BlueprintType, ClassGroup = (Spine))
+class SPINEPLUGIN_API USpineAtlasAsset : public UPrimaryDataAsset {
+	GENERATED_BODY()
+
+public:
+	spine::Atlas *GetAtlas();
+
+	UPROPERTY(EditAnywhere, BlueprintReadWrite)
+	TArray atlasPages;
+
+	void SetRawData(const FString &RawData);
+
+	FName GetAtlasFileName() const;
+
+	virtual void BeginDestroy() override;
+
+protected:
+	spine::Atlas *atlas = nullptr;
+
+	UPROPERTY()
+	FString rawData;
+
+	UPROPERTY()
+	FName atlasFileName;
+
+#if WITH_EDITORONLY_DATA
+
+public:
+	void SetAtlasFileName(const FName &AtlasFileName);
+
+protected:
+	UPROPERTY(VisibleAnywhere, Instanced, Category = ImportSettings)
+	class UAssetImportData *importData;
+
+	virtual void PostInitProperties() override;
+	virtual void Serialize(FArchive &Ar) override;
+#endif
+};
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpineBoneDriverComponent.h b/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpineBoneDriverComponent.h
new file mode 100644
index 0000000..9c21fb0
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpineBoneDriverComponent.h
@@ -0,0 +1,72 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#pragma once
+
+#include "Components/SceneComponent.h"
+#include "SpineBoneDriverComponent.generated.h"
+
+class USpineSkeletonComponent;
+
+UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+class SPINEPLUGIN_API USpineBoneDriverComponent : public USceneComponent {
+	GENERATED_BODY()
+
+public:
+	UPROPERTY(EditAnywhere, BlueprintReadWrite)
+	AActor *Target = 0;
+
+	UPROPERTY(EditAnywhere, BlueprintReadWrite)
+	FString BoneName;
+
+	//Uses just this component when set to true. Updates owning actor otherwise.
+	UPROPERTY(EditAnywhere, BlueprintReadWrite)
+	bool UseComponentTransform = false;
+
+	UPROPERTY(EditAnywhere, BlueprintReadWrite)
+	bool UsePosition = true;
+
+	UPROPERTY(EditAnywhere, BlueprintReadWrite)
+	bool UseRotation = true;
+
+	UPROPERTY(EditAnywhere, BlueprintReadWrite)
+	bool UseScale = true;
+
+	USpineBoneDriverComponent();
+
+	virtual void BeginPlay() override;
+
+	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;
+
+protected:
+	UFUNCTION()
+	void BeforeUpdateWorldTransform(USpineSkeletonComponent *skeleton);
+
+	USpineSkeletonComponent *lastBoundComponent = nullptr;
+};
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpineBoneFollowerComponent.h b/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpineBoneFollowerComponent.h
new file mode 100644
index 0000000..181eafd
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpineBoneFollowerComponent.h
@@ -0,0 +1,66 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#pragma once
+
+#include "Components/ActorComponent.h"
+#include "Components/SceneComponent.h"
+#include "SpineBoneFollowerComponent.generated.h"
+
+
+UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+class SPINEPLUGIN_API USpineBoneFollowerComponent : public USceneComponent {
+	GENERATED_BODY()
+
+public:
+	UPROPERTY(EditAnywhere, BlueprintReadWrite)
+	AActor *Target = 0;
+
+	UPROPERTY(EditAnywhere, BlueprintReadWrite)
+	FString BoneName;
+
+	//Updates just this component when set to true. Updates owning actor otherwise.
+	UPROPERTY(EditAnywhere, BlueprintReadWrite)
+	bool UseComponentTransform = false;
+
+	UPROPERTY(EditAnywhere, BlueprintReadWrite)
+	bool UsePosition = true;
+
+	UPROPERTY(EditAnywhere, BlueprintReadWrite)
+	bool UseRotation = true;
+
+	UPROPERTY(EditAnywhere, BlueprintReadWrite)
+	bool UseScale = true;
+
+	USpineBoneFollowerComponent();
+
+	virtual void BeginPlay() override;
+
+	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;
+};
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpinePlugin.h b/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpinePlugin.h
new file mode 100644
index 0000000..ffbb736
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpinePlugin.h
@@ -0,0 +1,46 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#pragma once
+
+#include "Modules/ModuleManager.h"
+
+DECLARE_LOG_CATEGORY_EXTERN(SpineLog, Log, All);
+
+class SPINEPLUGIN_API SpinePlugin : public IModuleInterface {
+
+public:
+	static inline SpinePlugin &Get() {
+		return FModuleManager::LoadModuleChecked("SpinePlugin");
+	}
+
+	static inline bool IsAvailable() {
+		return FModuleManager::Get().IsModuleLoaded("SpinePlugin");
+	}
+};
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpineSkeletonAnimationComponent.h b/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpineSkeletonAnimationComponent.h
new file mode 100644
index 0000000..9dede80
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpineSkeletonAnimationComponent.h
@@ -0,0 +1,339 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#pragma once
+
+// clang-format off
+#include "Components/ActorComponent.h"
+#include "SpineSkeletonComponent.h"
+#include "spine/spine.h"
+#include "SpineSkeletonAnimationComponent.generated.h"
+// clang-format on
+
+USTRUCT(BlueprintType, Category = "Spine")
+struct SPINEPLUGIN_API FSpineEvent {
+	GENERATED_BODY();
+
+public:
+	FSpineEvent() : IntValue(0), FloatValue(0.0f), Time(0.0f) {}
+
+	void SetEvent(spine::Event *event) {
+		Name = FString(UTF8_TO_TCHAR(event->getData().getName().buffer()));
+		if (!event->getStringValue().isEmpty()) {
+			StringValue = FString(UTF8_TO_TCHAR(event->getStringValue().buffer()));
+		}
+		this->IntValue = event->getIntValue();
+		this->FloatValue = event->getFloatValue();
+		this->Time = event->getTime();
+	}
+
+	UPROPERTY(BlueprintReadonly)
+	FString Name;
+
+	UPROPERTY(BlueprintReadOnly)
+	FString StringValue;
+
+	UPROPERTY(BlueprintReadOnly)
+	int IntValue;
+
+	UPROPERTY(BlueprintReadOnly)
+	float FloatValue;
+
+	UPROPERTY(BlueprintReadOnly)
+	float Time;
+};
+
+DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpineAnimationStartDelegate, UTrackEntry *, entry);
+DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FSpineAnimationEventDelegate, UTrackEntry *, entry, FSpineEvent, evt);
+DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpineAnimationInterruptDelegate, UTrackEntry *, entry);
+DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpineAnimationCompleteDelegate, UTrackEntry *, entry);
+DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpineAnimationEndDelegate, UTrackEntry *, entry);
+DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpineAnimationDisposeDelegate, UTrackEntry *, entry);
+
+UCLASS(ClassGroup = (Spine), meta = (BlueprintSpawnableComponent), BlueprintType)
+class SPINEPLUGIN_API UTrackEntry : public UObject {
+	GENERATED_BODY()
+
+public:
+	UTrackEntry() {}
+
+	void SetTrackEntry(spine::TrackEntry *trackEntry);
+	spine::TrackEntry *GetTrackEntry() { return entry; }
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	int GetTrackIndex() { return entry ? entry->getTrackIndex() : 0; }
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	bool GetLoop() { return entry ? entry->getLoop() : false; }
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	void SetLoop(bool loop) {
+		if (entry) entry->setLoop(loop);
+	}
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	float GetEventThreshold() { return entry ? entry->getEventThreshold() : 0; }
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	void SetEventThreshold(float eventThreshold) {
+		if (entry) entry->setEventThreshold(eventThreshold);
+	}
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	float GetAlphaAttachmentThreshold() { return entry ? entry->getAlphaAttachmentThreshold() : 0; }
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	void SetAlphaAttachmentThreshold(float attachmentThreshold) {
+		if (entry) entry->setAlphaAttachmentThreshold(attachmentThreshold);
+	}
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	float GetMixDrawOrderThreshold() { return entry ? entry->getMixDrawOrderThreshold() : 0; }
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	void SetMixDrawOrderThreshold(float drawOrderThreshold) {
+		if (entry) entry->setMixDrawOrderThreshold(drawOrderThreshold);
+	}
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	float GetMixAttachmentThreshold() { return entry ? entry->getMixAttachmentThreshold() : 0; }
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	void SetMixAttachmentThreshold(float drawOrderThreshold) {
+		if (entry) entry->setMixAttachmentThreshold(drawOrderThreshold);
+	}
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	float GetAnimationStart() { return entry ? entry->getAnimationStart() : 0; }
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	void SetAnimationStart(float animationStart) {
+		if (entry) entry->setAnimationStart(animationStart);
+	}
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	float GetAnimationEnd() { return entry ? entry->getAnimationEnd() : 0; }
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	void SetAnimationEnd(float animationEnd) {
+		if (entry) entry->setAnimationEnd(animationEnd);
+	}
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	float GetAnimationLast() { return entry ? entry->getAnimationLast() : 0; }
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	void SetAnimationLast(float animationLast) {
+		if (entry) entry->setAnimationLast(animationLast);
+	}
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	float GetDelay() { return entry ? entry->getDelay() : 0; }
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	void SetDelay(float delay) {
+		if (entry) entry->setDelay(delay);
+	}
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	float GetTrackTime() { return entry ? entry->getTrackTime() : 0; }
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	void SetTrackTime(float trackTime) {
+		if (entry) entry->setTrackTime(trackTime);
+	}
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	float GetTrackEnd() { return entry ? entry->getTrackEnd() : 0; }
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	void SetTrackEnd(float trackEnd) {
+		if (entry) entry->setTrackEnd(trackEnd);
+	}
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	float GetTimeScale() { return entry ? entry->getTimeScale() : 0; }
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	void SetTimeScale(float timeScale) {
+		if (entry) entry->setTimeScale(timeScale);
+	}
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	float GetAlpha() { return entry ? entry->getAlpha() : 0; }
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	void SetAlpha(float alpha) {
+		if (entry) entry->setAlpha(alpha);
+	}
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	float GetMixTime() { return entry ? entry->getMixTime() : 0; }
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	void SetMixTime(float mixTime) {
+		if (entry) entry->setMixTime(mixTime);
+	}
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	float GetMixDuration() { return entry ? entry->getMixDuration() : 0; }
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	void SetMixDuration(float mixDuration) {
+		if (entry) entry->setMixDuration(mixDuration);
+	}
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	FString getAnimationName() { return entry ? entry->getAnimation()->getName().buffer() : ""; }
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	float getAnimationDuration() { return entry ? entry->getAnimation()->getDuration() : 0; }
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
+	bool isValidAnimation() { return entry != nullptr; }
+
+	UPROPERTY(BlueprintAssignable, Category = "Components|Spine|TrackEntry")
+	FSpineAnimationStartDelegate AnimationStart;
+
+	UPROPERTY(BlueprintAssignable, Category = "Components|Spine|TrackEntry")
+	FSpineAnimationInterruptDelegate AnimationInterrupt;
+
+	UPROPERTY(BlueprintAssignable, Category = "Components|Spine|TrackEntry")
+	FSpineAnimationEventDelegate AnimationEvent;
+
+	UPROPERTY(BlueprintAssignable, Category = "Components|Spine|TrackEntry")
+	FSpineAnimationCompleteDelegate AnimationComplete;
+
+	UPROPERTY(BlueprintAssignable, Category = "Components|Spine|TrackEntry")
+	FSpineAnimationEndDelegate AnimationEnd;
+
+	UPROPERTY(BlueprintAssignable, Category = "Components|Spine|TrackEntry")
+	FSpineAnimationDisposeDelegate AnimationDispose;
+
+	virtual void BeginDestroy() override;
+
+protected:
+	spine::TrackEntry *entry = nullptr;
+};
+
+class USpineAtlasAsset;
+UCLASS(ClassGroup = (Spine), meta = (BlueprintSpawnableComponent))
+class SPINEPLUGIN_API USpineSkeletonAnimationComponent : public USpineSkeletonComponent {
+	GENERATED_BODY()
+
+public:
+	spine::AnimationState *GetAnimationState() { return state; };
+
+	USpineSkeletonAnimationComponent();
+
+	virtual void BeginPlay() override;
+
+	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;
+
+	virtual void FinishDestroy() override;
+
+	//Added functions for manual configuration
+
+	/* Manages if this skeleton should update automatically or is paused. */
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
+	void SetAutoPlay(bool bInAutoPlays);
+
+	/* Directly set the time of the current animation, will clamp to animation range. */
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
+	void SetPlaybackTime(float InPlaybackTime, bool bCallDelegates = true);
+
+	// Blueprint functions
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
+	void SetTimeScale(float timeScale);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
+	float GetTimeScale();
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
+	UTrackEntry *SetAnimation(int trackIndex, FString animationName, bool loop);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
+	UTrackEntry *AddAnimation(int trackIndex, FString animationName, bool loop, float delay);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
+	UTrackEntry *SetEmptyAnimation(int trackIndex, float mixDuration);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
+	UTrackEntry *AddEmptyAnimation(int trackIndex, float mixDuration, float delay);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
+	UTrackEntry *GetCurrent(int trackIndex);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
+	void ClearTracks();
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
+	void ClearTrack(int trackIndex);
+
+	UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Animation")
+	FSpineAnimationStartDelegate AnimationStart;
+
+	UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Animation")
+	FSpineAnimationInterruptDelegate AnimationInterrupt;
+
+	UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Animation")
+	FSpineAnimationEventDelegate AnimationEvent;
+
+	UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Animation")
+	FSpineAnimationCompleteDelegate AnimationComplete;
+
+	UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Animation")
+	FSpineAnimationEndDelegate AnimationEnd;
+
+	UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Animation")
+	FSpineAnimationDisposeDelegate AnimationDispose;
+
+	UPROPERTY(EditAnywhere, Category = Spine)
+	FString PreviewAnimation;
+
+	UPROPERTY(EditAnywhere, Category = Spine)
+	FString PreviewSkin;
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	void SetPhysicsTimeScale(float scale);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	float GetPhysicsTimeScale();
+
+	// used in C event callback. Needs to be public as we can't call
+	// protected methods from plain old C function.
+	void GCTrackEntry(UTrackEntry *entry) { trackEntries.Remove(entry); }
+
+protected:
+	virtual void CheckState() override;
+	virtual void InternalTick(float DeltaTime, bool CallDelegates = true, bool Preview = false) override;
+	virtual void DisposeState() override;
+
+	spine::AnimationState *state;
+
+	// keep track of track entries so they won't get GCed while
+	// in transit within a blueprint
+	UPROPERTY()
+	TSet trackEntries;
+
+	float physicsTimeScale;
+
+private:
+	/* If the animation should update automatically. */
+	UPROPERTY()
+	bool bAutoPlaying;
+
+	FString lastPreviewAnimation;
+	FString lastPreviewSkin;
+};
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpineSkeletonComponent.h b/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpineSkeletonComponent.h
new file mode 100644
index 0000000..04a36e9
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpineSkeletonComponent.h
@@ -0,0 +1,163 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#pragma once
+
+// clang-format off
+#include "Components/ActorComponent.h"
+#include "SpineSkeletonDataAsset.h"
+#include "spine/spine.h"
+#include "SpineSkeletonComponent.generated.h"
+// clang-format on
+
+class USpineSkeletonComponent;
+
+DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpineBeforeUpdateWorldTransformDelegate, USpineSkeletonComponent *, skeleton);
+DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpineAfterUpdateWorldTransformDelegate, USpineSkeletonComponent *, skeleton);
+
+class USpineAtlasAsset;
+UCLASS(ClassGroup = (Spine), meta = (BlueprintSpawnableComponent))
+class SPINEPLUGIN_API USpineSkeletonComponent : public UActorComponent {
+	GENERATED_BODY()
+
+public:
+	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Spine)
+	USpineAtlasAsset *Atlas;
+
+	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Spine)
+	USpineSkeletonDataAsset *SkeletonData;
+
+	spine::Skeleton *GetSkeleton() {
+		CheckState();
+		return skeleton;
+	};
+
+	UFUNCTION(BlueprintPure, Category = "Components|Spine|Skeleton")
+	void GetSkins(TArray &Skins);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	bool SetSkins(UPARAM(ref) TArray &SkinNames);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	bool SetSkin(const FString SkinName);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	bool HasSkin(const FString SkinName);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	bool SetAttachment(const FString slotName, const FString attachmentName);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	FTransform GetBoneWorldTransform(const FString &BoneName);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	void SetBoneWorldPosition(const FString &BoneName, const FVector &position);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	void UpdateWorldTransform();
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	void SetToSetupPose();
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	void SetBonesToSetupPose();
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	void SetSlotsToSetupPose();
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	void SetScaleX(float scaleX);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	float GetScaleX();
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	void SetScaleY(float scaleY);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	float GetScaleY();
+
+	UFUNCTION(BlueprintPure, Category = "Components|Spine|Skeleton")
+	void GetBones(TArray &Bones);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	bool HasBone(const FString BoneName);
+
+	UFUNCTION(BlueprintPure, Category = "Components|Spine|Skeleton")
+	void GetSlots(TArray &Slots);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	bool HasSlot(const FString SlotName);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	void SetSlotColor(const FString SlotName, const FColor color);
+
+	UFUNCTION(BlueprintPure, Category = "Components|Spine|Skeleton")
+	void GetAnimations(TArray &Animations);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	bool HasAnimation(FString AnimationName);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	float GetAnimationDuration(FString AnimationName);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	void PhysicsTranslate(float x, float y);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	void PhysicsRotate(float x, float y, float degrees);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	void ResetPhysicsConstraints();
+
+	UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Skeleton")
+	FSpineBeforeUpdateWorldTransformDelegate BeforeUpdateWorldTransform;
+
+	UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Skeleton")
+	FSpineAfterUpdateWorldTransformDelegate AfterUpdateWorldTransform;
+
+	USpineSkeletonComponent();
+
+	virtual void BeginPlay() override;
+
+	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;
+
+	virtual void FinishDestroy() override;
+
+protected:
+	virtual void CheckState();
+	virtual void InternalTick(float DeltaTime, bool CallDelegates = true, bool Preview = false);
+	virtual void DisposeState();
+
+	spine::Skeleton *skeleton;
+	USpineAtlasAsset *lastAtlas = nullptr;
+	spine::Atlas *lastSpineAtlas = nullptr;
+	USpineSkeletonDataAsset *lastData = nullptr;
+	spine::Skin *customSkin = nullptr;
+};
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpineSkeletonDataAsset.h b/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpineSkeletonDataAsset.h
new file mode 100644
index 0000000..eb1a95c
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpineSkeletonDataAsset.h
@@ -0,0 +1,127 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#pragma once
+
+// clang-format off
+#include "spine/spine.h"
+#include "SpineSkeletonDataAsset.generated.h"
+// clang-format on
+
+USTRUCT(BlueprintType, Category = "Spine")
+struct SPINEPLUGIN_API FSpineAnimationStateMixData {
+	GENERATED_BODY();
+
+public:
+	UPROPERTY(EditAnywhere, BlueprintReadWrite)
+	FString From;
+
+	UPROPERTY(EditAnywhere, BlueprintReadWrite)
+	FString To;
+
+	UPROPERTY(EditAnywhere, BlueprintReadWrite)
+	float Mix = 0;
+};
+
+UCLASS(BlueprintType, ClassGroup = (Spine))
+class SPINEPLUGIN_API USpineSkeletonDataAsset : public UObject {
+	GENERATED_BODY()
+
+public:
+	spine::SkeletonData *GetSkeletonData(spine::Atlas *Atlas);
+
+	spine::AnimationStateData *GetAnimationStateData(spine::Atlas *atlas);
+	void SetMix(const FString &from, const FString &to, float mix);
+	float GetMix(const FString &from, const FString &to);
+
+	FName GetSkeletonDataFileName() const;
+	void SetRawData(TArray &Data);
+
+	virtual void BeginDestroy() override;
+
+	UPROPERTY(EditAnywhere, BlueprintReadWrite)
+	float DefaultMix = 0;
+
+	UPROPERTY(EditAnywhere, BlueprintReadWrite)
+	TArray MixData;
+
+	UPROPERTY(Transient, VisibleAnywhere)
+	TArray Bones;
+
+	UPROPERTY(Transient, VisibleAnywhere)
+	TArray Slots;
+
+	UPROPERTY(Transient, VisibleAnywhere)
+	TArray Skins;
+
+	UPROPERTY(Transient, VisibleAnywhere)
+	TArray Animations;
+
+	UPROPERTY(Transient, VisibleAnywhere)
+	TArray Events;
+
+protected:
+	UPROPERTY()
+	TArray rawData;
+
+	UPROPERTY()
+	FName skeletonDataFileName;
+
+	// These are created at runtime
+	struct NativeSkeletonData {
+		spine::SkeletonData *skeletonData;
+		spine::AnimationStateData *animationStateData;
+	};
+
+	TMap atlasToNativeData;
+
+	void ClearNativeData();
+
+	void SetMixes(spine::AnimationStateData *animationStateData);
+
+#if WITH_EDITORONLY_DATA
+public:
+	void SetSkeletonDataFileName(const FName &skeletonDataFileName);
+
+protected:
+	UPROPERTY(VisibleAnywhere, Instanced, Category = ImportSettings)
+	class UAssetImportData *importData = nullptr;
+
+	virtual void PostInitProperties() override;
+#if ((ENGINE_MAJOR_VERSION >= 5) && (ENGINE_MINOR_VERSION >= 4))
+	virtual void GetAssetRegistryTags(FAssetRegistryTagsContext Context) const override;
+#else
+	virtual void GetAssetRegistryTags(TArray &OutTags) const override;
+#endif
+
+	virtual void Serialize(FArchive &Ar) override;
+#endif
+
+	void LoadInfo();
+};
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpineSkeletonRendererComponent.h b/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpineSkeletonRendererComponent.h
new file mode 100644
index 0000000..99807f5
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpineSkeletonRendererComponent.h
@@ -0,0 +1,113 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#pragma once
+
+#include "Components/ActorComponent.h"
+#include "ProceduralMeshComponent.h"
+#include "SpineSkeletonAnimationComponent.h"
+#include "SpineSkeletonRendererComponent.generated.h"
+
+
+UCLASS(ClassGroup = (Spine), meta = (BlueprintSpawnableComponent))
+class SPINEPLUGIN_API USpineSkeletonRendererComponent : public UProceduralMeshComponent {
+	GENERATED_BODY()
+
+public:
+	USpineSkeletonRendererComponent(const FObjectInitializer &ObjectInitializer);
+
+	virtual void BeginPlay() override;
+
+	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;
+
+	/* Updates this skeleton renderer using the provided skeleton animation component. */
+	void UpdateRenderer(USpineSkeletonComponent *Skeleton);
+
+	// Material Instance parents
+	UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
+	UMaterialInterface *NormalBlendMaterial;
+
+	UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
+	UMaterialInterface *AdditiveBlendMaterial;
+
+	UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
+	UMaterialInterface *MultiplyBlendMaterial;
+
+	UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
+	UMaterialInterface *ScreenBlendMaterial;
+
+	// Need to hold on to the dynamic instances, or the GC will kill us while updating them
+	UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
+	TArray atlasNormalBlendMaterials;
+
+	UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
+	TArray atlasAdditiveBlendMaterials;
+
+	UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
+	TArray atlasMultiplyBlendMaterials;
+
+	UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
+	TArray atlasScreenBlendMaterials;
+
+	UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
+	float DepthOffset = 0.1f;
+
+	UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
+	FName TextureParameterName;
+
+	UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
+	FLinearColor Color = FLinearColor(1, 1, 1, 1);
+
+	/** Whether to generate collision geometry for the skeleton, or not. */
+	UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
+	bool bCreateCollision;
+
+	virtual void FinishDestroy() override;
+
+protected:
+	void UpdateMaterial(UTexture2D *Texture, UMaterialInstanceDynamic *&CurrentInstance, UMaterialInterface *ParentMaterial);
+
+	void UpdateMesh(USpineSkeletonComponent *component, spine::Skeleton *Skeleton);
+
+	void Flush(int &Idx, TArray &Vertices, TArray &Indices, TArray &Normals, TArray &Uvs, TArray &Colors, UMaterialInstanceDynamic *Material);
+
+	spine::Vector worldVertices;
+	spine::SkeletonClipping clipper;
+
+	UPROPERTY();
+	TArray vertices;
+	UPROPERTY();
+	TArray indices;
+	UPROPERTY();
+	TArray normals;
+	UPROPERTY();
+	TArray uvs;
+	UPROPERTY();
+	TArray colors;
+};
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpineWidget.h b/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpineWidget.h
new file mode 100644
index 0000000..b6a3ed6
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Public/SpineWidget.h
@@ -0,0 +1,287 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#pragma once
+
+// clang-format off
+#include "Runtime/UMG/Public/UMG.h"
+#include "SpineSkeletonDataAsset.h"
+#include "SpineSkeletonAnimationComponent.h"
+#include "spine/spine.h"
+#include "SpineWidget.generated.h"
+// clang-format on
+
+class SSpineWidget;
+class USpineWidget;
+
+DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpineWidgetBeforeUpdateWorldTransformDelegate, USpineWidget *, skeleton);
+DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpineWidgetAfterUpdateWorldTransformDelegate, USpineWidget *, skeleton);
+
+UCLASS(ClassGroup = (Spine), meta = (BlueprintSpawnableComponent))
+class SPINEPLUGIN_API USpineWidget : public UWidget {
+	GENERATED_UCLASS_BODY()
+
+public:
+	virtual void ReleaseSlateResources(bool bReleaseChildren) override;
+	virtual void SynchronizeProperties() override;
+#if WITH_EDITOR
+	virtual const FText GetPaletteCategory() override;
+#endif
+
+	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Spine)
+	FString InitialSkin;
+
+	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Spine)
+	USpineAtlasAsset *Atlas;
+
+	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Spine)
+	USpineSkeletonDataAsset *SkeletonData;
+
+	UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadOnly)
+	UMaterialInterface *NormalBlendMaterial;
+
+	UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadOnly)
+	UMaterialInterface *AdditiveBlendMaterial;
+
+	UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadOnly)
+	UMaterialInterface *MultiplyBlendMaterial;
+
+	UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadOnly)
+	UMaterialInterface *ScreenBlendMaterial;
+
+	UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
+	FName TextureParameterName;
+
+	UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
+	float DepthOffset = 0.1f;
+
+	UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
+	FLinearColor Color = FLinearColor(1, 1, 1, 1);
+
+	UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadOnly)
+	FSlateBrush Brush;
+
+	UFUNCTION(BlueprintPure, Category = "Components|Spine|Skeleton")
+	void GetSkins(TArray &Skins);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	bool SetSkin(const FString SkinName);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	bool SetSkins(UPARAM(ref) TArray &SkinNames);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	bool HasSkin(const FString SkinName);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	bool SetAttachment(const FString slotName, const FString attachmentName);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	void UpdateWorldTransform();
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	void SetToSetupPose();
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	void SetBonesToSetupPose();
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	void SetSlotsToSetupPose();
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	void SetScaleX(float scaleX);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	float GetScaleX();
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	void SetScaleY(float scaleY);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	float GetScaleY();
+
+	UFUNCTION(BlueprintPure, Category = "Components|Spine|Skeleton")
+	void GetBones(TArray &Bones);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	bool HasBone(const FString BoneName);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	FTransform GetBoneTransform(const FString &BoneName);
+
+	UFUNCTION(BlueprintPure, Category = "Components|Spine|Skeleton")
+	void GetSlots(TArray &Slots);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	bool HasSlot(const FString SlotName);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	void SetSlotColor(const FString SlotName, const FColor SlotColor);
+
+	UFUNCTION(BlueprintPure, Category = "Components|Spine|Skeleton")
+	void GetAnimations(TArray &Animations);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	bool HasAnimation(FString AnimationName);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	float GetAnimationDuration(FString AnimationName);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	void PhysicsTranslate(float x, float y);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	void PhysicsRotate(float x, float y, float degrees);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	void ResetPhysicsConstraints();
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	void SetPhysicsTimeScale(float scale);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
+	float GetPhysicsTimeScale();
+
+	UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Skeleton")
+	FSpineWidgetBeforeUpdateWorldTransformDelegate BeforeUpdateWorldTransform;
+
+	UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Skeleton")
+	FSpineWidgetAfterUpdateWorldTransformDelegate AfterUpdateWorldTransform;
+
+	/* Manages if this skeleton should update automatically or is paused. */
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
+	void SetAutoPlay(bool bInAutoPlays);
+
+	/* Directly set the time of the current animation, will clamp to animation range. */
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
+	void SetPlaybackTime(float InPlaybackTime, bool bCallDelegates = true);
+
+	// Blueprint functions
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
+	void SetTimeScale(float timeScale);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
+	float GetTimeScale();
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
+	UTrackEntry *SetAnimation(int trackIndex, FString animationName, bool loop);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
+	UTrackEntry *AddAnimation(int trackIndex, FString animationName, bool loop, float delay);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
+	UTrackEntry *SetEmptyAnimation(int trackIndex, float mixDuration);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
+	UTrackEntry *AddEmptyAnimation(int trackIndex, float mixDuration, float delay);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
+	UTrackEntry *GetCurrent(int trackIndex);
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
+	void ClearTracks();
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
+	void ClearTrack(int trackIndex);
+
+	UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Animation")
+	FSpineAnimationStartDelegate AnimationStart;
+
+	UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Animation")
+	FSpineAnimationInterruptDelegate AnimationInterrupt;
+
+	UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Animation")
+	FSpineAnimationEventDelegate AnimationEvent;
+
+	UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Animation")
+	FSpineAnimationCompleteDelegate AnimationComplete;
+
+	UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Animation")
+	FSpineAnimationEndDelegate AnimationEnd;
+
+	UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Animation")
+	FSpineAnimationDisposeDelegate AnimationDispose;
+
+	UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
+	void Tick(float DeltaTime, bool CallDelegates = true);
+
+	virtual void FinishDestroy() override;
+
+	// used in C event callback. Needs to be public as we can't call
+	// protected methods from plain old C function.
+	void GCTrackEntry(UTrackEntry *entry) { trackEntries.Remove(entry); }
+
+protected:
+	friend class SSpineWidget;
+
+	virtual TSharedRef RebuildWidget() override;
+	virtual void CheckState();
+	virtual void DisposeState();
+
+	TSharedPtr slateWidget;
+
+	spine::Skeleton *skeleton;
+	spine::AnimationState *state;
+	USpineAtlasAsset *lastAtlas = nullptr;
+	spine::Atlas *lastSpineAtlas = nullptr;
+	USpineSkeletonDataAsset *lastData = nullptr;
+	spine::Skin *customSkin = nullptr;
+	float physicsTimeScale;
+
+	// Need to hold on to the dynamic instances, or the GC will kill us while updating them
+	UPROPERTY()
+	TArray atlasNormalBlendMaterials;
+	TMap pageToNormalBlendMaterial;
+
+	UPROPERTY()
+	TArray atlasAdditiveBlendMaterials;
+	TMap pageToAdditiveBlendMaterial;
+
+	UPROPERTY()
+	TArray atlasMultiplyBlendMaterials;
+	TMap pageToMultiplyBlendMaterial;
+
+	UPROPERTY()
+	TArray atlasScreenBlendMaterials;
+	TMap pageToScreenBlendMaterial;
+
+	spine::Vector worldVertices;
+	spine::SkeletonClipping clipper;
+
+	// keep track of track entries so they won't get GCed while
+	// in transit within a blueprint
+	UPROPERTY()
+	TSet trackEntries;
+
+private:
+	/* If the animation should update automatically. */
+	UPROPERTY()
+	bool bAutoPlaying;
+	bool bSkinInitialized = false;
+};
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/Animation.h b/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/Animation.h
new file mode 100644
index 0000000..7c9355c
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/Animation.h
@@ -0,0 +1,131 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#ifndef Spine_Animation_h
+#define Spine_Animation_h
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace spine {
+	class Timeline;
+
+	class Skeleton;
+
+	class Event;
+
+	class AnimationState;
+
+	class SP_API Animation : public SpineObject {
+		friend class AnimationState;
+
+		friend class TrackEntry;
+
+		friend class AnimationStateData;
+
+		friend class AttachmentTimeline;
+
+		friend class RGBATimeline;
+
+		friend class RGBTimeline;
+
+		friend class AlphaTimeline;
+
+		friend class RGBA2Timeline;
+
+		friend class RGB2Timeline;
+
+		friend class DeformTimeline;
+
+		friend class DrawOrderTimeline;
+
+		friend class EventTimeline;
+
+		friend class IkConstraintTimeline;
+
+		friend class PathConstraintMixTimeline;
+
+		friend class PathConstraintPositionTimeline;
+
+		friend class PathConstraintSpacingTimeline;
+
+		friend class RotateTimeline;
+
+		friend class ScaleTimeline;
+
+		friend class ShearTimeline;
+
+		friend class TransformConstraintTimeline;
+
+		friend class TranslateTimeline;
+
+		friend class TranslateXTimeline;
+
+		friend class TranslateYTimeline;
+
+		friend class TwoColorTimeline;
+
+	public:
+		Animation(const String &name, Vector &timelines, float duration);
+
+		~Animation();
+
+		/// Applies all the animation's timelines to the specified skeleton.
+		/// See also Timeline::apply(Skeleton&, float, float, Vector, float, MixPose, MixDirection)
+		void apply(Skeleton &skeleton, float lastTime, float time, bool loop, Vector *pEvents, float alpha,
+				   MixBlend blend, MixDirection direction);
+
+		const String &getName();
+
+		Vector &getTimelines();
+
+		bool hasTimeline(Vector &ids);
+
+		float getDuration();
+
+		void setDuration(float inValue);
+
+		/// @param target After the first and before the last entry.
+		static int search(Vector &values, float target);
+
+		static int search(Vector &values, float target, int step);
+	private:
+		Vector _timelines;
+		HashMap _timelineIds;
+		float _duration;
+		String _name;
+	};
+}
+
+#endif /* Spine_Animation_h */
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/AnimationState.h b/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/AnimationState.h
new file mode 100644
index 0000000..501e722
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/AnimationState.h
@@ -0,0 +1,521 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#ifndef Spine_AnimationState_h
+#define Spine_AnimationState_h
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include "Slot.h"
+
+#ifdef SPINE_USE_STD_FUNCTION
+#include 
+#endif
+
+namespace spine {
+	enum EventType {
+		EventType_Start = 0,
+		EventType_Interrupt,
+		EventType_End,
+		EventType_Complete,
+		EventType_Dispose,
+		EventType_Event
+	};
+
+	class AnimationState;
+
+	class TrackEntry;
+
+	class Animation;
+
+	class Event;
+
+	class AnimationStateData;
+
+	class Skeleton;
+
+	class RotateTimeline;
+
+	class AttachmentTimeline;
+
+#ifdef SPINE_USE_STD_FUNCTION
+	typedef std::function AnimationStateListener;
+#else
+
+	typedef void (*AnimationStateListener)(AnimationState *state, EventType type, TrackEntry *entry, Event *event);
+
+#endif
+
+	/// Abstract class to inherit from to create a callback object
+	class SP_API AnimationStateListenerObject {
+	public:
+		AnimationStateListenerObject() {};
+
+		virtual ~AnimationStateListenerObject() {};
+	public:
+		/// The callback function to be called
+		virtual void callback(AnimationState *state, EventType type, TrackEntry *entry, Event *event) = 0;
+	};
+
+	/// State for the playback of an animation
+	class SP_API TrackEntry : public SpineObject, public HasRendererObject {
+		friend class EventQueue;
+
+		friend class AnimationState;
+
+	public:
+		TrackEntry();
+
+		virtual ~TrackEntry();
+
+		/// The index of the track where this entry is either current or queued.
+		int getTrackIndex();
+
+		/// The animation to apply for this track entry.
+		Animation *getAnimation();
+
+		TrackEntry *getPrevious();
+
+		/// If true, the animation will repeat. If false, it will not, instead its last frame is applied if played beyond its duration.
+		bool getLoop();
+
+		void setLoop(bool inValue);
+
+		/// If true, when mixing from the previous animation to this animation, the previous animation is applied as normal instead
+		/// of being mixed out.
+		///
+		/// When mixing between animations that key the same property, if a lower track also keys that property then the value will
+		/// briefly dip toward the lower track value during the mix. This happens because the first animation mixes from 100% to 0%
+		/// while the second animation mixes from 0% to 100%. Setting holdPrevious to true applies the first animation
+		/// at 100% during the mix so the lower track value is overwritten. Such dipping does not occur on the lowest track which
+		/// keys the property, only when a higher track also keys the property.
+		///
+		/// Snapping will occur if holdPrevious is true and this animation does not key all the same properties as the
+		/// previous animation.
+		bool getHoldPrevious();
+
+		void setHoldPrevious(bool inValue);
+
+		bool getReverse();
+
+		void setReverse(bool inValue);
+
+		bool getShortestRotation();
+
+		void setShortestRotation(bool inValue);
+
+		/// Seconds to postpone playing the animation. When a track entry is the current track entry, delay postpones incrementing
+		/// the track time. When a track entry is queued, delay is the time from the start of the previous animation to when the
+		/// track entry will become the current track entry.
+		float getDelay();
+
+		void setDelay(float inValue);
+
+		/// Current time in seconds this track entry has been the current track entry. The track time determines
+		/// TrackEntry.AnimationTime. The track time can be set to start the animation at a time other than 0, without affecting looping.
+		float getTrackTime();
+
+		void setTrackTime(float inValue);
+
+		/// The track time in seconds when this animation will be removed from the track. Defaults to the animation duration for
+		/// non-looping animations and to int.MaxValue for looping animations. If the track end time is reached and no
+		/// other animations are queued for playback, and mixing from any previous animations is complete, properties keyed by the animation,
+		/// are set to the setup pose and the track is cleared.
+		///
+		/// It may be desired to use AnimationState.addEmptyAnimation(int, float, float) to mix the properties back to the
+		/// setup pose over time, rather than have it happen instantly.
+		float getTrackEnd();
+
+		void setTrackEnd(float inValue);
+
+		/// Seconds when this animation starts, both initially and after looping. Defaults to 0.
+		///
+		/// When changing the animation start time, it often makes sense to set TrackEntry.AnimationLast to the same value to
+		/// prevent timeline keys before the start time from triggering.
+		float getAnimationStart();
+
+		void setAnimationStart(float inValue);
+
+		/// Seconds for the last frame of this animation. Non-looping animations won't play past this time. Looping animations will
+		/// loop back to TrackEntry.AnimationStart at this time. Defaults to the animation duration.
+		float getAnimationEnd();
+
+		void setAnimationEnd(float inValue);
+
+		/// The time in seconds this animation was last applied. Some timelines use this for one-time triggers. Eg, when this
+		/// animation is applied, event timelines will fire all events between the animation last time (exclusive) and animation time
+		/// (inclusive). Defaults to -1 to ensure triggers on frame 0 happen the first time this animation is applied.
+		float getAnimationLast();
+
+		void setAnimationLast(float inValue);
+
+		/// Uses TrackEntry.TrackTime to compute the animation time between TrackEntry.AnimationStart. and
+		/// TrackEntry.AnimationEnd. When the track time is 0, the animation time is equal to the animation start time.
+		float getAnimationTime();
+
+		/// Multiplier for the delta time when the animation state is updated, causing time for this animation to play slower or
+		/// faster. Defaults to 1.
+		float getTimeScale();
+
+		void setTimeScale(float inValue);
+
+		/// Values less than 1 mix this animation with the last skeleton pose. Defaults to 1, which overwrites the last skeleton pose with
+		/// this animation.
+		///
+		/// Typically track 0 is used to completely pose the skeleton, then alpha can be used on higher tracks. It doesn't make sense
+		/// to use alpha on track 0 if the skeleton pose is from the last frame render.
+		float getAlpha();
+
+		void setAlpha(float inValue);
+
+		///
+		/// When the mix percentage (mix time / mix duration) is less than the event threshold, event timelines for the animation
+		/// being mixed out will be applied. Defaults to 0, so event timelines are not applied for an animation being mixed out.
+		float getEventThreshold();
+
+		void setEventThreshold(float inValue);
+
+		/// When the mix percentage (mix time / mix duration) is less than the attachment threshold, attachment timelines for the
+		/// animation being mixed out will be applied. Defaults to 0, so attachment timelines are not applied for an animation being
+		/// mixed out.
+		float getMixAttachmentThreshold();
+
+		void setMixAttachmentThreshold(float inValue);
+
+        /// When getAlpha() is greater than alphaAttachmentThreshold, attachment timelines are applied.
+	    /// Defaults to 0, so attachment timelines are always applied. */
+        float getAlphaAttachmentThreshold();
+
+        void setAlphaAttachmentThreshold(float inValue);
+
+		/// When the mix percentage (mix time / mix duration) is less than the draw order threshold, draw order timelines for the
+		/// animation being mixed out will be applied. Defaults to 0, so draw order timelines are not applied for an animation being
+		/// mixed out.
+		float getMixDrawOrderThreshold();
+
+		void setMixDrawOrderThreshold(float inValue);
+
+		/// The animation queued to start after this animation, or NULL.
+		TrackEntry *getNext();
+
+		/// Returns true if at least one loop has been completed.
+		bool isComplete();
+
+		/// Seconds from 0 to the mix duration when mixing from the previous animation to this animation. May be slightly more than
+		/// TrackEntry.MixDuration when the mix is complete.
+		float getMixTime();
+
+		void setMixTime(float inValue);
+
+		/// Seconds for mixing from the previous animation to this animation. Defaults to the value provided by
+		/// AnimationStateData based on the animation before this animation (if any).
+		///
+		/// The mix duration can be set manually rather than use the value from AnimationStateData.GetMix.
+		/// In that case, the mixDuration must be set before AnimationState.update(float) is next called.
+		///
+		/// When using AnimationState::addAnimation(int, Animation, bool, float) with a delay
+		/// less than or equal to 0, note the Delay is set using the mix duration from the AnimationStateData
+		float getMixDuration();
+
+		void setMixDuration(float inValue);
+
+        void setMixDuration(float mixDuration, float delay);
+
+		MixBlend getMixBlend();
+
+		void setMixBlend(MixBlend blend);
+
+		/// The track entry for the previous animation when mixing from the previous animation to this animation, or NULL if no
+		/// mixing is currently occuring. When mixing from multiple animations, MixingFrom makes up a double linked list with MixingTo.
+		TrackEntry *getMixingFrom();
+
+		/// The track entry for the next animation when mixing from this animation, or NULL if no mixing is currently occuring.
+		/// When mixing from multiple animations, MixingTo makes up a double linked list with MixingFrom.
+		TrackEntry *getMixingTo();
+
+		/// Resets the rotation directions for mixing this entry's rotate timelines. This can be useful to avoid bones rotating the
+		/// long way around when using alpha and starting animations on other tracks.
+		///
+		/// Mixing involves finding a rotation between two others, which has two possible solutions: the short way or the long way around.
+		/// The two rotations likely change over time, so which direction is the short or long way also changes.
+		/// If the short way was always chosen, bones would flip to the other side when that direction became the long way.
+		/// TrackEntry chooses the short way the first time it is applied and remembers that direction.
+		void resetRotationDirections();
+
+		float getTrackComplete();
+
+		void setListener(AnimationStateListener listener);
+
+		void setListener(AnimationStateListenerObject *listener);
+
+        /// Returns true if this track entry has been applied at least once.
+        ///
+        /// See AnimationState::apply(Skeleton).
+        bool wasApplied();
+
+        /// Returns true if there is a getNext() track entry that is ready to become the current track entry during the
+        /// next AnimationState::update(float)}
+        bool isNextReady () {
+            return _next != NULL && _nextTrackLast - _next->_delay >= 0;
+        }
+
+	private:
+		Animation *_animation;
+		TrackEntry *_previous;
+		TrackEntry *_next;
+		TrackEntry *_mixingFrom;
+		TrackEntry *_mixingTo;
+		int _trackIndex;
+
+		bool _loop, _holdPrevious, _reverse, _shortestRotation;
+		float _eventThreshold, _mixAttachmentThreshold, _alphaAttachmentThreshold, _mixDrawOrderThreshold;
+		float _animationStart, _animationEnd, _animationLast, _nextAnimationLast;
+		float _delay, _trackTime, _trackLast, _nextTrackLast, _trackEnd, _timeScale;
+		float _alpha, _mixTime, _mixDuration, _interruptAlpha, _totalAlpha;
+		MixBlend _mixBlend;
+		Vector _timelineMode;
+		Vector _timelineHoldMix;
+		Vector _timelinesRotation;
+		AnimationStateListener _listener;
+		AnimationStateListenerObject *_listenerObject;
+
+		void reset();
+	};
+
+	class SP_API EventQueueEntry : public SpineObject {
+		friend class EventQueue;
+
+	public:
+		EventType _type;
+		TrackEntry *_entry;
+		Event *_event;
+
+		EventQueueEntry(EventType eventType, TrackEntry *trackEntry, Event *event = NULL);
+	};
+
+	class SP_API EventQueue : public SpineObject {
+		friend class AnimationState;
+
+	private:
+		Vector _eventQueueEntries;
+		AnimationState &_state;
+		bool _drainDisabled;
+
+		static EventQueue *newEventQueue(AnimationState &state);
+
+		static EventQueueEntry newEventQueueEntry(EventType eventType, TrackEntry *entry, Event *event = NULL);
+
+		EventQueue(AnimationState &state);
+
+		~EventQueue();
+
+		void start(TrackEntry *entry);
+
+		void interrupt(TrackEntry *entry);
+
+		void end(TrackEntry *entry);
+
+		void dispose(TrackEntry *entry);
+
+		void complete(TrackEntry *entry);
+
+		void event(TrackEntry *entry, Event *event);
+
+		/// Raises all events in the queue and drains the queue.
+		void drain();
+	};
+
+	class SP_API AnimationState : public SpineObject, public HasRendererObject {
+		friend class TrackEntry;
+
+		friend class EventQueue;
+
+	public:
+		explicit AnimationState(AnimationStateData *data);
+
+		~AnimationState();
+
+		/// Increments the track entry times, setting queued animations as current if needed
+		/// @param delta delta time
+		void update(float delta);
+
+		/// Poses the skeleton using the track entry animations. There are no side effects other than invoking listeners, so the
+		/// animation state can be applied to multiple skeletons to pose them identically.
+		bool apply(Skeleton &skeleton);
+
+		/// Removes all animations from all tracks, leaving skeletons in their previous pose.
+		/// It may be desired to use AnimationState.setEmptyAnimations(float) to mix the skeletons back to the setup pose,
+		/// rather than leaving them in their previous pose.
+		void clearTracks();
+
+		/// Removes all animations from the tracks, leaving skeletons in their previous pose.
+		/// It may be desired to use AnimationState.setEmptyAnimations(float) to mix the skeletons back to the setup pose,
+		/// rather than leaving them in their previous pose.
+		void clearTrack(size_t trackIndex);
+
+		/// Sets an animation by name. setAnimation(int, Animation, bool)
+		TrackEntry *setAnimation(size_t trackIndex, const String &animationName, bool loop);
+
+		/// Sets the current animation for a track, discarding any queued animations.
+		/// @param loop If true, the animation will repeat.
+		/// If false, it will not, instead its last frame is applied if played beyond its duration.
+		/// In either case TrackEntry.TrackEnd determines when the track is cleared.
+		/// @return
+		/// A track entry to allow further customization of animation playback. References to the track entry must not be kept
+		/// after AnimationState.Dispose.
+		TrackEntry *setAnimation(size_t trackIndex, Animation *animation, bool loop);
+
+		/// Queues an animation by name.
+		/// addAnimation(int, Animation, bool, float)
+		TrackEntry *addAnimation(size_t trackIndex, const String &animationName, bool loop, float delay);
+
+		/// Adds an animation to be played delay seconds after the current or last queued animation
+		/// for a track. If the track is empty, it is equivalent to calling setAnimation.
+		/// @param delay
+		/// Seconds to begin this animation after the start of the previous animation. May be <= 0 to use the animation
+		/// duration of the previous track minus any mix duration plus the negative delay.
+		///
+		/// @return A track entry to allow further customization of animation playback. References to the track entry must not be kept
+		/// after AnimationState.Dispose
+		TrackEntry *addAnimation(size_t trackIndex, Animation *animation, bool loop, float delay);
+
+		/// Sets an empty animation for a track, discarding any queued animations, and mixes to it over the specified mix duration.
+		TrackEntry *setEmptyAnimation(size_t trackIndex, float mixDuration);
+
+		/// Adds an empty animation to be played after the current or last queued animation for a track, and mixes to it over the
+		/// specified mix duration.
+		/// @return
+		/// A track entry to allow further customization of animation playback. References to the track entry must not be kept after AnimationState.Dispose.
+		///
+		/// @param trackIndex Track number.
+		/// @param mixDuration Mix duration.
+		/// @param delay Seconds to begin this animation after the start of the previous animation. May be <= 0 to use the animation
+		/// duration of the previous track minus any mix duration plus the negative delay.
+		TrackEntry *addEmptyAnimation(size_t trackIndex, float mixDuration, float delay);
+
+		/// Sets an empty animation for every track, discarding any queued animations, and mixes to it over the specified mix duration.
+		void setEmptyAnimations(float mixDuration);
+
+		/// @return The track entry for the animation currently playing on the track, or NULL if no animation is currently playing.
+		TrackEntry *getCurrent(size_t trackIndex);
+
+		AnimationStateData *getData();
+
+		/// A list of tracks that have animations, which may contain NULLs.
+		Vector &getTracks();
+
+		float getTimeScale();
+
+		void setTimeScale(float inValue);
+
+		void setListener(AnimationStateListener listener);
+
+		void setListener(AnimationStateListenerObject *listener);
+
+		void disableQueue();
+
+		void enableQueue();
+
+		void setManualTrackEntryDisposal(bool inValue);
+
+        bool getManualTrackEntryDisposal();
+
+		void disposeTrackEntry(TrackEntry *entry);
+
+	private:
+		static const int Subsequent = 0;
+		static const int First = 1;
+		static const int HoldSubsequent = 2;
+		static const int HoldFirst = 3;
+		static const int HoldMix = 4;
+
+		static const int Setup = 1;
+		static const int Current = 2;
+
+		AnimationStateData *_data;
+
+		Pool _trackEntryPool;
+		Vector _tracks;
+		Vector _events;
+		EventQueue *_queue;
+
+		HashMap _propertyIDs;
+		bool _animationsChanged;
+
+		AnimationStateListener _listener;
+		AnimationStateListenerObject *_listenerObject;
+
+		int _unkeyedState;
+
+		float _timeScale;
+
+		bool _manualTrackEntryDisposal;
+
+		static Animation *getEmptyAnimation();
+
+		static void
+		applyRotateTimeline(RotateTimeline *rotateTimeline, Skeleton &skeleton, float time, float alpha, MixBlend pose,
+							Vector &timelinesRotation, size_t i, bool firstFrame);
+
+		void applyAttachmentTimeline(AttachmentTimeline *attachmentTimeline, Skeleton &skeleton, float animationTime,
+									 MixBlend pose, bool firstFrame);
+
+		/// Returns true when all mixing from entries are complete.
+		bool updateMixingFrom(TrackEntry *to, float delta);
+
+		float applyMixingFrom(TrackEntry *to, Skeleton &skeleton, MixBlend currentPose);
+
+		void queueEvents(TrackEntry *entry, float animationTime);
+
+		/// Sets the active TrackEntry for a given track number.
+		void setCurrent(size_t index, TrackEntry *current, bool interrupt);
+
+		/// Removes the next entry and all entries after it for the specified entry. */
+		void clearNext(TrackEntry *entry);
+
+		TrackEntry *expandToIndex(size_t index);
+
+		/// Object-pooling version of new TrackEntry. Obtain an unused TrackEntry from the pool and clear/initialize its values.
+		/// @param last May be NULL.
+		TrackEntry *newTrackEntry(size_t trackIndex, Animation *animation, bool loop, TrackEntry *last);
+
+		void animationsChanged();
+
+		void computeHold(TrackEntry *entry);
+
+		void setAttachment(Skeleton &skeleton, spine::Slot &slot, const String &attachmentName, bool attachments);
+	};
+}
+
+#endif /* Spine_AnimationState_h */
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/AnimationStateData.h b/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/AnimationStateData.h
new file mode 100644
index 0000000..a315e76
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/AnimationStateData.h
@@ -0,0 +1,90 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#ifndef Spine_AnimationStateData_h
+#define Spine_AnimationStateData_h
+
+#include 
+#include 
+#include 
+
+#include 
+
+namespace spine {
+	class SkeletonData;
+
+	class Animation;
+
+	/// Stores mix (crossfade) durations to be applied when AnimationState animations are changed.
+	class SP_API AnimationStateData : public SpineObject {
+		friend class AnimationState;
+
+	public:
+		explicit AnimationStateData(SkeletonData *skeletonData);
+
+		/// The SkeletonData to look up animations when they are specified by name.
+		SkeletonData *getSkeletonData();
+
+		/// The mix duration to use when no mix duration has been specifically defined between two animations.
+		float getDefaultMix();
+
+		void setDefaultMix(float inValue);
+
+		/// Sets a mix duration by animation names.
+		void setMix(const String &fromName, const String &toName, float duration);
+
+		/// Sets a mix duration when changing from the specified animation to the other.
+		/// See TrackEntry.MixDuration.
+		void setMix(Animation *from, Animation *to, float duration);
+
+		/// The mix duration to use when changing from the specified animation to the other,
+		/// or the DefaultMix if no mix duration has been set.
+		float getMix(Animation *from, Animation *to);
+
+		/// Removes all mixes and sets the default mix to 0.
+		void clear();
+
+	private:
+		class AnimationPair : public SpineObject {
+		public:
+			Animation *_a1;
+			Animation *_a2;
+
+			explicit AnimationPair(Animation *a1 = NULL, Animation *a2 = NULL);
+
+			bool operator==(const AnimationPair &other) const;
+		};
+
+		SkeletonData *_skeletonData;
+		float _defaultMix;
+		HashMap _animationToMixTime;
+	};
+}
+
+#endif /* Spine_AnimationStateData_h */
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/Atlas.h b/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/Atlas.h
new file mode 100644
index 0000000..f6c7931
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/Atlas.h
@@ -0,0 +1,139 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#ifndef Spine_Atlas_h
+#define Spine_Atlas_h
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include "TextureRegion.h"
+
+namespace spine {
+	enum Format {
+		Format_Alpha,
+		Format_Intensity,
+		Format_LuminanceAlpha,
+		Format_RGB565,
+		Format_RGBA4444,
+		Format_RGB888,
+		Format_RGBA8888
+	};
+
+	// Our TextureFilter collides with UE4's TextureFilter in unity builds. We rename
+	// TextureFilter to SpineTextureFilter in UE4.
+#ifdef SPINE_UE4
+	#define TEXTURE_FILTER_ENUM SpineTextureFilter
+#else
+	#define TEXTURE_FILTER_ENUM TextureFilter
+#endif
+
+	enum TEXTURE_FILTER_ENUM {
+		TextureFilter_Unknown,
+		TextureFilter_Nearest,
+		TextureFilter_Linear,
+		TextureFilter_MipMap,
+		TextureFilter_MipMapNearestNearest,
+		TextureFilter_MipMapLinearNearest,
+		TextureFilter_MipMapNearestLinear,
+		TextureFilter_MipMapLinearLinear
+	};
+
+	enum TextureWrap {
+		TextureWrap_MirroredRepeat,
+		TextureWrap_ClampToEdge,
+		TextureWrap_Repeat
+	};
+
+	class SP_API AtlasPage : public SpineObject {
+	public:
+		String name;
+		String texturePath;
+		Format format;
+		TEXTURE_FILTER_ENUM minFilter;
+		TEXTURE_FILTER_ENUM magFilter;
+		TextureWrap uWrap;
+		TextureWrap vWrap;
+		int width, height;
+		bool pma;
+        int index;
+        void *texture;
+
+		explicit AtlasPage(const String &inName) : name(inName), format(Format_RGBA8888),
+												   minFilter(TextureFilter_Nearest),
+												   magFilter(TextureFilter_Nearest), uWrap(TextureWrap_ClampToEdge),
+												   vWrap(TextureWrap_ClampToEdge), width(0), height(0), pma(false), index(0), texture(NULL) {
+		}
+	};
+
+	class SP_API AtlasRegion : public TextureRegion {
+	public:
+		AtlasPage *page;
+		String name;
+		int index;
+		int x, y;
+		Vector splits;
+		Vector pads;
+		Vector  names;
+		Vector values;
+	};
+
+	class TextureLoader;
+
+	class SP_API Atlas : public SpineObject {
+	public:
+		Atlas(const String &path, TextureLoader *textureLoader, bool createTexture = true);
+
+		Atlas(const char *data, int length, const char *dir, TextureLoader *textureLoader, bool createTexture = true);
+
+		~Atlas();
+
+		void flipV();
+
+		/// Returns the first region found with the specified name. This method uses String comparison to find the region, so the result
+		/// should be cached rather than calling this method multiple times.
+		/// @return The region, or NULL.
+		AtlasRegion *findRegion(const String &name);
+
+		Vector &getPages();
+
+		Vector &getRegions();
+
+	private:
+		Vector _pages;
+		Vector _regions;
+		TextureLoader *_textureLoader;
+
+		void load(const char *begin, int length, const char *dir, bool createTexture);
+	};
+}
+
+#endif /* Spine_Atlas_h */
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/AtlasAttachmentLoader.h b/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/AtlasAttachmentLoader.h
new file mode 100644
index 0000000..8511167
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/AtlasAttachmentLoader.h
@@ -0,0 +1,72 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#ifndef Spine_AtlasAttachmentLoader_h
+#define Spine_AtlasAttachmentLoader_h
+
+#include 
+#include 
+#include 
+
+
+namespace spine {
+	class Atlas;
+
+	class AtlasRegion;
+
+	/// An AttachmentLoader that configures attachments using texture regions from an Atlas.
+	/// See http://esotericsoftware.com/spine-loading-skeleton-data#JSON-and-binary-data about Loading Skeleton Data in the Spine Runtimes Guide.
+	class SP_API AtlasAttachmentLoader : public AttachmentLoader {
+	public:
+	RTTI_DECL
+
+		explicit AtlasAttachmentLoader(Atlas *atlas);
+
+		virtual RegionAttachment *newRegionAttachment(Skin &skin, const String &name, const String &path, Sequence *sequence);
+
+		virtual MeshAttachment *newMeshAttachment(Skin &skin, const String &name, const String &path, Sequence *sequence);
+
+		virtual BoundingBoxAttachment *newBoundingBoxAttachment(Skin &skin, const String &name);
+
+		virtual PathAttachment *newPathAttachment(Skin &skin, const String &name);
+
+		virtual PointAttachment *newPointAttachment(Skin &skin, const String &name);
+
+		virtual ClippingAttachment *newClippingAttachment(Skin &skin, const String &name);
+
+		virtual void configureAttachment(Attachment *attachment);
+
+		AtlasRegion *findRegion(const String &name);
+
+	private:
+		Atlas *_atlas;
+	};
+}
+
+#endif /* Spine_AtlasAttachmentLoader_h */
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/Attachment.h b/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/Attachment.h
new file mode 100644
index 0000000..ae72b18
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/Attachment.h
@@ -0,0 +1,62 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#ifndef Spine_Attachment_h
+#define Spine_Attachment_h
+
+#include 
+#include 
+#include 
+
+namespace spine {
+	class SP_API Attachment : public SpineObject {
+	RTTI_DECL
+
+	public:
+		explicit Attachment(const String &name);
+
+		virtual ~Attachment();
+
+		const String &getName() const;
+
+		virtual Attachment *copy() = 0;
+
+		int getRefCount();
+
+		void reference();
+
+		void dereference();
+
+	private:
+		const String _name;
+		int _refCount;
+	};
+}
+
+#endif /* Spine_Attachment_h */
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/AttachmentLoader.h b/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/AttachmentLoader.h
new file mode 100644
index 0000000..9f02302
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/AttachmentLoader.h
@@ -0,0 +1,84 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#ifndef Spine_AttachmentLoader_h
+#define Spine_AttachmentLoader_h
+
+#include 
+#include 
+#include 
+
+namespace spine {
+	class Skin;
+
+	class Attachment;
+
+	class RegionAttachment;
+
+	class MeshAttachment;
+
+	class BoundingBoxAttachment;
+
+	class PathAttachment;
+
+	class PointAttachment;
+
+	class ClippingAttachment;
+
+	class Sequence;
+
+	class SP_API AttachmentLoader : public SpineObject {
+	public:
+	RTTI_DECL
+
+		AttachmentLoader();
+
+		virtual ~AttachmentLoader();
+
+		/// @return May be NULL to not load any attachment.
+		virtual RegionAttachment *newRegionAttachment(Skin &skin, const String &name, const String &path, Sequence *sequence) = 0;
+
+		/// @return May be NULL to not load any attachment.
+		virtual MeshAttachment *newMeshAttachment(Skin &skin, const String &name, const String &path, Sequence *sequence) = 0;
+
+		/// @return May be NULL to not load any attachment.
+		virtual BoundingBoxAttachment *newBoundingBoxAttachment(Skin &skin, const String &name) = 0;
+
+		/// @return May be NULL to not load any attachment
+		virtual PathAttachment *newPathAttachment(Skin &skin, const String &name) = 0;
+
+		virtual PointAttachment *newPointAttachment(Skin &skin, const String &name) = 0;
+
+		virtual ClippingAttachment *newClippingAttachment(Skin &skin, const String &name) = 0;
+
+		virtual void configureAttachment(Attachment *attachment) = 0;
+	};
+}
+
+#endif /* Spine_AttachmentLoader_h */
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/AttachmentTimeline.h b/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/AttachmentTimeline.h
new file mode 100644
index 0000000..a42280f
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/AttachmentTimeline.h
@@ -0,0 +1,82 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#ifndef Spine_AttachmentTimeline_h
+#define Spine_AttachmentTimeline_h
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace spine {
+
+	class Skeleton;
+
+	class Slot;
+
+	class Event;
+
+	class SP_API AttachmentTimeline : public Timeline {
+		friend class SkeletonBinary;
+
+		friend class SkeletonJson;
+
+	RTTI_DECL
+
+	public:
+		explicit AttachmentTimeline(size_t frameCount, int slotIndex);
+
+		virtual ~AttachmentTimeline();
+
+		virtual void
+		apply(Skeleton &skeleton, float lastTime, float time, Vector *pEvents, float alpha, MixBlend blend,
+			  MixDirection direction);
+
+		/// Sets the time and value of the specified keyframe.
+		void setFrame(int frame, float time, const String &attachmentName);
+
+		Vector &getAttachmentNames();
+
+		int getSlotIndex() { return _slotIndex; }
+
+		void setSlotIndex(int inValue) { _slotIndex = inValue; }
+
+	protected:
+		int _slotIndex;
+
+		Vector _attachmentNames;
+
+		void setAttachment(Skeleton &skeleton, Slot &slot, String *attachmentName);
+	};
+}
+
+#endif /* Spine_AttachmentTimeline_h */
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/AttachmentType.h b/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/AttachmentType.h
new file mode 100644
index 0000000..8b1a7ed
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/AttachmentType.h
@@ -0,0 +1,45 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#ifndef Spine_AttachmentType_h
+#define Spine_AttachmentType_h
+
+namespace spine {
+	enum AttachmentType {
+		AttachmentType_Region,
+		AttachmentType_Boundingbox,
+		AttachmentType_Mesh,
+		AttachmentType_Linkedmesh,
+		AttachmentType_Path,
+		AttachmentType_Point,
+		AttachmentType_Clipping
+	};
+}
+
+#endif /* Spine_AttachmentType_h */
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/BlendMode.h b/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/BlendMode.h
new file mode 100644
index 0000000..f34e95b
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/BlendMode.h
@@ -0,0 +1,42 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#ifndef Spine_BlendMode_h
+#define Spine_BlendMode_h
+
+namespace spine {
+	enum BlendMode {
+		BlendMode_Normal = 0,
+		BlendMode_Additive,
+		BlendMode_Multiply,
+		BlendMode_Screen
+	};
+}
+
+#endif /* Spine_BlendMode_h */
diff --git a/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/BlockAllocator.h b/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/BlockAllocator.h
new file mode 100644
index 0000000..749a69c
--- /dev/null
+++ b/Plugins/SpinePlugin/Source/SpinePlugin/Public/spine-cpp/include/spine/BlockAllocator.h
@@ -0,0 +1,114 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#ifndef Spine_BlockAllocator_h
+#define Spine_BlockAllocator_h
+
+#include 
+#include 
+#include