Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions UnityRiftCLI/Options/CLIOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ internal enum WorkMode
Godot,
GodotScene,
GodotScripts,
Spine,
}

internal enum AssetGroupOption
Expand Down Expand Up @@ -225,7 +226,7 @@ private static void InitOptions()
optionDefaultValue: WorkMode.Export,
optionName: "-m, --mode <value>",
optionDescription: "Specify working mode\n" +
"<Value: extract | export(default) | exportRaw | dump | info | live2d |\nsplitObjects | animator | dotnet | il2cpp | godot>\n" +
"<Value: extract | export(default) | exportRaw | dump | info | live2d |\nsplitObjects | animator | dotnet | il2cpp | godot | spine>\n" +
"Extract - Extract(Decompress) asset bundles\n" +
"Export - Convert and export assets\n" +
"ExportRaw - Export raw assets\n" +
Expand All @@ -238,7 +239,8 @@ private static void InitOptions()
"Il2Cpp - Generate Il2CppDumper-compatible Ghidra helpers (script.json, il2cpp.h) from GameAssembly/libil2cpp\n" +
"Godot - Convert materials to Godot 4 scaffolds (.gdshader + .tres) with their textures\n" +
"GodotScene - Export the scene as glTF model(s) + a Godot 4 scene (.tscn) that instances them\n" +
"GodotScripts - Generate Godot GDScript stubs from MonoBehaviours (class + serialized fields; Mono & IL2CPP)\n",
"GodotScripts - Generate Godot GDScript stubs from MonoBehaviours (class + serialized fields; Mono & IL2CPP)\n" +
"Spine - Detect and export Spine (esotericsoftware) models (skeleton + atlas + texture pages)\n",
optionExample: "Example: \"-m info\"\n",
optionHelpGroup: HelpGroups.General
);
Expand Down Expand Up @@ -891,6 +893,18 @@ public static void ParseArgs(string[] args)
ClassIDType.Texture2D,
};
break;
case "spine":
o_workMode.Value = WorkMode.Spine;
// Spine skeletons (.json/.skel) and atlases (.atlas) are TextAssets; the
// atlas texture pages are Texture2Ds; SkeletonDataAsset/AtlasAsset
// MonoBehaviours give the authoritative grouping when readable.
o_exportAssetTypes.Value = new List<ClassIDType>
{
ClassIDType.TextAsset,
ClassIDType.Texture2D,
ClassIDType.MonoBehaviour,
};
break;
default:
Console.WriteLine($"{"Error".Color(brightRed)} during parsing [{option.Color(brightYellow)}] option. Unsupported working mode: [{value.Color(brightRed)}].\n");
ShowOptionDescription(o_workMode);
Expand Down
3 changes: 3 additions & 0 deletions UnityRiftCLI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ private static void CLIRun()
case WorkMode.GodotScripts:
Studio.ExportGodotScripts();
break;
case WorkMode.Spine:
Studio.ExportSpine();
break;
default:
Studio.ExportAssets();
break;
Expand Down
63 changes: 63 additions & 0 deletions UnityRiftCLI/Studio.Spine.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using UnityRift;
using UnityRiftCLI.Options;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using SpineExtractor;
using Ansi = UnityRift.ColorConsole;

namespace UnityRiftCLI
{
/// <summary>
/// "-m spine": detect Spine (esotericsoftware) models embedded in the loaded assets and export
/// the raw Spine files (skeleton + atlas + texture pages) into a per-model folder, ready to
/// re-import into the Spine editor.
/// </summary>
internal static partial class Studio
{
public static void ExportSpine()
{
var outRoot = CLIOptions.o_outputFolder.Value;
var overwrite = CLIOptions.f_overwriteExisting.Value;

var textAssets = new List<TextAsset>();
var textures = new List<Texture2D>();
var monoBehaviours = new List<MonoBehaviour>();
foreach (var item in parsedAssetsList)
{
if (item.Type == ClassIDType.TextAsset && item.Asset is TextAsset ta)
textAssets.Add(ta);
else if (item.Type == ClassIDType.Texture2D && item.Asset is Texture2D tex)
textures.Add(tex);
else if (item.Type == ClassIDType.MonoBehaviour && item.Asset is MonoBehaviour mb)
monoBehaviours.Add(mb);
}

// Hybrid: authoritative SkeletonDataAsset grouping when the MonoBehaviour fields are
// readable (serialized type trees, or loaded/generated assemblies), else heuristics.
var links = SpineMonoBehaviourReader.BuildLinks(monoBehaviours, assemblyLoader, msg => Logger.Info(msg));
if (links.Count > 0)
Logger.Info($"Spine: grouped {links.Count} model(s) from SkeletonDataAsset MonoBehaviour(s).");

var models = SpineCollector.Collect(textAssets, textures, explicitLinks: links, log: msg => Logger.Info(msg));
if (models.Count == 0)
{
Logger.Warning("No Spine models detected. Spine skeletons (.json/.skel) and atlases (.atlas) are stored as TextAssets; make sure the file/bundle that contains them is loaded (for WebGL builds, point at the extracted data.unity3d).");
return;
}

Logger.Info($"Detected {models.Count} Spine model(s). Exporting to \"{outRoot.Color(Ansi.BrightCyan)}\"...");
Directory.CreateDirectory(outRoot);

var exported = 0;
foreach (var model in models)
{
var dir = SpineExporter.Export(model, outRoot, overwrite, msg => Logger.Info(msg));
if (dir != null)
exported++;
}

Logger.Info($"Finished exporting {exported} Spine model(s) to \"{outRoot.Color(Ansi.BrightCyan)}\".");
}
}
}
1 change: 1 addition & 0 deletions UnityRiftCLI/Studio.cs
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,7 @@ public static void Filter()
case WorkMode.Live2D:
case WorkMode.SplitObjects:
case WorkMode.Animator:
case WorkMode.Spine:
break;
default:
FilterAssets();
Expand Down
90 changes: 90 additions & 0 deletions UnityRiftGUI/UnityRiftGUIForm.Spine.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using UnityRift;
using SpineExtractor;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows.Forms;
using static UnityRiftGUI.Studio;

