diff --git a/NLog.Windows.Wpf/NLog.Windows.Wpf.csproj b/NLog.Windows.Wpf/NLog.Windows.Wpf.csproj
new file mode 100644
index 0000000..ccab8e1
--- /dev/null
+++ b/NLog.Windows.Wpf/NLog.Windows.Wpf.csproj
@@ -0,0 +1,18 @@
+
+
+
+ enable
+ enable
+ x64
+ net8.0-windows
+ $(SolutionDir)out
+ false
+ 0.15.0
+ true
+
+
+
+
+
+
+
diff --git a/NLog.Windows.Wpf/RichTextBoxRowColoringRule.cs b/NLog.Windows.Wpf/RichTextBoxRowColoringRule.cs
new file mode 100644
index 0000000..9999a41
--- /dev/null
+++ b/NLog.Windows.Wpf/RichTextBoxRowColoringRule.cs
@@ -0,0 +1,92 @@
+//
+// Copyright (c) 2004-2011 Jaroslaw Kowalski
+//
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+//
+// * Redistributions of source code must retain the above copyright notice,
+// this list of conditions and the following disclaimer.
+//
+// * Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+//
+// * Neither the name of Jaroslaw Kowalski nor the names of its
+// contributors may be used to endorse or promote products derived from this
+// software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) 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 THIS SOFTWARE, EVEN IF ADVISED OF
+// THE POSSIBILITY OF SUCH DAMAGE.
+//
+
+using NLog.Conditions;
+using NLog.Config;
+using NLog;
+using System.ComponentModel;
+using System.Windows;
+namespace NLog.Windows.Wpf
+{
+ [NLogConfigurationItem]
+ public class RichTextBoxRowColoringRule
+ {
+ static RichTextBoxRowColoringRule()
+ {
+ Default = new RichTextBoxRowColoringRule();
+ }
+
+ public RichTextBoxRowColoringRule()
+ : this(null, "Empty", "Empty", FontStyles.Normal, FontWeights.Normal)
+ {
+ }
+
+ public RichTextBoxRowColoringRule(string condition, string fontColor, string backColor, FontStyle fontStyle, FontWeight fontWeight)
+ {
+ Condition = condition;
+ FontColor = fontColor;
+ BackgroundColor = backColor;
+ Style = fontStyle;
+ Weight = fontWeight;
+ }
+
+ public RichTextBoxRowColoringRule(string condition, string fontColor, string backColor)
+ {
+ Condition = condition;
+ FontColor = fontColor;
+ BackgroundColor = backColor;
+ Style = FontStyles.Normal;
+ Weight = FontWeights.Normal;
+ }
+
+ public static RichTextBoxRowColoringRule Default { get; private set; }
+
+ [RequiredParameter]
+ public ConditionExpression Condition { get; set; }
+
+ [DefaultValue("Empty")]
+ public string FontColor { get; set; }
+
+ [DefaultValue("Empty")]
+ public string BackgroundColor { get; set; }
+
+ public FontStyle Style { get; set; }
+
+ public FontWeight Weight { get; set; }
+
+ public bool CheckCondition(LogEventInfo logEvent)
+ {
+ return true.Equals(Condition.Evaluate(logEvent));
+ }
+ }
+}
diff --git a/NLog.Windows.Wpf/RichTextBoxTarget.cs b/NLog.Windows.Wpf/RichTextBoxTarget.cs
new file mode 100644
index 0000000..04d905d
--- /dev/null
+++ b/NLog.Windows.Wpf/RichTextBoxTarget.cs
@@ -0,0 +1,256 @@
+using NLog.Config;
+using NLog.Layouts;
+using NLog.Targets;
+using NLog;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Controls;
+using System.Windows.Documents;
+using System.Windows.Media;
+using System.Windows;
+
+namespace NLog.Windows.Wpf
+{
+ // TODO: 完善日志实现
+ [Target("RichTextBox")]
+ public sealed class RichTextBoxTarget : TargetWithLayout
+ {
+ private int lineCount;
+ private int _width = 500;
+ private int _height = 500;
+ private static readonly TypeConverter colorConverter = new ColorConverter();
+
+ static RichTextBoxTarget()
+ {
+ var rules = new List()
+ {
+ new RichTextBoxRowColoringRule("level == LogLevel.Fatal", "White", "Red", FontStyles.Normal, FontWeights.Bold),
+ new RichTextBoxRowColoringRule("level == LogLevel.Error", "Red", "Empty", FontStyles.Italic, FontWeights.Bold),
+ new RichTextBoxRowColoringRule("level == LogLevel.Warn", "Orange", "Empty"),
+ new RichTextBoxRowColoringRule("level == LogLevel.Info", "Black", "Empty"),
+ new RichTextBoxRowColoringRule("level == LogLevel.Debug", "Gray", "Empty"),
+ new RichTextBoxRowColoringRule("level == LogLevel.Trace", "DarkGray", "Empty", FontStyles.Italic, FontWeights.Normal),
+ };
+
+ DefaultRowColoringRules = rules.AsReadOnly();
+ }
+
+ public RichTextBoxTarget()
+ {
+ WordColoringRules = new List();
+ RowColoringRules = new List();
+ ToolWindow = true;
+ }
+
+ private delegate void DelSendTheMessageToRichTextBox(string logMessage, RichTextBoxRowColoringRule rule);
+
+ private delegate void FormCloseDelegate();
+
+ public static ReadOnlyCollection DefaultRowColoringRules { get; private set; }
+
+ public string ControlName { get; set; }
+
+ public string FormName { get; set; }
+
+ [DefaultValue(false)]
+ public bool UseDefaultRowColoringRules { get; set; }
+
+ [ArrayParameter(typeof(RichTextBoxRowColoringRule), "row-coloring")]
+ public IList RowColoringRules { get; private set; }
+
+ [ArrayParameter(typeof(RichTextBoxWordColoringRule), "word-coloring")]
+ public IList WordColoringRules { get; private set; }
+
+ [DefaultValue(true)]
+ public bool ToolWindow { get; set; }
+
+ public bool ShowMinimized { get; set; }
+
+ public int Width
+ {
+ get { return _width; }
+ set { _width = value; }
+ }
+
+ public int Height
+ {
+ get { return _height; }
+ set { _height = value; }
+ }
+
+ public bool AutoScroll { get; set; }
+
+ public int MaxLines { get; set; }
+
+ internal Window TargetForm { get; set; }
+
+ internal RichTextBox TargetRichTextBox { get; set; }
+
+ internal bool CreatedForm { get; set; }
+
+ protected override void InitializeTarget()
+ {
+ TargetRichTextBox = Application.Current.MainWindow.FindName(ControlName) as RichTextBox;
+
+ if (TargetRichTextBox != null) return;
+ //this.TargetForm = FormHelper.CreateForm(this.FormName, this.Width, this.Height, false, this.ShowMinimized, this.ToolWindow);
+ //this.CreatedForm = true;
+
+ var openFormByName = Application.Current.Windows.Cast().FirstOrDefault(x => x.GetType().Name == FormName);
+ if (openFormByName != null)
+ {
+ TargetForm = openFormByName;
+ if (string.IsNullOrEmpty(ControlName))
+ {
+ // throw new NLogConfigurationException("Rich text box control name must be specified for " + GetType().Name + ".");
+ Trace.WriteLine("Rich text box control name must be specified for " + GetType().Name + ".");
+ }
+
+ CreatedForm = false;
+ TargetRichTextBox = TargetForm.FindName(ControlName) as RichTextBox;
+
+ if (TargetRichTextBox == null)
+ {
+ // throw new NLogConfigurationException("Rich text box control '" + ControlName + "' cannot be found on form '" + FormName + "'.");
+ Trace.WriteLine("Rich text box control '" + ControlName + "' cannot be found on form '" + FormName + "'.");
+ }
+ }
+
+ if (TargetRichTextBox == null)
+ {
+ TargetForm = new Window
+ {
+ Name = FormName,
+ Width = Width,
+ Height = Height,
+ WindowStyle = ToolWindow ? WindowStyle.ToolWindow : WindowStyle.None,
+ WindowState = ShowMinimized ? WindowState.Minimized : WindowState.Normal,
+ Title = "NLog Messages"
+ };
+ TargetForm.Show();
+
+ TargetRichTextBox = new RichTextBox { Name = ControlName };
+ var style = new Style(typeof(Paragraph));
+ TargetRichTextBox.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
+ style.Setters.Add(new Setter(Block.MarginProperty, new Thickness(0, 0, 0, 0)));
+ TargetRichTextBox.Resources.Add(typeof(Paragraph), style);
+ TargetForm.Content = TargetRichTextBox;
+
+ CreatedForm = true;
+ }
+ }
+
+ protected override void CloseTarget()
+ {
+ if (CreatedForm)
+ {
+ try
+ {
+ TargetForm.Dispatcher.Invoke(() =>
+ {
+ TargetForm.Close();
+ TargetForm = null;
+ });
+ }
+ catch
+ {
+ }
+
+
+
+ }
+ }
+
+ protected override void Write(LogEventInfo logEvent)
+ {
+ RichTextBoxRowColoringRule matchingRule = RowColoringRules.FirstOrDefault(rr => rr.CheckCondition(logEvent));
+
+ if (UseDefaultRowColoringRules && matchingRule == null)
+ {
+ foreach (var rr in DefaultRowColoringRules.Where(rr => rr.CheckCondition(logEvent)))
+ {
+ matchingRule = rr;
+ break;
+ }
+ }
+
+ if (matchingRule == null)
+ {
+ matchingRule = RichTextBoxRowColoringRule.Default;
+ }
+
+ var logMessage = Layout.Render(logEvent);
+
+ if (Application.Current == null) return;
+
+ try
+ {
+ if (Application.Current.Dispatcher.CheckAccess() == false)
+ {
+ Application.Current.Dispatcher.Invoke(() => SendTheMessageToRichTextBox(logMessage, matchingRule));
+ }
+ else
+ {
+ SendTheMessageToRichTextBox(logMessage, matchingRule);
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine(ex);
+ }
+
+ }
+
+
+ private static Color GetColorFromString(string color, Brush defaultColor)
+ {
+
+ if (color == "Empty")
+ {
+ return defaultColor is SolidColorBrush solidBrush ? solidBrush.Color : Colors.White;
+ }
+
+ return (Color)colorConverter.ConvertFromString(color);
+ }
+
+
+ private void SendTheMessageToRichTextBox(string logMessage, RichTextBoxRowColoringRule rule)
+ {
+ RichTextBox rtbx = TargetRichTextBox;
+
+ var tr = new TextRange(rtbx.Document.ContentEnd, rtbx.Document.ContentEnd);
+ tr.Text = logMessage + "\n";
+ tr.ApplyPropertyValue(TextElement.ForegroundProperty,
+ new SolidColorBrush(GetColorFromString(rule.FontColor, (Brush)tr.GetPropertyValue(TextElement.ForegroundProperty)))
+ );
+ tr.ApplyPropertyValue(TextElement.BackgroundProperty,
+ new SolidColorBrush(GetColorFromString(rule.BackgroundColor, (Brush)tr.GetPropertyValue(TextElement.BackgroundProperty)))
+ );
+ tr.ApplyPropertyValue(TextElement.FontStyleProperty, rule.Style);
+ tr.ApplyPropertyValue(TextElement.FontWeightProperty, rule.Weight);
+
+
+ if (MaxLines > 0)
+ {
+ lineCount++;
+ if (lineCount > MaxLines)
+ {
+ tr = new TextRange(rtbx.Document.ContentStart, rtbx.Document.ContentEnd);
+ tr.Text.Remove(0, tr.Text.IndexOf('\n'));
+ lineCount--;
+ }
+ }
+
+ if (AutoScroll)
+ {
+ rtbx.ScrollToEnd();
+ }
+ }
+ }
+}
diff --git a/NLog.Windows.Wpf/RichTextBoxWordColoringRule.cs b/NLog.Windows.Wpf/RichTextBoxWordColoringRule.cs
new file mode 100644
index 0000000..b2e79c5
--- /dev/null
+++ b/NLog.Windows.Wpf/RichTextBoxWordColoringRule.cs
@@ -0,0 +1,119 @@
+//
+// Copyright (c) 2004-2011 Jaroslaw Kowalski
+//
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+//
+// * Redistributions of source code must retain the above copyright notice,
+// this list of conditions and the following disclaimer.
+//
+// * Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+//
+// * Neither the name of Jaroslaw Kowalski nor the names of its
+// contributors may be used to endorse or promote products derived from this
+// software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) 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 THIS SOFTWARE, EVEN IF ADVISED OF
+// THE POSSIBILITY OF SUCH DAMAGE.
+//
+
+using System.ComponentModel;
+using System.Text.RegularExpressions;
+using System.Windows;
+using NLog.Config;
+
+namespace NLog.Windows.Wpf
+{
+ [NLogConfigurationItem]
+ public class RichTextBoxWordColoringRule
+ {
+ private Regex compiledRegex;
+
+ public RichTextBoxWordColoringRule()
+ {
+ FontColor = "Empty";
+ BackgroundColor = "Empty";
+ }
+
+ public RichTextBoxWordColoringRule(string text, string fontColor, string backgroundColor)
+ {
+ Text = text;
+ FontColor = fontColor;
+ BackgroundColor = backgroundColor;
+ Style = FontStyles.Normal;
+ Weight = FontWeights.Normal;
+ }
+
+ public RichTextBoxWordColoringRule(string text, string textColor, string backgroundColor, FontStyle fontStyle, FontWeight fontWeight)
+ {
+ Text = text;
+ FontColor = textColor;
+ BackgroundColor = backgroundColor;
+ Style = fontStyle;
+ Weight = fontWeight;
+ }
+
+ public string Regex { get; set; }
+
+ public string Text { get; set; }
+
+ [DefaultValue(false)]
+ public bool WholeWords { get; set; }
+
+ [DefaultValue(false)]
+ public bool IgnoreCase { get; set; }
+
+ public FontStyle Style { get; set; }
+
+ public FontWeight Weight { get; set; }
+
+ public Regex CompiledRegex
+ {
+ get
+ {
+ if (compiledRegex == null)
+ {
+ string regexpression = Regex;
+ if (regexpression == null && Text != null)
+ {
+ regexpression = System.Text.RegularExpressions.Regex.Escape(Text);
+ if (WholeWords)
+ {
+ regexpression = "\b" + regexpression + "\b";
+ }
+ }
+
+ RegexOptions regexOptions = RegexOptions.Compiled;
+ if (IgnoreCase)
+ {
+ regexOptions |= RegexOptions.IgnoreCase;
+ }
+
+ compiledRegex = new Regex(regexpression, regexOptions);
+ }
+
+ return compiledRegex;
+ }
+ }
+
+ [DefaultValue("Empty")]
+ public string FontColor { get; set; }
+
+ [DefaultValue("Empty")]
+ public string BackgroundColor { get; set; }
+ }
+}
diff --git a/SFMLRenderer/ISFMLRenderer.cs b/SFMLRenderer/ISFMLRenderer.cs
new file mode 100644
index 0000000..0a8df47
--- /dev/null
+++ b/SFMLRenderer/ISFMLRenderer.cs
@@ -0,0 +1,134 @@
+using SFML.Graphics;
+using SFML.System;
+using SFML.Window;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SFMLRenderer
+{
+ public interface ISFMLRenderer
+ {
+ ///
+ /// 发生在资源首次创建完成后
+ ///
+ public event EventHandler? RendererCreated;
+
+ ///
+ /// 发生在资源即将不可用之前
+ ///
+ public event EventHandler? RendererDisposing;
+
+ public event EventHandler? CanvasMouseMove;
+ public event EventHandler? CanvasMouseButtonPressed;
+ public event EventHandler? CanvasMouseButtonReleased;
+ public event EventHandler? CanvasMouseWheelScrolled;
+
+ ///
+ /// 分辨率, 影响画面的相对比例
+ ///
+ public Vector2u Resolution { get; set; }
+
+ ///
+ /// 快捷设置视区中心点
+ ///
+ public Vector2f Center { get; set; }
+
+ ///
+ /// 快捷设置视区缩放
+ ///
+ public float Zoom { get; set; }
+
+ ///
+ /// 快捷设置视区旋转
+ ///
+ public float Rotation { get; set; }
+
+ ///
+ /// 快捷设置视区水平翻转
+ ///
+ public bool FlipX { get; set; }
+
+ ///
+ /// 快捷设置视区垂直翻转
+ ///
+ public bool FlipY { get; set; }
+
+ ///
+ /// 最大帧率, 影响 Draw 的最大调用频率,
+ ///
+ public uint MaxFps { get; set; }
+
+ ///
+ /// 垂直同步
+ ///
+ public bool VerticalSync { get; set; }
+
+ ///
+ ///
+ ///
+ public void SetActive(bool active);
+
+ ///
+ ///
+ ///
+ public View GetView();
+
+ ///
+ ///
+ ///
+ public void SetView(View view);
+
+ ///
+ ///
+ ///
+ public Vector2f MapPixelToCoords(Vector2i point);
+
+ ///
+ ///
+ ///
+ public Vector2i MapCoordsToPixel(Vector2f point);
+
+ ///
+ ///
+ ///
+ public void Clear();
+
+ ///
+ ///
+ ///
+ public void Clear(Color color);
+
+ ///
+ ///
+ ///
+ public void Draw(Drawable drawable);
+
+ ///
+ ///
+ ///
+ public void Draw(Drawable drawable, RenderStates states);
+
+ ///
+ ///
+ ///
+ public void Draw(Vertex[] vertices, PrimitiveType type);
+
+ ///
+ ///
+ ///
+ public void Draw(Vertex[] vertices, PrimitiveType type, RenderStates states);
+
+ ///
+ ///
+ ///
+ public void Draw(Vertex[] vertices, uint start, uint count, PrimitiveType type);
+
+ ///
+ ///
+ ///
+ public void Display();
+ }
+}
diff --git a/SFMLRenderer/README.md b/SFMLRenderer/README.md
new file mode 100644
index 0000000..842c0f2
--- /dev/null
+++ b/SFMLRenderer/README.md
@@ -0,0 +1,3 @@
+# SFMLRenderer
+
+这个库封装了一个用于 WPF 的 SFML 渲染控件.
diff --git a/SFMLRenderer/SFMLHwndHost.cs b/SFMLRenderer/SFMLHwndHost.cs
new file mode 100644
index 0000000..7068b1e
--- /dev/null
+++ b/SFMLRenderer/SFMLHwndHost.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Input;
+using System.Windows.Interop;
+using System.Windows.Media;
+
+namespace SFMLRenderer
+{
+ public class SFMLHwndHost : HwndHost
+ {
+ private HwndSource? _hwndSource;
+ private SFML.Graphics.RenderWindow? _renderWindow;
+
+ public SFML.Graphics.RenderWindow? RenderWindow => _renderWindow;
+
+ public event EventHandler? RenderWindowBuilded;
+ public event EventHandler? RenderWindowDestroying;
+
+ protected override HandleRef BuildWindowCore(HandleRef hwndParent)
+ {
+ var ps = new HwndSourceParameters(GetType().Name, (int)Width, (int)Height)
+ {
+ ParentWindow = hwndParent.Handle,
+ WindowStyle = 0x40000000 | 0x10000000, // WS_CHILD | WS_VISIBLE
+ HwndSourceHook = HwndMessageHook
+ };
+ _hwndSource = new HwndSource(ps);
+ _renderWindow = new(_hwndSource.Handle);
+ _renderWindow.SetActive(false);
+
+ RenderWindowBuilded?.Invoke(this, EventArgs.Empty);
+ return new HandleRef(this, _hwndSource.Handle);
+ }
+
+ protected override void DestroyWindowCore(HandleRef hwnd)
+ {
+ RenderWindowDestroying?.Invoke(this, EventArgs.Empty);
+
+ _renderWindow?.Close();
+ var rw = _renderWindow;
+ _renderWindow = null;
+ rw?.Dispose();
+ var hs = _hwndSource;
+ _hwndSource = null;
+ hs?.Dispose();
+ }
+
+ private nint HwndMessageHook(nint hwnd, int msg, nint wParam, nint lParam, ref bool handled)
+ {
+ _renderWindow?.DispatchEvents();
+ return nint.Zero;
+ }
+ }
+}
diff --git a/SFMLRenderer/SFMLRenderPanel.xaml b/SFMLRenderer/SFMLRenderPanel.xaml
new file mode 100644
index 0000000..205d253
--- /dev/null
+++ b/SFMLRenderer/SFMLRenderPanel.xaml
@@ -0,0 +1,14 @@
+
+
+
diff --git a/SFMLRenderer/SFMLRenderPanel.xaml.cs b/SFMLRenderer/SFMLRenderPanel.xaml.cs
new file mode 100644
index 0000000..1edc5e1
--- /dev/null
+++ b/SFMLRenderer/SFMLRenderPanel.xaml.cs
@@ -0,0 +1,253 @@
+using SFML.Graphics;
+using SFML.System;
+using SFML.Window;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SFMLRenderer
+{
+ ///
+ /// SFMLRenderPanel.xaml 的交互逻辑
+ ///
+ public partial class SFMLRenderPanel : System.Windows.Controls.UserControl, ISFMLRenderer
+ {
+ private RenderWindow? RenderWindow => _hwndHost.RenderWindow;
+
+ public SFMLRenderPanel()
+ {
+ InitializeComponent();
+ }
+
+ public event EventHandler? RendererCreated
+ {
+ add => _hwndHost.RenderWindowBuilded += value;
+ remove => _hwndHost.RenderWindowBuilded -= value;
+ }
+
+ public event EventHandler? RendererDisposing
+ {
+ add => _hwndHost.RenderWindowDestroying += value;
+ remove => _hwndHost.RenderWindowDestroying -= value;
+ }
+
+ public event EventHandler? CanvasMouseMove
+ {
+ add { if (RenderWindow is RenderWindow w) w.MouseMoved += value; }
+ remove { if (RenderWindow is RenderWindow w) w.MouseMoved -= value; }
+ }
+
+ public event EventHandler? CanvasMouseButtonPressed
+ {
+ add { if (RenderWindow is RenderWindow w) w.MouseButtonPressed += value; }
+ remove { if (RenderWindow is RenderWindow w) w.MouseButtonPressed -= value; }
+ }
+
+ public event EventHandler? CanvasMouseButtonReleased
+ {
+ add { if (RenderWindow is RenderWindow w) w.MouseButtonReleased += value; }
+ remove { if (RenderWindow is RenderWindow w) w.MouseButtonReleased -= value; }
+ }
+
+ public event EventHandler? CanvasMouseWheelScrolled
+ {
+ add { if (RenderWindow is RenderWindow w) w.MouseWheelScrolled += value; }
+ remove { if (RenderWindow is RenderWindow w) w.MouseWheelScrolled -= value; }
+ }
+
+ public Vector2u Resolution
+ {
+ get => _resolution;
+ set
+ {
+ if (RenderWindow is null) return;
+ if (value == _resolution) return;
+ if (value.X <= 0 || value.Y <= 0) return;
+
+ var zoom = Zoom;
+
+ float parentW = (float)ActualWidth;
+ float parentH = (float)ActualHeight;
+ float renderW = value.X;
+ float renderH = value.Y;
+ float scale = Math.Min(parentW / renderW, parentH / renderH); // 两方向取较小值, 保证 parent 覆盖 render
+ renderW *= scale;
+ renderH *= scale;
+
+ _hwndHost.Width = renderW;
+ _hwndHost.Height = renderH;
+
+ _resolution = value;
+
+ // 设置完 resolution 后还原缩放比例
+ Zoom = zoom;
+ }
+ }
+ private Vector2u _resolution = new(100, 100);
+
+ public Vector2f Center
+ {
+ get
+ {
+ if (RenderWindow is null) return default;
+ using var view = RenderWindow.GetView();
+ return view.Center;
+ }
+ set
+ {
+ if (RenderWindow is null) return;
+ using var view = RenderWindow.GetView();
+ view.Center = value;
+ RenderWindow.SetView(view);
+ }
+ }
+
+ public float Zoom
+ {
+ get
+ {
+ if (RenderWindow is null) return 1;
+ using var view = RenderWindow.GetView();
+ return Math.Abs(_resolution.X / view.Size.X); // XXX: 仅使用宽度进行缩放计算
+ }
+ set
+ {
+ value = Math.Abs(value);
+ if (RenderWindow is null || value <= 0) return;
+ using var view = RenderWindow.GetView();
+ var signX = Math.Sign(view.Size.X);
+ var signY = Math.Sign(view.Size.Y);
+ view.Size = new(_resolution.X / value * signX, _resolution.Y / value * signY);
+ RenderWindow.SetView(view);
+ }
+ }
+
+ public float Rotation
+ {
+ get
+ {
+ if (RenderWindow is null) return default;
+ using var view = RenderWindow.GetView();
+ return view.Rotation;
+ }
+ set
+ {
+ if (RenderWindow is null) return;
+ using var view = RenderWindow.GetView();
+ view.Rotation = value;
+ RenderWindow.SetView(view);
+ }
+ }
+
+ public bool FlipX
+ {
+ get
+ {
+ if (RenderWindow is null) return false;
+ using var view = RenderWindow.GetView();
+ return view.Size.X < 0;
+ }
+ set
+ {
+ if (RenderWindow is null) return;
+
+ using var view = RenderWindow.GetView();
+ var size = view.Size;
+ if (size.X > 0 && value || size.X < 0 && !value)
+ size.X *= -1;
+ view.Size = size;
+ RenderWindow.SetView(view);
+ }
+ }
+
+ public bool FlipY
+ {
+ get
+ {
+ if (RenderWindow is null) return false;
+ using var view = RenderWindow.GetView();
+ return view.Size.Y < 0;
+ }
+ set
+ {
+ if (RenderWindow is null) return;
+
+ using var view = RenderWindow.GetView();
+ var size = view.Size;
+ if (size.Y > 0 && value || size.Y < 0 && !value)
+ size.Y *= -1;
+ view.Size = size;
+ RenderWindow.SetView(view);
+ }
+ }
+
+ public uint MaxFps
+ {
+ get => _maxFps;
+ set
+ {
+ if (RenderWindow is null) return;
+ RenderWindow.SetFramerateLimit(value);
+ _maxFps = value;
+ }
+ }
+ private uint _maxFps = 0;
+
+ public bool VerticalSync
+ {
+ get => _verticalSync;
+ set
+ {
+ if (RenderWindow is null) return;
+ RenderWindow.SetVerticalSyncEnabled(value);
+ _verticalSync = value;
+ }
+ }
+ private bool _verticalSync = false;
+
+ public void Clear() => RenderWindow?.Clear();
+
+ public void Clear(Color color) => RenderWindow?.Clear(color);
+
+ public void Display() => RenderWindow?.Display();
+
+ public void Draw(Drawable drawable) => RenderWindow?.Draw(drawable);
+
+ public void Draw(Drawable drawable, RenderStates states) => RenderWindow?.Draw(drawable, states);
+
+ public void Draw(Vertex[] vertices, PrimitiveType type) => RenderWindow?.Draw(vertices, type);
+
+ public void Draw(Vertex[] vertices, PrimitiveType type, RenderStates states) => RenderWindow?.Draw(vertices, type, states);
+
+ public void Draw(Vertex[] vertices, uint start, uint count, PrimitiveType type) => RenderWindow?.Draw(vertices, start, count, type);
+
+ public View GetView() => RenderWindow?.GetView() ?? new();
+
+ public Vector2i MapCoordsToPixel(Vector2f point) => RenderWindow?.MapCoordsToPixel(point) ?? default;
+
+ public Vector2f MapPixelToCoords(Vector2i point) => RenderWindow?.MapPixelToCoords(point) ?? default;
+
+ public void SetActive(bool active) => RenderWindow?.SetActive(active);
+
+ public void SetView(View view) => RenderWindow?.SetView(view);
+
+ protected override void OnRenderSizeChanged(System.Windows.SizeChangedInfo sizeInfo)
+ {
+ base.OnRenderSizeChanged(sizeInfo);
+
+ if (RenderWindow is null) return;
+ float parentW = (float)sizeInfo.NewSize.Width;
+ float parentH = (float)sizeInfo.NewSize.Height;
+ float renderW = (float)_hwndHost.ActualWidth;
+ float renderH = (float)_hwndHost.ActualHeight;
+ float scale = Math.Min(parentW / renderW, parentH / renderH); // 两方向取较小值, 保证 parent 覆盖 render
+ renderW *= scale;
+ renderH *= scale;
+
+ _hwndHost.Width = renderW;
+ _hwndHost.Height = renderH;
+ }
+ }
+}
diff --git a/SFMLRenderer/SFMLRenderer.csproj b/SFMLRenderer/SFMLRenderer.csproj
new file mode 100644
index 0000000..4a770d5
--- /dev/null
+++ b/SFMLRenderer/SFMLRenderer.csproj
@@ -0,0 +1,22 @@
+
+
+
+ enable
+ enable
+ x64
+ net8.0-windows
+ $(SolutionDir)out
+ false
+ 0.15.0
+ true
+
+
+
+ $(NoWarn);NETSDK1206
+
+
+
+
+
+
+
diff --git a/Spine/Exporters/BaseExporter.cs b/Spine/Exporters/BaseExporter.cs
new file mode 100644
index 0000000..858cf76
--- /dev/null
+++ b/Spine/Exporters/BaseExporter.cs
@@ -0,0 +1,198 @@
+using NLog;
+using SFML.Graphics;
+using SFML.System;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Spine.Exporters
+{
+ ///
+ /// 导出类基类, 提供基本的帧渲染功能
+ ///
+ public abstract class BaseExporter : IDisposable
+ {
+ ///
+ /// 日志器
+ ///
+ protected static readonly Logger _logger = LogManager.GetCurrentClassLogger();
+
+ ///
+ /// 用于渲染的画布
+ ///
+ protected RenderTexture _renderTexture;
+
+ ///
+ /// 初始化导出器
+ ///
+ /// 画布宽像素值
+ /// 画布高像素值
+ public BaseExporter(uint width , uint height)
+ {
+ if (width <= 0 || height <= 0)
+ throw new ArgumentException($"Invalid resolution: {width}, {height}");
+ _renderTexture = new(width, height);
+ _renderTexture.SetActive(false);
+ }
+
+ ///
+ /// 初始化导出器
+ ///
+ public BaseExporter(Vector2u resolution)
+ {
+ if (resolution.X <= 0 || resolution.Y <= 0)
+ throw new ArgumentException($"Invalid resolution: {resolution}");
+ _renderTexture = new(resolution.X, resolution.Y);
+ _renderTexture.SetActive(false);
+ }
+
+ ///
+ /// 可选的进度回调函数
+ ///
+ /// - total: 任务总量
+ /// - done: 已完成量
+ /// - progressText: 需要设置的进度提示文本
+ ///
+ ///
+ public Action? ProgressReporter { get => _progressReporter; set => _progressReporter = value; }
+ protected Action? _progressReporter;
+
+ ///
+ /// 背景颜色
+ ///
+ public Color BackgroundColor
+ {
+ get => _backgroundColor;
+ set
+ {
+ _backgroundColor = value;
+ var bcPma = value;
+ var a = bcPma.A / 255f;
+ bcPma.R = (byte)(bcPma.R * a);
+ bcPma.G = (byte)(bcPma.G * a);
+ bcPma.B = (byte)(bcPma.B * a);
+ _backgroundColorPma = bcPma;
+ }
+ }
+ protected Color _backgroundColor = Color.Transparent;
+
+ ///
+ /// 预乘后的背景颜色
+ ///
+ protected Color _backgroundColorPma = Color.Transparent;
+
+ ///
+ /// 画面分辨率
+ ///
+ ///
+ public Vector2u Resolution
+ {
+ get => _renderTexture.Size;
+ set
+ {
+ if (value.X <= 0 || value.Y <= 0)
+ {
+ _logger.Warn("Omit invalid exporter resolution: {0}", value);
+ return;
+ }
+ if (_renderTexture.Size != value)
+ {
+ using var old = _renderTexture;
+ using var view = old.GetView();
+ var renderTexture = new RenderTexture(value.X, value.Y);
+ renderTexture.SetActive(false);
+ renderTexture.SetView(view);
+ _renderTexture = renderTexture;
+ }
+ }
+ }
+
+ ///
+ ///
+ ///
+ public FloatRect Viewport
+ {
+ get { using var view = _renderTexture.GetView(); return view.Viewport; }
+ set { using var view = _renderTexture.GetView(); view.Viewport = value; _renderTexture.SetView(view); }
+ }
+
+ ///
+ ///
+ ///
+ public Vector2f Center
+ {
+ get { using var view = _renderTexture.GetView(); return view.Center; }
+ set { using var view = _renderTexture.GetView(); view.Center = value; _renderTexture.SetView(view); }
+ }
+
+ ///
+ ///
+ ///
+ public Vector2f Size
+ {
+ get { using var view = _renderTexture.GetView(); return view.Size; }
+ set { using var view = _renderTexture.GetView(); view.Size = value; _renderTexture.SetView(view); }
+ }
+
+ ///
+ ///
+ ///
+ public float Rotation
+ {
+ get { using var view = _renderTexture.GetView(); return view.Rotation; }
+ set { using var view = _renderTexture.GetView(); view.Rotation = value; _renderTexture.SetView(view); }
+ }
+
+ ///
+ /// 获取的一帧, 结果是预乘的
+ ///
+ protected virtual SFMLImageVideoFrame GetFrame(SpineObject[] spines)
+ {
+ _renderTexture.SetActive(true);
+ _renderTexture.Clear(_backgroundColorPma);
+ foreach (var sp in spines.Reverse()) _renderTexture.Draw(sp);
+ _renderTexture.Display();
+ _renderTexture.SetActive(false);
+ return new(_renderTexture.Texture.CopyToImage());
+ }
+
+ ///
+ /// 导出给定的模型, 从前往后对应从上往下的渲染顺序
+ ///
+ /// 输出路径, 一般而言都是文件路径, 少数情况指定的是文件夹
+ /// 要导出的模型, 从前往后对应从上往下的渲染顺序
+ public abstract void Export(string output, params SpineObject[] spines);
+
+ #region IDisposable 接口实现
+
+ private bool _disposed = false;
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed) return;
+ if (disposing)
+ {
+ _renderTexture.Dispose();
+ }
+ _disposed = true;
+ }
+
+ ~BaseExporter()
+ {
+ Dispose(false);
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ if (_disposed)
+ {
+ GC.SuppressFinalize(this);
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/Spine/Exporters/CustomFFmpegExporter.cs b/Spine/Exporters/CustomFFmpegExporter.cs
new file mode 100644
index 0000000..174a70b
--- /dev/null
+++ b/Spine/Exporters/CustomFFmpegExporter.cs
@@ -0,0 +1,82 @@
+using FFMpegCore;
+using FFMpegCore.Pipes;
+using SFML.System;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Spine.Exporters
+{
+ ///
+ /// 自定义参数的 FFmpeg 导出类
+ ///
+ public class CustomFFmpegExporter : VideoExporter
+ {
+ public CustomFFmpegExporter(uint width = 100, uint height = 100) : base(width, height) { }
+ public CustomFFmpegExporter(Vector2u resolution) : base(resolution) { }
+
+ ///
+ /// -f
+ ///
+ public string? Format { get => _format; set => _format = value; }
+ private string? _format;
+
+ ///
+ /// -c:v
+ ///
+ public string? Codec { get => _codec; set => _codec = value; }
+ private string? _codec;
+
+ ///
+ /// -pix_fmt
+ ///
+ public string? PixelFormat { get => _pixelFormat; set => _pixelFormat = value; }
+ private string? _pixelFormat;
+
+ ///
+ /// -b:v
+ ///
+ public string? Bitrate { get => _bitrate; set => _bitrate = value; }
+ private string? _bitrate;
+
+ ///
+ /// -vf
+ ///
+ public string? Filter { get => _filter; set => _filter = value; }
+ private string? _filter;
+
+ ///
+ /// 其他自定义参数
+ ///
+ public string? CustomArgs { get => _customArgs; set => _customArgs = value; }
+ private string? _customArgs;
+
+ private void SetOutputOptions(FFMpegArgumentOptions options)
+ {
+ if (!string.IsNullOrEmpty(_format)) options.ForceFormat(_format);
+ if (!string.IsNullOrEmpty(_codec)) options.WithVideoCodec(_codec);
+ if (!string.IsNullOrEmpty(_pixelFormat)) options.ForcePixelFormat(_pixelFormat);
+ if (!string.IsNullOrEmpty(_bitrate)) options.WithCustomArgument($"-b:v {_bitrate}");
+ if (!string.IsNullOrEmpty(_filter)) options.WithCustomArgument($"-vf unpremultiply=inplace=1, {_customArgs}");
+ else options.WithCustomArgument("-vf unpremultiply=inplace=1");
+ }
+
+ public override void Export(string output, CancellationToken ct, params SpineObject[] spines)
+ {
+ var videoFramesSource = new RawVideoPipeSource(GetFrames(spines, output, ct)) { FrameRate = _fps };
+ try
+ {
+ var ffmpegArgs = FFMpegArguments.FromPipeInput(videoFramesSource).OutputToFile(output, true, SetOutputOptions);
+ _logger.Info("FFmpeg arguments: {0}", ffmpegArgs.Arguments);
+ ffmpegArgs.ProcessSynchronously();
+ }
+ catch (Exception ex)
+ {
+ _logger.Trace(ex.ToString());
+ _logger.Error("Failed to export {0} {1}, {2}", _format, output, ex.Message);
+ }
+ }
+ }
+}
diff --git a/Spine/Exporters/FFmpegVideoExporter.cs b/Spine/Exporters/FFmpegVideoExporter.cs
new file mode 100644
index 0000000..5bc2346
--- /dev/null
+++ b/Spine/Exporters/FFmpegVideoExporter.cs
@@ -0,0 +1,146 @@
+using FFMpegCore;
+using FFMpegCore.Enums;
+using FFMpegCore.Pipes;
+using NLog;
+using SFML.Graphics;
+using SFML.System;
+using SkiaSharp;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Spine.Exporters
+{
+ ///
+ /// 基于 FFmpeg 命令行的导出类, 可以导出动图及视频格式
+ ///
+ public class FFmpegVideoExporter : VideoExporter
+ {
+ public FFmpegVideoExporter(uint width = 100, uint height = 100) : base(width, height) { }
+ public FFmpegVideoExporter(Vector2u resolution) : base(resolution) { }
+
+ ///
+ /// FFmpeg 导出格式
+ ///
+ public enum VideoFormat
+ {
+ Gif,
+ Webp,
+ Mp4,
+ Webm,
+ Mkv,
+ }
+
+ ///
+ /// 视频格式
+ ///
+ public VideoFormat Format { get => _format; set => _format = value; }
+ private VideoFormat _format = VideoFormat.Mp4;
+
+ ///
+ /// 动图是否循环
+ ///
+ public bool Loop { get => _loop; set => _loop = value; }
+ private bool _loop = true;
+
+ ///
+ /// 质量
+ ///
+ public int Quality { get => _quality; set => _quality = Math.Clamp(value, 0, 100); }
+ private int _quality = 75;
+
+ ///
+ /// CRF
+ ///
+ public int Crf { get => _crf; set => _crf = Math.Clamp(value, 0, 63); }
+ private int _crf = 23;
+
+ ///
+ /// 获取的一帧, 结果是预乘的
+ ///
+ protected override SFMLImageVideoFrame GetFrame(SpineObject[] spines)
+ {
+ // XXX: 不知道为什么用 FFmpeg 必须临时创建 RenderTexture, 否则无法正常渲染
+ using var tex = new RenderTexture(_renderTexture.Size.X, _renderTexture.Size.Y);
+ using var view = _renderTexture.GetView();
+ tex.SetView(view);
+ tex.Clear(_backgroundColorPma);
+ foreach (var sp in spines.Reverse()) tex.Draw(sp);
+ tex.Display();
+ return new(tex.Texture.CopyToImage());
+ }
+
+ public override void Export(string output, CancellationToken ct, params SpineObject[] spines)
+ {
+ var videoFramesSource = new RawVideoPipeSource(GetFrames(spines, output, ct)) { FrameRate = _fps };
+ Action setOutputOptions = _format switch
+ {
+ VideoFormat.Gif => SetGifOptions,
+ VideoFormat.Webp => SetWebpOptions,
+ VideoFormat.Mp4 => SetMp4Options,
+ VideoFormat.Webm => SetWebmOptions,
+ VideoFormat.Mkv => SetMkvOptions,
+ _ => throw new NotImplementedException(),
+ };
+
+ try
+ {
+ var ffmpegArgs = FFMpegArguments.FromPipeInput(videoFramesSource).OutputToFile(output, true, setOutputOptions);
+
+ _logger.Info("FFmpeg arguments: {0}", ffmpegArgs.Arguments);
+ ffmpegArgs.ProcessSynchronously();
+ }
+ catch (Exception ex)
+ {
+ _logger.Trace(ex.ToString());
+ _logger.Error("Failed to export {0} {1}, {2}", _format, output, ex.Message);
+ }
+ }
+
+ private void SetGifOptions(FFMpegArgumentOptions options)
+ {
+ // Gif 固定使用 256 调色板和 128 透明度阈值
+ var v = "split [s0][s1]";
+ var s0 = "[s0] palettegen=reserve_transparent=1:max_colors=256 [p]";
+ var s1 = "[s1][p] paletteuse=dither=bayer:alpha_threshold=128";
+ var customArgs = $"-vf \"unpremultiply=inplace=1, {v};{s0};{s1}\" -loop {(_loop ? 0 : -1)}";
+ options.ForceFormat("gif")
+ .WithCustomArgument(customArgs);
+ }
+
+ private void SetWebpOptions(FFMpegArgumentOptions options)
+ {
+ var customArgs = $"-vf unpremultiply=inplace=1 -quality {_quality} -loop {(_loop ? 0 : 1)}";
+ options.ForceFormat("webp").WithVideoCodec("libwebp_anim").ForcePixelFormat("yuva420p")
+ .WithCustomArgument(customArgs);
+ }
+
+ private void SetMp4Options(FFMpegArgumentOptions options)
+ {
+ var customArgs = "-vf unpremultiply=inplace=1";
+ options.ForceFormat("mp4").WithVideoCodec("libx264").ForcePixelFormat("yuv444p")
+ .WithFastStart()
+ .WithConstantRateFactor(_crf)
+ .WithCustomArgument(customArgs);
+ }
+
+ private void SetWebmOptions(FFMpegArgumentOptions options)
+ {
+ var customArgs = "-vf unpremultiply=inplace=1";
+ options.ForceFormat("webm").WithVideoCodec("libvpx-vp9").ForcePixelFormat("yuva420p")
+ .WithConstantRateFactor(_crf)
+ .WithCustomArgument(customArgs);
+ }
+
+ private void SetMkvOptions(FFMpegArgumentOptions options)
+ {
+ var customArgs = "-vf unpremultiply=inplace=1";
+ options.ForceFormat("matroska").WithVideoCodec("libx265").ForcePixelFormat("yuv444p")
+ .WithConstantRateFactor(_crf)
+ .WithCustomArgument(customArgs);
+ }
+
+ }
+}
diff --git a/Spine/Exporters/FrameExporter.cs b/Spine/Exporters/FrameExporter.cs
new file mode 100644
index 0000000..f73f202
--- /dev/null
+++ b/Spine/Exporters/FrameExporter.cs
@@ -0,0 +1,37 @@
+using SFML.System;
+using SkiaSharp;
+using System;
+using System.Collections.Frozen;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Spine.Exporters
+{
+ ///
+ /// 单帧画面导出类
+ ///
+ public class FrameExporter : BaseExporter
+ {
+ public FrameExporter(uint width = 100, uint height = 100) : base(width, height) { }
+ public FrameExporter(Vector2u resolution) : base(resolution) { }
+
+ public SKEncodedImageFormat Format { get => _format; set => _format = value; }
+ protected SKEncodedImageFormat _format = SKEncodedImageFormat.Png;
+
+ public int Quality { get => _quality; set => _quality = Math.Clamp(value, 0, 100); }
+ protected int _quality = 80;
+
+ public override void Export(string output, params SpineObject[] spines)
+ {
+ using var frame = GetFrame(spines);
+ var info = new SKImageInfo(frame.Width, frame.Height, SKColorType.Rgba8888, SKAlphaType.Premul);
+ using var skImage = SKImage.FromPixelCopy(info, frame.Image.Pixels);
+ using var data = skImage.Encode(_format, _quality);
+ using var stream = File.OpenWrite(output);
+ data.SaveTo(stream);
+ }
+ }
+}
diff --git a/Spine/Exporters/FrameSequenceExporter.cs b/Spine/Exporters/FrameSequenceExporter.cs
new file mode 100644
index 0000000..4655178
--- /dev/null
+++ b/Spine/Exporters/FrameSequenceExporter.cs
@@ -0,0 +1,61 @@
+using NLog;
+using SFML.System;
+using SkiaSharp;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Spine.Exporters
+{
+ ///
+ /// 帧序列导出器, 导出 png 帧序列
+ ///
+ public class FrameSequenceExporter : VideoExporter
+ {
+ public FrameSequenceExporter(uint width = 100, uint height = 100) : base(width, height) { }
+ public FrameSequenceExporter(Vector2u resolution) : base(resolution) { }
+
+ public override void Export(string output, CancellationToken ct, params SpineObject[] spines)
+ {
+ Directory.CreateDirectory(output);
+
+ int frameCount = GetFrameCount();
+ int frameIdx = 0;
+
+ _progressReporter?.Invoke(frameCount, 0, $"[{frameIdx}/{frameCount}] {output}");
+ foreach (var frame in GetFrames(spines))
+ {
+ if (ct.IsCancellationRequested)
+ {
+ _logger.Info("Export cancelled");
+ frame.Dispose();
+ break;
+ }
+
+ var savePath = Path.Combine(output, $"frame_{_fps}_{frameIdx:d6}.png");
+ var info = new SKImageInfo(frame.Width, frame.Height, SKColorType.Rgba8888, SKAlphaType.Premul);
+
+ _progressReporter?.Invoke(frameCount, frameIdx, $"[{frameIdx + 1}/{frameCount}] {savePath}");
+ try
+ {
+ using var skImage = SKImage.FromPixelCopy(info, frame.Image.Pixels);
+ using var data = skImage.Encode(SKEncodedImageFormat.Png, 100);
+ using var stream = File.OpenWrite(savePath);
+ data.SaveTo(stream);
+ }
+ catch (Exception ex)
+ {
+ _logger.Trace(ex.ToString());
+ _logger.Error("Failed to save frame {0}, {1}", savePath, ex.Message);
+ }
+ finally
+ {
+ frame.Dispose();
+ }
+ frameIdx++;
+ }
+ }
+ }
+}
diff --git a/Spine/Exporters/SFMLImageVideoFrame.cs b/Spine/Exporters/SFMLImageVideoFrame.cs
new file mode 100644
index 0000000..72872c3
--- /dev/null
+++ b/Spine/Exporters/SFMLImageVideoFrame.cs
@@ -0,0 +1,52 @@
+using FFMpegCore.Pipes;
+using SFML.Graphics;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Spine.Exporters
+{
+ ///
+ /// 帧对象包装类, 将接管给定对象生命周期
+ ///
+ public class SFMLImageVideoFrame(Image image) : IVideoFrame, IDisposable
+ {
+ private readonly Image _image = image;
+
+ ///
+ /// 接管的 内部对象
+ ///
+ public Image Image => _image;
+
+ public int Width => (int)_image.Size.X;
+ public int Height => (int)_image.Size.Y;
+ public string Format => "rgba";
+ public void Serialize(Stream pipe) => pipe.Write(_image.Pixels);
+ public async Task SerializeAsync(Stream pipe, CancellationToken token) => await pipe.WriteAsync(_image.Pixels, token);
+
+ #region IDisposable 接口实现
+
+ private bool _disposed = false;
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ _image.Dispose();
+ }
+ _disposed = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ #endregion
+ }
+}
diff --git a/Spine/Exporters/VideoExporter.cs b/Spine/Exporters/VideoExporter.cs
new file mode 100644
index 0000000..c3755dd
--- /dev/null
+++ b/Spine/Exporters/VideoExporter.cs
@@ -0,0 +1,142 @@
+using NLog;
+using SFML.System;
+using SkiaSharp;
+using System;
+using System.Collections.Frozen;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Spine.Exporters
+{
+ ///
+ /// 多帧画面导出基类, 可以获取连续的帧序列
+ ///
+ public abstract class VideoExporter : BaseExporter
+ {
+ public VideoExporter(uint width, uint height) : base(width, height) { }
+ public VideoExporter(Vector2u resolution) : base(resolution) { }
+
+ ///
+ /// 导出时长
+ ///
+ public float Duration
+ {
+ get => _duration;
+ set
+ {
+ if (value < 0)
+ {
+ _logger.Warn("Omit invalid duration: {0}", value);
+ return;
+ }
+ _duration = value;
+ }
+ }
+ protected float _duration = 0;
+
+ ///
+ /// 帧率
+ ///
+ public float Fps
+ {
+ get => _fps;
+ set
+ {
+ if (value <= 0)
+ {
+ _logger.Warn("Omit invalid fps: {0}", value);
+ return;
+ }
+ _fps = value;
+ }
+ }
+ protected float _fps = 24;
+
+ ///
+ /// 是否保留最后一帧
+ ///
+ public bool KeepLast { get => _keepLast; set => _keepLast = value; }
+ protected bool _keepLast = true;
+
+ ///
+ /// 获取总帧数
+ ///
+ public int GetFrameCount()
+ {
+ var delta = 1f / _fps;
+ var total = (int)(_duration * _fps); // 完整帧的数量
+
+ var deltaFinal = _duration - delta * total; // 最后一帧时长
+ var final = _keepLast && deltaFinal > 1e-3 ? 1 : 0;
+
+ var frameCount = 1 + total + final; // 所有帧的数量 = 起始帧 + 完整帧 + 最后一帧
+ return frameCount;
+ }
+
+ ///
+ /// 生成帧序列
+ ///
+ protected IEnumerable GetFrames(SpineObject[] spines)
+ {
+ float delta = 1f / _fps;
+ int total = (int)(_duration * _fps); // 完整帧的数量
+ bool hasFinal = _keepLast && (_duration - delta * total) > 1e-3;
+
+ // 导出首帧
+ var firstFrame = GetFrame(spines);
+ yield return firstFrame;
+
+ // 导出完整帧
+ for (int i = 0; i < total; i++)
+ {
+ foreach (var spine in spines) spine.Update(delta);
+ yield return GetFrame(spines);
+ }
+
+ // 导出最后一帧
+ if (hasFinal)
+ {
+ // XXX: 此处还是按照完整的一帧时长进行更新, 也许可以只更新准确的最后一帧时长
+ foreach (var spine in spines) spine.Update(delta);
+ yield return GetFrame(spines);
+ }
+ }
+
+ ///
+ /// 生成帧序列, 支持中途取消和进度输出
+ ///
+ protected IEnumerable GetFrames(SpineObject[] spines, string output, CancellationToken ct)
+ {
+ int frameCount = GetFrameCount();
+ int frameIdx = 0;
+
+ _progressReporter?.Invoke(frameCount, 0, $"[{frameIdx}/{frameCount}] {output}");
+ foreach (var frame in GetFrames(spines))
+ {
+ if (ct.IsCancellationRequested)
+ {
+ _logger.Info("Export cancelled");
+ frame.Dispose();
+ break;
+ }
+
+ _progressReporter?.Invoke(frameCount, frameIdx, $"[{frameIdx + 1}/{frameCount}] {output}");
+ yield return frame;
+ frameIdx++;
+ }
+ }
+
+ public sealed override void Export(string output, params SpineObject[] spines) => Export(output, default, spines);
+
+ ///
+ /// 导出给定的模型, 从前往后对应从上往下的渲染顺序
+ ///
+ /// 输出路径, 一般而言都是文件路径, 少数情况指定的是文件夹
+ /// 取消令牌
+ /// 要导出的模型, 从前往后对应从上往下的渲染顺序
+ public abstract void Export(string output, CancellationToken ct, params SpineObject[] spines);
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V21/Animation21.cs b/Spine/Implementations/SpineWrappers/V21/Animation21.cs
new file mode 100644
index 0000000..8ce3b98
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V21/Animation21.cs
@@ -0,0 +1,23 @@
+using Spine.SpineWrappers;
+using SpineRuntime21;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Spine.Implementations.SpineWrappers.V21
+{
+ internal sealed class Animation21(Animation innerObject) : IAnimation
+ {
+ private readonly Animation _o = innerObject;
+
+ public Animation InnerObject => _o;
+
+ public string Name => _o.Name;
+
+ public float Duration => _o.Duration;
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V21/AnimationState21.cs b/Spine/Implementations/SpineWrappers/V21/AnimationState21.cs
new file mode 100644
index 0000000..d0728f2
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V21/AnimationState21.cs
@@ -0,0 +1,179 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using SpineRuntime21;
+
+namespace Spine.Implementations.SpineWrappers.V21
+{
+ internal sealed class AnimationState21(AnimationState innerObject, SpineObjectData21 data) : IAnimationState
+ {
+ private readonly AnimationState _o = innerObject;
+ private readonly SpineObjectData21 _data = data;
+
+ private readonly Dictionary _trackEntryPool = [];
+
+ private readonly Dictionary _eventMapping = [];
+ private readonly Dictionary _eventCount = [];
+
+ public AnimationState InnerObject => _o;
+
+#pragma warning disable CS0067
+
+ // NOTE: 2.1 没有这两个事件
+ public event IAnimationState.TrackEntryDelegate? Interrupt;
+ public event IAnimationState.TrackEntryDelegate? Dispose;
+
+#pragma warning restore CS0067
+
+ public event IAnimationState.TrackEntryDelegate? Start
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Start += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Start -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? End
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.End += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.End -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Complete
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Complete += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Complete -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
+
+ public void Update(float delta) => _o.Update(delta);
+
+ public void Apply(ISkeleton skeleton)
+ {
+ if (skeleton is Skeleton21 skel)
+ {
+ _o.Apply(skel.InnerObject);
+ return;
+ }
+ throw new ArgumentException($"Received {skeleton.GetType().Name}", nameof(skeleton));
+ }
+
+ ///
+ /// 获取 对象, 不存在则创建
+ ///
+ public ITrackEntry GetTrackEntry(TrackEntry trackEntry)
+ {
+ if (!_trackEntryPool.TryGetValue(trackEntry, out var tr))
+ _trackEntryPool[trackEntry] = tr = new(trackEntry, this, _data);
+ return tr;
+ }
+
+ public IEnumerable IterTracks() => _o.Tracks.Select(t => t is null ? null : GetTrackEntry(t));
+
+ public ITrackEntry? GetCurrent(int index) { var t = _o.GetCurrent(index); return t is null ? null : GetTrackEntry(t); }
+
+ public void ClearTrack(int index) => _o.ClearTrack(index);
+
+ public void ClearTracks() => _o.ClearTracks();
+
+ public ITrackEntry SetAnimation(int trackIndex, string animationName, bool loop)
+ => GetTrackEntry(_o.SetAnimation(trackIndex, animationName, loop));
+
+ public ITrackEntry SetAnimation(int trackIndex, IAnimation animation, bool loop)
+ {
+ if (animation is Animation21 anime)
+ return GetTrackEntry(_o.SetAnimation(trackIndex, anime.InnerObject, loop));
+ throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
+ }
+
+ public ITrackEntry SetEmptyAnimation(int trackIndex, float mixDuration) => GetTrackEntry(_o.SetEmptyAnimation(trackIndex, mixDuration));
+
+ public void SetEmptyAnimations(float mixDuration) => _o.SetEmptyAnimations(mixDuration);
+
+ public ITrackEntry AddAnimation(int trackIndex, string animationName, bool loop, float delay)
+ => GetTrackEntry(_o.AddAnimation(trackIndex, animationName, loop, delay));
+
+ public ITrackEntry AddAnimation(int trackIndex, IAnimation animation, bool loop, float delay)
+ {
+ if (animation is Animation21 anime)
+ return GetTrackEntry(_o.AddAnimation(trackIndex, anime.InnerObject, loop, delay));
+ throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
+ }
+
+ public ITrackEntry AddEmptyAnimation(int trackIndex, float mixDuration, float delay)
+ => GetTrackEntry(_o.AddEmptyAnimation(trackIndex, mixDuration, delay));
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V21/Attachments/Attachment21.cs b/Spine/Implementations/SpineWrappers/V21/Attachments/Attachment21.cs
new file mode 100644
index 0000000..21e2f73
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V21/Attachments/Attachment21.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime21;
+
+namespace Spine.Implementations.SpineWrappers.V21.Attachments
+{
+ internal abstract class Attachment21(Attachment innerObject) : IAttachment
+ {
+ private readonly Attachment _o = innerObject;
+
+ public virtual Attachment InnerObject => _o;
+
+ public string Name => _o.Name;
+
+ public abstract int ComputeWorldVertices(ISlot slot, ref float[] worldVertices);
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V21/Attachments/BoundingBoxAttachment21.cs b/Spine/Implementations/SpineWrappers/V21/Attachments/BoundingBoxAttachment21.cs
new file mode 100644
index 0000000..049e530
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V21/Attachments/BoundingBoxAttachment21.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime21;
+
+namespace Spine.Implementations.SpineWrappers.V21.Attachments
+{
+ internal sealed class BoundingBoxAttachment21(BoundingBoxAttachment innerObject) :
+ Attachment21(innerObject),
+ IBoundingBoxAttachment
+ {
+ private readonly BoundingBoxAttachment _o = innerObject;
+
+ public override BoundingBoxAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot21 st)
+ {
+ var length = _o.Vertices.Length;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject.Bone, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot21)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V21/Attachments/MeshAttachment21.cs b/Spine/Implementations/SpineWrappers/V21/Attachments/MeshAttachment21.cs
new file mode 100644
index 0000000..c77666c
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V21/Attachments/MeshAttachment21.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime21;
+
+namespace Spine.Implementations.SpineWrappers.V21.Attachments
+{
+ internal sealed class MeshAttachment21(MeshAttachment innerObject) :
+ Attachment21(innerObject),
+ IMeshAttachment
+ {
+ private readonly MeshAttachment _o = innerObject;
+
+ public override MeshAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot21 st)
+ {
+ var length = _o.Vertices.Length;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot21)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+
+ public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
+
+ public float[] UVs => _o.UVs;
+
+ public int[] Triangles => _o.Triangles;
+
+ public int HullLength => _o.HullLength;
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V21/Attachments/RegionAttachment21.cs b/Spine/Implementations/SpineWrappers/V21/Attachments/RegionAttachment21.cs
new file mode 100644
index 0000000..9f6c9fd
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V21/Attachments/RegionAttachment21.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime21;
+
+namespace Spine.Implementations.SpineWrappers.V21.Attachments
+{
+ internal sealed class RegionAttachment21(RegionAttachment innerObject) :
+ Attachment21(innerObject),
+ IRegionAttachment
+ {
+ private readonly RegionAttachment _o = innerObject;
+
+ public override RegionAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot21 st)
+ {
+ if (worldVertices.Length < 8) worldVertices = new float[8];
+ _o.ComputeWorldVertices(st.InnerObject.Bone, worldVertices);
+ return 8;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot21)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+
+ public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
+
+ public float[] UVs => _o.UVs;
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V21/Attachments/SkinnedMeshAttachment21.cs b/Spine/Implementations/SpineWrappers/V21/Attachments/SkinnedMeshAttachment21.cs
new file mode 100644
index 0000000..28f15a3
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V21/Attachments/SkinnedMeshAttachment21.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime21;
+
+namespace Spine.Implementations.SpineWrappers.V21.Attachments
+{
+ internal sealed class SkinnedMeshAttachment21(SkinnedMeshAttachment innerObject) :
+ Attachment21(innerObject),
+ ISkinnedMeshAttachment
+ {
+ private readonly SkinnedMeshAttachment _o = innerObject;
+
+ public override SkinnedMeshAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot21 st)
+ {
+ var length = _o.UVs.Length;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot21)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+
+ public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
+
+ public float[] UVs => _o.UVs;
+
+ public int[] Triangles => _o.Triangles;
+
+ public int HullLength => _o.HullLength;
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V21/Bone21.cs b/Spine/Implementations/SpineWrappers/V21/Bone21.cs
new file mode 100644
index 0000000..19e0e2a
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V21/Bone21.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using SpineRuntime21;
+
+namespace Spine.Implementations.SpineWrappers.V21
+{
+ internal sealed class Bone21(Bone innerObject, Bone21? parent = null) : IBone
+ {
+ private readonly Bone _o = innerObject;
+ private readonly Bone21? _parent = parent;
+
+ public Bone InnerObject => _o;
+
+ public string Name => _o.Data.Name;
+ public int Index => _o.Data.Index;
+
+ public IBone? Parent => _parent;
+ public bool Active => true; // NOTE: 3.7 及以下没有 Active 属性, 此处总是返回 true
+ public float Length => _o.Data.Length;
+ public float WorldX => _o.WorldX;
+ public float WorldY => _o.WorldY;
+ public float A => _o.M00;
+ public float B => _o.M01;
+ public float C => _o.M10;
+ public float D => _o.M11;
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V21/Skeleton21.cs b/Spine/Implementations/SpineWrappers/V21/Skeleton21.cs
new file mode 100644
index 0000000..0c8915a
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V21/Skeleton21.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Collections.Frozen;
+using System.Collections.Immutable;
+using Spine.SpineWrappers;
+using SpineRuntime21;
+
+namespace Spine.Implementations.SpineWrappers.V21
+{
+ internal sealed class Skeleton21 : ISkeleton
+ {
+ private readonly Skeleton _o;
+ private readonly SpineObjectData21 _data;
+
+ private readonly ImmutableArray _bones;
+ private readonly FrozenDictionary _bonesByName;
+ private readonly ImmutableArray _slots;
+ private readonly FrozenDictionary _slotsByName;
+
+ private Skin21? _skin;
+
+ public Skeleton21(Skeleton innerObject, SpineObjectData21 data)
+ {
+ _o = innerObject;
+ _data = data;
+
+ List bones = [];
+ Dictionary bonesByName = [];
+ foreach (var b in _o.Bones)
+ {
+ var bone = new Bone21(b, b.Parent is null ? null : bones[b.Parent.Data.Index]);
+ bones.Add(bone);
+ bonesByName[bone.Name] = bone;
+ }
+ _bones = bones.Cast().ToImmutableArray();
+ _bonesByName = bonesByName.ToFrozenDictionary();
+
+ List slots = [];
+ Dictionary slotsByName = [];
+ foreach (var s in _o.Slots)
+ {
+ var slot = new Slot21(s, _data, bones[s.Bone.Data.Index]);
+ slots.Add(slot);
+ slotsByName[slot.Name] = slot;
+ }
+ _slots = slots.Cast().ToImmutableArray();
+ _slotsByName = slotsByName.ToFrozenDictionary();
+ }
+
+ public Skeleton InnerObject => _o;
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+ public float X { get => _o.X; set => _o.X = value; }
+ public float Y { get => _o.Y; set => _o.Y = value; }
+ public float ScaleX { get => _o.ScaleX; set => _o.ScaleX = value; }
+ public float ScaleY { get => _o.ScaleY; set => _o.ScaleY = value; }
+
+ public ImmutableArray Bones => _bones;
+ public FrozenDictionary BonesByName => _bonesByName;
+ public ImmutableArray Slots => _slots;
+ public FrozenDictionary SlotsByName => _slotsByName;
+
+ public ISkin? Skin
+ {
+ get => _skin;
+ set
+ {
+ if (value is null)
+ {
+ _o.Skin = null;
+ _skin = null;
+ return;
+ }
+ if (value is Skin21 sk)
+ {
+ _o.Skin = sk.InnerObject;
+ _skin = sk;
+ return;
+ }
+ throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
+ }
+ }
+
+ public IEnumerable IterDrawOrder() => _o.DrawOrder.Select(s => _slots[s.Data.Index]);
+ public void UpdateCache() => _o.UpdateCache();
+ public void UpdateWorldTransform(ISkeleton.Physics physics) => _o.UpdateWorldTransform();
+ public void SetToSetupPose() => _o.SetToSetupPose();
+ public void SetBonesToSetupPose() => _o.SetBonesToSetupPose();
+ public void SetSlotsToSetupPose() => _o.SetSlotsToSetupPose();
+ public void Update(float delta) => _o.Update(delta);
+
+ public void GetBounds(out float x, out float y, out float w, out float h)
+ {
+ _o.GetBounds(out x, out y, out w, out h);
+ }
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V21/SkeletonClipping21.cs b/Spine/Implementations/SpineWrappers/V21/SkeletonClipping21.cs
new file mode 100644
index 0000000..cedba18
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V21/SkeletonClipping21.cs
@@ -0,0 +1,42 @@
+using Spine.SpineWrappers;
+using Spine.SpineWrappers.Attachments;
+using Spine.Utils;
+using SpineRuntime21;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Spine.Implementations.SpineWrappers.V21
+{
+ internal sealed class SkeletonClipping21 : ISkeletonClipping
+ {
+ public bool IsClipping => false;
+
+ public float[] ClippedVertices { get; private set; } = [];
+
+ public int ClippedVerticesLength { get; private set; } = 0;
+
+ public int[] ClippedTriangles { get; private set; } = [];
+
+ public int ClippedTrianglesLength { get; private set; } = 0;
+
+ public float[] ClippedUVs { get; private set; } = [];
+
+ public void ClipEnd(ISlot slot) { }
+
+ public void ClipEnd() { }
+
+ public void ClipStart(ISlot slot, IClippingAttachment clippingAttachment) { }
+
+ public void ClipTriangles(float[] vertices, int verticesLength, int[] triangles, int trianglesLength, float[] uvs)
+ {
+ ClippedVertices = vertices.ToArray();
+ ClippedVerticesLength = verticesLength;
+ ClippedTriangles = triangles.ToArray();
+ ClippedTrianglesLength = trianglesLength;
+ ClippedUVs = uvs.ToArray();
+ }
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V21/Skin21.cs b/Spine/Implementations/SpineWrappers/V21/Skin21.cs
new file mode 100644
index 0000000..08e1b00
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V21/Skin21.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using SpineRuntime21;
+
+namespace Spine.Implementations.SpineWrappers.V21
+{
+ internal sealed class Skin21 : ISkin
+ {
+ private readonly Skin _o;
+
+ ///
+ /// 使用指定名字创建空皮肤
+ ///
+ public Skin21(string name) => _o = new(name);
+
+ ///
+ /// 包装已有皮肤对象
+ ///
+ public Skin21(Skin innerObject) => _o = innerObject;
+
+ public Skin InnerObject => _o;
+
+ public string Name => _o.Name;
+
+ public void AddSkin(ISkin skin)
+ {
+ if (skin is Skin21 sk)
+ {
+ // NOTE: 3.7 及以下不支持 AddSkin
+ foreach (var (k, v) in sk._o.Attachments)
+ _o.AddAttachment(k.Key, k.Value, v);
+ return;
+ }
+ throw new ArgumentException($"Received {skin.GetType().Name}", nameof(skin));
+ }
+
+ public void Clear() => _o.Attachments.Clear();
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V21/Slot21.cs b/Spine/Implementations/SpineWrappers/V21/Slot21.cs
new file mode 100644
index 0000000..19a7321
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V21/Slot21.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Collections.Frozen;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.Utils;
+using Spine.SpineWrappers;
+using SpineRuntime21;
+
+namespace Spine.Implementations.SpineWrappers.V21
+{
+ internal sealed class Slot21 : ISlot
+ {
+ private readonly Slot _o;
+ private readonly SpineObjectData21 _data;
+
+ private readonly Bone21 _bone;
+ private readonly SFML.Graphics.BlendMode _blendMode;
+
+ public Slot21(Slot innerObject, SpineObjectData21 data, Bone21 bone)
+ {
+ _o = innerObject;
+ _data = data;
+
+ _bone = bone;
+ _blendMode = _o.Data.AdditiveBlending ? SFMLBlendMode.AdditivePma : SFMLBlendMode.NormalPma; // NOTE: 2.1 没有完整的 BlendMode
+ }
+
+ public Slot InnerObject => _o;
+
+ public string Name => _o.Data.Name;
+ public int Index => _o.Data.Index;
+ public SFML.Graphics.BlendMode Blend => _blendMode;
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+ public IBone Bone => _bone;
+
+ public Spine.SpineWrappers.Attachments.IAttachment? Attachment
+ {
+ get
+ {
+ if (_o.Attachment is Attachment att)
+ {
+ return _data.SlotAttachments[Name][att.Name];
+ }
+ return null;
+ }
+
+ set
+ {
+ if (value is null)
+ {
+ _o.Attachment = null;
+ return;
+ }
+ if (value is Attachments.Attachment21 att)
+ {
+ _o.Attachment = att.InnerObject;
+ return;
+ }
+ throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
+ }
+ }
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V21/SpineObjectData21.cs b/Spine/Implementations/SpineWrappers/V21/SpineObjectData21.cs
new file mode 100644
index 0000000..abe247d
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V21/SpineObjectData21.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Frozen;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.Utils;
+using Spine.SpineWrappers;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime21;
+using Spine.Implementations.SpineWrappers.V21.Attachments;
+
+namespace Spine.Implementations.SpineWrappers.V21
+{
+ [SpineImplementation(2, 1)]
+ internal sealed class SpineObjectData21 : SpineObjectData
+ {
+ private readonly Atlas _atlas;
+ private readonly SkeletonData _skeletonData;
+ private readonly AnimationStateData _animationStateData;
+
+ private readonly ImmutableArray _skins;
+ private readonly FrozenDictionary _skinsByName;
+ private readonly FrozenDictionary> _slotAttachments;
+ private readonly ImmutableArray _animations;
+ private readonly FrozenDictionary _animationsByName;
+
+ public SpineObjectData21(string skelPath, string atlasPath) : base(skelPath, atlasPath)
+ {
+ // 加载 atlas
+ try { _atlas = new Atlas(atlasPath, _textureLoader); }
+ catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
+
+ try
+ {
+ if (Utf8Validator.IsUtf8(skelPath))
+ _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
+ else
+ _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
+ }
+ catch (Exception ex)
+ {
+ _atlas.Dispose();
+ throw new InvalidDataException($"Failed to load skeleton file {skelPath}", ex);
+ }
+
+ // 加载动画数据
+ _animationStateData = new AnimationStateData(_skeletonData);
+
+ // 整理皮肤和附件
+ Dictionary> slotAttachments = [];
+ List skins = [];
+ Dictionary skinsByName = [];
+ foreach (var s in _skeletonData.Skins)
+ {
+ var skin = new Skin21(s);
+ skins.Add(skin);
+ skinsByName[s.Name] = skin;
+ foreach (var (k, att) in s.Attachments)
+ {
+ var slotName = _skeletonData.Slots[k.Key].Name;
+ if (!slotAttachments.TryGetValue(slotName, out var attachments))
+ slotAttachments[slotName] = attachments = [];
+
+ attachments[att.Name] = att switch
+ {
+ RegionAttachment regionAtt => new RegionAttachment21(regionAtt),
+ MeshAttachment meshAtt => new MeshAttachment21(meshAtt),
+ SkinnedMeshAttachment skMeshAtt => new SkinnedMeshAttachment21(skMeshAtt),
+ BoundingBoxAttachment bbAtt => new BoundingBoxAttachment21(bbAtt),
+ _ => throw new InvalidOperationException($"Unrecognized attachment type {att.GetType().FullName}")
+ };
+ }
+ }
+ _slotAttachments = slotAttachments.ToFrozenDictionary(it => it.Key, it => it.Value.ToFrozenDictionary());
+ _skins = skins.ToImmutableArray();
+ _skinsByName = skinsByName.ToFrozenDictionary();
+
+ // 整理所有动画数据
+ List animations = [];
+ Dictionary animationsByName = [];
+ foreach (var a in _skeletonData.Animations)
+ {
+ var anime = new Animation21(a);
+ animations.Add(anime);
+ animationsByName[anime.Name] = anime;
+ }
+ _animations = animations.ToImmutableArray();
+ _animationsByName = animationsByName.ToFrozenDictionary();
+ }
+
+ public override string SkeletonVersion => _skeletonData.Version;
+
+ public override ImmutableArray Skins => _skins;
+
+ public override FrozenDictionary SkinsByName => _skinsByName;
+
+ public override FrozenDictionary> SlotAttachments => _slotAttachments;
+
+ public override float DefaultMix { get => _animationStateData.DefaultMix; set => _animationStateData.DefaultMix = value; }
+
+ public override ImmutableArray Animations => _animations;
+
+ public override FrozenDictionary AnimationsByName => _animationsByName;
+
+ protected override void DisposeAtlas() => _atlas.Dispose();
+
+ public override ISkeleton CreateSkeleton() => new Skeleton21(new(_skeletonData), this);
+
+ public override IAnimationState CreateAnimationState() => new AnimationState21(new(_animationStateData), this);
+
+ public override ISkeletonClipping CreateSkeletonClipping() => new SkeletonClipping21();
+
+ public override ISkin CreateSkin(string name) => new Skin21(name);
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V21/TrackEntry21.cs b/Spine/Implementations/SpineWrappers/V21/TrackEntry21.cs
new file mode 100644
index 0000000..bd0121d
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V21/TrackEntry21.cs
@@ -0,0 +1,135 @@
+using Spine.SpineWrappers;
+using SpineRuntime21;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Spine.Implementations.SpineWrappers.V21
+{
+ internal sealed class TrackEntry21(TrackEntry innerObject, AnimationState21 animationState, SpineObjectData21 data): ITrackEntry
+ {
+ private readonly TrackEntry _o = innerObject;
+ private readonly AnimationState21 _animationState = animationState;
+ private readonly SpineObjectData21 _data = data;
+
+ private readonly Dictionary _eventMapping = [];
+ private readonly Dictionary _eventCount = [];
+
+ public TrackEntry InnerObject => _o;
+
+#pragma warning disable CS0067
+
+ // 2.1 没有这两个事件
+ public event IAnimationState.TrackEntryDelegate? Interrupt;
+ public event IAnimationState.TrackEntryDelegate? Dispose;
+
+#pragma warning restore CS0067
+
+ public event IAnimationState.TrackEntryDelegate? Start
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Start += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Start -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? End
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.End += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.End -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Complete
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Complete += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Complete -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public int TrackIndex { get => _o.TrackIndex; }
+
+ public IAnimation Animation { get => _data.AnimationsByName[_o.Animation.Name]; }
+
+ public ITrackEntry? Next { get { var t = _o.Next; return t is null ? null : _animationState.GetTrackEntry(t); } }
+
+ public bool Loop { get => _o.Loop; set => _o.Loop = value; }
+
+ public float TrackTime { get => _o.Time; set => _o.Time = value; }
+
+ public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
+
+ public float Alpha { get => _o.Mix; set => _o.Mix = value; }
+
+ public float MixDuration { get => _o.MixDuration; set => _o.MixDuration = value; }
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V36/Animation36.cs b/Spine/Implementations/SpineWrappers/V36/Animation36.cs
new file mode 100644
index 0000000..51cf8c2
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V36/Animation36.cs
@@ -0,0 +1,23 @@
+using Spine.SpineWrappers;
+using SpineRuntime36;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Spine.Implementations.SpineWrappers.V36
+{
+ internal sealed class Animation36(Animation innerObject) : IAnimation
+ {
+ private readonly Animation _o = innerObject;
+
+ public Animation InnerObject => _o;
+
+ public string Name => _o.Name;
+
+ public float Duration => _o.Duration;
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V36/AnimationState36.cs b/Spine/Implementations/SpineWrappers/V36/AnimationState36.cs
new file mode 100644
index 0000000..e5b0c58
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V36/AnimationState36.cs
@@ -0,0 +1,229 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using SpineRuntime36;
+
+namespace Spine.Implementations.SpineWrappers.V36
+{
+ internal sealed class AnimationState36(AnimationState innerObject, SpineObjectData36 data) : IAnimationState
+ {
+ private readonly AnimationState _o = innerObject;
+ private readonly SpineObjectData36 _data = data;
+
+ private readonly Dictionary _trackEntryPool = [];
+
+ private readonly Dictionary _eventMapping = [];
+ private readonly Dictionary _eventCount = [];
+
+ public AnimationState InnerObject => _o;
+
+ public event IAnimationState.TrackEntryDelegate? Start
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Start += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Start -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Interrupt
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Interrupt += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Interrupt -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? End
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.End += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.End -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Complete
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Complete += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Complete -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Dispose
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Dispose += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Dispose -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
+
+ public void Update(float delta) => _o.Update(delta);
+
+ public void Apply(ISkeleton skeleton)
+ {
+ if (skeleton is Skeleton36 skel)
+ {
+ _o.Apply(skel.InnerObject);
+ return;
+ }
+ throw new ArgumentException($"Received {skeleton.GetType().Name}", nameof(skeleton));
+ }
+
+ ///
+ /// 获取 对象, 不存在则创建
+ ///
+ public ITrackEntry GetTrackEntry(TrackEntry trackEntry)
+ {
+ if (!_trackEntryPool.TryGetValue(trackEntry, out var tr))
+ _trackEntryPool[trackEntry] = tr = new(trackEntry, this, _data);
+ return tr;
+ }
+
+ public IEnumerable IterTracks() => _o.Tracks.Select(t => t is null ? null : GetTrackEntry(t));
+
+ public ITrackEntry? GetCurrent(int index) { var t = _o.GetCurrent(index); return t is null ? null : GetTrackEntry(t); }
+
+ public void ClearTrack(int index) => _o.ClearTrack(index);
+
+ public void ClearTracks() => _o.ClearTracks();
+
+ public ITrackEntry SetAnimation(int trackIndex, string animationName, bool loop)
+ => GetTrackEntry(_o.SetAnimation(trackIndex, animationName, loop));
+
+ public ITrackEntry SetAnimation(int trackIndex, IAnimation animation, bool loop)
+ {
+ if (animation is Animation36 anime)
+ return GetTrackEntry(_o.SetAnimation(trackIndex, anime.InnerObject, loop));
+ throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
+ }
+
+ public ITrackEntry SetEmptyAnimation(int trackIndex, float mixDuration) => GetTrackEntry(_o.SetEmptyAnimation(trackIndex, mixDuration));
+
+ public void SetEmptyAnimations(float mixDuration) => _o.SetEmptyAnimations(mixDuration);
+
+ public ITrackEntry AddAnimation(int trackIndex, string animationName, bool loop, float delay)
+ => GetTrackEntry(_o.AddAnimation(trackIndex, animationName, loop, delay));
+
+ public ITrackEntry AddAnimation(int trackIndex, IAnimation animation, bool loop, float delay)
+ {
+ if (animation is Animation36 anime)
+ return GetTrackEntry(_o.AddAnimation(trackIndex, anime.InnerObject, loop, delay));
+ throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
+ }
+
+ public ITrackEntry AddEmptyAnimation(int trackIndex, float mixDuration, float delay)
+ => GetTrackEntry(_o.AddEmptyAnimation(trackIndex, mixDuration, delay));
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V36/Attachments/Attachment36.cs b/Spine/Implementations/SpineWrappers/V36/Attachments/Attachment36.cs
new file mode 100644
index 0000000..d61bffc
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V36/Attachments/Attachment36.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime36;
+
+namespace Spine.Implementations.SpineWrappers.V36.Attachments
+{
+ internal abstract class Attachment36(Attachment innerObject) : IAttachment
+ {
+ private readonly Attachment _o = innerObject;
+
+ public virtual Attachment InnerObject => _o;
+
+ public string Name => _o.Name;
+
+ public abstract int ComputeWorldVertices(ISlot slot, ref float[] worldVertices);
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V36/Attachments/BoundingBoxAttachment36.cs b/Spine/Implementations/SpineWrappers/V36/Attachments/BoundingBoxAttachment36.cs
new file mode 100644
index 0000000..6674bcf
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V36/Attachments/BoundingBoxAttachment36.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime36;
+
+namespace Spine.Implementations.SpineWrappers.V36.Attachments
+{
+ internal sealed class BoundingBoxAttachment36(BoundingBoxAttachment innerObject) :
+ Attachment36(innerObject),
+ IBoundingBoxAttachment
+ {
+ private readonly BoundingBoxAttachment _o = innerObject;
+
+ public override BoundingBoxAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot36 st)
+ {
+ var length = _o.WorldVerticesLength;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot36)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V36/Attachments/ClippingAttachment36.cs b/Spine/Implementations/SpineWrappers/V36/Attachments/ClippingAttachment36.cs
new file mode 100644
index 0000000..e8fae73
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V36/Attachments/ClippingAttachment36.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime36;
+
+namespace Spine.Implementations.SpineWrappers.V36.Attachments
+{
+ internal sealed class ClippingAttachment36(ClippingAttachment innerObject) :
+ Attachment36(innerObject),
+ IClippingAttachment
+ {
+ private readonly ClippingAttachment _o = innerObject;
+
+ public override ClippingAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot36 st)
+ {
+ var length = _o.WorldVerticesLength;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot36)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V36/Attachments/MeshAttachment36.cs b/Spine/Implementations/SpineWrappers/V36/Attachments/MeshAttachment36.cs
new file mode 100644
index 0000000..6040e62
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V36/Attachments/MeshAttachment36.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime36;
+
+namespace Spine.Implementations.SpineWrappers.V36.Attachments
+{
+ internal sealed class MeshAttachment36(MeshAttachment innerObject) :
+ Attachment36(innerObject),
+ IMeshAttachment
+ {
+ private readonly MeshAttachment _o = innerObject;
+
+ public override MeshAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot36 st)
+ {
+ var length = _o.WorldVerticesLength;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot36)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+
+ public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
+
+ public float[] UVs => _o.UVs;
+
+ public int[] Triangles => _o.Triangles;
+
+ public int HullLength => _o.HullLength;
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V36/Attachments/PathAttachment36.cs b/Spine/Implementations/SpineWrappers/V36/Attachments/PathAttachment36.cs
new file mode 100644
index 0000000..b25fc91
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V36/Attachments/PathAttachment36.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime36;
+
+namespace Spine.Implementations.SpineWrappers.V36.Attachments
+{
+ internal sealed class PathAttachment36(PathAttachment innerObject) :
+ Attachment36(innerObject),
+ IPathAttachment
+ {
+ private readonly PathAttachment _o = innerObject;
+
+ public override PathAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot36 st)
+ {
+ var length = _o.WorldVerticesLength;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot36)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Spine/Implementations/SpineWrappers/V36/Attachments/PointAttachment36.cs b/Spine/Implementations/SpineWrappers/V36/Attachments/PointAttachment36.cs
new file mode 100644
index 0000000..56db482
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V36/Attachments/PointAttachment36.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime36;
+
+namespace Spine.Implementations.SpineWrappers.V36.Attachments
+{
+ internal sealed class PointAttachment36(PointAttachment innerObject) :
+ Attachment36(innerObject),
+ IPointAttachment
+ {
+ private readonly PointAttachment _o = innerObject;
+
+ public override PointAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot36 st)
+ {
+ if (worldVertices.Length < 2) worldVertices = new float[2];
+ _o.ComputeWorldPosition(st.InnerObject.Bone, out worldVertices[0], out worldVertices[1]);
+ return 2;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot36)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Spine/Implementations/SpineWrappers/V36/Attachments/RegionAttachment36.cs b/Spine/Implementations/SpineWrappers/V36/Attachments/RegionAttachment36.cs
new file mode 100644
index 0000000..c0a98fe
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V36/Attachments/RegionAttachment36.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime36;
+
+namespace Spine.Implementations.SpineWrappers.V36.Attachments
+{
+ internal sealed class RegionAttachment36(RegionAttachment innerObject) :
+ Attachment36(innerObject),
+ IRegionAttachment
+ {
+ private readonly RegionAttachment _o = innerObject;
+
+ public override RegionAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot36 st)
+ {
+ if (worldVertices.Length < 8) worldVertices = new float[8];
+ _o.ComputeWorldVertices(st.InnerObject.Bone, worldVertices, 0);
+ return 8;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot36)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+
+ public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
+
+ public float[] UVs => _o.UVs;
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V36/Bone36.cs b/Spine/Implementations/SpineWrappers/V36/Bone36.cs
new file mode 100644
index 0000000..cca0d7b
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V36/Bone36.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using SpineRuntime36;
+
+namespace Spine.Implementations.SpineWrappers.V36
+{
+ internal sealed class Bone36(Bone innerObject, Bone36? parent = null) : IBone
+ {
+ private readonly Bone _o = innerObject;
+ private readonly Bone36? _parent = parent;
+
+ public Bone InnerObject => _o;
+
+ public string Name => _o.Data.Name;
+ public int Index => _o.Data.Index;
+
+ public IBone? Parent => _parent;
+ public bool Active => true; // NOTE: 3.7 及以下没有 Active 属性, 此处总是返回 true
+ public float Length => _o.Data.Length;
+ public float WorldX => _o.WorldX;
+ public float WorldY => _o.WorldY;
+ public float A => _o.A;
+ public float B => _o.B;
+ public float C => _o.C;
+ public float D => _o.D;
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V36/Skeleton36.cs b/Spine/Implementations/SpineWrappers/V36/Skeleton36.cs
new file mode 100644
index 0000000..5429dc9
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V36/Skeleton36.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Collections.Frozen;
+using System.Collections.Immutable;
+using Spine.SpineWrappers;
+using SpineRuntime36;
+
+namespace Spine.Implementations.SpineWrappers.V36
+{
+ internal sealed class Skeleton36 : ISkeleton
+ {
+ private readonly Skeleton _o;
+ private readonly SpineObjectData36 _data;
+
+ private readonly ImmutableArray _bones;
+ private readonly FrozenDictionary _bonesByName;
+ private readonly ImmutableArray _slots;
+ private readonly FrozenDictionary _slotsByName;
+
+ private Skin36? _skin;
+
+ public Skeleton36(Skeleton innerObject, SpineObjectData36 data)
+ {
+ _o = innerObject;
+ _data = data;
+
+ List bones = [];
+ Dictionary bonesByName = [];
+ foreach (var b in _o.Bones)
+ {
+ var bone = new Bone36(b, b.Parent is null ? null : bones[b.Parent.Data.Index]);
+ bones.Add(bone);
+ bonesByName[bone.Name] = bone;
+ }
+ _bones = bones.Cast().ToImmutableArray();
+ _bonesByName = bonesByName.ToFrozenDictionary();
+
+ List slots = [];
+ Dictionary slotsByName = [];
+ foreach (var s in _o.Slots)
+ {
+ var slot = new Slot36(s, _data, bones[s.Bone.Data.Index]);
+ slots.Add(slot);
+ slotsByName[slot.Name] = slot;
+ }
+ _slots = slots.Cast().ToImmutableArray();
+ _slotsByName = slotsByName.ToFrozenDictionary();
+ }
+
+ public Skeleton InnerObject => _o;
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+ public float X { get => _o.X; set => _o.X = value; }
+ public float Y { get => _o.Y; set => _o.Y = value; }
+ public float ScaleX { get => _o.ScaleX; set => _o.ScaleX = value; }
+ public float ScaleY { get => _o.ScaleY; set => _o.ScaleY = value; }
+
+ public ImmutableArray Bones => _bones;
+ public FrozenDictionary BonesByName => _bonesByName;
+ public ImmutableArray Slots => _slots;
+ public FrozenDictionary SlotsByName => _slotsByName;
+
+ public ISkin? Skin
+ {
+ get => _skin;
+ set
+ {
+ if (value is null)
+ {
+ _o.Skin = null;
+ _skin = null;
+ return;
+ }
+ if (value is Skin36 sk)
+ {
+ _o.Skin = sk.InnerObject;
+ _skin = sk;
+ return;
+ }
+ throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
+ }
+ }
+
+ public IEnumerable IterDrawOrder() => _o.DrawOrder.Select(s => _slots[s.Data.Index]);
+ public void UpdateCache() => _o.UpdateCache();
+ public void UpdateWorldTransform(ISkeleton.Physics physics) => _o.UpdateWorldTransform();
+ public void SetToSetupPose() => _o.SetToSetupPose();
+ public void SetBonesToSetupPose() => _o.SetBonesToSetupPose();
+ public void SetSlotsToSetupPose() => _o.SetSlotsToSetupPose();
+ public void Update(float delta) => _o.Update(delta);
+
+ public void GetBounds(out float x, out float y, out float w, out float h)
+ {
+ float[] _ = [];
+ _o.GetBounds(out x, out y, out w, out h, ref _);
+ }
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V36/SkeletonClipping36.cs b/Spine/Implementations/SpineWrappers/V36/SkeletonClipping36.cs
new file mode 100644
index 0000000..5386eed
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V36/SkeletonClipping36.cs
@@ -0,0 +1,56 @@
+using Spine.SpineWrappers;
+using Spine.SpineWrappers.Attachments;
+using Spine.Utils;
+using SpineRuntime36;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Spine.Implementations.SpineWrappers.V36
+{
+ internal sealed class SkeletonClipping36 : ISkeletonClipping
+ {
+ private readonly SkeletonClipping _o = new();
+
+ public bool IsClipping => _o.IsClipping;
+
+ public float[] ClippedVertices => _o.ClippedVertices.Items;
+
+ public int ClippedVerticesLength => _o.ClippedVertices.Count;
+
+ public int[] ClippedTriangles => _o.ClippedTriangles.Items;
+
+ public int ClippedTrianglesLength => _o.ClippedTriangles.Count;
+
+ public float[] ClippedUVs => _o.ClippedUVs.Items;
+
+ public void ClipTriangles(float[] vertices, int verticesLength, int[] triangles, int trianglesLength, float[] uvs)
+ => _o.ClipTriangles(vertices, verticesLength, triangles, trianglesLength, uvs);
+
+ public void ClipStart(ISlot slot, IClippingAttachment clippingAttachment)
+ {
+ if (slot is Slot36 st && clippingAttachment is Attachments.ClippingAttachment36 att)
+ {
+ _o.ClipStart(st.InnerObject, att.InnerObject);
+ return;
+ }
+ throw new ArgumentException($"Received {slot.GetType().Name} {clippingAttachment.GetType().Name}");
+ }
+
+ public void ClipEnd(ISlot slot)
+ {
+ if (slot is Slot36 st)
+ {
+ _o.ClipEnd(st.InnerObject);
+ return;
+ }
+ throw new ArgumentException($"Received {slot.GetType().Name}", nameof(slot));
+ }
+
+ public void ClipEnd() => _o.ClipEnd();
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V36/Skin36.cs b/Spine/Implementations/SpineWrappers/V36/Skin36.cs
new file mode 100644
index 0000000..32ccfb1
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V36/Skin36.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using SpineRuntime36;
+
+namespace Spine.Implementations.SpineWrappers.V36
+{
+ internal sealed class Skin36 : ISkin
+ {
+ private readonly Skin _o;
+
+ ///
+ /// 使用指定名字创建空皮肤
+ ///
+ public Skin36(string name) => _o = new(name);
+
+ ///
+ /// 包装已有皮肤对象
+ ///
+ public Skin36(Skin innerObject) => _o = innerObject;
+
+ public Skin InnerObject => _o;
+
+ public string Name => _o.Name;
+
+ public void AddSkin(ISkin skin)
+ {
+ if (skin is Skin36 sk)
+ {
+ // NOTE: 3.7 及以下不支持 AddSkin
+ foreach (var (k, v) in sk._o.Attachments)
+ _o.AddAttachment(k.slotIndex, k.name, v);
+ return;
+ }
+ throw new ArgumentException($"Received {skin.GetType().Name}", nameof(skin));
+ }
+
+ public void Clear() => _o.Attachments.Clear();
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V36/Slot36.cs b/Spine/Implementations/SpineWrappers/V36/Slot36.cs
new file mode 100644
index 0000000..8b595d8
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V36/Slot36.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Frozen;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.Utils;
+using Spine.SpineWrappers;
+using SpineRuntime36;
+
+namespace Spine.Implementations.SpineWrappers.V36
+{
+ internal sealed class Slot36 : ISlot
+ {
+ private readonly Slot _o;
+ private readonly SpineObjectData36 _data;
+
+ private readonly Bone36 _bone;
+ private readonly SFML.Graphics.BlendMode _blendMode;
+
+ public Slot36(Slot innerObject, SpineObjectData36 data, Bone36 bone)
+ {
+ _o = innerObject;
+ _data = data;
+
+ _bone = bone;
+ _blendMode = _o.Data.BlendMode switch
+ {
+ BlendMode.Normal => SFMLBlendMode.NormalPma,
+ BlendMode.Additive => SFMLBlendMode.AdditivePma,
+ BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
+ BlendMode.Screen => SFMLBlendMode.ScreenPma,
+ _ => throw new NotImplementedException($"{_o.Data.BlendMode}"),
+ };
+ }
+
+ public Slot InnerObject => _o;
+
+ public string Name => _o.Data.Name;
+ public int Index => _o.Data.Index;
+ public SFML.Graphics.BlendMode Blend => _blendMode;
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+ public IBone Bone => _bone;
+
+ public Spine.SpineWrappers.Attachments.IAttachment? Attachment
+ {
+ get
+ {
+ if (_o.Attachment is Attachment att)
+ {
+ return _data.SlotAttachments[Name][att.Name];
+ }
+ return null;
+ }
+
+ set
+ {
+ if (value is null)
+ {
+ _o.Attachment = null;
+ return;
+ }
+ if (value is Attachments.Attachment36 att)
+ {
+ _o.Attachment = att.InnerObject;
+ return;
+ }
+ throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
+ }
+ }
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V36/SpineObjectData36.cs b/Spine/Implementations/SpineWrappers/V36/SpineObjectData36.cs
new file mode 100644
index 0000000..159063c
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V36/SpineObjectData36.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Frozen;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.Utils;
+using Spine.SpineWrappers;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime36;
+using Spine.Implementations.SpineWrappers.V36.Attachments;
+
+namespace Spine.Implementations.SpineWrappers.V36
+{
+ [SpineImplementation(3, 6)]
+ internal sealed class SpineObjectData36 : SpineObjectData
+ {
+ private readonly Atlas _atlas;
+ private readonly SkeletonData _skeletonData;
+ private readonly AnimationStateData _animationStateData;
+
+ private readonly ImmutableArray _skins;
+ private readonly FrozenDictionary _skinsByName;
+ private readonly FrozenDictionary> _slotAttachments;
+ private readonly ImmutableArray _animations;
+ private readonly FrozenDictionary _animationsByName;
+
+ public SpineObjectData36(string skelPath, string atlasPath) : base(skelPath, atlasPath)
+ {
+ // 加载 atlas
+ try { _atlas = new Atlas(atlasPath, _textureLoader); }
+ catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
+
+ try
+ {
+ if (Utf8Validator.IsUtf8(skelPath))
+ _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
+ else
+ _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
+ }
+ catch (Exception ex)
+ {
+ _atlas.Dispose();
+ throw new InvalidDataException($"Failed to load skeleton file {skelPath}", ex);
+ }
+
+ // 加载动画数据
+ _animationStateData = new AnimationStateData(_skeletonData);
+
+ // 整理皮肤和附件
+ Dictionary> slotAttachments = [];
+ List skins = [];
+ Dictionary skinsByName = [];
+ foreach (var s in _skeletonData.Skins)
+ {
+ var skin = new Skin36(s);
+ skins.Add(skin);
+ skinsByName[s.Name] = skin;
+ foreach (var (k, att) in s.Attachments)
+ {
+ var slotName = _skeletonData.Slots.Items[k.slotIndex].Name;
+ if (!slotAttachments.TryGetValue(slotName, out var attachments))
+ slotAttachments[slotName] = attachments = [];
+
+ attachments[att.Name] = att switch
+ {
+ RegionAttachment regionAtt => new RegionAttachment36(regionAtt),
+ MeshAttachment meshAtt => new MeshAttachment36(meshAtt),
+ ClippingAttachment clipAtt => new ClippingAttachment36(clipAtt),
+ BoundingBoxAttachment bbAtt => new BoundingBoxAttachment36(bbAtt),
+ PathAttachment pathAtt => new PathAttachment36(pathAtt),
+ PointAttachment ptAtt => new PointAttachment36(ptAtt),
+ _ => throw new InvalidOperationException($"Unrecognized attachment type {att.GetType().FullName}")
+ };
+ }
+ }
+ _slotAttachments = slotAttachments.ToFrozenDictionary(it => it.Key, it => it.Value.ToFrozenDictionary());
+ _skins = skins.ToImmutableArray();
+ _skinsByName = skinsByName.ToFrozenDictionary();
+
+ // 整理所有动画数据
+ List animations = [];
+ Dictionary animationsByName = [];
+ foreach (var a in _skeletonData.Animations)
+ {
+ var anime = new Animation36(a);
+ animations.Add(anime);
+ animationsByName[anime.Name] = anime;
+ }
+ _animations = animations.ToImmutableArray();
+ _animationsByName = animationsByName.ToFrozenDictionary();
+ }
+
+ public override string SkeletonVersion => _skeletonData.Version;
+
+ public override ImmutableArray Skins => _skins;
+
+ public override FrozenDictionary SkinsByName => _skinsByName;
+
+ public override FrozenDictionary> SlotAttachments => _slotAttachments;
+
+ public override float DefaultMix { get => _animationStateData.DefaultMix; set => _animationStateData.DefaultMix = value; }
+
+ public override ImmutableArray Animations => _animations;
+
+ public override FrozenDictionary AnimationsByName => _animationsByName;
+
+ protected override void DisposeAtlas() => _atlas.Dispose();
+
+ public override ISkeleton CreateSkeleton() => new Skeleton36(new(_skeletonData), this);
+
+ public override IAnimationState CreateAnimationState() => new AnimationState36(new(_animationStateData), this);
+
+ public override ISkeletonClipping CreateSkeletonClipping() => new SkeletonClipping36();
+
+ public override ISkin CreateSkin(string name) => new Skin36(name);
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V36/TrackEntry36.cs b/Spine/Implementations/SpineWrappers/V36/TrackEntry36.cs
new file mode 100644
index 0000000..6b86817
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V36/TrackEntry36.cs
@@ -0,0 +1,185 @@
+using Spine.SpineWrappers;
+using SpineRuntime36;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Spine.Implementations.SpineWrappers.V36
+{
+ internal sealed class TrackEntry36(TrackEntry innerObject, AnimationState36 animationState, SpineObjectData36 data): ITrackEntry
+ {
+ private readonly TrackEntry _o = innerObject;
+ private readonly AnimationState36 _animationState = animationState;
+ private readonly SpineObjectData36 _data = data;
+
+ private readonly Dictionary _eventMapping = [];
+ private readonly Dictionary _eventCount = [];
+
+ public TrackEntry InnerObject => _o;
+
+ public event IAnimationState.TrackEntryDelegate? Start
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Start += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Start -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Interrupt
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Interrupt += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Interrupt -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? End
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.End += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.End -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Complete
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Complete += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Complete -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Dispose
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Dispose += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Dispose -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public int TrackIndex { get => _o.TrackIndex; }
+
+ public IAnimation Animation { get => _data.AnimationsByName[_o.Animation.Name]; }
+
+ public ITrackEntry? Next { get { var t = _o.Next; return t is null ? null : _animationState.GetTrackEntry(t); } }
+
+ public bool Loop { get => _o.Loop; set => _o.Loop = value; }
+
+ public float TrackTime { get => _o.TrackTime; set => _o.TrackTime = value; }
+
+ public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
+
+ public float Alpha { get => _o.Alpha; set => _o.Alpha = value; }
+
+ public float MixDuration { get => _o.MixDuration; set => _o.MixDuration = value; }
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V37/Animation37.cs b/Spine/Implementations/SpineWrappers/V37/Animation37.cs
new file mode 100644
index 0000000..9b21d5a
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V37/Animation37.cs
@@ -0,0 +1,23 @@
+using Spine.SpineWrappers;
+using SpineRuntime37;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Spine.Implementations.SpineWrappers.V37
+{
+ internal sealed class Animation37(Animation innerObject) : IAnimation
+ {
+ private readonly Animation _o = innerObject;
+
+ public Animation InnerObject => _o;
+
+ public string Name => _o.Name;
+
+ public float Duration => _o.Duration;
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V37/AnimationState37.cs b/Spine/Implementations/SpineWrappers/V37/AnimationState37.cs
new file mode 100644
index 0000000..f14f93c
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V37/AnimationState37.cs
@@ -0,0 +1,229 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using SpineRuntime37;
+
+namespace Spine.Implementations.SpineWrappers.V37
+{
+ internal sealed class AnimationState37(AnimationState innerObject, SpineObjectData37 data) : IAnimationState
+ {
+ private readonly AnimationState _o = innerObject;
+ private readonly SpineObjectData37 _data = data;
+
+ private readonly Dictionary _trackEntryPool = [];
+
+ private readonly Dictionary _eventMapping = [];
+ private readonly Dictionary _eventCount = [];
+
+ public AnimationState InnerObject => _o;
+
+ public event IAnimationState.TrackEntryDelegate? Start
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Start += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Start -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Interrupt
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Interrupt += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Interrupt -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? End
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.End += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.End -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Complete
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Complete += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Complete -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Dispose
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Dispose += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Dispose -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
+
+ public void Update(float delta) => _o.Update(delta);
+
+ public void Apply(ISkeleton skeleton)
+ {
+ if (skeleton is Skeleton37 skel)
+ {
+ _o.Apply(skel.InnerObject);
+ return;
+ }
+ throw new ArgumentException($"Received {skeleton.GetType().Name}", nameof(skeleton));
+ }
+
+ ///
+ /// 获取 对象, 不存在则创建
+ ///
+ public ITrackEntry GetTrackEntry(TrackEntry trackEntry)
+ {
+ if (!_trackEntryPool.TryGetValue(trackEntry, out var tr))
+ _trackEntryPool[trackEntry] = tr = new(trackEntry, this, _data);
+ return tr;
+ }
+
+ public IEnumerable IterTracks() => _o.Tracks.Select(t => t is null ? null : GetTrackEntry(t));
+
+ public ITrackEntry? GetCurrent(int index) { var t = _o.GetCurrent(index); return t is null ? null : GetTrackEntry(t); }
+
+ public void ClearTrack(int index) => _o.ClearTrack(index);
+
+ public void ClearTracks() => _o.ClearTracks();
+
+ public ITrackEntry SetAnimation(int trackIndex, string animationName, bool loop)
+ => GetTrackEntry(_o.SetAnimation(trackIndex, animationName, loop));
+
+ public ITrackEntry SetAnimation(int trackIndex, IAnimation animation, bool loop)
+ {
+ if (animation is Animation37 anime)
+ return GetTrackEntry(_o.SetAnimation(trackIndex, anime.InnerObject, loop));
+ throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
+ }
+
+ public ITrackEntry SetEmptyAnimation(int trackIndex, float mixDuration) => GetTrackEntry(_o.SetEmptyAnimation(trackIndex, mixDuration));
+
+ public void SetEmptyAnimations(float mixDuration) => _o.SetEmptyAnimations(mixDuration);
+
+ public ITrackEntry AddAnimation(int trackIndex, string animationName, bool loop, float delay)
+ => GetTrackEntry(_o.AddAnimation(trackIndex, animationName, loop, delay));
+
+ public ITrackEntry AddAnimation(int trackIndex, IAnimation animation, bool loop, float delay)
+ {
+ if (animation is Animation37 anime)
+ return GetTrackEntry(_o.AddAnimation(trackIndex, anime.InnerObject, loop, delay));
+ throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
+ }
+
+ public ITrackEntry AddEmptyAnimation(int trackIndex, float mixDuration, float delay)
+ => GetTrackEntry(_o.AddEmptyAnimation(trackIndex, mixDuration, delay));
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V37/Attachments/Attachment37.cs b/Spine/Implementations/SpineWrappers/V37/Attachments/Attachment37.cs
new file mode 100644
index 0000000..2a23a2c
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V37/Attachments/Attachment37.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime37;
+
+namespace Spine.Implementations.SpineWrappers.V37.Attachments
+{
+ internal abstract class Attachment37(Attachment innerObject) : IAttachment
+ {
+ private readonly Attachment _o = innerObject;
+
+ public virtual Attachment InnerObject => _o;
+
+ public string Name => _o.Name;
+
+ public abstract int ComputeWorldVertices(ISlot slot, ref float[] worldVertices);
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V37/Attachments/BoundingBoxAttachment37.cs b/Spine/Implementations/SpineWrappers/V37/Attachments/BoundingBoxAttachment37.cs
new file mode 100644
index 0000000..270ac8d
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V37/Attachments/BoundingBoxAttachment37.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime37;
+
+namespace Spine.Implementations.SpineWrappers.V37.Attachments
+{
+ internal sealed class BoundingBoxAttachment37(BoundingBoxAttachment innerObject) :
+ Attachment37(innerObject),
+ IBoundingBoxAttachment
+ {
+ private readonly BoundingBoxAttachment _o = innerObject;
+
+ public override BoundingBoxAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot37 st)
+ {
+ var length = _o.WorldVerticesLength;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot37)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V37/Attachments/ClippingAttachment37.cs b/Spine/Implementations/SpineWrappers/V37/Attachments/ClippingAttachment37.cs
new file mode 100644
index 0000000..b8761df
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V37/Attachments/ClippingAttachment37.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime37;
+
+namespace Spine.Implementations.SpineWrappers.V37.Attachments
+{
+ internal sealed class ClippingAttachment37(ClippingAttachment innerObject) :
+ Attachment37(innerObject),
+ IClippingAttachment
+ {
+ private readonly ClippingAttachment _o = innerObject;
+
+ public override ClippingAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot37 st)
+ {
+ var length = _o.WorldVerticesLength;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot37)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V37/Attachments/MeshAttachment37.cs b/Spine/Implementations/SpineWrappers/V37/Attachments/MeshAttachment37.cs
new file mode 100644
index 0000000..6937945
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V37/Attachments/MeshAttachment37.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime37;
+
+namespace Spine.Implementations.SpineWrappers.V37.Attachments
+{
+ internal sealed class MeshAttachment37(MeshAttachment innerObject) :
+ Attachment37(innerObject),
+ IMeshAttachment
+ {
+ private readonly MeshAttachment _o = innerObject;
+
+ public override MeshAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot37 st)
+ {
+ var length = _o.WorldVerticesLength;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot37)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+
+ public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
+
+ public float[] UVs => _o.UVs;
+
+ public int[] Triangles => _o.Triangles;
+
+ public int HullLength => _o.HullLength;
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V37/Attachments/PathAttachment37.cs b/Spine/Implementations/SpineWrappers/V37/Attachments/PathAttachment37.cs
new file mode 100644
index 0000000..412051b
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V37/Attachments/PathAttachment37.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime37;
+
+namespace Spine.Implementations.SpineWrappers.V37.Attachments
+{
+ internal sealed class PathAttachment37(PathAttachment innerObject) :
+ Attachment37(innerObject),
+ IPathAttachment
+ {
+ private readonly PathAttachment _o = innerObject;
+
+ public override PathAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot37 st)
+ {
+ var length = _o.WorldVerticesLength;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot37)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Spine/Implementations/SpineWrappers/V37/Attachments/PointAttachment37.cs b/Spine/Implementations/SpineWrappers/V37/Attachments/PointAttachment37.cs
new file mode 100644
index 0000000..fc9a1fc
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V37/Attachments/PointAttachment37.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime37;
+
+namespace Spine.Implementations.SpineWrappers.V37.Attachments
+{
+ internal sealed class PointAttachment37(PointAttachment innerObject) :
+ Attachment37(innerObject),
+ IPointAttachment
+ {
+ private readonly PointAttachment _o = innerObject;
+
+ public override PointAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot37 st)
+ {
+ if (worldVertices.Length < 2) worldVertices = new float[2];
+ _o.ComputeWorldPosition(st.InnerObject.Bone, out worldVertices[0], out worldVertices[1]);
+ return 2;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot37)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Spine/Implementations/SpineWrappers/V37/Attachments/RegionAttachment37.cs b/Spine/Implementations/SpineWrappers/V37/Attachments/RegionAttachment37.cs
new file mode 100644
index 0000000..f772c09
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V37/Attachments/RegionAttachment37.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime37;
+
+namespace Spine.Implementations.SpineWrappers.V37.Attachments
+{
+ internal sealed class RegionAttachment37(RegionAttachment innerObject) :
+ Attachment37(innerObject),
+ IRegionAttachment
+ {
+ private readonly RegionAttachment _o = innerObject;
+
+ public override RegionAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot37 st)
+ {
+ if (worldVertices.Length < 8) worldVertices = new float[8];
+ _o.ComputeWorldVertices(st.InnerObject.Bone, worldVertices, 0);
+ return 8;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot37)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+
+ public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
+
+ public float[] UVs => _o.UVs;
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V37/Bone37.cs b/Spine/Implementations/SpineWrappers/V37/Bone37.cs
new file mode 100644
index 0000000..571d023
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V37/Bone37.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using SpineRuntime37;
+
+namespace Spine.Implementations.SpineWrappers.V37
+{
+ internal sealed class Bone37(Bone innerObject, Bone37? parent = null) : IBone
+ {
+ private readonly Bone _o = innerObject;
+ private readonly Bone37? _parent = parent;
+
+ public Bone InnerObject => _o;
+
+ public string Name => _o.Data.Name;
+ public int Index => _o.Data.Index;
+
+ public IBone? Parent => _parent;
+ public bool Active => true; // NOTE: 3.7 及以下没有 Active 属性, 此处总是返回 true
+ public float Length => _o.Data.Length;
+ public float WorldX => _o.WorldX;
+ public float WorldY => _o.WorldY;
+ public float A => _o.A;
+ public float B => _o.B;
+ public float C => _o.C;
+ public float D => _o.D;
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V37/Skeleton37.cs b/Spine/Implementations/SpineWrappers/V37/Skeleton37.cs
new file mode 100644
index 0000000..fad3cf6
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V37/Skeleton37.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Collections.Frozen;
+using System.Collections.Immutable;
+using Spine.SpineWrappers;
+using SpineRuntime37;
+
+namespace Spine.Implementations.SpineWrappers.V37
+{
+ internal sealed class Skeleton37 : ISkeleton
+ {
+ private readonly Skeleton _o;
+ private readonly SpineObjectData37 _data;
+
+ private readonly ImmutableArray _bones;
+ private readonly FrozenDictionary _bonesByName;
+ private readonly ImmutableArray _slots;
+ private readonly FrozenDictionary _slotsByName;
+
+ private Skin37? _skin;
+
+ public Skeleton37(Skeleton innerObject, SpineObjectData37 data)
+ {
+ _o = innerObject;
+ _data = data;
+
+ List bones = [];
+ Dictionary bonesByName = [];
+ foreach (var b in _o.Bones)
+ {
+ var bone = new Bone37(b, b.Parent is null ? null : bones[b.Parent.Data.Index]);
+ bones.Add(bone);
+ bonesByName[bone.Name] = bone;
+ }
+ _bones = bones.Cast().ToImmutableArray();
+ _bonesByName = bonesByName.ToFrozenDictionary();
+
+ List slots = [];
+ Dictionary slotsByName = [];
+ foreach (var s in _o.Slots)
+ {
+ var slot = new Slot37(s, _data, bones[s.Bone.Data.Index]);
+ slots.Add(slot);
+ slotsByName[slot.Name] = slot;
+ }
+ _slots = slots.Cast().ToImmutableArray();
+ _slotsByName = slotsByName.ToFrozenDictionary();
+ }
+
+ public Skeleton InnerObject => _o;
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+ public float X { get => _o.X; set => _o.X = value; }
+ public float Y { get => _o.Y; set => _o.Y = value; }
+ public float ScaleX { get => _o.ScaleX; set => _o.ScaleX = value; }
+ public float ScaleY { get => _o.ScaleY; set => _o.ScaleY = value; }
+
+ public ImmutableArray Bones => _bones;
+ public FrozenDictionary BonesByName => _bonesByName;
+ public ImmutableArray Slots => _slots;
+ public FrozenDictionary SlotsByName => _slotsByName;
+
+ public ISkin? Skin
+ {
+ get => _skin;
+ set
+ {
+ if (value is null)
+ {
+ _o.Skin = null;
+ _skin = null;
+ return;
+ }
+ if (value is Skin37 sk)
+ {
+ _o.Skin = sk.InnerObject;
+ _skin = sk;
+ return;
+ }
+ throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
+ }
+ }
+
+ public IEnumerable IterDrawOrder() => _o.DrawOrder.Select(s => _slots[s.Data.Index]);
+ public void UpdateCache() => _o.UpdateCache();
+ public void UpdateWorldTransform(ISkeleton.Physics physics) => _o.UpdateWorldTransform();
+ public void SetToSetupPose() => _o.SetToSetupPose();
+ public void SetBonesToSetupPose() => _o.SetBonesToSetupPose();
+ public void SetSlotsToSetupPose() => _o.SetSlotsToSetupPose();
+ public void Update(float delta) => _o.Update(delta);
+
+ public void GetBounds(out float x, out float y, out float w, out float h)
+ {
+ float[] _ = [];
+ _o.GetBounds(out x, out y, out w, out h, ref _);
+ }
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V37/SkeletonClipping37.cs b/Spine/Implementations/SpineWrappers/V37/SkeletonClipping37.cs
new file mode 100644
index 0000000..ed6d412
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V37/SkeletonClipping37.cs
@@ -0,0 +1,56 @@
+using Spine.SpineWrappers;
+using Spine.SpineWrappers.Attachments;
+using Spine.Utils;
+using SpineRuntime37;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Spine.Implementations.SpineWrappers.V37
+{
+ internal sealed class SkeletonClipping37 : ISkeletonClipping
+ {
+ private readonly SkeletonClipping _o = new();
+
+ public bool IsClipping => _o.IsClipping;
+
+ public float[] ClippedVertices => _o.ClippedVertices.Items;
+
+ public int ClippedVerticesLength => _o.ClippedVertices.Count;
+
+ public int[] ClippedTriangles => _o.ClippedTriangles.Items;
+
+ public int ClippedTrianglesLength => _o.ClippedTriangles.Count;
+
+ public float[] ClippedUVs => _o.ClippedUVs.Items;
+
+ public void ClipTriangles(float[] vertices, int verticesLength, int[] triangles, int trianglesLength, float[] uvs)
+ => _o.ClipTriangles(vertices, verticesLength, triangles, trianglesLength, uvs);
+
+ public void ClipStart(ISlot slot, IClippingAttachment clippingAttachment)
+ {
+ if (slot is Slot37 st && clippingAttachment is Attachments.ClippingAttachment37 att)
+ {
+ _o.ClipStart(st.InnerObject, att.InnerObject);
+ return;
+ }
+ throw new ArgumentException($"Received {slot.GetType().Name} {clippingAttachment.GetType().Name}");
+ }
+
+ public void ClipEnd(ISlot slot)
+ {
+ if (slot is Slot37 st)
+ {
+ _o.ClipEnd(st.InnerObject);
+ return;
+ }
+ throw new ArgumentException($"Received {slot.GetType().Name}", nameof(slot));
+ }
+
+ public void ClipEnd() => _o.ClipEnd();
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V37/Skin37.cs b/Spine/Implementations/SpineWrappers/V37/Skin37.cs
new file mode 100644
index 0000000..41c41fc
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V37/Skin37.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using SpineRuntime37;
+
+namespace Spine.Implementations.SpineWrappers.V37
+{
+ internal sealed class Skin37 : ISkin
+ {
+ private readonly Skin _o;
+
+ ///
+ /// 使用指定名字创建空皮肤
+ ///
+ public Skin37(string name) => _o = new(name);
+
+ ///
+ /// 包装已有皮肤对象
+ ///
+ public Skin37(Skin innerObject) => _o = innerObject;
+
+ public Skin InnerObject => _o;
+
+ public string Name => _o.Name;
+
+ public void AddSkin(ISkin skin)
+ {
+ if (skin is Skin37 sk)
+ {
+ // NOTE: 3.7 及以下不支持 AddSkin
+ foreach (var (k, v) in sk._o.Attachments)
+ _o.AddAttachment(k.slotIndex, k.name, v);
+ return;
+ }
+ throw new ArgumentException($"Received {skin.GetType().Name}", nameof(skin));
+ }
+
+ public void Clear() => _o.Attachments.Clear();
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V37/Slot37.cs b/Spine/Implementations/SpineWrappers/V37/Slot37.cs
new file mode 100644
index 0000000..28a68a7
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V37/Slot37.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Frozen;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.Utils;
+using Spine.SpineWrappers;
+using SpineRuntime37;
+
+namespace Spine.Implementations.SpineWrappers.V37
+{
+ internal sealed class Slot37 : ISlot
+ {
+ private readonly Slot _o;
+ private readonly SpineObjectData37 _data;
+
+ private readonly Bone37 _bone;
+ private readonly SFML.Graphics.BlendMode _blendMode;
+
+ public Slot37(Slot innerObject, SpineObjectData37 data, Bone37 bone)
+ {
+ _o = innerObject;
+ _data = data;
+
+ _bone = bone;
+ _blendMode = _o.Data.BlendMode switch
+ {
+ BlendMode.Normal => SFMLBlendMode.NormalPma,
+ BlendMode.Additive => SFMLBlendMode.AdditivePma,
+ BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
+ BlendMode.Screen => SFMLBlendMode.ScreenPma,
+ _ => throw new NotImplementedException($"{_o.Data.BlendMode}"),
+ };
+ }
+
+ public Slot InnerObject => _o;
+
+ public string Name => _o.Data.Name;
+ public int Index => _o.Data.Index;
+ public SFML.Graphics.BlendMode Blend => _blendMode;
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+ public IBone Bone => _bone;
+
+ public Spine.SpineWrappers.Attachments.IAttachment? Attachment
+ {
+ get
+ {
+ if (_o.Attachment is Attachment att)
+ {
+ return _data.SlotAttachments[Name][att.Name];
+ }
+ return null;
+ }
+
+ set
+ {
+ if (value is null)
+ {
+ _o.Attachment = null;
+ return;
+ }
+ if (value is Attachments.Attachment37 att)
+ {
+ _o.Attachment = att.InnerObject;
+ return;
+ }
+ throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
+ }
+ }
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V37/SpineObjectData37.cs b/Spine/Implementations/SpineWrappers/V37/SpineObjectData37.cs
new file mode 100644
index 0000000..4fdfee2
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V37/SpineObjectData37.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Frozen;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.Utils;
+using Spine.SpineWrappers;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime37;
+using Spine.Implementations.SpineWrappers.V37.Attachments;
+
+namespace Spine.Implementations.SpineWrappers.V37
+{
+ [SpineImplementation(3, 7)]
+ internal sealed class SpineObjectData37 : SpineObjectData
+ {
+ private readonly Atlas _atlas;
+ private readonly SkeletonData _skeletonData;
+ private readonly AnimationStateData _animationStateData;
+
+ private readonly ImmutableArray _skins;
+ private readonly FrozenDictionary _skinsByName;
+ private readonly FrozenDictionary> _slotAttachments;
+ private readonly ImmutableArray _animations;
+ private readonly FrozenDictionary _animationsByName;
+
+ public SpineObjectData37(string skelPath, string atlasPath) : base(skelPath, atlasPath)
+ {
+ // 加载 atlas
+ try { _atlas = new Atlas(atlasPath, _textureLoader); }
+ catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
+
+ try
+ {
+ if (Utf8Validator.IsUtf8(skelPath))
+ _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
+ else
+ _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
+ }
+ catch (Exception ex)
+ {
+ _atlas.Dispose();
+ throw new InvalidDataException($"Failed to load skeleton file {skelPath}", ex);
+ }
+
+ // 加载动画数据
+ _animationStateData = new AnimationStateData(_skeletonData);
+
+ // 整理皮肤和附件
+ Dictionary> slotAttachments = [];
+ List skins = [];
+ Dictionary skinsByName = [];
+ foreach (var s in _skeletonData.Skins)
+ {
+ var skin = new Skin37(s);
+ skins.Add(skin);
+ skinsByName[s.Name] = skin;
+ foreach (var (k, att) in s.Attachments)
+ {
+ var slotName = _skeletonData.Slots.Items[k.slotIndex].Name;
+ if (!slotAttachments.TryGetValue(slotName, out var attachments))
+ slotAttachments[slotName] = attachments = [];
+
+ attachments[att.Name] = att switch
+ {
+ RegionAttachment regionAtt => new RegionAttachment37(regionAtt),
+ MeshAttachment meshAtt => new MeshAttachment37(meshAtt),
+ ClippingAttachment clipAtt => new ClippingAttachment37(clipAtt),
+ BoundingBoxAttachment bbAtt => new BoundingBoxAttachment37(bbAtt),
+ PathAttachment pathAtt => new PathAttachment37(pathAtt),
+ PointAttachment ptAtt => new PointAttachment37(ptAtt),
+ _ => throw new InvalidOperationException($"Unrecognized attachment type {att.GetType().FullName}")
+ };
+ }
+ }
+ _slotAttachments = slotAttachments.ToFrozenDictionary(it => it.Key, it => it.Value.ToFrozenDictionary());
+ _skins = skins.ToImmutableArray();
+ _skinsByName = skinsByName.ToFrozenDictionary();
+
+ // 整理所有动画数据
+ List animations = [];
+ Dictionary animationsByName = [];
+ foreach (var a in _skeletonData.Animations)
+ {
+ var anime = new Animation37(a);
+ animations.Add(anime);
+ animationsByName[anime.Name] = anime;
+ }
+ _animations = animations.ToImmutableArray();
+ _animationsByName = animationsByName.ToFrozenDictionary();
+ }
+
+ public override string SkeletonVersion => _skeletonData.Version;
+
+ public override ImmutableArray Skins => _skins;
+
+ public override FrozenDictionary SkinsByName => _skinsByName;
+
+ public override FrozenDictionary> SlotAttachments => _slotAttachments;
+
+ public override float DefaultMix { get => _animationStateData.DefaultMix; set => _animationStateData.DefaultMix = value; }
+
+ public override ImmutableArray Animations => _animations;
+
+ public override FrozenDictionary AnimationsByName => _animationsByName;
+
+ protected override void DisposeAtlas() => _atlas.Dispose();
+
+ public override ISkeleton CreateSkeleton() => new Skeleton37(new(_skeletonData), this);
+
+ public override IAnimationState CreateAnimationState() => new AnimationState37(new(_animationStateData), this);
+
+ public override ISkeletonClipping CreateSkeletonClipping() => new SkeletonClipping37();
+
+ public override ISkin CreateSkin(string name) => new Skin37(name);
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V37/TrackEntry37.cs b/Spine/Implementations/SpineWrappers/V37/TrackEntry37.cs
new file mode 100644
index 0000000..cf1c90e
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V37/TrackEntry37.cs
@@ -0,0 +1,185 @@
+using Spine.SpineWrappers;
+using SpineRuntime37;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Spine.Implementations.SpineWrappers.V37
+{
+ internal sealed class TrackEntry37(TrackEntry innerObject, AnimationState37 animationState, SpineObjectData37 data): ITrackEntry
+ {
+ private readonly TrackEntry _o = innerObject;
+ private readonly AnimationState37 _animationState = animationState;
+ private readonly SpineObjectData37 _data = data;
+
+ private readonly Dictionary _eventMapping = [];
+ private readonly Dictionary _eventCount = [];
+
+ public TrackEntry InnerObject => _o;
+
+ public event IAnimationState.TrackEntryDelegate? Start
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Start += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Start -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Interrupt
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Interrupt += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Interrupt -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? End
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.End += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.End -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Complete
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Complete += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Complete -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Dispose
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Dispose += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Dispose -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public int TrackIndex { get => _o.TrackIndex; }
+
+ public IAnimation Animation { get => _data.AnimationsByName[_o.Animation.Name]; }
+
+ public ITrackEntry? Next { get { var t = _o.Next; return t is null ? null : _animationState.GetTrackEntry(t); } }
+
+ public bool Loop { get => _o.Loop; set => _o.Loop = value; }
+
+ public float TrackTime { get => _o.TrackTime; set => _o.TrackTime = value; }
+
+ public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
+
+ public float Alpha { get => _o.Alpha; set => _o.Alpha = value; }
+
+ public float MixDuration { get => _o.MixDuration; set => _o.MixDuration = value; }
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V38/Animation38.cs b/Spine/Implementations/SpineWrappers/V38/Animation38.cs
new file mode 100644
index 0000000..5b05e73
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V38/Animation38.cs
@@ -0,0 +1,23 @@
+using Spine.SpineWrappers;
+using SpineRuntime38;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Spine.Implementations.SpineWrappers.V38
+{
+ internal sealed class Animation38(Animation innerObject) : IAnimation
+ {
+ private readonly Animation _o = innerObject;
+
+ public Animation InnerObject => _o;
+
+ public string Name => _o.Name;
+
+ public float Duration => _o.Duration;
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V38/AnimationState38.cs b/Spine/Implementations/SpineWrappers/V38/AnimationState38.cs
new file mode 100644
index 0000000..db83d08
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V38/AnimationState38.cs
@@ -0,0 +1,229 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using SpineRuntime38;
+
+namespace Spine.Implementations.SpineWrappers.V38
+{
+ internal sealed class AnimationState38(AnimationState innerObject, SpineObjectData38 data) : IAnimationState
+ {
+ private readonly AnimationState _o = innerObject;
+ private readonly SpineObjectData38 _data = data;
+
+ private readonly Dictionary _trackEntryPool = [];
+
+ private readonly Dictionary _eventMapping = [];
+ private readonly Dictionary _eventCount = [];
+
+ public AnimationState InnerObject => _o;
+
+ public event IAnimationState.TrackEntryDelegate? Start
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Start += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Start -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Interrupt
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Interrupt += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Interrupt -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? End
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.End += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.End -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Complete
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Complete += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Complete -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Dispose
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Dispose += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Dispose -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
+
+ public void Update(float delta) => _o.Update(delta);
+
+ public void Apply(ISkeleton skeleton)
+ {
+ if (skeleton is Skeleton38 skel)
+ {
+ _o.Apply(skel.InnerObject);
+ return;
+ }
+ throw new ArgumentException($"Received {skeleton.GetType().Name}", nameof(skeleton));
+ }
+
+ ///
+ /// 获取 对象, 不存在则创建
+ ///
+ public ITrackEntry GetTrackEntry(TrackEntry trackEntry)
+ {
+ if (!_trackEntryPool.TryGetValue(trackEntry, out var tr))
+ _trackEntryPool[trackEntry] = tr = new(trackEntry, this, _data);
+ return tr;
+ }
+
+ public IEnumerable IterTracks() => _o.Tracks.Select(t => t is null ? null : GetTrackEntry(t));
+
+ public ITrackEntry? GetCurrent(int index) { var t = _o.GetCurrent(index); return t is null ? null : GetTrackEntry(t); }
+
+ public void ClearTrack(int index) => _o.ClearTrack(index);
+
+ public void ClearTracks() => _o.ClearTracks();
+
+ public ITrackEntry SetAnimation(int trackIndex, string animationName, bool loop)
+ => GetTrackEntry(_o.SetAnimation(trackIndex, animationName, loop));
+
+ public ITrackEntry SetAnimation(int trackIndex, IAnimation animation, bool loop)
+ {
+ if (animation is Animation38 anime)
+ return GetTrackEntry(_o.SetAnimation(trackIndex, anime.InnerObject, loop));
+ throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
+ }
+
+ public ITrackEntry SetEmptyAnimation(int trackIndex, float mixDuration) => GetTrackEntry(_o.SetEmptyAnimation(trackIndex, mixDuration));
+
+ public void SetEmptyAnimations(float mixDuration) => _o.SetEmptyAnimations(mixDuration);
+
+ public ITrackEntry AddAnimation(int trackIndex, string animationName, bool loop, float delay)
+ => GetTrackEntry(_o.AddAnimation(trackIndex, animationName, loop, delay));
+
+ public ITrackEntry AddAnimation(int trackIndex, IAnimation animation, bool loop, float delay)
+ {
+ if (animation is Animation38 anime)
+ return GetTrackEntry(_o.AddAnimation(trackIndex, anime.InnerObject, loop, delay));
+ throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
+ }
+
+ public ITrackEntry AddEmptyAnimation(int trackIndex, float mixDuration, float delay)
+ => GetTrackEntry(_o.AddEmptyAnimation(trackIndex, mixDuration, delay));
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V38/Attachments/Attachment38.cs b/Spine/Implementations/SpineWrappers/V38/Attachments/Attachment38.cs
new file mode 100644
index 0000000..277cfbd
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V38/Attachments/Attachment38.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime38;
+using SpineRuntime38.Attachments;
+
+namespace Spine.Implementations.SpineWrappers.V38.Attachments
+{
+ internal abstract class Attachment38(Attachment innerObject) : IAttachment
+ {
+ private readonly Attachment _o = innerObject;
+
+ public virtual Attachment InnerObject => _o;
+
+ public string Name => _o.Name;
+
+ public abstract int ComputeWorldVertices(ISlot slot, ref float[] worldVertices);
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V38/Attachments/BoundingBoxAttachment38.cs b/Spine/Implementations/SpineWrappers/V38/Attachments/BoundingBoxAttachment38.cs
new file mode 100644
index 0000000..4ce6da8
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V38/Attachments/BoundingBoxAttachment38.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime38;
+using SpineRuntime38.Attachments;
+
+namespace Spine.Implementations.SpineWrappers.V38.Attachments
+{
+ internal sealed class BoundingBoxAttachment38(BoundingBoxAttachment innerObject) :
+ Attachment38(innerObject),
+ IBoundingBoxAttachment
+ {
+ private readonly BoundingBoxAttachment _o = innerObject;
+
+ public override BoundingBoxAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot38 st)
+ {
+ var length = _o.WorldVerticesLength;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot38)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V38/Attachments/ClippingAttachment38.cs b/Spine/Implementations/SpineWrappers/V38/Attachments/ClippingAttachment38.cs
new file mode 100644
index 0000000..ef75ecb
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V38/Attachments/ClippingAttachment38.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime38;
+using SpineRuntime38.Attachments;
+
+namespace Spine.Implementations.SpineWrappers.V38.Attachments
+{
+ internal sealed class ClippingAttachment38(ClippingAttachment innerObject) :
+ Attachment38(innerObject),
+ IClippingAttachment
+ {
+ private readonly ClippingAttachment _o = innerObject;
+
+ public override ClippingAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot38 st)
+ {
+ var length = _o.WorldVerticesLength;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot38)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V38/Attachments/MeshAttachment38.cs b/Spine/Implementations/SpineWrappers/V38/Attachments/MeshAttachment38.cs
new file mode 100644
index 0000000..46bd51e
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V38/Attachments/MeshAttachment38.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime38;
+using SpineRuntime38.Attachments;
+
+namespace Spine.Implementations.SpineWrappers.V38.Attachments
+{
+ internal sealed class MeshAttachment38(MeshAttachment innerObject) :
+ Attachment38(innerObject),
+ IMeshAttachment
+ {
+ private readonly MeshAttachment _o = innerObject;
+
+ public override MeshAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot38 st)
+ {
+ var length = _o.WorldVerticesLength;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot38)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+
+ public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
+
+ public float[] UVs => _o.UVs;
+
+ public int[] Triangles => _o.Triangles;
+
+ public int HullLength => _o.HullLength;
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V38/Attachments/PathAttachment38.cs b/Spine/Implementations/SpineWrappers/V38/Attachments/PathAttachment38.cs
new file mode 100644
index 0000000..ef908f8
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V38/Attachments/PathAttachment38.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime38;
+using SpineRuntime38.Attachments;
+
+namespace Spine.Implementations.SpineWrappers.V38.Attachments
+{
+ internal sealed class PathAttachment38(PathAttachment innerObject) :
+ Attachment38(innerObject),
+ IPathAttachment
+ {
+ private readonly PathAttachment _o = innerObject;
+
+ public override PathAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot38 st)
+ {
+ var length = _o.WorldVerticesLength;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot38)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Spine/Implementations/SpineWrappers/V38/Attachments/PointAttachment38.cs b/Spine/Implementations/SpineWrappers/V38/Attachments/PointAttachment38.cs
new file mode 100644
index 0000000..3d1c80c
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V38/Attachments/PointAttachment38.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime38;
+using SpineRuntime38.Attachments;
+
+namespace Spine.Implementations.SpineWrappers.V38.Attachments
+{
+ internal sealed class PointAttachment38(PointAttachment innerObject) :
+ Attachment38(innerObject),
+ IPointAttachment
+ {
+ private readonly PointAttachment _o = innerObject;
+
+ public override PointAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot38 st)
+ {
+ if (worldVertices.Length < 2) worldVertices = new float[2];
+ _o.ComputeWorldPosition(st.InnerObject.Bone, out worldVertices[0], out worldVertices[1]);
+ return 2;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot38)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Spine/Implementations/SpineWrappers/V38/Attachments/RegionAttachment38.cs b/Spine/Implementations/SpineWrappers/V38/Attachments/RegionAttachment38.cs
new file mode 100644
index 0000000..5d901f6
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V38/Attachments/RegionAttachment38.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime38;
+using SpineRuntime38.Attachments;
+
+namespace Spine.Implementations.SpineWrappers.V38.Attachments
+{
+ internal sealed class RegionAttachment38(RegionAttachment innerObject) :
+ Attachment38(innerObject),
+ IRegionAttachment
+ {
+ private readonly RegionAttachment _o = innerObject;
+
+ public override RegionAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot38 st)
+ {
+ if (worldVertices.Length < 8) worldVertices = new float[8];
+ _o.ComputeWorldVertices(st.InnerObject.Bone, worldVertices, 0);
+ return 8;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot38)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+
+ public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
+
+ public float[] UVs => _o.UVs;
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V38/Bone38.cs b/Spine/Implementations/SpineWrappers/V38/Bone38.cs
new file mode 100644
index 0000000..6b49878
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V38/Bone38.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using SpineRuntime38;
+
+namespace Spine.Implementations.SpineWrappers.V38
+{
+ internal sealed class Bone38(Bone innerObject, Bone38? parent = null) : IBone
+ {
+ private readonly Bone _o = innerObject;
+ private readonly Bone38? _parent = parent;
+
+ public Bone InnerObject => _o;
+
+ public string Name => _o.Data.Name;
+ public int Index => _o.Data.Index;
+
+ public IBone? Parent => _parent;
+ public bool Active => _o.Active;
+ public float Length => _o.Data.Length;
+ public float WorldX => _o.WorldX;
+ public float WorldY => _o.WorldY;
+ public float A => _o.A;
+ public float B => _o.B;
+ public float C => _o.C;
+ public float D => _o.D;
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V38/Skeleton38.cs b/Spine/Implementations/SpineWrappers/V38/Skeleton38.cs
new file mode 100644
index 0000000..7b11cdb
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V38/Skeleton38.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Collections.Frozen;
+using System.Collections.Immutable;
+using Spine.SpineWrappers;
+using SpineRuntime38;
+
+namespace Spine.Implementations.SpineWrappers.V38
+{
+ internal sealed class Skeleton38 : ISkeleton
+ {
+ private readonly Skeleton _o;
+ private readonly SpineObjectData38 _data;
+
+ private readonly ImmutableArray _bones;
+ private readonly FrozenDictionary _bonesByName;
+ private readonly ImmutableArray _slots;
+ private readonly FrozenDictionary _slotsByName;
+
+ private Skin38? _skin;
+
+ public Skeleton38(Skeleton innerObject, SpineObjectData38 data)
+ {
+ _o = innerObject;
+ _data = data;
+
+ List bones = [];
+ Dictionary bonesByName = [];
+ foreach (var b in _o.Bones)
+ {
+ var bone = new Bone38(b, b.Parent is null ? null : bones[b.Parent.Data.Index]);
+ bones.Add(bone);
+ bonesByName[bone.Name] = bone;
+ }
+ _bones = bones.Cast().ToImmutableArray();
+ _bonesByName = bonesByName.ToFrozenDictionary();
+
+ List slots = [];
+ Dictionary slotsByName = [];
+ foreach (var s in _o.Slots)
+ {
+ var slot = new Slot38(s, _data, bones[s.Bone.Data.Index]);
+ slots.Add(slot);
+ slotsByName[slot.Name] = slot;
+ }
+ _slots = slots.Cast().ToImmutableArray();
+ _slotsByName = slotsByName.ToFrozenDictionary();
+ }
+
+ public Skeleton InnerObject => _o;
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+ public float X { get => _o.X; set => _o.X = value; }
+ public float Y { get => _o.Y; set => _o.Y = value; }
+ public float ScaleX { get => _o.ScaleX; set => _o.ScaleX = value; }
+ public float ScaleY { get => _o.ScaleY; set => _o.ScaleY = value; }
+
+ public ImmutableArray Bones => _bones;
+ public FrozenDictionary BonesByName => _bonesByName;
+ public ImmutableArray Slots => _slots;
+ public FrozenDictionary SlotsByName => _slotsByName;
+
+ public ISkin? Skin
+ {
+ get => _skin;
+ set
+ {
+ if (value is null)
+ {
+ _o.Skin = null;
+ _skin = null;
+ return;
+ }
+ if (value is Skin38 sk)
+ {
+ _o.Skin = sk.InnerObject;
+ _skin = sk;
+ return;
+ }
+ throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
+ }
+ }
+
+ public IEnumerable IterDrawOrder() => _o.DrawOrder.Select(s => _slots[s.Data.Index]);
+ public void UpdateCache() => _o.UpdateCache();
+ public void UpdateWorldTransform(ISkeleton.Physics physics) => _o.UpdateWorldTransform();
+ public void SetToSetupPose() => _o.SetToSetupPose();
+ public void SetBonesToSetupPose() => _o.SetBonesToSetupPose();
+ public void SetSlotsToSetupPose() => _o.SetSlotsToSetupPose();
+ public void Update(float delta) => _o.Update(delta);
+
+ public void GetBounds(out float x, out float y, out float w, out float h)
+ {
+ float[] _ = [];
+ _o.GetBounds(out x, out y, out w, out h, ref _);
+ }
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V38/SkeletonClipping38.cs b/Spine/Implementations/SpineWrappers/V38/SkeletonClipping38.cs
new file mode 100644
index 0000000..c3e07dd
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V38/SkeletonClipping38.cs
@@ -0,0 +1,56 @@
+using Spine.SpineWrappers;
+using Spine.SpineWrappers.Attachments;
+using Spine.Utils;
+using SpineRuntime38;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Spine.Implementations.SpineWrappers.V38
+{
+ internal sealed class SkeletonClipping38 : ISkeletonClipping
+ {
+ private readonly SkeletonClipping _o = new();
+
+ public bool IsClipping => _o.IsClipping;
+
+ public float[] ClippedVertices => _o.ClippedVertices.Items;
+
+ public int ClippedVerticesLength => _o.ClippedVertices.Count;
+
+ public int[] ClippedTriangles => _o.ClippedTriangles.Items;
+
+ public int ClippedTrianglesLength => _o.ClippedTriangles.Count;
+
+ public float[] ClippedUVs => _o.ClippedUVs.Items;
+
+ public void ClipTriangles(float[] vertices, int verticesLength, int[] triangles, int trianglesLength, float[] uvs)
+ => _o.ClipTriangles(vertices, verticesLength, triangles, trianglesLength, uvs);
+
+ public void ClipStart(ISlot slot, IClippingAttachment clippingAttachment)
+ {
+ if (slot is Slot38 st && clippingAttachment is Attachments.ClippingAttachment38 att)
+ {
+ _o.ClipStart(st.InnerObject, att.InnerObject);
+ return;
+ }
+ throw new ArgumentException($"Received {slot.GetType().Name} {clippingAttachment.GetType().Name}");
+ }
+
+ public void ClipEnd(ISlot slot)
+ {
+ if (slot is Slot38 st)
+ {
+ _o.ClipEnd(st.InnerObject);
+ return;
+ }
+ throw new ArgumentException($"Received {slot.GetType().Name}", nameof(slot));
+ }
+
+ public void ClipEnd() => _o.ClipEnd();
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V38/Skin38.cs b/Spine/Implementations/SpineWrappers/V38/Skin38.cs
new file mode 100644
index 0000000..77af872
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V38/Skin38.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using SpineRuntime38;
+
+namespace Spine.Implementations.SpineWrappers.V38
+{
+ internal sealed class Skin38 : ISkin
+ {
+ private readonly Skin _o;
+
+ ///
+ /// 使用指定名字创建空皮肤
+ ///
+ public Skin38(string name) => _o = new(name);
+
+ ///
+ /// 包装已有皮肤对象
+ ///
+ public Skin38(Skin innerObject) => _o = innerObject;
+
+ public Skin InnerObject => _o;
+
+ public string Name => _o.Name;
+
+ public void AddSkin(ISkin skin)
+ {
+ if (skin is Skin38 sk)
+ {
+ _o.AddSkin(sk._o);
+ return;
+ }
+ throw new ArgumentException($"Received {skin.GetType().Name}", nameof(skin));
+ }
+
+ public void Clear() => _o.Clear();
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V38/Slot38.cs b/Spine/Implementations/SpineWrappers/V38/Slot38.cs
new file mode 100644
index 0000000..d640a1c
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V38/Slot38.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Frozen;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.Utils;
+using Spine.SpineWrappers;
+using SpineRuntime38;
+using SpineRuntime38.Attachments;
+
+namespace Spine.Implementations.SpineWrappers.V38
+{
+ internal sealed class Slot38 : ISlot
+ {
+ private readonly Slot _o;
+ private readonly SpineObjectData38 _data;
+
+ private readonly Bone38 _bone;
+ private readonly SFML.Graphics.BlendMode _blendMode;
+
+ public Slot38(Slot innerObject, SpineObjectData38 data, Bone38 bone)
+ {
+ _o = innerObject;
+ _data = data;
+
+ _bone = bone;
+ _blendMode = _o.Data.BlendMode switch
+ {
+ BlendMode.Normal => SFMLBlendMode.NormalPma,
+ BlendMode.Additive => SFMLBlendMode.AdditivePma,
+ BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
+ BlendMode.Screen => SFMLBlendMode.ScreenPma,
+ _ => throw new NotImplementedException($"{_o.Data.BlendMode}"),
+ };
+ }
+
+ public Slot InnerObject => _o;
+
+ public string Name => _o.Data.Name;
+ public int Index => _o.Data.Index;
+ public SFML.Graphics.BlendMode Blend => _blendMode;
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+ public IBone Bone => _bone;
+
+ public Spine.SpineWrappers.Attachments.IAttachment? Attachment
+ {
+ get
+ {
+ if (_o.Attachment is Attachment att)
+ {
+ return _data.SlotAttachments[Name][att.Name];
+ }
+ return null;
+ }
+
+ set
+ {
+ if (value is null)
+ {
+ _o.Attachment = null;
+ return;
+ }
+ if (value is Attachments.Attachment38 att)
+ {
+ _o.Attachment = att.InnerObject;
+ return;
+ }
+ throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
+ }
+ }
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V38/SpineObjectData38.cs b/Spine/Implementations/SpineWrappers/V38/SpineObjectData38.cs
new file mode 100644
index 0000000..b46076a
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V38/SpineObjectData38.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Frozen;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.Utils;
+using Spine.SpineWrappers;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime38;
+using SpineRuntime38.Attachments;
+using Spine.Implementations.SpineWrappers.V38.Attachments;
+
+namespace Spine.Implementations.SpineWrappers.V38
+{
+ [SpineImplementation(3, 8)]
+ internal sealed class SpineObjectData38 : SpineObjectData
+ {
+ private readonly Atlas _atlas;
+ private readonly SkeletonData _skeletonData;
+ private readonly AnimationStateData _animationStateData;
+
+ private readonly ImmutableArray _skins;
+ private readonly FrozenDictionary _skinsByName;
+ private readonly FrozenDictionary> _slotAttachments;
+ private readonly ImmutableArray _animations;
+ private readonly FrozenDictionary _animationsByName;
+
+ public SpineObjectData38(string skelPath, string atlasPath) : base(skelPath, atlasPath)
+ {
+ // 加载 atlas
+ try { _atlas = new Atlas(atlasPath, _textureLoader); }
+ catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
+
+ try
+ {
+ if (Utf8Validator.IsUtf8(skelPath))
+ _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
+ else
+ _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
+ }
+ catch (Exception ex)
+ {
+ _atlas.Dispose();
+ throw new InvalidDataException($"Failed to load skeleton file {skelPath}", ex);
+ }
+
+ // 加载动画数据
+ _animationStateData = new AnimationStateData(_skeletonData);
+
+ // 整理皮肤和附件
+ Dictionary> slotAttachments = [];
+ List skins = [];
+ Dictionary skinsByName = [];
+ foreach (var s in _skeletonData.Skins)
+ {
+ var skin = new Skin38(s);
+ skins.Add(skin);
+ skinsByName[s.Name] = skin;
+ foreach (var (k, att) in s.Attachments)
+ {
+ var slotName = _skeletonData.Slots.Items[k.SlotIndex].Name;
+ if (!slotAttachments.TryGetValue(slotName, out var attachments))
+ slotAttachments[slotName] = attachments = [];
+
+ attachments[att.Name] = att switch
+ {
+ RegionAttachment regionAtt => new RegionAttachment38(regionAtt),
+ MeshAttachment meshAtt => new MeshAttachment38(meshAtt),
+ ClippingAttachment clipAtt => new ClippingAttachment38(clipAtt),
+ BoundingBoxAttachment bbAtt => new BoundingBoxAttachment38(bbAtt),
+ PathAttachment pathAtt => new PathAttachment38(pathAtt),
+ PointAttachment ptAtt => new PointAttachment38(ptAtt),
+ _ => throw new InvalidOperationException($"Unrecognized attachment type {att.GetType().FullName}")
+ };
+ }
+ }
+ _slotAttachments = slotAttachments.ToFrozenDictionary(it => it.Key, it => it.Value.ToFrozenDictionary());
+ _skins = skins.ToImmutableArray();
+ _skinsByName = skinsByName.ToFrozenDictionary();
+
+ // 整理所有动画数据
+ List animations = [];
+ Dictionary animationsByName = [];
+ foreach (var a in _skeletonData.Animations)
+ {
+ var anime = new Animation38(a);
+ animations.Add(anime);
+ animationsByName[anime.Name] = anime;
+ }
+ _animations = animations.ToImmutableArray();
+ _animationsByName = animationsByName.ToFrozenDictionary();
+ }
+
+ public override string SkeletonVersion => _skeletonData.Version;
+
+ public override ImmutableArray Skins => _skins;
+
+ public override FrozenDictionary SkinsByName => _skinsByName;
+
+ public override FrozenDictionary> SlotAttachments => _slotAttachments;
+
+ public override float DefaultMix { get => _animationStateData.DefaultMix; set => _animationStateData.DefaultMix = value; }
+
+ public override ImmutableArray Animations => _animations;
+
+ public override FrozenDictionary AnimationsByName => _animationsByName;
+
+ protected override void DisposeAtlas() => _atlas.Dispose();
+
+ public override ISkeleton CreateSkeleton() => new Skeleton38(new(_skeletonData), this);
+
+ public override IAnimationState CreateAnimationState() => new AnimationState38(new(_animationStateData), this);
+
+ public override ISkeletonClipping CreateSkeletonClipping() => new SkeletonClipping38();
+
+ public override ISkin CreateSkin(string name) => new Skin38(name);
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V38/TrackEntry38.cs b/Spine/Implementations/SpineWrappers/V38/TrackEntry38.cs
new file mode 100644
index 0000000..48f8822
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V38/TrackEntry38.cs
@@ -0,0 +1,185 @@
+using Spine.SpineWrappers;
+using SpineRuntime38;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Spine.Implementations.SpineWrappers.V38
+{
+ internal sealed class TrackEntry38(TrackEntry innerObject, AnimationState38 animationState, SpineObjectData38 data): ITrackEntry
+ {
+ private readonly TrackEntry _o = innerObject;
+ private readonly AnimationState38 _animationState = animationState;
+ private readonly SpineObjectData38 _data = data;
+
+ private readonly Dictionary _eventMapping = [];
+ private readonly Dictionary _eventCount = [];
+
+ public TrackEntry InnerObject => _o;
+
+ public event IAnimationState.TrackEntryDelegate? Start
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Start += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Start -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Interrupt
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Interrupt += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Interrupt -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? End
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.End += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.End -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Complete
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Complete += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Complete -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Dispose
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Dispose += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Dispose -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public int TrackIndex { get => _o.TrackIndex; }
+
+ public IAnimation Animation { get => _data.AnimationsByName[_o.Animation.Name]; }
+
+ public ITrackEntry? Next { get { var t = _o.Next; return t is null ? null : _animationState.GetTrackEntry(t); } }
+
+ public bool Loop { get => _o.Loop; set => _o.Loop = value; }
+
+ public float TrackTime { get => _o.TrackTime; set => _o.TrackTime = value; }
+
+ public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
+
+ public float Alpha { get => _o.Alpha; set => _o.Alpha = value; }
+
+ public float MixDuration { get => _o.MixDuration; set => _o.MixDuration = value; }
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V40/Animation40.cs b/Spine/Implementations/SpineWrappers/V40/Animation40.cs
new file mode 100644
index 0000000..5198441
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V40/Animation40.cs
@@ -0,0 +1,23 @@
+using Spine.SpineWrappers;
+using SpineRuntime40;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Spine.Implementations.SpineWrappers.V40
+{
+ internal sealed class Animation40(Animation innerObject) : IAnimation
+ {
+ private readonly Animation _o = innerObject;
+
+ public Animation InnerObject => _o;
+
+ public string Name => _o.Name;
+
+ public float Duration => _o.Duration;
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V40/AnimationState40.cs b/Spine/Implementations/SpineWrappers/V40/AnimationState40.cs
new file mode 100644
index 0000000..e9d12ca
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V40/AnimationState40.cs
@@ -0,0 +1,229 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using SpineRuntime40;
+
+namespace Spine.Implementations.SpineWrappers.V40
+{
+ internal sealed class AnimationState40(AnimationState innerObject, SpineObjectData40 data) : IAnimationState
+ {
+ private readonly AnimationState _o = innerObject;
+ private readonly SpineObjectData40 _data = data;
+
+ private readonly Dictionary _trackEntryPool = [];
+
+ private readonly Dictionary _eventMapping = [];
+ private readonly Dictionary _eventCount = [];
+
+ public AnimationState InnerObject => _o;
+
+ public event IAnimationState.TrackEntryDelegate? Start
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Start += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Start -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Interrupt
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Interrupt += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Interrupt -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? End
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.End += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.End -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Complete
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Complete += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Complete -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Dispose
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Dispose += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Dispose -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
+
+ public void Update(float delta) => _o.Update(delta);
+
+ public void Apply(ISkeleton skeleton)
+ {
+ if (skeleton is Skeleton40 skel)
+ {
+ _o.Apply(skel.InnerObject);
+ return;
+ }
+ throw new ArgumentException($"Received {skeleton.GetType().Name}", nameof(skeleton));
+ }
+
+ ///
+ /// 获取 对象, 不存在则创建
+ ///
+ public ITrackEntry GetTrackEntry(TrackEntry trackEntry)
+ {
+ if (!_trackEntryPool.TryGetValue(trackEntry, out var tr))
+ _trackEntryPool[trackEntry] = tr = new(trackEntry, this, _data);
+ return tr;
+ }
+
+ public IEnumerable IterTracks() => _o.Tracks.Select(t => t is null ? null : GetTrackEntry(t));
+
+ public ITrackEntry? GetCurrent(int index) { var t = _o.GetCurrent(index); return t is null ? null : GetTrackEntry(t); }
+
+ public void ClearTrack(int index) => _o.ClearTrack(index);
+
+ public void ClearTracks() => _o.ClearTracks();
+
+ public ITrackEntry SetAnimation(int trackIndex, string animationName, bool loop)
+ => GetTrackEntry(_o.SetAnimation(trackIndex, animationName, loop));
+
+ public ITrackEntry SetAnimation(int trackIndex, IAnimation animation, bool loop)
+ {
+ if (animation is Animation40 anime)
+ return GetTrackEntry(_o.SetAnimation(trackIndex, anime.InnerObject, loop));
+ throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
+ }
+
+ public ITrackEntry SetEmptyAnimation(int trackIndex, float mixDuration) => GetTrackEntry(_o.SetEmptyAnimation(trackIndex, mixDuration));
+
+ public void SetEmptyAnimations(float mixDuration) => _o.SetEmptyAnimations(mixDuration);
+
+ public ITrackEntry AddAnimation(int trackIndex, string animationName, bool loop, float delay)
+ => GetTrackEntry(_o.AddAnimation(trackIndex, animationName, loop, delay));
+
+ public ITrackEntry AddAnimation(int trackIndex, IAnimation animation, bool loop, float delay)
+ {
+ if (animation is Animation40 anime)
+ return GetTrackEntry(_o.AddAnimation(trackIndex, anime.InnerObject, loop, delay));
+ throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
+ }
+
+ public ITrackEntry AddEmptyAnimation(int trackIndex, float mixDuration, float delay)
+ => GetTrackEntry(_o.AddEmptyAnimation(trackIndex, mixDuration, delay));
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V40/Attachments/Attachment40.cs b/Spine/Implementations/SpineWrappers/V40/Attachments/Attachment40.cs
new file mode 100644
index 0000000..9241919
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V40/Attachments/Attachment40.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime40;
+
+namespace Spine.Implementations.SpineWrappers.V40.Attachments
+{
+ internal abstract class Attachment40(Attachment innerObject) : IAttachment
+ {
+ private readonly Attachment _o = innerObject;
+
+ public virtual Attachment InnerObject => _o;
+
+ public string Name => _o.Name;
+
+ public abstract int ComputeWorldVertices(ISlot slot, ref float[] worldVertices);
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V40/Attachments/BoundingBoxAttachment40.cs b/Spine/Implementations/SpineWrappers/V40/Attachments/BoundingBoxAttachment40.cs
new file mode 100644
index 0000000..23b6043
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V40/Attachments/BoundingBoxAttachment40.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime40;
+
+namespace Spine.Implementations.SpineWrappers.V40.Attachments
+{
+ internal sealed class BoundingBoxAttachment40(BoundingBoxAttachment innerObject) :
+ Attachment40(innerObject),
+ IBoundingBoxAttachment
+ {
+ private readonly BoundingBoxAttachment _o = innerObject;
+
+ public override BoundingBoxAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot40 st)
+ {
+ var length = _o.WorldVerticesLength;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot40)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V40/Attachments/ClippingAttachment40.cs b/Spine/Implementations/SpineWrappers/V40/Attachments/ClippingAttachment40.cs
new file mode 100644
index 0000000..5ce1e20
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V40/Attachments/ClippingAttachment40.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime40;
+
+namespace Spine.Implementations.SpineWrappers.V40.Attachments
+{
+ internal sealed class ClippingAttachment40(ClippingAttachment innerObject) :
+ Attachment40(innerObject),
+ IClippingAttachment
+ {
+ private readonly ClippingAttachment _o = innerObject;
+
+ public override ClippingAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot40 st)
+ {
+ var length = _o.WorldVerticesLength;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot40)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V40/Attachments/MeshAttachment40.cs b/Spine/Implementations/SpineWrappers/V40/Attachments/MeshAttachment40.cs
new file mode 100644
index 0000000..d32f730
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V40/Attachments/MeshAttachment40.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime40;
+
+namespace Spine.Implementations.SpineWrappers.V40.Attachments
+{
+ internal sealed class MeshAttachment40(MeshAttachment innerObject) :
+ Attachment40(innerObject),
+ IMeshAttachment
+ {
+ private readonly MeshAttachment _o = innerObject;
+
+ public override MeshAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot40 st)
+ {
+ var length = _o.WorldVerticesLength;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot40)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+
+ public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
+
+ public float[] UVs => _o.UVs;
+
+ public int[] Triangles => _o.Triangles;
+
+ public int HullLength => _o.HullLength;
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V40/Attachments/PathAttachment40.cs b/Spine/Implementations/SpineWrappers/V40/Attachments/PathAttachment40.cs
new file mode 100644
index 0000000..7636c0f
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V40/Attachments/PathAttachment40.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime40;
+
+namespace Spine.Implementations.SpineWrappers.V40.Attachments
+{
+ internal sealed class PathAttachment40(PathAttachment innerObject) :
+ Attachment40(innerObject),
+ IPathAttachment
+ {
+ private readonly PathAttachment _o = innerObject;
+
+ public override PathAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot40 st)
+ {
+ var length = _o.WorldVerticesLength;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot40)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Spine/Implementations/SpineWrappers/V40/Attachments/PointAttachment40.cs b/Spine/Implementations/SpineWrappers/V40/Attachments/PointAttachment40.cs
new file mode 100644
index 0000000..d844f82
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V40/Attachments/PointAttachment40.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime40;
+
+namespace Spine.Implementations.SpineWrappers.V40.Attachments
+{
+ internal sealed class PointAttachment40(PointAttachment innerObject) :
+ Attachment40(innerObject),
+ IPointAttachment
+ {
+ private readonly PointAttachment _o = innerObject;
+
+ public override PointAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot40 st)
+ {
+ if (worldVertices.Length < 2) worldVertices = new float[2];
+ _o.ComputeWorldPosition(st.InnerObject.Bone, out worldVertices[0], out worldVertices[1]);
+ return 2;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot40)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Spine/Implementations/SpineWrappers/V40/Attachments/RegionAttachment40.cs b/Spine/Implementations/SpineWrappers/V40/Attachments/RegionAttachment40.cs
new file mode 100644
index 0000000..e514c50
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V40/Attachments/RegionAttachment40.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime40;
+
+namespace Spine.Implementations.SpineWrappers.V40.Attachments
+{
+ internal sealed class RegionAttachment40(RegionAttachment innerObject) :
+ Attachment40(innerObject),
+ IRegionAttachment
+ {
+ private readonly RegionAttachment _o = innerObject;
+
+ public override RegionAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot40 st)
+ {
+ if (worldVertices.Length < 8) worldVertices = new float[8];
+ _o.ComputeWorldVertices(st.InnerObject.Bone, worldVertices, 0);
+ return 8;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot40)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+
+ public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
+
+ public float[] UVs => _o.UVs;
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V40/Bone40.cs b/Spine/Implementations/SpineWrappers/V40/Bone40.cs
new file mode 100644
index 0000000..cfb8818
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V40/Bone40.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using SpineRuntime40;
+
+namespace Spine.Implementations.SpineWrappers.V40
+{
+ internal sealed class Bone40(Bone innerObject, Bone40? parent = null) : IBone
+ {
+ private readonly Bone _o = innerObject;
+ private readonly Bone40? _parent = parent;
+
+ public Bone InnerObject => _o;
+
+ public string Name => _o.Data.Name;
+ public int Index => _o.Data.Index;
+
+ public IBone? Parent => _parent;
+ public bool Active => _o.Active;
+ public float Length => _o.Data.Length;
+ public float WorldX => _o.WorldX;
+ public float WorldY => _o.WorldY;
+ public float A => _o.A;
+ public float B => _o.B;
+ public float C => _o.C;
+ public float D => _o.D;
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V40/Skeleton40.cs b/Spine/Implementations/SpineWrappers/V40/Skeleton40.cs
new file mode 100644
index 0000000..ec4b055
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V40/Skeleton40.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Collections.Frozen;
+using System.Collections.Immutable;
+using Spine.SpineWrappers;
+using SpineRuntime40;
+
+namespace Spine.Implementations.SpineWrappers.V40
+{
+ internal sealed class Skeleton40 : ISkeleton
+ {
+ private readonly Skeleton _o;
+ private readonly SpineObjectData40 _data;
+
+ private readonly ImmutableArray _bones;
+ private readonly FrozenDictionary _bonesByName;
+ private readonly ImmutableArray _slots;
+ private readonly FrozenDictionary _slotsByName;
+
+ private Skin40? _skin;
+
+ public Skeleton40(Skeleton innerObject, SpineObjectData40 data)
+ {
+ _o = innerObject;
+ _data = data;
+
+ List bones = [];
+ Dictionary bonesByName = [];
+ foreach (var b in _o.Bones)
+ {
+ var bone = new Bone40(b, b.Parent is null ? null : bones[b.Parent.Data.Index]);
+ bones.Add(bone);
+ bonesByName[bone.Name] = bone;
+ }
+ _bones = bones.Cast().ToImmutableArray();
+ _bonesByName = bonesByName.ToFrozenDictionary();
+
+ List slots = [];
+ Dictionary slotsByName = [];
+ foreach (var s in _o.Slots)
+ {
+ var slot = new Slot40(s, _data, bones[s.Bone.Data.Index]);
+ slots.Add(slot);
+ slotsByName[slot.Name] = slot;
+ }
+ _slots = slots.Cast().ToImmutableArray();
+ _slotsByName = slotsByName.ToFrozenDictionary();
+ }
+
+ public Skeleton InnerObject => _o;
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+ public float X { get => _o.X; set => _o.X = value; }
+ public float Y { get => _o.Y; set => _o.Y = value; }
+ public float ScaleX { get => _o.ScaleX; set => _o.ScaleX = value; }
+ public float ScaleY { get => _o.ScaleY; set => _o.ScaleY = value; }
+
+ public ImmutableArray Bones => _bones;
+ public FrozenDictionary BonesByName => _bonesByName;
+ public ImmutableArray Slots => _slots;
+ public FrozenDictionary SlotsByName => _slotsByName;
+
+ public ISkin? Skin
+ {
+ get => _skin;
+ set
+ {
+ if (value is null)
+ {
+ _o.Skin = null;
+ _skin = null;
+ return;
+ }
+ if (value is Skin40 sk)
+ {
+ _o.Skin = sk.InnerObject;
+ _skin = sk;
+ return;
+ }
+ throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
+ }
+ }
+
+ public IEnumerable IterDrawOrder() => _o.DrawOrder.Select(s => _slots[s.Data.Index]);
+ public void UpdateCache() => _o.UpdateCache();
+ public void UpdateWorldTransform(ISkeleton.Physics physics) => _o.UpdateWorldTransform();
+ public void SetToSetupPose() => _o.SetToSetupPose();
+ public void SetBonesToSetupPose() => _o.SetBonesToSetupPose();
+ public void SetSlotsToSetupPose() => _o.SetSlotsToSetupPose();
+ public void Update(float delta) => _o.Update(delta);
+
+ public void GetBounds(out float x, out float y, out float w, out float h)
+ {
+ float[] _ = [];
+ _o.GetBounds(out x, out y, out w, out h, ref _);
+ }
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V40/SkeletonClipping40.cs b/Spine/Implementations/SpineWrappers/V40/SkeletonClipping40.cs
new file mode 100644
index 0000000..ff334f9
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V40/SkeletonClipping40.cs
@@ -0,0 +1,56 @@
+using Spine.SpineWrappers;
+using Spine.SpineWrappers.Attachments;
+using Spine.Utils;
+using SpineRuntime40;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Spine.Implementations.SpineWrappers.V40
+{
+ internal sealed class SkeletonClipping40 : ISkeletonClipping
+ {
+ private readonly SkeletonClipping _o = new();
+
+ public bool IsClipping => _o.IsClipping;
+
+ public float[] ClippedVertices => _o.ClippedVertices.Items;
+
+ public int ClippedVerticesLength => _o.ClippedVertices.Count;
+
+ public int[] ClippedTriangles => _o.ClippedTriangles.Items;
+
+ public int ClippedTrianglesLength => _o.ClippedTriangles.Count;
+
+ public float[] ClippedUVs => _o.ClippedUVs.Items;
+
+ public void ClipTriangles(float[] vertices, int verticesLength, int[] triangles, int trianglesLength, float[] uvs)
+ => _o.ClipTriangles(vertices, verticesLength, triangles, trianglesLength, uvs);
+
+ public void ClipStart(ISlot slot, IClippingAttachment clippingAttachment)
+ {
+ if (slot is Slot40 st && clippingAttachment is Attachments.ClippingAttachment40 att)
+ {
+ _o.ClipStart(st.InnerObject, att.InnerObject);
+ return;
+ }
+ throw new ArgumentException($"Received {slot.GetType().Name} {clippingAttachment.GetType().Name}");
+ }
+
+ public void ClipEnd(ISlot slot)
+ {
+ if (slot is Slot40 st)
+ {
+ _o.ClipEnd(st.InnerObject);
+ return;
+ }
+ throw new ArgumentException($"Received {slot.GetType().Name}", nameof(slot));
+ }
+
+ public void ClipEnd() => _o.ClipEnd();
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V40/Skin40.cs b/Spine/Implementations/SpineWrappers/V40/Skin40.cs
new file mode 100644
index 0000000..d656154
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V40/Skin40.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using SpineRuntime40;
+
+namespace Spine.Implementations.SpineWrappers.V40
+{
+ internal sealed class Skin40 : ISkin
+ {
+ private readonly Skin _o;
+
+ ///
+ /// 使用指定名字创建空皮肤
+ ///
+ public Skin40(string name) => _o = new(name);
+
+ ///
+ /// 包装已有皮肤对象
+ ///
+ public Skin40(Skin innerObject) => _o = innerObject;
+
+ public Skin InnerObject => _o;
+
+ public string Name => _o.Name;
+
+ public void AddSkin(ISkin skin)
+ {
+ if (skin is Skin40 sk)
+ {
+ _o.AddSkin(sk._o);
+ return;
+ }
+ throw new ArgumentException($"Received {skin.GetType().Name}", nameof(skin));
+ }
+
+ public void Clear() => _o.Clear();
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V40/Slot40.cs b/Spine/Implementations/SpineWrappers/V40/Slot40.cs
new file mode 100644
index 0000000..6467a5b
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V40/Slot40.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Frozen;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.Utils;
+using Spine.SpineWrappers;
+using SpineRuntime40;
+
+namespace Spine.Implementations.SpineWrappers.V40
+{
+ internal sealed class Slot40 : ISlot
+ {
+ private readonly Slot _o;
+ private readonly SpineObjectData40 _data;
+
+ private readonly Bone40 _bone;
+ private readonly SFML.Graphics.BlendMode _blendMode;
+
+ public Slot40(Slot innerObject, SpineObjectData40 data, Bone40 bone)
+ {
+ _o = innerObject;
+ _data = data;
+
+ _bone = bone;
+ _blendMode = _o.Data.BlendMode switch
+ {
+ BlendMode.Normal => SFMLBlendMode.NormalPma,
+ BlendMode.Additive => SFMLBlendMode.AdditivePma,
+ BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
+ BlendMode.Screen => SFMLBlendMode.ScreenPma,
+ _ => throw new NotImplementedException($"{_o.Data.BlendMode}"),
+ };
+ }
+
+ public Slot InnerObject => _o;
+
+ public string Name => _o.Data.Name;
+ public int Index => _o.Data.Index;
+ public SFML.Graphics.BlendMode Blend => _blendMode;
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+ public IBone Bone => _bone;
+
+ public Spine.SpineWrappers.Attachments.IAttachment? Attachment
+ {
+ get
+ {
+ if (_o.Attachment is Attachment att)
+ {
+ return _data.SlotAttachments[Name][att.Name];
+ }
+ return null;
+ }
+
+ set
+ {
+ if (value is null)
+ {
+ _o.Attachment = null;
+ return;
+ }
+ if (value is Attachments.Attachment40 att)
+ {
+ _o.Attachment = att.InnerObject;
+ return;
+ }
+ throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
+ }
+ }
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V40/SpineObjectData40.cs b/Spine/Implementations/SpineWrappers/V40/SpineObjectData40.cs
new file mode 100644
index 0000000..3c8370f
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V40/SpineObjectData40.cs
@@ -0,0 +1,121 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Frozen;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.Utils;
+using Spine.SpineWrappers;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime40;
+using Spine.Implementations.SpineWrappers.V40.Attachments;
+
+namespace Spine.Implementations.SpineWrappers.V40
+{
+ [SpineImplementation(4, 0)]
+ internal sealed class SpineObjectData40 : SpineObjectData
+ {
+ private readonly Atlas _atlas;
+ private readonly SkeletonData _skeletonData;
+ private readonly AnimationStateData _animationStateData;
+
+ private readonly ImmutableArray _skins;
+ private readonly FrozenDictionary _skinsByName;
+ private readonly FrozenDictionary> _slotAttachments;
+ private readonly ImmutableArray _animations;
+ private readonly FrozenDictionary _animationsByName;
+
+ public SpineObjectData40(string skelPath, string atlasPath) : base(skelPath, atlasPath)
+ {
+ // 加载 atlas
+ try { _atlas = new Atlas(atlasPath, _textureLoader); }
+ catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
+
+ // 加载 skel
+ try
+ {
+ if (Utf8Validator.IsUtf8(skelPath))
+ _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
+ else
+ _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
+ }
+ catch (Exception ex)
+ {
+ _atlas.Dispose();
+ throw new InvalidDataException($"Failed to load skeleton file {skelPath}", ex);
+ }
+
+ // 加载动画数据
+ _animationStateData = new AnimationStateData(_skeletonData);
+
+ // 整理皮肤和附件
+ Dictionary> slotAttachments = [];
+ List skins = [];
+ Dictionary skinsByName = [];
+ foreach (var s in _skeletonData.Skins)
+ {
+ var skin = new Skin40(s);
+ skins.Add(skin);
+ skinsByName[s.Name] = skin;
+ foreach (var entry in s.Attachments)
+ {
+ var att = entry.Attachment;
+ var slotName = _skeletonData.Slots.Items[entry.SlotIndex].Name;
+ if (!slotAttachments.TryGetValue(slotName, out var attachments))
+ slotAttachments[slotName] = attachments = [];
+
+ attachments[att.Name] = att switch
+ {
+ RegionAttachment regionAtt => new RegionAttachment40(regionAtt),
+ MeshAttachment meshAtt => new MeshAttachment40(meshAtt),
+ ClippingAttachment clipAtt => new ClippingAttachment40(clipAtt),
+ BoundingBoxAttachment bbAtt => new BoundingBoxAttachment40(bbAtt),
+ PathAttachment pathAtt => new PathAttachment40(pathAtt),
+ PointAttachment ptAtt => new PointAttachment40(ptAtt),
+ _ => throw new InvalidOperationException($"Unrecognized attachment type {att.GetType().FullName}")
+ };
+ }
+ }
+ _slotAttachments = slotAttachments.ToFrozenDictionary(it => it.Key, it => it.Value.ToFrozenDictionary());
+ _skins = skins.ToImmutableArray();
+ _skinsByName = skinsByName.ToFrozenDictionary();
+
+ // 整理所有动画数据
+ List animations = [];
+ Dictionary animationsByName = [];
+ foreach (var a in _skeletonData.Animations)
+ {
+ var anime = new Animation40(a);
+ animations.Add(anime);
+ animationsByName[anime.Name] = anime;
+ }
+ _animations = animations.ToImmutableArray();
+ _animationsByName = animationsByName.ToFrozenDictionary();
+ }
+
+ public override string SkeletonVersion => _skeletonData.Version;
+
+ public override ImmutableArray Skins => _skins;
+
+ public override FrozenDictionary SkinsByName => _skinsByName;
+
+ public override FrozenDictionary> SlotAttachments => _slotAttachments;
+
+ public override float DefaultMix { get => _animationStateData.DefaultMix; set => _animationStateData.DefaultMix = value; }
+
+ public override ImmutableArray Animations => _animations;
+
+ public override FrozenDictionary AnimationsByName => _animationsByName;
+
+ protected override void DisposeAtlas() => _atlas.Dispose();
+
+ public override ISkeleton CreateSkeleton() => new Skeleton40(new(_skeletonData), this);
+
+ public override IAnimationState CreateAnimationState() => new AnimationState40(new(_animationStateData), this);
+
+ public override ISkeletonClipping CreateSkeletonClipping() => new SkeletonClipping40();
+
+ public override ISkin CreateSkin(string name) => new Skin40(name);
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V40/TrackEntry40.cs b/Spine/Implementations/SpineWrappers/V40/TrackEntry40.cs
new file mode 100644
index 0000000..8fe5cda
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V40/TrackEntry40.cs
@@ -0,0 +1,185 @@
+using Spine.SpineWrappers;
+using SpineRuntime40;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Spine.Implementations.SpineWrappers.V40
+{
+ internal sealed class TrackEntry40(TrackEntry innerObject, AnimationState40 animationState, SpineObjectData40 data): ITrackEntry
+ {
+ private readonly TrackEntry _o = innerObject;
+ private readonly AnimationState40 _animationState = animationState;
+ private readonly SpineObjectData40 _data = data;
+
+ private readonly Dictionary _eventMapping = [];
+ private readonly Dictionary _eventCount = [];
+
+ public TrackEntry InnerObject => _o;
+
+ public event IAnimationState.TrackEntryDelegate? Start
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Start += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Start -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Interrupt
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Interrupt += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Interrupt -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? End
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.End += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.End -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Complete
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Complete += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Complete -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Dispose
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Dispose += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Dispose -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public int TrackIndex { get => _o.TrackIndex; }
+
+ public IAnimation Animation { get => _data.AnimationsByName[_o.Animation.Name]; }
+
+ public ITrackEntry? Next { get { var t = _o.Next; return t is null ? null : _animationState.GetTrackEntry(t); } }
+
+ public bool Loop { get => _o.Loop; set => _o.Loop = value; }
+
+ public float TrackTime { get => _o.TrackTime; set => _o.TrackTime = value; }
+
+ public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
+
+ public float Alpha { get => _o.Alpha; set => _o.Alpha = value; }
+
+ public float MixDuration { get => _o.MixDuration; set => _o.MixDuration = value; }
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V41/Animation41.cs b/Spine/Implementations/SpineWrappers/V41/Animation41.cs
new file mode 100644
index 0000000..928bab4
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V41/Animation41.cs
@@ -0,0 +1,23 @@
+using Spine.SpineWrappers;
+using SpineRuntime41;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Spine.Implementations.SpineWrappers.V41
+{
+ internal sealed class Animation41(Animation innerObject) : IAnimation
+ {
+ private readonly Animation _o = innerObject;
+
+ public Animation InnerObject => _o;
+
+ public string Name => _o.Name;
+
+ public float Duration => _o.Duration;
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V41/AnimationState41.cs b/Spine/Implementations/SpineWrappers/V41/AnimationState41.cs
new file mode 100644
index 0000000..e5b658a
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V41/AnimationState41.cs
@@ -0,0 +1,229 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using SpineRuntime41;
+
+namespace Spine.Implementations.SpineWrappers.V41
+{
+ internal sealed class AnimationState41(AnimationState innerObject, SpineObjectData41 data) : IAnimationState
+ {
+ private readonly AnimationState _o = innerObject;
+ private readonly SpineObjectData41 _data = data;
+
+ private readonly Dictionary _trackEntryPool = [];
+
+ private readonly Dictionary _eventMapping = [];
+ private readonly Dictionary _eventCount = [];
+
+ public AnimationState InnerObject => _o;
+
+ public event IAnimationState.TrackEntryDelegate? Start
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Start += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Start -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Interrupt
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Interrupt += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Interrupt -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? End
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.End += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.End -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Complete
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Complete += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Complete -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Dispose
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Dispose += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Dispose -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
+
+ public void Update(float delta) => _o.Update(delta);
+
+ public void Apply(ISkeleton skeleton)
+ {
+ if (skeleton is Skeleton41 skel)
+ {
+ _o.Apply(skel.InnerObject);
+ return;
+ }
+ throw new ArgumentException($"Received {skeleton.GetType().Name}", nameof(skeleton));
+ }
+
+ ///
+ /// 获取 对象, 不存在则创建
+ ///
+ public ITrackEntry GetTrackEntry(TrackEntry trackEntry)
+ {
+ if (!_trackEntryPool.TryGetValue(trackEntry, out var tr))
+ _trackEntryPool[trackEntry] = tr = new(trackEntry, this, _data);
+ return tr;
+ }
+
+ public IEnumerable IterTracks() => _o.Tracks.Select(t => t is null ? null : GetTrackEntry(t));
+
+ public ITrackEntry? GetCurrent(int index) { var t = _o.GetCurrent(index); return t is null ? null : GetTrackEntry(t); }
+
+ public void ClearTrack(int index) => _o.ClearTrack(index);
+
+ public void ClearTracks() => _o.ClearTracks();
+
+ public ITrackEntry SetAnimation(int trackIndex, string animationName, bool loop)
+ => GetTrackEntry(_o.SetAnimation(trackIndex, animationName, loop));
+
+ public ITrackEntry SetAnimation(int trackIndex, IAnimation animation, bool loop)
+ {
+ if (animation is Animation41 anime)
+ return GetTrackEntry(_o.SetAnimation(trackIndex, anime.InnerObject, loop));
+ throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
+ }
+
+ public ITrackEntry SetEmptyAnimation(int trackIndex, float mixDuration) => GetTrackEntry(_o.SetEmptyAnimation(trackIndex, mixDuration));
+
+ public void SetEmptyAnimations(float mixDuration) => _o.SetEmptyAnimations(mixDuration);
+
+ public ITrackEntry AddAnimation(int trackIndex, string animationName, bool loop, float delay)
+ => GetTrackEntry(_o.AddAnimation(trackIndex, animationName, loop, delay));
+
+ public ITrackEntry AddAnimation(int trackIndex, IAnimation animation, bool loop, float delay)
+ {
+ if (animation is Animation41 anime)
+ return GetTrackEntry(_o.AddAnimation(trackIndex, anime.InnerObject, loop, delay));
+ throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
+ }
+
+ public ITrackEntry AddEmptyAnimation(int trackIndex, float mixDuration, float delay)
+ => GetTrackEntry(_o.AddEmptyAnimation(trackIndex, mixDuration, delay));
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V41/Attachments/Attachment41.cs b/Spine/Implementations/SpineWrappers/V41/Attachments/Attachment41.cs
new file mode 100644
index 0000000..2b0c6c2
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V41/Attachments/Attachment41.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime41;
+
+namespace Spine.Implementations.SpineWrappers.V41.Attachments
+{
+ internal abstract class Attachment41(Attachment innerObject) : IAttachment
+ {
+ private readonly Attachment _o = innerObject;
+
+ public virtual Attachment InnerObject => _o;
+
+ public string Name => _o.Name;
+
+ public abstract int ComputeWorldVertices(ISlot slot, ref float[] worldVertices);
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V41/Attachments/BoundingBoxAttachment41.cs b/Spine/Implementations/SpineWrappers/V41/Attachments/BoundingBoxAttachment41.cs
new file mode 100644
index 0000000..9ce42b7
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V41/Attachments/BoundingBoxAttachment41.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime41;
+
+namespace Spine.Implementations.SpineWrappers.V41.Attachments
+{
+ internal sealed class BoundingBoxAttachment41(BoundingBoxAttachment innerObject) :
+ Attachment41(innerObject),
+ IBoundingBoxAttachment
+ {
+ private readonly BoundingBoxAttachment _o = innerObject;
+
+ public override BoundingBoxAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot41 st)
+ {
+ var length = _o.WorldVerticesLength;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot41)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V41/Attachments/ClippingAttachment41.cs b/Spine/Implementations/SpineWrappers/V41/Attachments/ClippingAttachment41.cs
new file mode 100644
index 0000000..4b97e3b
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V41/Attachments/ClippingAttachment41.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime41;
+
+namespace Spine.Implementations.SpineWrappers.V41.Attachments
+{
+ internal sealed class ClippingAttachment41(ClippingAttachment innerObject) :
+ Attachment41(innerObject),
+ IClippingAttachment
+ {
+ private readonly ClippingAttachment _o = innerObject;
+
+ public override ClippingAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot41 st)
+ {
+ var length = _o.WorldVerticesLength;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot41)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V41/Attachments/MeshAttachment41.cs b/Spine/Implementations/SpineWrappers/V41/Attachments/MeshAttachment41.cs
new file mode 100644
index 0000000..187e3a7
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V41/Attachments/MeshAttachment41.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime41;
+
+namespace Spine.Implementations.SpineWrappers.V41.Attachments
+{
+ internal sealed class MeshAttachment41(MeshAttachment innerObject) :
+ Attachment41(innerObject),
+ IMeshAttachment
+ {
+ private readonly MeshAttachment _o = innerObject;
+
+ public override MeshAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot41 st)
+ {
+ var length = _o.WorldVerticesLength;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot41)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+
+ public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.Region).page.rendererObject;
+
+ public float[] UVs => _o.UVs;
+
+ public int[] Triangles => _o.Triangles;
+
+ public int HullLength => _o.HullLength;
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V41/Attachments/PathAttachment41.cs b/Spine/Implementations/SpineWrappers/V41/Attachments/PathAttachment41.cs
new file mode 100644
index 0000000..4f03609
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V41/Attachments/PathAttachment41.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime41;
+
+namespace Spine.Implementations.SpineWrappers.V41.Attachments
+{
+ internal sealed class PathAttachment41(PathAttachment innerObject) :
+ Attachment41(innerObject),
+ IPathAttachment
+ {
+ private readonly PathAttachment _o = innerObject;
+
+ public override PathAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot41 st)
+ {
+ var length = _o.WorldVerticesLength;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot41)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Spine/Implementations/SpineWrappers/V41/Attachments/PointAttachment41.cs b/Spine/Implementations/SpineWrappers/V41/Attachments/PointAttachment41.cs
new file mode 100644
index 0000000..27ebbcf
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V41/Attachments/PointAttachment41.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime41;
+
+namespace Spine.Implementations.SpineWrappers.V41.Attachments
+{
+ internal sealed class PointAttachment41(PointAttachment innerObject) :
+ Attachment41(innerObject),
+ IPointAttachment
+ {
+ private readonly PointAttachment _o = innerObject;
+
+ public override PointAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot41 st)
+ {
+ if (worldVertices.Length < 2) worldVertices = new float[2];
+ _o.ComputeWorldPosition(st.InnerObject.Bone, out worldVertices[0], out worldVertices[1]);
+ return 2;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot41)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Spine/Implementations/SpineWrappers/V41/Attachments/RegionAttachment41.cs b/Spine/Implementations/SpineWrappers/V41/Attachments/RegionAttachment41.cs
new file mode 100644
index 0000000..d61f43d
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V41/Attachments/RegionAttachment41.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime41;
+
+namespace Spine.Implementations.SpineWrappers.V41.Attachments
+{
+ internal sealed class RegionAttachment41(RegionAttachment innerObject) :
+ Attachment41(innerObject),
+ IRegionAttachment
+ {
+ private readonly RegionAttachment _o = innerObject;
+
+ public override RegionAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot41 st)
+ {
+ if (worldVertices.Length < 8) worldVertices = new float[8];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices, 0);
+ return 8;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot41)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+
+ public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.Region).page.rendererObject;
+
+ public float[] UVs => _o.UVs;
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V41/Bone41.cs b/Spine/Implementations/SpineWrappers/V41/Bone41.cs
new file mode 100644
index 0000000..25a45cc
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V41/Bone41.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using SpineRuntime41;
+
+namespace Spine.Implementations.SpineWrappers.V41
+{
+ internal sealed class Bone41(Bone innerObject, Bone41? parent = null) : IBone
+ {
+ private readonly Bone _o = innerObject;
+ private readonly Bone41? _parent = parent;
+
+ public Bone InnerObject => _o;
+
+ public string Name => _o.Data.Name;
+ public int Index => _o.Data.Index;
+
+ public IBone? Parent => _parent;
+ public bool Active => _o.Active;
+ public float Length => _o.Data.Length;
+ public float WorldX => _o.WorldX;
+ public float WorldY => _o.WorldY;
+ public float A => _o.A;
+ public float B => _o.B;
+ public float C => _o.C;
+ public float D => _o.D;
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V41/Skeleton41.cs b/Spine/Implementations/SpineWrappers/V41/Skeleton41.cs
new file mode 100644
index 0000000..994139a
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V41/Skeleton41.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Collections.Frozen;
+using System.Collections.Immutable;
+using Spine.SpineWrappers;
+using SpineRuntime41;
+
+namespace Spine.Implementations.SpineWrappers.V41
+{
+ internal sealed class Skeleton41 : ISkeleton
+ {
+ private readonly Skeleton _o;
+ private readonly SpineObjectData41 _data;
+
+ private readonly ImmutableArray _bones;
+ private readonly FrozenDictionary _bonesByName;
+ private readonly ImmutableArray _slots;
+ private readonly FrozenDictionary _slotsByName;
+
+ private Skin41? _skin;
+
+ public Skeleton41(Skeleton innerObject, SpineObjectData41 data)
+ {
+ _o = innerObject;
+ _data = data;
+
+ List bones = [];
+ Dictionary bonesByName = [];
+ foreach (var b in _o.Bones)
+ {
+ var bone = new Bone41(b, b.Parent is null ? null : bones[b.Parent.Data.Index]);
+ bones.Add(bone);
+ bonesByName[bone.Name] = bone;
+ }
+ _bones = bones.Cast().ToImmutableArray();
+ _bonesByName = bonesByName.ToFrozenDictionary();
+
+ List slots = [];
+ Dictionary slotsByName = [];
+ foreach (var s in _o.Slots)
+ {
+ var slot = new Slot41(s, _data, bones[s.Bone.Data.Index]);
+ slots.Add(slot);
+ slotsByName[slot.Name] = slot;
+ }
+ _slots = slots.Cast().ToImmutableArray();
+ _slotsByName = slotsByName.ToFrozenDictionary();
+ }
+
+ public Skeleton InnerObject => _o;
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+ public float X { get => _o.X; set => _o.X = value; }
+ public float Y { get => _o.Y; set => _o.Y = value; }
+ public float ScaleX { get => _o.ScaleX; set => _o.ScaleX = value; }
+ public float ScaleY { get => _o.ScaleY; set => _o.ScaleY = value; }
+
+ public ImmutableArray Bones => _bones;
+ public FrozenDictionary BonesByName => _bonesByName;
+ public ImmutableArray Slots => _slots;
+ public FrozenDictionary SlotsByName => _slotsByName;
+
+ public ISkin? Skin
+ {
+ get => _skin;
+ set
+ {
+ if (value is null)
+ {
+ _o.Skin = null;
+ _skin = null;
+ return;
+ }
+ if (value is Skin41 sk)
+ {
+ _o.Skin = sk.InnerObject;
+ _skin = sk;
+ return;
+ }
+ throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
+ }
+ }
+
+ public IEnumerable IterDrawOrder() => _o.DrawOrder.Select(s => _slots[s.Data.Index]);
+ public void UpdateCache() => _o.UpdateCache();
+ public void UpdateWorldTransform(ISkeleton.Physics physics) => _o.UpdateWorldTransform();
+ public void SetToSetupPose() => _o.SetToSetupPose();
+ public void SetBonesToSetupPose() => _o.SetBonesToSetupPose();
+ public void SetSlotsToSetupPose() => _o.SetSlotsToSetupPose();
+ public void Update(float delta) { } // 4.1 没有 Update 方法
+
+ public void GetBounds(out float x, out float y, out float w, out float h)
+ {
+ float[] _ = [];
+ _o.GetBounds(out x, out y, out w, out h, ref _);
+ }
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V41/SkeletonClipping41.cs b/Spine/Implementations/SpineWrappers/V41/SkeletonClipping41.cs
new file mode 100644
index 0000000..b9714fb
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V41/SkeletonClipping41.cs
@@ -0,0 +1,56 @@
+using Spine.SpineWrappers;
+using Spine.SpineWrappers.Attachments;
+using Spine.Utils;
+using SpineRuntime41;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Spine.Implementations.SpineWrappers.V41
+{
+ internal sealed class SkeletonClipping41 : ISkeletonClipping
+ {
+ private readonly SkeletonClipping _o = new();
+
+ public bool IsClipping => _o.IsClipping;
+
+ public float[] ClippedVertices => _o.ClippedVertices.Items;
+
+ public int ClippedVerticesLength => _o.ClippedVertices.Count;
+
+ public int[] ClippedTriangles => _o.ClippedTriangles.Items;
+
+ public int ClippedTrianglesLength => _o.ClippedTriangles.Count;
+
+ public float[] ClippedUVs => _o.ClippedUVs.Items;
+
+ public void ClipTriangles(float[] vertices, int verticesLength, int[] triangles, int trianglesLength, float[] uvs)
+ => _o.ClipTriangles(vertices, verticesLength, triangles, trianglesLength, uvs);
+
+ public void ClipStart(ISlot slot, IClippingAttachment clippingAttachment)
+ {
+ if (slot is Slot41 st && clippingAttachment is Attachments.ClippingAttachment41 att)
+ {
+ _o.ClipStart(st.InnerObject, att.InnerObject);
+ return;
+ }
+ throw new ArgumentException($"Received {slot.GetType().Name} {clippingAttachment.GetType().Name}");
+ }
+
+ public void ClipEnd(ISlot slot)
+ {
+ if (slot is Slot41 st)
+ {
+ _o.ClipEnd(st.InnerObject);
+ return;
+ }
+ throw new ArgumentException($"Received {slot.GetType().Name}", nameof(slot));
+ }
+
+ public void ClipEnd() => _o.ClipEnd();
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V41/Skin41.cs b/Spine/Implementations/SpineWrappers/V41/Skin41.cs
new file mode 100644
index 0000000..b5acacf
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V41/Skin41.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using SpineRuntime41;
+
+namespace Spine.Implementations.SpineWrappers.V41
+{
+ internal sealed class Skin41 : ISkin
+ {
+ private readonly Skin _o;
+
+ ///
+ /// 使用指定名字创建空皮肤
+ ///
+ public Skin41(string name) => _o = new(name);
+
+ ///
+ /// 包装已有皮肤对象
+ ///
+ public Skin41(Skin innerObject) => _o = innerObject;
+
+ public Skin InnerObject => _o;
+
+ public string Name => _o.Name;
+
+ public void AddSkin(ISkin skin)
+ {
+ if (skin is Skin41 sk)
+ {
+ _o.AddSkin(sk._o);
+ return;
+ }
+ throw new ArgumentException($"Received {skin.GetType().Name}", nameof(skin));
+ }
+
+ public void Clear() => _o.Clear();
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V41/Slot41.cs b/Spine/Implementations/SpineWrappers/V41/Slot41.cs
new file mode 100644
index 0000000..138edb6
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V41/Slot41.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Frozen;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.Utils;
+using Spine.SpineWrappers;
+using SpineRuntime41;
+
+namespace Spine.Implementations.SpineWrappers.V41
+{
+ internal sealed class Slot41 : ISlot
+ {
+ private readonly Slot _o;
+ private readonly SpineObjectData41 _data;
+
+ private readonly Bone41 _bone;
+ private readonly SFML.Graphics.BlendMode _blendMode;
+
+ public Slot41(Slot innerObject, SpineObjectData41 data, Bone41 bone)
+ {
+ _o = innerObject;
+ _data = data;
+
+ _bone = bone;
+ _blendMode = _o.Data.BlendMode switch
+ {
+ BlendMode.Normal => SFMLBlendMode.NormalPma,
+ BlendMode.Additive => SFMLBlendMode.AdditivePma,
+ BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
+ BlendMode.Screen => SFMLBlendMode.ScreenPma,
+ _ => throw new NotImplementedException($"{_o.Data.BlendMode}"),
+ };
+ }
+
+ public Slot InnerObject => _o;
+
+ public string Name => _o.Data.Name;
+ public int Index => _o.Data.Index;
+ public SFML.Graphics.BlendMode Blend => _blendMode;
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+ public IBone Bone => _bone;
+
+ public Spine.SpineWrappers.Attachments.IAttachment? Attachment
+ {
+ get
+ {
+ if (_o.Attachment is Attachment att)
+ {
+ return _data.SlotAttachments[Name][att.Name];
+ }
+ return null;
+ }
+
+ set
+ {
+ if (value is null)
+ {
+ _o.Attachment = null;
+ return;
+ }
+ if (value is Attachments.Attachment41 att)
+ {
+ _o.Attachment = att.InnerObject;
+ return;
+ }
+ throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
+ }
+ }
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V41/SpineObjectData41.cs b/Spine/Implementations/SpineWrappers/V41/SpineObjectData41.cs
new file mode 100644
index 0000000..49ed2b7
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V41/SpineObjectData41.cs
@@ -0,0 +1,121 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Frozen;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.Utils;
+using Spine.SpineWrappers;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime41;
+using Spine.Implementations.SpineWrappers.V41.Attachments;
+
+namespace Spine.Implementations.SpineWrappers.V41
+{
+ [SpineImplementation(4, 1)]
+ internal sealed class SpineObjectData41 : SpineObjectData
+ {
+ private readonly Atlas _atlas;
+ private readonly SkeletonData _skeletonData;
+ private readonly AnimationStateData _animationStateData;
+
+ private readonly ImmutableArray _skins;
+ private readonly FrozenDictionary _skinsByName;
+ private readonly FrozenDictionary> _slotAttachments;
+ private readonly ImmutableArray _animations;
+ private readonly FrozenDictionary _animationsByName;
+
+ public SpineObjectData41(string skelPath, string atlasPath) : base(skelPath, atlasPath)
+ {
+ // 加载 atlas
+ try { _atlas = new Atlas(atlasPath, _textureLoader); }
+ catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
+
+ // 加载 skel
+ try
+ {
+ if (Utf8Validator.IsUtf8(skelPath))
+ _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
+ else
+ _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
+ }
+ catch (Exception ex)
+ {
+ _atlas.Dispose();
+ throw new InvalidDataException($"Failed to load skeleton file {skelPath}", ex);
+ }
+
+ // 加载动画数据
+ _animationStateData = new AnimationStateData(_skeletonData);
+
+ // 整理皮肤和附件
+ Dictionary> slotAttachments = [];
+ List skins = [];
+ Dictionary skinsByName = [];
+ foreach (var s in _skeletonData.Skins)
+ {
+ var skin = new Skin41(s);
+ skins.Add(skin);
+ skinsByName[s.Name] = skin;
+ foreach (var entry in s.Attachments)
+ {
+ var att = entry.Attachment;
+ var slotName = _skeletonData.Slots.Items[entry.SlotIndex].Name;
+ if (!slotAttachments.TryGetValue(slotName, out var attachments))
+ slotAttachments[slotName] = attachments = [];
+
+ attachments[att.Name] = att switch
+ {
+ RegionAttachment regionAtt => new RegionAttachment41(regionAtt),
+ MeshAttachment meshAtt => new MeshAttachment41(meshAtt),
+ ClippingAttachment clipAtt => new ClippingAttachment41(clipAtt),
+ BoundingBoxAttachment bbAtt => new BoundingBoxAttachment41(bbAtt),
+ PathAttachment pathAtt => new PathAttachment41(pathAtt),
+ PointAttachment ptAtt => new PointAttachment41(ptAtt),
+ _ => throw new InvalidOperationException($"Unrecognized attachment type {att.GetType().FullName}")
+ };
+ }
+ }
+ _slotAttachments = slotAttachments.ToFrozenDictionary(it => it.Key, it => it.Value.ToFrozenDictionary());
+ _skins = skins.ToImmutableArray();
+ _skinsByName = skinsByName.ToFrozenDictionary();
+
+ // 整理所有动画数据
+ List animations = [];
+ Dictionary animationsByName = [];
+ foreach (var a in _skeletonData.Animations)
+ {
+ var anime = new Animation41(a);
+ animations.Add(anime);
+ animationsByName[anime.Name] = anime;
+ }
+ _animations = animations.ToImmutableArray();
+ _animationsByName = animationsByName.ToFrozenDictionary();
+ }
+
+ public override string SkeletonVersion => _skeletonData.Version;
+
+ public override ImmutableArray Skins => _skins;
+
+ public override FrozenDictionary SkinsByName => _skinsByName;
+
+ public override FrozenDictionary> SlotAttachments => _slotAttachments;
+
+ public override float DefaultMix { get => _animationStateData.DefaultMix; set => _animationStateData.DefaultMix = value; }
+
+ public override ImmutableArray Animations => _animations;
+
+ public override FrozenDictionary AnimationsByName => _animationsByName;
+
+ protected override void DisposeAtlas() => _atlas.Dispose();
+
+ public override ISkeleton CreateSkeleton() => new Skeleton41(new(_skeletonData), this);
+
+ public override IAnimationState CreateAnimationState() => new AnimationState41(new(_animationStateData), this);
+
+ public override ISkeletonClipping CreateSkeletonClipping() => new SkeletonClipping41();
+
+ public override ISkin CreateSkin(string name) => new Skin41(name);
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V41/TrackEntry41.cs b/Spine/Implementations/SpineWrappers/V41/TrackEntry41.cs
new file mode 100644
index 0000000..ca359a7
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V41/TrackEntry41.cs
@@ -0,0 +1,185 @@
+using Spine.SpineWrappers;
+using SpineRuntime41;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Spine.Implementations.SpineWrappers.V41
+{
+ internal sealed class TrackEntry41(TrackEntry innerObject, AnimationState41 animationState, SpineObjectData41 data): ITrackEntry
+ {
+ private readonly TrackEntry _o = innerObject;
+ private readonly AnimationState41 _animationState = animationState;
+ private readonly SpineObjectData41 _data = data;
+
+ private readonly Dictionary _eventMapping = [];
+ private readonly Dictionary _eventCount = [];
+
+ public TrackEntry InnerObject => _o;
+
+ public event IAnimationState.TrackEntryDelegate? Start
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Start += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Start -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Interrupt
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Interrupt += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Interrupt -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? End
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.End += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.End -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Complete
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Complete += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Complete -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Dispose
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Dispose += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Dispose -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public int TrackIndex { get => _o.TrackIndex; }
+
+ public IAnimation Animation { get => _data.AnimationsByName[_o.Animation.Name]; }
+
+ public ITrackEntry? Next { get { var t = _o.Next; return t is null ? null : _animationState.GetTrackEntry(t); } }
+
+ public bool Loop { get => _o.Loop; set => _o.Loop = value; }
+
+ public float TrackTime { get => _o.TrackTime; set => _o.TrackTime = value; }
+
+ public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
+
+ public float Alpha { get => _o.Alpha; set => _o.Alpha = value; }
+
+ public float MixDuration { get => _o.MixDuration; set => _o.MixDuration = value; }
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V42/Animation42.cs b/Spine/Implementations/SpineWrappers/V42/Animation42.cs
new file mode 100644
index 0000000..d16e0db
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V42/Animation42.cs
@@ -0,0 +1,23 @@
+using Spine.SpineWrappers;
+using SpineRuntime42;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Spine.Implementations.SpineWrappers.V42
+{
+ internal sealed class Animation42(Animation innerObject) : IAnimation
+ {
+ private readonly Animation _o = innerObject;
+
+ public Animation InnerObject => _o;
+
+ public string Name => _o.Name;
+
+ public float Duration => _o.Duration;
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V42/AnimationState42.cs b/Spine/Implementations/SpineWrappers/V42/AnimationState42.cs
new file mode 100644
index 0000000..9909848
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V42/AnimationState42.cs
@@ -0,0 +1,229 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using SpineRuntime42;
+
+namespace Spine.Implementations.SpineWrappers.V42
+{
+ internal sealed class AnimationState42(AnimationState innerObject, SpineObjectData42 data) : IAnimationState
+ {
+ private readonly AnimationState _o = innerObject;
+ private readonly SpineObjectData42 _data = data;
+
+ private readonly Dictionary _trackEntryPool = [];
+
+ private readonly Dictionary _eventMapping = [];
+ private readonly Dictionary _eventCount = [];
+
+ public AnimationState InnerObject => _o;
+
+ public event IAnimationState.TrackEntryDelegate? Start
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Start += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Start -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Interrupt
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Interrupt += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Interrupt -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? End
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.End += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.End -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Complete
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Complete += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Complete -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public event IAnimationState.TrackEntryDelegate? Dispose
+ {
+ add
+ {
+ if (value is null) return;
+ if (!_eventMapping.TryGetValue(value, out var f))
+ {
+ _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
+ _eventCount[value] = 0;
+ }
+ _o.Dispose += f;
+ _eventCount[value]++;
+ }
+ remove
+ {
+ if (value is null) return;
+ if (_eventMapping.TryGetValue(value, out var f))
+ {
+ _o.Dispose -= f;
+ _eventCount[value]--;
+ if (_eventCount[value] <= 0)
+ {
+ _eventMapping.Remove(value);
+ _eventCount.Remove(value);
+ }
+ }
+ }
+ }
+
+ public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
+
+ public void Update(float delta) => _o.Update(delta);
+
+ public void Apply(ISkeleton skeleton)
+ {
+ if (skeleton is Skeleton42 skel)
+ {
+ _o.Apply(skel.InnerObject);
+ return;
+ }
+ throw new ArgumentException($"Received {skeleton.GetType().Name}", nameof(skeleton));
+ }
+
+ ///
+ /// 获取 对象, 不存在则创建
+ ///
+ public ITrackEntry GetTrackEntry(TrackEntry trackEntry)
+ {
+ if (!_trackEntryPool.TryGetValue(trackEntry, out var tr))
+ _trackEntryPool[trackEntry] = tr = new(trackEntry, this, _data);
+ return tr;
+ }
+
+ public IEnumerable IterTracks() => _o.Tracks.Select(t => t is null ? null : GetTrackEntry(t));
+
+ public ITrackEntry? GetCurrent(int index) { var t = _o.GetCurrent(index); return t is null ? null : GetTrackEntry(t); }
+
+ public void ClearTrack(int index) => _o.ClearTrack(index);
+
+ public void ClearTracks() => _o.ClearTracks();
+
+ public ITrackEntry SetAnimation(int trackIndex, string animationName, bool loop)
+ => GetTrackEntry(_o.SetAnimation(trackIndex, animationName, loop));
+
+ public ITrackEntry SetAnimation(int trackIndex, IAnimation animation, bool loop)
+ {
+ if (animation is Animation42 anime)
+ return GetTrackEntry(_o.SetAnimation(trackIndex, anime.InnerObject, loop));
+ throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
+ }
+
+ public ITrackEntry SetEmptyAnimation(int trackIndex, float mixDuration) => GetTrackEntry(_o.SetEmptyAnimation(trackIndex, mixDuration));
+
+ public void SetEmptyAnimations(float mixDuration) => _o.SetEmptyAnimations(mixDuration);
+
+ public ITrackEntry AddAnimation(int trackIndex, string animationName, bool loop, float delay)
+ => GetTrackEntry(_o.AddAnimation(trackIndex, animationName, loop, delay));
+
+ public ITrackEntry AddAnimation(int trackIndex, IAnimation animation, bool loop, float delay)
+ {
+ if (animation is Animation42 anime)
+ return GetTrackEntry(_o.AddAnimation(trackIndex, anime.InnerObject, loop, delay));
+ throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
+ }
+
+ public ITrackEntry AddEmptyAnimation(int trackIndex, float mixDuration, float delay)
+ => GetTrackEntry(_o.AddEmptyAnimation(trackIndex, mixDuration, delay));
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V42/Attachments/Attachment42.cs b/Spine/Implementations/SpineWrappers/V42/Attachments/Attachment42.cs
new file mode 100644
index 0000000..9ab0208
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V42/Attachments/Attachment42.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime42;
+
+namespace Spine.Implementations.SpineWrappers.V42.Attachments
+{
+ internal abstract class Attachment42(Attachment innerObject) : IAttachment
+ {
+ private readonly Attachment _o = innerObject;
+
+ public virtual Attachment InnerObject => _o;
+
+ public string Name => _o.Name;
+
+ public abstract int ComputeWorldVertices(ISlot slot, ref float[] worldVertices);
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V42/Attachments/BoundingBoxAttachment42.cs b/Spine/Implementations/SpineWrappers/V42/Attachments/BoundingBoxAttachment42.cs
new file mode 100644
index 0000000..9b15c9b
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V42/Attachments/BoundingBoxAttachment42.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime42;
+
+namespace Spine.Implementations.SpineWrappers.V42.Attachments
+{
+ internal sealed class BoundingBoxAttachment42(BoundingBoxAttachment innerObject) :
+ Attachment42(innerObject),
+ IBoundingBoxAttachment
+ {
+ private readonly BoundingBoxAttachment _o = innerObject;
+
+ public override BoundingBoxAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot42 st)
+ {
+ var length = _o.WorldVerticesLength;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot42)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V42/Attachments/ClippingAttachment42.cs b/Spine/Implementations/SpineWrappers/V42/Attachments/ClippingAttachment42.cs
new file mode 100644
index 0000000..c6430df
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V42/Attachments/ClippingAttachment42.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime42;
+
+namespace Spine.Implementations.SpineWrappers.V42.Attachments
+{
+ internal sealed class ClippingAttachment42(ClippingAttachment innerObject) :
+ Attachment42(innerObject),
+ IClippingAttachment
+ {
+ private readonly ClippingAttachment _o = innerObject;
+
+ public override ClippingAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot42 st)
+ {
+ var length = _o.WorldVerticesLength;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot42)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V42/Attachments/MeshAttachment42.cs b/Spine/Implementations/SpineWrappers/V42/Attachments/MeshAttachment42.cs
new file mode 100644
index 0000000..93b230e
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V42/Attachments/MeshAttachment42.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime42;
+
+namespace Spine.Implementations.SpineWrappers.V42.Attachments
+{
+ internal sealed class MeshAttachment42(MeshAttachment innerObject) :
+ Attachment42(innerObject),
+ IMeshAttachment
+ {
+ private readonly MeshAttachment _o = innerObject;
+
+ public override MeshAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot42 st)
+ {
+ var length = _o.WorldVerticesLength;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot42)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+
+ public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.Region).page.rendererObject;
+
+ public float[] UVs => _o.UVs;
+
+ public int[] Triangles => _o.Triangles;
+
+ public int HullLength => _o.HullLength;
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V42/Attachments/PathAttachment42.cs b/Spine/Implementations/SpineWrappers/V42/Attachments/PathAttachment42.cs
new file mode 100644
index 0000000..af80ae1
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V42/Attachments/PathAttachment42.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime42;
+
+namespace Spine.Implementations.SpineWrappers.V42.Attachments
+{
+ internal sealed class PathAttachment42(PathAttachment innerObject) :
+ Attachment42(innerObject),
+ IPathAttachment
+ {
+ private readonly PathAttachment _o = innerObject;
+
+ public override PathAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot42 st)
+ {
+ var length = _o.WorldVerticesLength;
+ if (worldVertices.Length < length) worldVertices = new float[length];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices);
+ return length;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot42)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Spine/Implementations/SpineWrappers/V42/Attachments/PointAttachment42.cs b/Spine/Implementations/SpineWrappers/V42/Attachments/PointAttachment42.cs
new file mode 100644
index 0000000..2570f55
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V42/Attachments/PointAttachment42.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime42;
+
+namespace Spine.Implementations.SpineWrappers.V42.Attachments
+{
+ internal sealed class PointAttachment42(PointAttachment innerObject) :
+ Attachment42(innerObject),
+ IPointAttachment
+ {
+ private readonly PointAttachment _o = innerObject;
+
+ public override PointAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot42 st)
+ {
+ if (worldVertices.Length < 2) worldVertices = new float[2];
+ _o.ComputeWorldPosition(st.InnerObject.Bone, out worldVertices[0], out worldVertices[1]);
+ return 2;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot42)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Spine/Implementations/SpineWrappers/V42/Attachments/RegionAttachment42.cs b/Spine/Implementations/SpineWrappers/V42/Attachments/RegionAttachment42.cs
new file mode 100644
index 0000000..9f3c762
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V42/Attachments/RegionAttachment42.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers.Attachments;
+using SpineRuntime42;
+
+namespace Spine.Implementations.SpineWrappers.V42.Attachments
+{
+ internal sealed class RegionAttachment42(RegionAttachment innerObject) :
+ Attachment42(innerObject),
+ IRegionAttachment
+ {
+ private readonly RegionAttachment _o = innerObject;
+
+ public override RegionAttachment InnerObject => _o;
+
+ public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
+ {
+ if (slot is Slot42 st)
+ {
+ if (worldVertices.Length < 8) worldVertices = new float[8];
+ _o.ComputeWorldVertices(st.InnerObject, worldVertices, 0);
+ return 8;
+ }
+ throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot42)}, but received {slot.GetType().Name}", nameof(slot));
+ }
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+
+ public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.Region).page.rendererObject;
+
+ public float[] UVs => _o.UVs;
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V42/Bone42.cs b/Spine/Implementations/SpineWrappers/V42/Bone42.cs
new file mode 100644
index 0000000..5525eb7
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V42/Bone42.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Spine.SpineWrappers;
+using SpineRuntime42;
+
+namespace Spine.Implementations.SpineWrappers.V42
+{
+ internal sealed class Bone42(Bone innerObject, Bone42? parent = null) : IBone
+ {
+ private readonly Bone _o = innerObject;
+ private readonly Bone42? _parent = parent;
+
+ public Bone InnerObject => _o;
+
+ public string Name => _o.Data.Name;
+ public int Index => _o.Data.Index;
+
+ public IBone? Parent => _parent;
+ public bool Active => _o.Active;
+ public float Length => _o.Data.Length;
+ public float WorldX => _o.WorldX;
+ public float WorldY => _o.WorldY;
+ public float A => _o.A;
+ public float B => _o.B;
+ public float C => _o.C;
+ public float D => _o.D;
+
+ public override string ToString() => _o.ToString();
+ }
+}
diff --git a/Spine/Implementations/SpineWrappers/V42/Skeleton42.cs b/Spine/Implementations/SpineWrappers/V42/Skeleton42.cs
new file mode 100644
index 0000000..9866c1c
--- /dev/null
+++ b/Spine/Implementations/SpineWrappers/V42/Skeleton42.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Collections.Frozen;
+using System.Collections.Immutable;
+using Spine.SpineWrappers;
+using SpineRuntime42;
+
+namespace Spine.Implementations.SpineWrappers.V42
+{
+ internal sealed class Skeleton42 : ISkeleton
+ {
+ private static readonly FrozenDictionary _physicsMapping = new Dictionary()
+ {
+ [ISkeleton.Physics.None] = Skeleton.Physics.None,
+ [ISkeleton.Physics.Reset] = Skeleton.Physics.Reset,
+ [ISkeleton.Physics.Update] = Skeleton.Physics.Update,
+ [ISkeleton.Physics.Pose] = Skeleton.Physics.Pose,
+ }.ToFrozenDictionary();
+
+ private readonly Skeleton _o;
+ private readonly SpineObjectData42 _data;
+
+ private readonly ImmutableArray _bones;
+ private readonly FrozenDictionary _bonesByName;
+ private readonly ImmutableArray _slots;
+ private readonly FrozenDictionary _slotsByName;
+
+ private Skin42? _skin;
+
+ public Skeleton42(Skeleton innerObject, SpineObjectData42 data)
+ {
+ _o = innerObject;
+ _data = data;
+
+ List bones = [];
+ Dictionary bonesByName = [];
+ foreach (var b in _o.Bones)
+ {
+ var bone = new Bone42(b, b.Parent is null ? null : bones[b.Parent.Data.Index]);
+ bones.Add(bone);
+ bonesByName[bone.Name] = bone;
+ }
+ _bones = bones.Cast().ToImmutableArray();
+ _bonesByName = bonesByName.ToFrozenDictionary();
+
+ List slots = [];
+ Dictionary slotsByName = [];
+ foreach (var s in _o.Slots)
+ {
+ var slot = new Slot42(s, _data, bones[s.Bone.Data.Index]);
+ slots.Add(slot);
+ slotsByName[slot.Name] = slot;
+ }
+ _slots = slots.Cast().ToImmutableArray();
+ _slotsByName = slotsByName.ToFrozenDictionary();
+ }
+
+ public Skeleton InnerObject => _o;
+
+ public float R { get => _o.R; set => _o.R = value; }
+ public float G { get => _o.G; set => _o.G = value; }
+ public float B { get => _o.B; set => _o.B = value; }
+ public float A { get => _o.A; set => _o.A = value; }
+ public float X { get => _o.X; set => _o.X = value; }
+ public float Y { get => _o.Y; set => _o.Y = value; }
+ public float ScaleX { get => _o.ScaleX; set => _o.ScaleX = value; }
+ public float ScaleY { get => _o.ScaleY; set => _o.ScaleY = value; }
+
+ public ImmutableArray