Add slide atlas tool
This commit is contained in:
@@ -0,0 +1,656 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
public class DDSAtlasSlicerWindow : EditorWindow
|
||||
{
|
||||
[SerializeField] private Texture2D _ddsAtlas;
|
||||
[SerializeField] private TextAsset _txtAsset; // Reference to the .txt in Project
|
||||
[SerializeField] private bool _txtIsGB2312 = true;
|
||||
[SerializeField] private bool _trimEmptyNames = true;
|
||||
[SerializeField] private bool _padIndexIfMissing = true;
|
||||
[SerializeField] private int _paddingPixels = 0;
|
||||
[SerializeField] private float _pixelsPerUnit = 100f;
|
||||
[SerializeField] private bool _exportAsIndividualPngs = false;
|
||||
[SerializeField] private string _exportFolder = string.Empty; // Project relative (starts with Assets/)
|
||||
[SerializeField] private bool _autoFitGridToAtlas = true;
|
||||
[SerializeField] private int _nameStartIndex = 0;
|
||||
[SerializeField] private bool _rowMajor = true; // if false => column-major
|
||||
[SerializeField] private bool _startTop = true; // if false => start bottom
|
||||
[SerializeField] private bool _startLeft = true; // if false => start right
|
||||
[SerializeField] private bool _overrideGridFromAtlas = false; // ignore TXT col/row and derive from atlas size
|
||||
|
||||
private string _status;
|
||||
|
||||
[MenuItem("Tools/Sprites/Slice DDS Atlas From TXT")]
|
||||
private static void Open()
|
||||
{
|
||||
var win = GetWindow<DDSAtlasSlicerWindow>(true, "DDS Atlas Slicer", true);
|
||||
win.minSize = new Vector2(520, 280);
|
||||
win.Show();
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
EditorGUILayout.LabelField("Input", EditorStyles.boldLabel);
|
||||
_ddsAtlas = (Texture2D)EditorGUILayout.ObjectField("DDS Atlas", _ddsAtlas, typeof(Texture2D), false);
|
||||
_txtAsset = (TextAsset)EditorGUILayout.ObjectField("TXT (name list)", _txtAsset, typeof(TextAsset), false);
|
||||
_txtIsGB2312 = EditorGUILayout.ToggleLeft("Decode TXT with GB2312 (Chinese)", _txtIsGB2312);
|
||||
_trimEmptyNames = EditorGUILayout.ToggleLeft("Trim empty or whitespace-only names", _trimEmptyNames);
|
||||
_padIndexIfMissing = EditorGUILayout.ToggleLeft("If name missing, use index as fallback", _padIndexIfMissing);
|
||||
_paddingPixels = EditorGUILayout.IntField("Sprite Padding (px)", _paddingPixels);
|
||||
_pixelsPerUnit = EditorGUILayout.FloatField("Pixels Per Unit", _pixelsPerUnit);
|
||||
|
||||
GUILayout.Space(6);
|
||||
_autoFitGridToAtlas = EditorGUILayout.ToggleLeft("Auto-fit columns/rows to atlas size (prevents OOB)", _autoFitGridToAtlas);
|
||||
_nameStartIndex = EditorGUILayout.IntField("Name Start Index (offset)", _nameStartIndex);
|
||||
_rowMajor = EditorGUILayout.ToggleLeft("Row-Major order (else Column-Major)", _rowMajor);
|
||||
_startTop = EditorGUILayout.ToggleLeft("Names start at Top row (else Bottom)", _startTop);
|
||||
_startLeft = EditorGUILayout.ToggleLeft("Names start at Left col (else Right)", _startLeft);
|
||||
_overrideGridFromAtlas = EditorGUILayout.ToggleLeft("Override grid from atlas size (ignore TXT col/row)", _overrideGridFromAtlas);
|
||||
|
||||
GUILayout.Space(6);
|
||||
EditorGUILayout.LabelField("Output", EditorStyles.boldLabel);
|
||||
_exportAsIndividualPngs = EditorGUILayout.ToggleLeft("Export each sprite as an individual PNG file", _exportAsIndividualPngs);
|
||||
if (_exportAsIndividualPngs)
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.PrefixLabel("Export Folder (Project)");
|
||||
EditorGUILayout.SelectableLabel(string.IsNullOrEmpty(_exportFolder) ? "(auto: <atlasDir>/<atlasName>_slices)" : _exportFolder, GUILayout.Height(16));
|
||||
if (GUILayout.Button("Change...", GUILayout.Width(90)))
|
||||
{
|
||||
string start = string.IsNullOrEmpty(_exportFolder) ? Application.dataPath : ProjectPathToAbsolute(_exportFolder);
|
||||
string chosen = EditorUtility.OpenFolderPanel("Choose export folder (inside Assets)", start, "");
|
||||
if (!string.IsNullOrEmpty(chosen))
|
||||
{
|
||||
string projRel = AbsoluteToProjectPath(chosen);
|
||||
if (!string.IsNullOrEmpty(projRel))
|
||||
{
|
||||
_exportFolder = projRel;
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorUtility.DisplayDialog("Export Folder", "Please choose a folder inside the project's Assets/ directory.", "OK");
|
||||
}
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
GUILayout.Space(8);
|
||||
if (GUILayout.Button("Slice Atlas From TXT", GUILayout.Height(32)))
|
||||
{
|
||||
TrySlice();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_status))
|
||||
{
|
||||
EditorGUILayout.HelpBox(_status, MessageType.Info);
|
||||
}
|
||||
}
|
||||
|
||||
private void TrySlice()
|
||||
{
|
||||
_status = string.Empty;
|
||||
|
||||
if (_ddsAtlas == null)
|
||||
{
|
||||
_status = "Please assign a DDS atlas Texture2D asset.";
|
||||
Repaint();
|
||||
return;
|
||||
}
|
||||
if (_txtAsset == null)
|
||||
{
|
||||
_status = "Please assign the TXT definition asset.";
|
||||
Repaint();
|
||||
return;
|
||||
}
|
||||
|
||||
string txtPath = AssetDatabase.GetAssetPath(_txtAsset);
|
||||
string atlasPath = AssetDatabase.GetAssetPath(_ddsAtlas);
|
||||
if (string.IsNullOrEmpty(txtPath) || string.IsNullOrEmpty(atlasPath))
|
||||
{
|
||||
_status = "Invalid asset references.";
|
||||
Repaint();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var parsed = ParseTxt(txtPath, _txtIsGB2312);
|
||||
if (parsed == null)
|
||||
{
|
||||
_status = "Failed to parse TXT (null).";
|
||||
Repaint();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_exportAsIndividualPngs)
|
||||
{
|
||||
ExportSpritesAsPNGs(atlasPath, parsed.SpriteWidth, parsed.SpriteHeight, parsed.Columns, parsed.Rows, parsed.Names);
|
||||
_status = $"Exported {parsed.Columns * parsed.Rows} PNG sprites.";
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplySlicing(atlasPath, parsed.SpriteWidth, parsed.SpriteHeight, parsed.Columns, parsed.Rows, parsed.Names);
|
||||
_status = $"Sliced {_ddsAtlas.name} into {parsed.Columns * parsed.Rows} sprites.";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[DDSAtlasSlicer] Error: {ex}");
|
||||
_status = "Error. See Console.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
Repaint();
|
||||
}
|
||||
}
|
||||
|
||||
private class TxtDefinition
|
||||
{
|
||||
public int SpriteWidth;
|
||||
public int SpriteHeight;
|
||||
public int Columns;
|
||||
public int Rows;
|
||||
public List<string> Names;
|
||||
}
|
||||
|
||||
private TxtDefinition ParseTxt(string path, bool gb2312)
|
||||
{
|
||||
Encoding GetEncoding()
|
||||
{
|
||||
#if NET_STANDARD_2_1 || NET_6_0_OR_GREATER
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
#endif
|
||||
return Encoding.GetEncoding("GB2312");
|
||||
}
|
||||
|
||||
string[] lines;
|
||||
if (gb2312)
|
||||
{
|
||||
var enc = GetEncoding();
|
||||
lines = File.ReadAllLines(path, enc);
|
||||
}
|
||||
else
|
||||
{
|
||||
lines = File.ReadAllLines(path, Encoding.UTF8);
|
||||
}
|
||||
|
||||
if (lines.Length < 4)
|
||||
throw new InvalidDataException("TXT must have at least 4 lines: width, height, columns, rows.");
|
||||
|
||||
int ReadInt(string s)
|
||||
{
|
||||
if (int.TryParse(s.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int v)) return v;
|
||||
if (int.TryParse(s.Trim(), out v)) return v;
|
||||
throw new InvalidDataException($"Cannot parse integer: '{s}'");
|
||||
}
|
||||
|
||||
int spriteWidth = ReadInt(lines[0]);
|
||||
int spriteHeight = ReadInt(lines[1]);
|
||||
int columns = ReadInt(lines[2]);
|
||||
int rows = ReadInt(lines[3]);
|
||||
|
||||
var names = new List<string>();
|
||||
for (int i = 4; i < lines.Length; i++)
|
||||
{
|
||||
string raw = lines[i];
|
||||
string name = _trimEmptyNames ? raw.Trim() : raw;
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
if (_padIndexIfMissing)
|
||||
{
|
||||
names.Add((i - 4 + _nameStartIndex).ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
names.Add(string.Empty);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Usually the list contains ".dds" suffix; strip it
|
||||
if (name.EndsWith(".dds", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
name = name.Substring(0, name.Length - 4);
|
||||
}
|
||||
names.Add(name);
|
||||
}
|
||||
}
|
||||
|
||||
return new TxtDefinition
|
||||
{
|
||||
SpriteWidth = spriteWidth,
|
||||
SpriteHeight = spriteHeight,
|
||||
Columns = columns,
|
||||
Rows = rows,
|
||||
Names = names
|
||||
};
|
||||
}
|
||||
|
||||
private void ApplySlicing(string atlasAssetPath, int spriteWidth, int spriteHeight, int columns, int rows, List<string> names)
|
||||
{
|
||||
var importer = AssetImporter.GetAtPath(atlasAssetPath) as TextureImporter;
|
||||
if (importer == null)
|
||||
{
|
||||
// Fallback path for formats like DDS that don't use TextureImporter for sprite slicing
|
||||
CreateSpritesAsSubAssets(atlasAssetPath, spriteWidth, spriteHeight, columns, rows, names);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure Readable settings and Multiple sprite mode
|
||||
importer.textureType = TextureImporterType.Sprite;
|
||||
importer.spriteImportMode = SpriteImportMode.Multiple;
|
||||
importer.isReadable = true;
|
||||
importer.mipmapEnabled = false;
|
||||
importer.alphaIsTransparency = true;
|
||||
|
||||
Texture2D tex = AssetDatabase.LoadAssetAtPath<Texture2D>(atlasAssetPath);
|
||||
if (tex == null)
|
||||
throw new InvalidOperationException("Failed to load Texture2D from path.");
|
||||
|
||||
int atlasWidth = tex.width;
|
||||
int atlasHeight = tex.height;
|
||||
|
||||
if (_overrideGridFromAtlas)
|
||||
{
|
||||
columns = Mathf.Max(1, atlasWidth / spriteWidth);
|
||||
rows = Mathf.Max(1, atlasHeight / spriteHeight);
|
||||
}
|
||||
else if (_autoFitGridToAtlas)
|
||||
{
|
||||
int maxCols = Mathf.Max(1, atlasWidth / spriteWidth);
|
||||
int maxRows = Mathf.Max(1, atlasHeight / spriteHeight);
|
||||
columns = Mathf.Min(columns, maxCols);
|
||||
rows = Mathf.Min(rows, maxRows);
|
||||
}
|
||||
|
||||
if (spriteWidth <= 0 || spriteHeight <= 0)
|
||||
throw new InvalidDataException("Sprite width/height must be > 0.");
|
||||
if (columns <= 0 || rows <= 0)
|
||||
throw new InvalidDataException("Columns/Rows must be > 0.");
|
||||
|
||||
int expected = columns * rows;
|
||||
var metas = new List<SpriteMetaData>(expected);
|
||||
for (int r = 0; r < rows; r++)
|
||||
{
|
||||
for (int c = 0; c < columns; c++)
|
||||
{
|
||||
int rr = _startTop ? r : (rows - 1 - r);
|
||||
int cc = _startLeft ? c : (columns - 1 - c);
|
||||
int index = _rowMajor ? (rr * columns + cc) : (cc * rows + rr);
|
||||
int x = c * spriteWidth;
|
||||
int yFromBottom = (rows - 1 - r) * spriteHeight; // Unity rect y starts from bottom
|
||||
|
||||
var rect = new Rect(x, yFromBottom, spriteWidth, spriteHeight);
|
||||
if (rect.x < 0 || rect.y < 0 || rect.xMax > atlasWidth || rect.yMax > atlasHeight)
|
||||
{
|
||||
if (_autoFitGridToAtlas)
|
||||
{
|
||||
float nx = Mathf.Clamp(rect.x, 0, Mathf.Max(0, atlasWidth - 1));
|
||||
float ny = Mathf.Clamp(rect.y, 0, Mathf.Max(0, atlasHeight - 1));
|
||||
float nw = Mathf.Clamp(rect.width, 0, atlasWidth - nx);
|
||||
float nh = Mathf.Clamp(rect.height, 0, atlasHeight - ny);
|
||||
rect = new Rect(nx, ny, nw, nh);
|
||||
if (rect.width <= 0 || rect.height <= 0) continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
string rawName = GetNameForIndex(names, index);
|
||||
var meta = new SpriteMetaData
|
||||
{
|
||||
name = SafeSpriteName(rawName, index),
|
||||
rect = rect,
|
||||
alignment = (int)SpriteAlignment.Center,
|
||||
border = Vector4.zero,
|
||||
pivot = new Vector2(0.5f, 0.5f)
|
||||
};
|
||||
metas.Add(meta);
|
||||
}
|
||||
}
|
||||
|
||||
if (_paddingPixels > 0)
|
||||
{
|
||||
for (int i = 0; i < metas.Count; i++)
|
||||
metas[i] = ApplyPadding(metas[i], _paddingPixels, atlasWidth, atlasHeight);
|
||||
}
|
||||
|
||||
importer.spritesheet = metas.ToArray();
|
||||
EditorUtility.SetDirty(importer);
|
||||
importer.SaveAndReimport();
|
||||
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
private void CreateSpritesAsSubAssets(string atlasAssetPath, int spriteWidth, int spriteHeight, int columns, int rows, List<string> names)
|
||||
{
|
||||
Texture2D tex = AssetDatabase.LoadAssetAtPath<Texture2D>(atlasAssetPath);
|
||||
if (tex == null)
|
||||
throw new InvalidOperationException($"Failed to load Texture2D from path: {atlasAssetPath}");
|
||||
|
||||
int atlasWidth = tex.width;
|
||||
int atlasHeight = tex.height;
|
||||
|
||||
if (_overrideGridFromAtlas)
|
||||
{
|
||||
columns = Mathf.Max(1, atlasWidth / spriteWidth);
|
||||
rows = Mathf.Max(1, atlasHeight / spriteHeight);
|
||||
}
|
||||
else if (_autoFitGridToAtlas)
|
||||
{
|
||||
int maxCols = Mathf.Max(1, atlasWidth / spriteWidth);
|
||||
int maxRows = Mathf.Max(1, atlasHeight / spriteHeight);
|
||||
columns = Mathf.Min(columns, maxCols);
|
||||
rows = Mathf.Min(rows, maxRows);
|
||||
}
|
||||
|
||||
if (spriteWidth <= 0 || spriteHeight <= 0)
|
||||
throw new InvalidDataException("Sprite width/height must be > 0.");
|
||||
if (columns <= 0 || rows <= 0)
|
||||
throw new InvalidDataException("Columns/Rows must be > 0.");
|
||||
if (columns * spriteWidth > atlasWidth || rows * spriteHeight > atlasHeight)
|
||||
Debug.LogWarning($"[DDSAtlasSlicer] Grid exceeds atlas bounds: grid={columns}x{rows} cell={spriteWidth}x{spriteHeight} atlas={atlasWidth}x{atlasHeight}");
|
||||
|
||||
int expected = columns * rows;
|
||||
|
||||
// Clear previous Sprite sub-assets under this texture
|
||||
var all = AssetDatabase.LoadAllAssetsAtPath(atlasAssetPath);
|
||||
foreach (var obj in all)
|
||||
{
|
||||
if (obj is Sprite)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(obj, true);
|
||||
}
|
||||
}
|
||||
|
||||
for (int r = 0; r < rows; r++)
|
||||
{
|
||||
for (int c = 0; c < columns; c++)
|
||||
{
|
||||
int rr = _startTop ? r : (rows - 1 - r);
|
||||
int cc = _startLeft ? c : (columns - 1 - c);
|
||||
int index = _rowMajor ? (rr * columns + cc) : (cc * rows + rr);
|
||||
int x = c * spriteWidth;
|
||||
int yFromBottom = (rows - 1 - r) * spriteHeight; // bottom-left origin
|
||||
|
||||
var rect = new Rect(x, yFromBottom, spriteWidth, spriteHeight);
|
||||
if (rect.x < 0 || rect.y < 0 || rect.xMax > atlasWidth || rect.yMax > atlasHeight)
|
||||
{
|
||||
if (_autoFitGridToAtlas)
|
||||
{
|
||||
float nx = Mathf.Clamp(rect.x, 0, Mathf.Max(0, atlasWidth - 1));
|
||||
float ny = Mathf.Clamp(rect.y, 0, Mathf.Max(0, atlasHeight - 1));
|
||||
float nw = Mathf.Clamp(rect.width, 0, atlasWidth - nx);
|
||||
float nh = Mathf.Clamp(rect.height, 0, atlasHeight - ny);
|
||||
rect = new Rect(nx, ny, nw, nh);
|
||||
if (rect.width <= 0 || rect.height <= 0) continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (_paddingPixels > 0)
|
||||
{
|
||||
float nx = Mathf.Max(0, rect.x - _paddingPixels);
|
||||
float ny = Mathf.Max(0, rect.y - _paddingPixels);
|
||||
float nw = Mathf.Min(atlasWidth - nx, rect.width + 2 * _paddingPixels);
|
||||
float nh = Mathf.Min(atlasHeight - ny, rect.height + 2 * _paddingPixels);
|
||||
rect = new Rect(nx, ny, nw, nh);
|
||||
}
|
||||
|
||||
string sprName = SafeSpriteName(GetNameForIndex(names, index), index);
|
||||
var sprite = Sprite.Create(tex, rect, new Vector2(0.5f, 0.5f), Mathf.Max(1f, _pixelsPerUnit));
|
||||
sprite.name = sprName;
|
||||
AssetDatabase.AddObjectToAsset(sprite, tex);
|
||||
}
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(tex);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
private void ExportSpritesAsPNGs(string atlasAssetPath, int spriteWidth, int spriteHeight, int columns, int rows, List<string> names)
|
||||
{
|
||||
Texture2D atlasTex = AssetDatabase.LoadAssetAtPath<Texture2D>(atlasAssetPath);
|
||||
if (atlasTex == null)
|
||||
throw new InvalidOperationException($"Failed to load Texture2D from path: {atlasAssetPath}");
|
||||
|
||||
int atlasWidth = atlasTex.width;
|
||||
int atlasHeight = atlasTex.height;
|
||||
|
||||
if (_overrideGridFromAtlas)
|
||||
{
|
||||
columns = Mathf.Max(1, atlasWidth / spriteWidth);
|
||||
rows = Mathf.Max(1, atlasHeight / spriteHeight);
|
||||
}
|
||||
else if (_autoFitGridToAtlas)
|
||||
{
|
||||
int maxCols = Mathf.Max(1, atlasWidth / spriteWidth);
|
||||
int maxRows = Mathf.Max(1, atlasHeight / spriteHeight);
|
||||
columns = Mathf.Min(columns, maxCols);
|
||||
rows = Mathf.Min(rows, maxRows);
|
||||
}
|
||||
|
||||
if (spriteWidth <= 0 || spriteHeight <= 0)
|
||||
throw new InvalidDataException("Sprite width/height must be > 0.");
|
||||
if (columns <= 0 || rows <= 0)
|
||||
throw new InvalidDataException("Columns/Rows must be > 0.");
|
||||
|
||||
int expected = columns * rows;
|
||||
|
||||
// Resolve export folder
|
||||
string targetFolderProject = _exportFolder;
|
||||
if (string.IsNullOrEmpty(targetFolderProject))
|
||||
{
|
||||
string atlasDir = Path.GetDirectoryName(atlasAssetPath).Replace('\\', '/');
|
||||
string atlasName = Path.GetFileNameWithoutExtension(atlasAssetPath);
|
||||
targetFolderProject = $"{atlasDir}/{atlasName}_slices";
|
||||
}
|
||||
EnsureFolder(targetFolderProject);
|
||||
string targetFolderAbs = ProjectPathToAbsolute(targetFolderProject);
|
||||
|
||||
var rt = new RenderTexture(atlasWidth, atlasHeight, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
|
||||
var prevActive = RenderTexture.active;
|
||||
Graphics.Blit(atlasTex, rt);
|
||||
RenderTexture.active = rt;
|
||||
|
||||
try
|
||||
{
|
||||
for (int r = 0; r < rows; r++)
|
||||
{
|
||||
for (int c = 0; c < columns; c++)
|
||||
{
|
||||
int rr = _startTop ? r : (rows - 1 - r);
|
||||
int cc = _startLeft ? c : (columns - 1 - c);
|
||||
int index = _rowMajor ? (rr * columns + cc) : (cc * rows + rr);
|
||||
int x = c * spriteWidth;
|
||||
int yFromBottom = (rows - 1 - r) * spriteHeight;
|
||||
|
||||
var rect = new Rect(x, yFromBottom, spriteWidth, spriteHeight);
|
||||
if (rect.x < 0 || rect.y < 0 || rect.xMax > atlasWidth || rect.yMax > atlasHeight)
|
||||
{
|
||||
if (_autoFitGridToAtlas)
|
||||
{
|
||||
float nx = Mathf.Clamp(rect.x, 0, Mathf.Max(0, atlasWidth - 1));
|
||||
float ny = Mathf.Clamp(rect.y, 0, Mathf.Max(0, atlasHeight - 1));
|
||||
float nw = Mathf.Clamp(rect.width, 0, atlasWidth - nx);
|
||||
float nh = Mathf.Clamp(rect.height, 0, atlasHeight - ny);
|
||||
rect = new Rect(nx, ny, nw, nh);
|
||||
if (rect.width <= 0 || rect.height <= 0) continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (_paddingPixels > 0)
|
||||
{
|
||||
float nx = Mathf.Max(0, rect.x - _paddingPixels);
|
||||
float ny = Mathf.Max(0, rect.y - _paddingPixels);
|
||||
float nw = Mathf.Min(atlasWidth - nx, rect.width + 2 * _paddingPixels);
|
||||
float nh = Mathf.Min(atlasHeight - ny, rect.height + 2 * _paddingPixels);
|
||||
rect = new Rect(nx, ny, nw, nh);
|
||||
}
|
||||
|
||||
int w = Mathf.RoundToInt(rect.width);
|
||||
int h = Mathf.RoundToInt(rect.height);
|
||||
if (w <= 0 || h <= 0) continue;
|
||||
|
||||
var slice = new Texture2D(w, h, TextureFormat.RGBA32, false, false);
|
||||
slice.ReadPixels(rect, 0, 0);
|
||||
slice.Apply(false, false);
|
||||
|
||||
string sprName = SafeSpriteName(GetNameForIndex(names, index), index);
|
||||
if (string.IsNullOrWhiteSpace(sprName)) sprName = $"sprite_{index:0000}";
|
||||
string fileAbs = Path.Combine(targetFolderAbs, sprName + ".png");
|
||||
fileAbs = GetUniqueFilePath(fileAbs);
|
||||
var png = ImageConversion.EncodeToPNG(slice);
|
||||
File.WriteAllBytes(fileAbs, png);
|
||||
UnityEngine.Object.DestroyImmediate(slice);
|
||||
|
||||
string fileProject = AbsoluteToProjectPath(fileAbs).Replace('\\', '/');
|
||||
if (!string.IsNullOrEmpty(fileProject))
|
||||
{
|
||||
AssetDatabase.ImportAsset(fileProject);
|
||||
var pngImporter = AssetImporter.GetAtPath(fileProject) as TextureImporter;
|
||||
if (pngImporter != null)
|
||||
{
|
||||
pngImporter.textureType = TextureImporterType.Sprite;
|
||||
pngImporter.spriteImportMode = SpriteImportMode.Single;
|
||||
pngImporter.mipmapEnabled = false;
|
||||
pngImporter.alphaIsTransparency = true;
|
||||
pngImporter.spritePixelsPerUnit = Mathf.Max(1f, _pixelsPerUnit);
|
||||
pngImporter.SaveAndReimport();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
RenderTexture.active = prevActive;
|
||||
rt.Release();
|
||||
UnityEngine.Object.DestroyImmediate(rt);
|
||||
}
|
||||
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
private static void EnsureFolder(string projectFolderPath)
|
||||
{
|
||||
projectFolderPath = projectFolderPath.Replace('\\', '/');
|
||||
if (string.IsNullOrEmpty(projectFolderPath)) return;
|
||||
if (!projectFolderPath.StartsWith("Assets"))
|
||||
throw new InvalidOperationException("Export folder must be inside Assets/.");
|
||||
|
||||
if (AssetDatabase.IsValidFolder(projectFolderPath)) return;
|
||||
|
||||
string[] parts = projectFolderPath.Split('/');
|
||||
string current = parts[0]; // "Assets"
|
||||
for (int i = 1; i < parts.Length; i++)
|
||||
{
|
||||
string next = current + "/" + parts[i];
|
||||
if (!AssetDatabase.IsValidFolder(next))
|
||||
{
|
||||
AssetDatabase.CreateFolder(current, parts[i]);
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ProjectPathToAbsolute(string projectPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(projectPath)) return string.Empty;
|
||||
string pp = projectPath.Replace('\\', '/');
|
||||
if (!pp.StartsWith("Assets")) return string.Empty;
|
||||
string data = Application.dataPath.Replace('\\', '/');
|
||||
return data + pp.Substring("Assets".Length);
|
||||
}
|
||||
|
||||
private static string AbsoluteToProjectPath(string absolutePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(absolutePath)) return string.Empty;
|
||||
string abs = absolutePath.Replace('\\', '/');
|
||||
string data = Application.dataPath.Replace('\\', '/');
|
||||
if (abs.StartsWith(data, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "Assets" + abs.Substring(data.Length);
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string GetUniqueFilePath(string absolutePath)
|
||||
{
|
||||
string dir = Path.GetDirectoryName(absolutePath);
|
||||
string name = Path.GetFileNameWithoutExtension(absolutePath);
|
||||
string ext = Path.GetExtension(absolutePath);
|
||||
string candidate = absolutePath;
|
||||
int suffix = 1;
|
||||
while (File.Exists(candidate))
|
||||
{
|
||||
candidate = Path.Combine(dir, $"{name}_{suffix}{ext}");
|
||||
suffix++;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private static SpriteMetaData ApplyPadding(SpriteMetaData meta, int pad, int atlasWidth, int atlasHeight)
|
||||
{
|
||||
var r = meta.rect;
|
||||
float nx = Mathf.Max(0, r.x - pad);
|
||||
float ny = Mathf.Max(0, r.y - pad);
|
||||
float nw = Mathf.Min(atlasWidth - nx, r.width + 2 * pad);
|
||||
float nh = Mathf.Min(atlasHeight - ny, r.height + 2 * pad);
|
||||
meta.rect = new Rect(nx, ny, nw, nh);
|
||||
return meta;
|
||||
}
|
||||
|
||||
private static string SafeSpriteName(string raw, int index)
|
||||
{
|
||||
string name = raw;
|
||||
if (string.IsNullOrWhiteSpace(name)) name = index.ToString();
|
||||
// Unity dislikes slashes and some symbols in asset names
|
||||
char[] invalid = Path.GetInvalidFileNameChars();
|
||||
var sb = new StringBuilder(name.Length);
|
||||
foreach (char ch in name)
|
||||
{
|
||||
if (invalid.Contains(ch) || ch == '/' || ch == '\\' || ch == ':' || ch == '*')
|
||||
{
|
||||
sb.Append('_');
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(ch);
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private string GetNameForIndex(List<string> names, int gridIndex)
|
||||
{
|
||||
int idx = gridIndex;
|
||||
if (_nameStartIndex != 0)
|
||||
{
|
||||
idx = gridIndex + _nameStartIndex;
|
||||
}
|
||||
if (idx >= 0 && idx < names.Count)
|
||||
{
|
||||
string n = names[idx];
|
||||
if (!string.IsNullOrWhiteSpace(n)) return n;
|
||||
}
|
||||
return gridIndex.ToString();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f66c1988f2c6e574c84c02a7c552f18f
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6561a7d3c5c2b52439357c35aae55f99
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,21 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f6ef10a15137cb44481b51ab73c71a98
|
||||
IHVImageFormatImporter:
|
||||
externalObjects: {}
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
isReadable: 0
|
||||
sRGBTexture: 1
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
ignoreMipmapLimit: 0
|
||||
mipmapLimitGroupName:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d0079e79add41774e9c860b7d90ba716
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 39f58d06fe920fc46bcba0b2f7d6a98e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -294,17 +294,19 @@ namespace PerfectWorld.Scripts.Managers
|
||||
var image = button.GetComponent<Image>();
|
||||
if (image != null)
|
||||
{
|
||||
if (hasItem && itemData != null && itemData.State != 0)
|
||||
// Set icon sprite based on item TemplateId
|
||||
if (hasItem && itemData != null && itemData.Count > 0)
|
||||
{
|
||||
image.color = Color.yellow;
|
||||
}
|
||||
else if (!hasItem)
|
||||
{
|
||||
image.color = Color.white;
|
||||
var sprite = EC_IvtrItem.ResolveItemIconSprite(itemData.TemplateId);
|
||||
image.sprite = sprite;
|
||||
image.color = sprite != null ? Color.white : Color.white;
|
||||
image.enabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
image.sprite = null;
|
||||
image.color = Color.white;
|
||||
image.enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.IO;
|
||||
using BrewMonster;
|
||||
using ModelRenderer.Scripts.Common;
|
||||
using ModelRenderer.Scripts.GameData;
|
||||
@@ -26,6 +27,8 @@ namespace PerfectWorld.Scripts.Managers
|
||||
{
|
||||
private static readonly Dictionary<int, string> _tidNameCache = new Dictionary<int, string>();
|
||||
private static readonly Dictionary<int, int> _pileLimitCache = new Dictionary<int, int>();
|
||||
private static readonly Dictionary<int, string> _tidIconKeyCache = new Dictionary<int, string>();
|
||||
private static readonly Dictionary<string, Sprite> _iconSpriteCache = new Dictionary<string, Sprite>(StringComparer.OrdinalIgnoreCase);
|
||||
private const int MaxContentHexToLog = 64;
|
||||
|
||||
public static string ResolveItemName(int templateId)
|
||||
@@ -52,6 +55,160 @@ namespace PerfectWorld.Scripts.Managers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve and load the item's icon Sprite from Resources/UI/IconSprites based on its element data file_icon (DDS) name.
|
||||
/// </summary>
|
||||
public static Sprite ResolveItemIconSprite(int templateId)
|
||||
{
|
||||
if (templateId <= 0) return null;
|
||||
if (_tidIconKeyCache.TryGetValue(templateId, out var cachedKey))
|
||||
{
|
||||
if (string.IsNullOrEmpty(cachedKey)) return null;
|
||||
if (_iconSpriteCache.TryGetValue(cachedKey, out var cachedSprite) && cachedSprite != null)
|
||||
return cachedSprite;
|
||||
var sprite = LoadIconSpriteByKey(cachedKey);
|
||||
_iconSpriteCache[cachedKey] = sprite;
|
||||
return sprite;
|
||||
}
|
||||
|
||||
string key = string.Empty;
|
||||
try
|
||||
{
|
||||
var edm = ElementDataManProvider.GetElementDataMan();
|
||||
if (edm != null)
|
||||
{
|
||||
uint id = unchecked((uint)templateId);
|
||||
object data = edm.get_data_ptr(id, ID_SPACE.ID_SPACE_ESSENCE);
|
||||
if (data == null)
|
||||
{
|
||||
data = TryFindElementByScanningArrays(edm, id);
|
||||
}
|
||||
string iconPath = ExtractIconPathFromElement(data);
|
||||
key = NormalizeIconResourceKeyFromPath(iconPath);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
_tidIconKeyCache[templateId] = key ?? string.Empty;
|
||||
if (string.IsNullOrEmpty(key))
|
||||
return null;
|
||||
|
||||
if (_iconSpriteCache.TryGetValue(key, out var spriteCached) && spriteCached != null)
|
||||
return spriteCached;
|
||||
|
||||
var spriteLoaded = LoadIconSpriteByKey(key);
|
||||
_iconSpriteCache[key] = spriteLoaded;
|
||||
return spriteLoaded;
|
||||
}
|
||||
|
||||
private static Sprite LoadIconSpriteByKey(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key)) return null;
|
||||
// Resources path without extension
|
||||
string resPath = $"UI/IconSprites/{key}";
|
||||
var sprite = Resources.Load<Sprite>(resPath);
|
||||
if (sprite == null)
|
||||
{
|
||||
// Try lowercase/uppercase variants or zero-stripped variants as a fallback
|
||||
var alt = Resources.Load<Sprite>($"UI/IconSprites/{key.ToLowerInvariant()}");
|
||||
if (alt != null) return alt;
|
||||
alt = Resources.Load<Sprite>($"UI/IconSprites/{key.ToUpperInvariant()}");
|
||||
if (alt != null) return alt;
|
||||
}
|
||||
return sprite;
|
||||
}
|
||||
|
||||
private static object TryFindElementByScanningArrays(object edm, uint id)
|
||||
{
|
||||
if (edm == null) return null;
|
||||
try
|
||||
{
|
||||
var fields = edm.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance);
|
||||
foreach (var f in fields)
|
||||
{
|
||||
if (!f.FieldType.IsArray) continue;
|
||||
var arr = f.GetValue(edm) as Array;
|
||||
if (arr == null || arr.Length == 0) continue;
|
||||
var elemType = f.FieldType.GetElementType();
|
||||
var idField = elemType.GetField("id", BindingFlags.Public | BindingFlags.Instance);
|
||||
if (idField == null || idField.FieldType != typeof(uint)) continue;
|
||||
for (int i = 0; i < arr.Length; i++)
|
||||
{
|
||||
var element = arr.GetValue(i);
|
||||
if (element == null) continue;
|
||||
var value = (uint)idField.GetValue(element);
|
||||
if (value == id)
|
||||
{
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string ExtractIconPathFromElement(object data)
|
||||
{
|
||||
if (data == null) return string.Empty;
|
||||
var t = data.GetType();
|
||||
// Common field/property name for icon path in element data is file_icon (byte[128])
|
||||
var field = t.GetField("file_icon", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||
if (field != null && typeof(byte[]).IsAssignableFrom(field.FieldType))
|
||||
{
|
||||
try
|
||||
{
|
||||
var bytes = field.GetValue(data) as byte[];
|
||||
string s = ByteToStringUtils.ByteArrayToCP936String(bytes);
|
||||
if (string.IsNullOrEmpty(s)) s = ByteToStringUtils.ByteArrayToUTF8String(bytes);
|
||||
if (string.IsNullOrEmpty(s)) s = ByteToStringUtils.ByteArrayToUnicodeString(bytes);
|
||||
return s ?? string.Empty;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
var prop = t.GetProperty("file_icon", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||
if (prop != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (prop.PropertyType == typeof(string))
|
||||
{
|
||||
var s = prop.GetValue(data, null) as string;
|
||||
return s ?? string.Empty;
|
||||
}
|
||||
if (prop.PropertyType == typeof(byte[]))
|
||||
{
|
||||
var bytes = prop.GetValue(data, null) as byte[];
|
||||
string s = ByteToStringUtils.ByteArrayToCP936String(bytes);
|
||||
if (string.IsNullOrEmpty(s)) s = ByteToStringUtils.ByteArrayToUTF8String(bytes);
|
||||
if (string.IsNullOrEmpty(s)) s = ByteToStringUtils.ByteArrayToUnicodeString(bytes);
|
||||
return s ?? string.Empty;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string NormalizeIconResourceKeyFromPath(string iconPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(iconPath)) return string.Empty;
|
||||
try
|
||||
{
|
||||
string p = iconPath.Replace('\\', '/');
|
||||
// Some data might contain leading directories; take file name
|
||||
string fileName = Path.GetFileName(p);
|
||||
if (string.IsNullOrEmpty(fileName)) fileName = p;
|
||||
// Remove extension (.dds/.tga/.png)
|
||||
string name = Path.GetFileNameWithoutExtension(fileName);
|
||||
if (string.IsNullOrEmpty(name)) name = fileName;
|
||||
// Many PW icons are numeric (e.g., 8800). Use as-is
|
||||
return name.Trim();
|
||||
}
|
||||
catch { }
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string CacheAndReturn(int tid, string name)
|
||||
{
|
||||
_tidNameCache[tid] = name ?? "";
|
||||
|
||||
Reference in New Issue
Block a user