namespace UnityRiftGUI
{
// Spine (esotericsoftware) export, added in code so no Designer edits are needed. Mirrors the
// CLI "-m spine" mode, reusing the shared detector/exporter in UnityRiftUtility (SpineExtractor).
partial class UnityRiftGUIForm
{
private void InitSpineMenu()
{
var item = new ToolStripMenuItem("To Spine (skeleton + atlas + textures)")
{
Name = "exportSpineMenuItem",
};
item.Click += exportSpineMenuItem_Click;
exportToolStripMenuItem.DropDownItems.Add(item);
}

private void exportSpineMenuItem_Click(object sender, EventArgs e)
{
// Gather the Spine inputs. TextAssets are lazy objects, so resolve by type rather than
// pattern-matching the placeholder.
var textAssets = new List<TextAsset>();
var textures = new List<Texture2D>();
var monoBehaviours = new List<MonoBehaviour>();
foreach (var file in assetsManager.AssetsFileList)
foreach (var obj in file.Objects)
{
if (obj.type == ClassIDType.TextAsset)
{
if (obj.Resolve() is TextAsset ta) textAssets.Add(ta);
}
else if (obj.type == ClassIDType.Texture2D)
{
if (obj.Resolve() is Texture2D tex) textures.Add(tex);
}
else if (obj.type == ClassIDType.MonoBehaviour)
{
if (obj.Resolve() is MonoBehaviour mb) monoBehaviours.Add(mb);
}
}

if (textAssets.Count == 0)
{
StatusStripUpdate("No TextAssets loaded — Spine skeletons/atlases are stored as TextAssets.");
return;
}

// Hybrid: authoritative SkeletonDataAsset grouping when the MonoBehaviour fields are
// readable (serialized type trees, or assemblies loaded via the .NET menu), else heuristics.
var links = SpineMonoBehaviourReader.BuildLinks(monoBehaviours, assemblyLoader, msg => Logger.Info(msg));
var models = SpineCollector.Collect(textAssets, textures, explicitLinks: links, log: msg => Logger.Info(msg));
if (models.Count == 0)
{
StatusStripUpdate("No Spine models detected in the loaded assets.");
return;
}

var dialog = new OpenFolderDialog { InitialFolder = saveDirectoryBackup };
if (dialog.ShowDialog(this) != DialogResult.OK)
return;
saveDirectoryBackup = dialog.Folder;
var outRoot = dialog.Folder;

timer.Stop();
StatusStripUpdate($"Exporting {models.Count} Spine model(s)...");
Task.Run(() =>
{
var exported = 0;
try
{
foreach (var model in models)
if (SpineExporter.Export(model, outRoot, overwrite: true, log: msg => Logger.Info(msg)) != null)
exported++;
}
catch (Exception ex)
{
Logger.Error($"Spine export failed: {ex.Message}");
}
StatusStripUpdate($"Spine export done: {exported} model(s) -> {outRoot}");
});
}
}
}
1 change: 1 addition & 0 deletions UnityRiftGUI/UnityRiftGUIForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ public UnityRiftGUIForm()
InitDotNetTab();
InitRecentProjectsMenu();
InitGodotExportMenu();
InitSpineMenu();
WrapTreeSearch();
WrapListSearch();
ApplyUiFonts();
Expand Down
Loading
Loading