Core - Add ResourcePackImporter.cs

This commit is contained in:
miku-666
2025-12-02 19:20:35 +01:00
parent 440dadec35
commit 8a8c4330fd
28 changed files with 3384 additions and 126 deletions

View File

@@ -0,0 +1,18 @@
using System.ComponentModel;
using System.Diagnostics;
namespace PckStudio.Controls
{
static class BackgroundWorkerExtensions
{
public static void ReportProgressInfo(this BackgroundWorker worker, string message)
=> worker.ReportProgress(0, ProgressReportMessage.Info(message));
public static void ReportProgressWarning(this BackgroundWorker worker, string message)
=> worker.ReportProgress(0, ProgressReportMessage.Warning(message));
public static void ReportProgressError(this BackgroundWorker worker, string message)
=> worker.ReportProgress(0, ProgressReportMessage.Error(message));
[Conditional("DEBUG")]
public static void ReportProgressDebug(this BackgroundWorker worker, string message)
=> worker.ReportProgress(0, ProgressReportMessage.Debug(message));
}
}

View File

@@ -0,0 +1,88 @@
namespace PckStudio.Controls
{
partial class JavaTextFormatForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(JavaTextFormatForm));
this.richTextBox1 = new System.Windows.Forms.RichTextBox();
this.importWorker = new System.ComponentModel.BackgroundWorker();
this.cancelButton = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// richTextBox1
//
this.richTextBox1.BackColor = System.Drawing.SystemColors.ControlDarkDark;
this.richTextBox1.BorderStyle = System.Windows.Forms.BorderStyle.None;
this.richTextBox1.Dock = System.Windows.Forms.DockStyle.Fill;
this.richTextBox1.ForeColor = System.Drawing.SystemColors.Window;
this.richTextBox1.Location = new System.Drawing.Point(0, 0);
this.richTextBox1.Name = "richTextBox1";
this.richTextBox1.ReadOnly = true;
this.richTextBox1.ScrollBars = System.Windows.Forms.RichTextBoxScrollBars.Vertical;
this.richTextBox1.Size = new System.Drawing.Size(754, 434);
this.richTextBox1.TabIndex = 0;
this.richTextBox1.Text = "";
//
// importWorker
//
this.importWorker.WorkerReportsProgress = true;
this.importWorker.WorkerSupportsCancellation = true;
//
// cancelButton
//
this.cancelButton.Dock = System.Windows.Forms.DockStyle.Bottom;
this.cancelButton.FlatStyle = System.Windows.Forms.FlatStyle.System;
this.cancelButton.Location = new System.Drawing.Point(0, 411);
this.cancelButton.Name = "cancelButton";
this.cancelButton.Size = new System.Drawing.Size(754, 23);
this.cancelButton.TabIndex = 1;
this.cancelButton.Text = "Cancel";
this.cancelButton.UseVisualStyleBackColor = true;
this.cancelButton.Click += new System.EventHandler(this.cancelButton_Click);
//
// JavaTextFormatForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(754, 434);
this.Controls.Add(this.cancelButton);
this.Controls.Add(this.richTextBox1);
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.Name = "JavaTextFormatForm";
this.Text = "JavaTextFormatForm";
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.RichTextBox richTextBox1;
private System.ComponentModel.BackgroundWorker importWorker;
private System.Windows.Forms.Button cancelButton;
}
}

View File

@@ -0,0 +1,201 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading;
using System.Windows.Forms;
using PckStudio.Core;
using PckStudio.Core.IO.Java;
using PckStudio.ToolboxItems;
namespace PckStudio.Controls
{
public partial class JavaTextFormatForm : ImmersiveForm
{
static Dictionary<string, RichTextBoxColor> _javaColorCodeToColor = new Dictionary<string, RichTextBoxColor>()
{
["§0"] = new RichTextBoxColor(Color.FromArgb(0x00, 0x00, 0x00), Color.FromArgb(18, 18, 18)),
["§1"] = new RichTextBoxColor(Color.FromArgb(0x00, 0x00, 0xAA), Color.FromArgb(0x00, 0x00, 0x2A)),
["§2"] = new RichTextBoxColor(Color.FromArgb(0x00, 0xAA, 0x00), Color.FromArgb(0x00, 0x2A, 0x00)),
["§3"] = new RichTextBoxColor(Color.FromArgb(0x00, 0xAA, 0xAA), Color.FromArgb(0x00, 0x2A, 0x2A)),
["§4"] = new RichTextBoxColor(Color.FromArgb(0xAA, 0x00, 0x00), Color.FromArgb(0x2A, 0x00, 0x00)),
["§5"] = new RichTextBoxColor(Color.FromArgb(0xAA, 0x00, 0xAA), Color.FromArgb(0x2A, 0x00, 0x2A)),
["§6"] = new RichTextBoxColor(Color.FromArgb(0xFF, 0xAA, 0x00), Color.FromArgb(0x2A, 0x2A, 0x00)),
["§7"] = new RichTextBoxColor(Color.FromArgb(0xAA, 0xAA, 0xAA), Color.FromArgb(0x2A, 0x2A, 0x2A)),
["§8"] = new RichTextBoxColor(Color.FromArgb(0x55, 0x55, 0x55), Color.FromArgb(0x15, 0x15, 0x15)),
["§9"] = new RichTextBoxColor(Color.FromArgb(0x55, 0x55, 0xFF), Color.FromArgb(0x15, 0x15, 0x3F)),
["§a"] = new RichTextBoxColor(Color.FromArgb(0x55, 0xFF, 0x55), Color.FromArgb(0x15, 0x3F, 0x15)),
["§b"] = new RichTextBoxColor(Color.FromArgb(0x55, 0xFF, 0xFF), Color.FromArgb(0x15, 0x3F, 0x3F)),
["§c"] = new RichTextBoxColor(Color.FromArgb(0xFF, 0x55, 0x55), Color.FromArgb(0x3F, 0x15, 0x15)),
["§d"] = new RichTextBoxColor(Color.FromArgb(0xFF, 0x55, 0xFF), Color.FromArgb(0x3F, 0x15, 0x3F)),
["§e"] = new RichTextBoxColor(Color.FromArgb(0xFF, 0xFF, 0x55), Color.FromArgb(0x3F, 0x3F, 0x15)),
["§f"] = new RichTextBoxColor(Color.FromArgb(0xFF, 0xFF, 0xFF), Color.FromArgb(0x3F, 0x3F, 0x3F)),
["§g"] = new RichTextBoxColor(Color.FromArgb(0xDD, 0xD6, 0x05), Color.FromArgb(0x37, 0x35, 0x01)),
["§h"] = new RichTextBoxColor(Color.FromArgb(0xE3, 0xD4, 0xD1), Color.FromArgb(0x38, 0x35, 0x34)),
["§i"] = new RichTextBoxColor(Color.FromArgb(0xCE, 0xCA, 0xCA), Color.FromArgb(0x33, 0x32, 0x32)),
["§j"] = new RichTextBoxColor(Color.FromArgb(0x44, 0x3A, 0x3B), Color.FromArgb(0x11, 0x0E, 0x0E)),
["§m"] = new RichTextBoxColor(Color.FromArgb(0x97, 0x16, 0x07), Color.FromArgb(0x25, 0x05, 0x01)),
["§n"] = new RichTextBoxColor(Color.FromArgb(0xB4, 0x68, 0x4D), Color.FromArgb(0x2D, 0x1A, 0x13)),
["§p"] = new RichTextBoxColor(Color.FromArgb(0xDE, 0xB1, 0x2D), Color.FromArgb(0x37, 0x2C, 0x0B)),
["§q"] = new RichTextBoxColor(Color.FromArgb(0x47, 0xA0, 0x36), Color.FromArgb(0x04, 0x28, 0x0D)),
["§s"] = new RichTextBoxColor(Color.FromArgb(0x2C, 0xBA, 0xA8), Color.FromArgb(0x0B, 0x2E, 0x2A)),
["§t"] = new RichTextBoxColor(Color.FromArgb(0x21, 0x49, 0x7B), Color.FromArgb(0x08, 0x12, 0x1E)),
["§u"] = new RichTextBoxColor(Color.FromArgb(0x9A, 0x5C, 0xC6), Color.FromArgb(0x26, 0x17, 0x31)),
};
public JavaTextFormatForm(FileInfo fileInfo)
{
InitializeComponent();
var zip = new ZipArchive(fileInfo.OpenRead(), ZipArchiveMode.Read);
ResourcePackImporter importer = new ResourcePackImporter(zip, default);
FormatPackDescription(fileInfo.Name, importer.ReadPackMeta(zip).Description);
importWorker.DoWork += Import;
importWorker.ProgressChanged += ImportProgressChanged;
importWorker.RunWorkerCompleted += ImportCompleted;
importWorker.RunWorkerAsync(importer);
}
private void ImportProgressChanged(object sender, ProgressChangedEventArgs eventArgs)
{
if (eventArgs?.UserState is not ProgressReportMessage reportMessage)
return;
richTextBox1.AppendLine($"[{reportMessage.Type}]: {reportMessage.Message}", reportMessage.MessageColor);
}
private void Import(object sender, DoWorkEventArgs eventArgs)
{
if (sender is not BackgroundWorker worker)
throw new Exception();
if (eventArgs.Argument is not ResourcePackImporter importer)
{
worker.ReportProgressError("Invalid argument passed to background worker.");
eventArgs.Cancel = true;
return;
}
worker.ReportProgressInfo($"Start import");
while(!(eventArgs.Cancel = worker.CancellationPending))
{
AtlasResource.AtlasType atlasType = AtlasResource.AtlasType.BlockAtlas;
ImportResult<Atlas, ResourcePackImporter.ImportStats> res = importer.ImportAtlas(ResourceLocations.GetFromCategory(AtlasResource.GetId(atlasType)) as AtlasResource);
worker.ReportProgressDebug("Import Stats");
worker.ReportProgressDebug($"Textures: {res.Stats.Textures}/{res.Stats.MaxTextures}({res.Stats.MissingTextures} missing)");
worker.ReportProgressDebug($"Animations: {res.Stats.Animations}");
// on success do:
if (true)
{
worker.ReportProgressInfo($"Import successful");
eventArgs.Result = res.Result;
break;
}
}
}
private void ImportCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
{
MessageBox.Show("Import cancelled", $"Import cancelled.");
return;
}
MessageBox.Show("Import successful", $"");
var f = new Form();
f.Size = new Size(600, 600);
var picBox = new InterpolationPictureBox();
picBox.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
picBox.SizeMode = PictureBoxSizeMode.Zoom;
picBox.Dock = DockStyle.Fill;
picBox.Image = e.Result as Atlas;
f.Controls.Add(picBox);
cancelButton.Enabled = false;
f.ShowDialog();
}
private void FormatPackDescription(string title, string text)
{
richTextBox1.SelectionAlignment = HorizontalAlignment.Center;
richTextBox1.AppendLine("Name: ");
richTextBox1.AppendLine(title);
richTextBox1.AppendLine("Description: ");
if (!text.Contains("§"))
{
richTextBox1.AppendLine(text);
richTextBox1.SelectionAlignment = HorizontalAlignment.Left;
return;
}
foreach (KeyValuePair<string, string> textSection in text.Split(['§'], StringSplitOptions.RemoveEmptyEntries).Select(s => new KeyValuePair<string, string>("§" + char.ToLower(s[0]), string.IsNullOrWhiteSpace(s) ? string.Empty : s.Substring(1))))
{
switch (textSection.Key)
{
// obfuscated/MTS*
case "§k":
var rng = new Random();
string value = new string(textSection.Value.Select(c => Convert.ToChar(c + rng.Next(26 - (char.ToLower(c) - 0x30)))).ToArray());
richTextBox1.AppendText(value);
break;
// bold
case "§l":
if (richTextBox1.Font.FontFamily.IsStyleAvailable(FontStyle.Bold))
richTextBox1.SelectionFont = new Font(richTextBox1.SelectionFont, FontStyle.Bold);
break;
// strikethrough
case "§m":
if (richTextBox1.Font.FontFamily.IsStyleAvailable(FontStyle.Strikeout))
richTextBox1.SelectionFont = new Font(richTextBox1.SelectionFont, FontStyle.Strikeout);
break;
// underline
case "§n":
if (richTextBox1.Font.FontFamily.IsStyleAvailable(FontStyle.Underline))
richTextBox1.SelectionFont = new Font(richTextBox1.SelectionFont, FontStyle.Underline);
break;
// italic
case "§o":
if (richTextBox1.Font.FontFamily.IsStyleAvailable(FontStyle.Italic))
richTextBox1.SelectionFont = new Font(richTextBox1.SelectionFont, FontStyle.Italic);
break;
// reset
case "§r":
richTextBox1.SelectionColor = richTextBox1.ForeColor;
richTextBox1.SelectionFont = richTextBox1.Font;
break;
default:
if (_javaColorCodeToColor.TryGetValue(textSection.Key, out RichTextBoxColor textColor))
{
richTextBox1.AppendText(textSection.Value, textColor);
break;
}
richTextBox1.AppendText(textSection.Key);
richTextBox1.AppendText(textSection.Value);
break;
}
}
richTextBox1.AppendLine("");
richTextBox1.SelectionAlignment = HorizontalAlignment.Left;
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
importWorker.DoWork -= Import;
importWorker.ProgressChanged -= ImportProgressChanged;
importWorker.RunWorkerCompleted -= ImportCompleted;
if (importWorker.IsBusy)
importWorker.CancelAsync();
base.OnFormClosing(e);
}
private void cancelButton_Click(object sender, EventArgs e)
{
if (importWorker.IsBusy)
{
importWorker.CancelAsync();
cancelButton.Enabled = false;
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
using System.Drawing;
namespace PckStudio.Controls
{
internal class ProgressReportMessage
{
private ProgressReportMessage(string message, MessageType type, Color messageColor)
{
Message = message;
Type = type;
MessageColor = messageColor;
}
public enum MessageType
{
Info,
Warning,
Error,
Debug
}
public string Message { get; }
public MessageType Type { get; }
public Color MessageColor { get; }
public static ProgressReportMessage Info(string message) => new ProgressReportMessage(message, MessageType.Info, Color.AliceBlue);
public static ProgressReportMessage Warning(string message) => new ProgressReportMessage(message, MessageType.Warning, Color.Yellow);
public static ProgressReportMessage Error(string message) => new ProgressReportMessage(message, MessageType.Error, Color.Red);
public static ProgressReportMessage Debug(string message) => new ProgressReportMessage(message, MessageType.Debug, Color.DarkBlue);
}
}

View File

@@ -0,0 +1,18 @@
using System.Drawing;
namespace PckStudio.Controls
{
public class RichTextBoxColor
{
public Color Foreground;
public Color Background;
public RichTextBoxColor(Color foreground, Color background)
{
Foreground = foreground;
Background = background;
}
public RichTextBoxColor(Color foreground) : this(foreground, Color.Transparent) { }
}
}

View File

@@ -0,0 +1,49 @@
using System;
using System.Drawing;
using System.Windows.Forms;
namespace PckStudio.Controls
{
public static class RichTextBoxHelper
{
private static void _AppendText(RichTextBox box, string text, Color foreColor, Color backColor)
{
if (string.IsNullOrEmpty(text))
return;
box.SelectionStart = box.TextLength;
box.SelectionLength = 0;
if (foreColor == Color.Transparent)
foreColor = box.ForeColor;
box.SelectionColor = foreColor;
if (backColor == Color.Transparent)
backColor = box.BackColor;
box.SelectionBackColor = backColor;
box.AppendText(text);
box.SelectionColor = box.ForeColor;
}
public static void AppendLine(this RichTextBox box, string text)
{
if (!text.EndsWith("\n") && !text.EndsWith("\r\n"))
text += Environment.NewLine;
box.AppendText(text);
}
public static void AppendText(this RichTextBox box, string text, RichTextBoxColor colors)
{
_AppendText(box, text, colors.Foreground, colors.Background);
}
public static void AppendLine(this RichTextBox box, string text, Color color)
=> box.AppendLine(text, new RichTextBoxColor(color));
public static void AppendLine(this RichTextBox box, string text, RichTextBoxColor colors)
{
if (!text.EndsWith("\n") && !text.EndsWith("\r\n"))
text += Environment.NewLine;
box.AppendText(text, colors);
}
}
}

View File

@@ -98,12 +98,12 @@
// toolStripSeparator2
//
toolStripSeparator2.Name = "toolStripSeparator2";
toolStripSeparator2.Size = new System.Drawing.Size(177, 6);
toolStripSeparator2.Size = new System.Drawing.Size(211, 6);
//
// toolStripSeparator4
//
toolStripSeparator4.Name = "toolStripSeparator4";
toolStripSeparator4.Size = new System.Drawing.Size(177, 6);
toolStripSeparator4.Size = new System.Drawing.Size(211, 6);
//
// toolStripSeparator3
//
@@ -156,7 +156,7 @@
this.mashUpPackToolStripMenuItem});
this.newToolStripMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("newToolStripMenuItem.Image")));
this.newToolStripMenuItem.Name = "newToolStripMenuItem";
this.newToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
this.newToolStripMenuItem.Size = new System.Drawing.Size(214, 22);
this.newToolStripMenuItem.Text = "New";
//
// skinPackToolStripMenuItem
@@ -185,7 +185,7 @@
this.openToolStripMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("openToolStripMenuItem.Image")));
this.openToolStripMenuItem.Name = "openToolStripMenuItem";
this.openToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.O)));
this.openToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
this.openToolStripMenuItem.Size = new System.Drawing.Size(214, 22);
this.openToolStripMenuItem.Text = "Open";
this.openToolStripMenuItem.Click += new System.EventHandler(this.openToolStripMenuItem_Click);
//
@@ -193,16 +193,16 @@
//
this.recentlyOpenToolStripMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("recentlyOpenToolStripMenuItem.Image")));
this.recentlyOpenToolStripMenuItem.Name = "recentlyOpenToolStripMenuItem";
this.recentlyOpenToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
this.recentlyOpenToolStripMenuItem.Size = new System.Drawing.Size(214, 22);
this.recentlyOpenToolStripMenuItem.Text = "Recently open";
//
// packSettingsToolStripMenuItem
//
this.packSettingsToolStripMenuItem.Image = global::PckStudio.Properties.Resources.ranch;
this.packSettingsToolStripMenuItem.Name = "packSettingsToolStripMenuItem";
this.packSettingsToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
this.packSettingsToolStripMenuItem.Text = "Pack Settings";
this.packSettingsToolStripMenuItem.Visible = false;
this.packSettingsToolStripMenuItem.Size = new System.Drawing.Size(214, 22);
this.packSettingsToolStripMenuItem.Text = "Import Java Resource Pack";
this.packSettingsToolStripMenuItem.Click += new System.EventHandler(this.packSettingsToolStripMenuItem_Click);
//
// saveToolStripMenuItem
//
@@ -210,7 +210,7 @@
this.saveToolStripMenuItem.Name = "saveToolStripMenuItem";
this.saveToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)(((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.Shift)
| System.Windows.Forms.Keys.S)));
this.saveToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
this.saveToolStripMenuItem.Size = new System.Drawing.Size(214, 22);
this.saveToolStripMenuItem.Text = "Save";
this.saveToolStripMenuItem.Visible = false;
this.saveToolStripMenuItem.Click += new System.EventHandler(this.saveToolStripMenuItem_Click);
@@ -219,7 +219,7 @@
//
this.saveAsToolStripMenuItem.Image = global::PckStudio.Properties.Resources.Save;
this.saveAsToolStripMenuItem.Name = "saveAsToolStripMenuItem";
this.saveAsToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
this.saveAsToolStripMenuItem.Size = new System.Drawing.Size(214, 22);
this.saveAsToolStripMenuItem.Text = "Save As";
this.saveAsToolStripMenuItem.Visible = false;
this.saveAsToolStripMenuItem.Click += new System.EventHandler(this.saveAsToolStripMenuItem_Click);
@@ -228,7 +228,7 @@
//
this.closeToolStripMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("closeToolStripMenuItem.Image")));
this.closeToolStripMenuItem.Name = "closeToolStripMenuItem";
this.closeToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
this.closeToolStripMenuItem.Size = new System.Drawing.Size(214, 22);
this.closeToolStripMenuItem.Text = "Close";
this.closeToolStripMenuItem.Visible = false;
this.closeToolStripMenuItem.Click += new System.EventHandler(this.closeToolStripMenuItem_Click);
@@ -236,14 +236,14 @@
// closeAllToolStripMenuItem
//
this.closeAllToolStripMenuItem.Name = "closeAllToolStripMenuItem";
this.closeAllToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
this.closeAllToolStripMenuItem.Size = new System.Drawing.Size(214, 22);
this.closeAllToolStripMenuItem.Text = "Close all";
//
// exitToolStripMenuItem
//
this.exitToolStripMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("exitToolStripMenuItem.Image")));
this.exitToolStripMenuItem.Name = "exitToolStripMenuItem";
this.exitToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
this.exitToolStripMenuItem.Size = new System.Drawing.Size(214, 22);
this.exitToolStripMenuItem.Text = "Exit";
this.exitToolStripMenuItem.Click += new System.EventHandler(this.exitToolStripMenuItem_Click);
//

View File

@@ -554,5 +554,16 @@ namespace PckStudio
PckManager?.Close();
Application.Exit();
}
private void packSettingsToolStripMenuItem_Click(object sender, EventArgs e)
{
OpenFileDialog fileDialog = new OpenFileDialog()
{
Filter = "Minecraft texturepack|*.zip"
};
if (fileDialog.ShowDialog() != DialogResult.OK)
return;
new JavaTextFormatForm(new FileInfo(fileDialog.FileName)).ShowDialog();
}
}
}

View File

@@ -136,6 +136,9 @@
<Reference Include="WindowsFormsIntegration" />
</ItemGroup>
<ItemGroup>
<Compile Include="Controls\BackgroundWorkerExtensions.cs" />
<Compile Include="Controls\ProgressReportMessage.cs" />
<Compile Include="Controls\RichTextBoxColor.cs" />
<Compile Include="Controls\DefaultPanel.cs">
<SubType>UserControl</SubType>
</Compile>
@@ -154,6 +157,12 @@
<Compile Include="Controls\ImmersiveForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Controls\JavaTextFormatForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Controls\JavaTextFormatForm.Designer.cs">
<DependentUpon>JavaTextFormatForm.cs</DependentUpon>
</Compile>
<Compile Include="Controls\ModelsPanel.cs">
<SubType>UserControl</SubType>
</Compile>
@@ -161,6 +170,7 @@
<DependentUpon>ModelsPanel.cs</DependentUpon>
</Compile>
<Compile Include="Controls\NamedTextureTreeNode.cs" />
<Compile Include="Controls\RichTextBoxHelper.cs" />
<Compile Include="Controls\SkinsPanel.cs">
<SubType>UserControl</SubType>
</Compile>
@@ -423,6 +433,9 @@
<EmbeddedResource Include="Controls\DefaultPanel.resx">
<DependentUpon>DefaultPanel.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Controls\JavaTextFormatForm.resx">
<DependentUpon>JavaTextFormatForm.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Controls\ModelsPanel.resx">
<DependentUpon>ModelsPanel.cs</DependentUpon>
</EmbeddedResource>

View File

@@ -56,13 +56,13 @@ namespace PckStudio.Core
_groups = new List<AtlasGroup>();
}
public static Atlas FromResourceLocation(Image source, ResourceLocation resourceLocation, ImageLayoutDirection imageLayout = default)
public static Atlas FromResourceLocation(Image source, ResourceLocation resourceLocation, LCEGameVersion gameVersion = default, ImageLayoutDirection imageLayout = default)
{
AtlasResource atlasResource = resourceLocation as AtlasResource ?? throw new InvalidDataException(nameof(resourceLocation));
Json.JsonTileInfo[] tilesInfo = atlasResource.TilesInfo.ToArray();
Size tileArea = atlasResource.GetTileArea(source.Size);
int rows = source.Width / tileArea.Width;
int columns = source.Height / tileArea.Height;
int columns = GameConstants.GetColumnCountForGameVersion(atlasResource.Type, gameVersion);
IEnumerable<AtlasTile> tiles = source.Split(tileArea, imageLayout).enumerate().Select(((int index, Image img) data) => new AtlasTile(data.img, GetSelectedPoint(data.index, out int col, rows, columns, imageLayout), col, index: data.index, userData: tilesInfo.IndexInRange(data.index) ? tilesInfo[data.index] : default));
var atlas = new Atlas(atlasResource.Path, rows, columns, tiles, tileArea, imageLayout);
atlas.AddGroups(atlasResource.AtlasGroups);

View File

@@ -17,17 +17,22 @@ namespace PckStudio.Core.Extensions
yield break;
}
public static ImageList ToImageList(this Image[] images)
public static ImageList ToImageList(this IEnumerable<Image> images)
{
ImageList imageList = new ImageList
{
ColorDepth = ColorDepth.Depth32Bit
};
imageList.Images.AddRange(images);
imageList.Images.AddRange(images.ToArray());
return imageList;
}
public static string ToString<T>(this IEnumerable<T> range, string seperator)
=> range
.Select(t => t.ToString())
.Aggregate((res, next) => string.IsNullOrWhiteSpace(next) ? res : res + seperator + next);
public static bool EqualsAny<T>(this T type, params T[] items)
{
foreach (T item in items)
@@ -38,14 +43,6 @@ namespace PckStudio.Core.Extensions
return false;
}
public static bool ContainsAny<T>(this IEnumerable<T> array, params T[] items)
{
foreach (T item in array)
{
if (items.Contains(item))
return true;
}
return false;
}
public static bool ContainsAny<T>(this IEnumerable<T> source, params T[] items) => source.Any(t => items.Contains(t));
}
}

View File

@@ -142,12 +142,13 @@ namespace PckStudio.Core.Extensions
using (var graphic = Graphics.FromImage(image))
{
graphic.ApplyConfig(GraphicsConfig.PixelPerfect());
foreach ((int i, Image texture) in sources.enumerate())
{
int x = Math.DivRem(i, columns, out int y);
if (horizontal)
y = Math.DivRem(i, rows, out x);
graphic.DrawImage(texture, new Point(x * texture.Width, y * texture.Height));
graphic.DrawImage(texture, new Rectangle(new Point(x * texture.Width, y * texture.Height), texture.Size));
}
}
return image;

View File

@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PckStudio.Core.Extensions
{
internal static class ZipArchiveEntryExtensions
{
public static string ReadAllText(this ZipArchiveEntry entry)
{
if (entry == null)
return string.Empty;
using StreamReader reader = new StreamReader(entry.Open());
return reader.ReadToEnd();
}
}
}

View File

@@ -77,5 +77,21 @@ namespace PckStudio.Core
Color.FromArgb(0xb02e26), // Red
Color.FromArgb(0x1d1d21), // Black
];
public static int GetColumnCountForGameVersion(AtlasResource.AtlasType atlasType, LCEGameVersion gameVersion)
{
return gameVersion switch
{
LCEGameVersion._1_13 when atlasType == AtlasResource.AtlasType.ItemAtlas => 17,
LCEGameVersion._1_13 when atlasType == AtlasResource.AtlasType.BlockAtlas => 34,
LCEGameVersion.TU0 when atlasType == AtlasResource.AtlasType.BlockAtlas => 16,
LCEGameVersion._1_14 when atlasType == AtlasResource.AtlasType.ItemAtlas => 18,
LCEGameVersion._1_14 when atlasType == AtlasResource.AtlasType.BlockAtlas => 39,
_ when atlasType == AtlasResource.AtlasType.PaintingAtlas => 16,
_ => throw new NotImplementedException()
};
}
}
}

View File

@@ -0,0 +1,9 @@
using System;
namespace PckStudio.Core.IO.Java
{
interface IVersion : IEquatable<Version>
{
string ToString(string seperator);
}
}

View File

@@ -5,32 +5,9 @@ using Newtonsoft.Json.Linq;
namespace PckStudio.Core.IO.Java
{
public readonly struct ImportResult<T>(T result, int count)
public readonly struct ImportResult<TResult, TStats>(TResult result, TStats stats)
{
public readonly T Result = result;
private const int STRIPE = sizeof(uint) * 8;
private readonly BitVector32[] _masks = new BitVector32[count / STRIPE +1];
private readonly int _count = count;
public void SetMarked(int i)
{
if (i >= _count)
return;
int bitVectorIndex = Math.DivRem(i, STRIPE, out int bit);
_masks[bitVectorIndex][bit] = true;
}
// MAGIC
private static int CountBits(int i)
{
i -= ((i >> 1) & 0x55555555);
i = (i & 0x33333333) + ((i >> 2) & 0x33333333);
return (((i + (i >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24;
}
public bool Success => _masks.All(m => m.Data == -1);
public int RemainingCount => _masks.Select(m => CountBits(m.Data)).Sum();
public int ImportCount => _count - RemainingCount;
public readonly TResult Result = result;
public readonly TStats Stats = stats;
}
}

View File

@@ -2,7 +2,7 @@
namespace PckStudio.Core.IO.Java
{
struct McPackmeta
public struct McPackmeta
{
[JsonProperty("pack")]
public McPack Pack;

View File

@@ -7,7 +7,11 @@ using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Documents;
using ICSharpCode.SharpZipLib.GZip;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PckStudio.Core.Deserializer;
using PckStudio.Core.DLC;
using PckStudio.Core.Extensions;
using PckStudio.Core.Properties;
@@ -17,71 +21,99 @@ namespace PckStudio.Core.IO.Java
public class ResourcePackImporter
{
internal const int TARGET_FORMAT_VERSION = 4;
internal const string JAVA_RESOURCE_PACK_PATH = "assets/minecraft/textures";
static readonly IReadOnlyDictionary<int, string> _formatToVersion = new Dictionary<int, string>()
static readonly IReadOnlyDictionary<int, IVersion> _formatToVersion = new Dictionary<int, IVersion>()
{
[1] = "1.6.1 1.8.9",
[2] = "1.9 1.10.2",
[3] = "1.11 1.12.2",
[1] = new VersionRange("1.6.1", "1.8.9"),
[2] = new VersionRange("1.9", "1.10.2"),
[3] = new VersionRange("1.11", "1.12.2"),
// ----- TARGET ----- //
[4] = "1.13 1.14.4",
[4] = new VersionRange("1.13", "1.14.4"),
// ------------------ //
[5] = "1.15 1.16.1",
[6] = "1.16.2 1.16.5",
[7] = "1.17 1.17.1",
[8] = "1.18 1.18.2",
[9] = "1.19 1.19.2",
[12] = "1.19.3",
[13] = "1.19.4",
[15] = "1.20 1.20.1",
[18] = "1.20.2",
[22] = "1.20.3 1.20.4",
[32] = "1.20.5 1.20.6",
[34] = "1.21 1.21.1",
[42] = "1.21.2 1.21.3",
[46] = "1.21.4",
[55] = "1.21.5",
[63] = "1.21.6",
[64] = "1.21.7 1.21.8",
[69] = "1.21.9 1.21.10",
[5] = new VersionRange("1.15", "1.16.1"),
[6] = new VersionRange("1.16.2", "1.16.5"),
[7] = new VersionRange("1.17", "1.17.1"),
[8] = new VersionRange("1.18", "1.18.2"),
[9] = new VersionRange("1.19", "1.19.2"),
[12] = new SingleVersion("1.19.3"),
[13] = new SingleVersion("1.19.4"),
[15] = new VersionRange("1.20", "1.20.1"),
[18] = new SingleVersion("1.20.2"),
[22] = new VersionRange("1.20.3", "1.20.4"),
[32] = new VersionRange("1.20.5", "1.20.6"),
[34] = new VersionRange("1.21", "1.21.1"),
[42] = new VersionRange("1.21.2", "1.21.3"),
[46] = new SingleVersion("1.21.4"),
[55] = new SingleVersion("1.21.5"),
[63] = new SingleVersion("1.21.6"),
[64] = new VersionRange("1.21.7", "1.21.8"),
[69] = new VersionRange("1.21.9", "1.21.10"),
};
public static DLCTexturePackage ImportTexturePack(string zipfilepath, LCEGameVersion gameVersion = default) => ImportTexturePack(new FileInfo(zipfilepath), gameVersion);
public static DLCTexturePackage ImportTexturePack(FileInfo zipfile, LCEGameVersion gameVersion = default) => ImportTexturePack(new ZipArchive(zipfile.OpenRead()), gameVersion);
public static DLCTexturePackage ImportTexturePack(ZipArchive zip, LCEGameVersion gameVersion = default)
public class ImportStats(int maxTextures)
{
StreamReader packmeta = new StreamReader(zip.GetEntry("pack.mcmeta").Open());
int format = JsonConvert.DeserializeObject<McPackmeta>(packmeta.ReadToEnd()).Pack.Format;
Debug.WriteLine($"Pack format: {format}");
throw new NotImplementedException();
public int Animations => animations;
public int Textures => textures;
public int MissingTextures => _maxTextures - textures;
public int MaxTextures => _maxTextures;
internal int animations = 0;
internal int textures = 0;
private int _maxTextures = maxTextures;
}
public static ImportResult<Atlas> ImportAtlas(ZipArchive zip, AtlasResource atlasResource, LCEGameVersion gameVersion = default)
LCEGameVersion _gameVersion;
ZipArchive _zip;
McPackmeta.McPack packMeta;
public ResourcePackImporter(ZipArchive zip, LCEGameVersion gameVersion)
{
_zip = zip;
_gameVersion = gameVersion;
packMeta = ReadPackMeta(zip);
}
public static bool IsJavaResourcePack(ZipArchive zip) => zip.GetEntry("pack.mcmeta") is ZipArchiveEntry;
public McPackmeta.McPack ReadPackMeta(ZipArchive zip)
{
StreamReader packmeta = new StreamReader(zip.GetEntry("pack.mcmeta").Open());
int format = JsonConvert.DeserializeObject<McPackmeta>(packmeta.ReadToEnd()).Pack.Format;
Debug.WriteLineIf(format == TARGET_FORMAT_VERSION, "Target format version... less work?");
Debug.WriteLine($"Importing textures from resource pack of version: {GetVersionFromFormat(format)}(Format:{format})");
McPackmeta.McPack pack = JsonConvert.DeserializeObject<McPackmeta>(packmeta.ReadToEnd()).Pack;
Debug.WriteLineIf(pack.Format == TARGET_FORMAT_VERSION, "Target format version... less work?");
Debug.WriteLine($"Importing textures from resource pack of version: {GetVersionFromFormat(pack.Format)}(Format:{pack.Format})");
return pack;
}
Atlas atlas = Atlas.CreateDefault(atlasResource, gameVersion);
string atlasPath = GetAtlasPathFromFormat(format, atlasResource.Type);
string path = $"assets/minecraft/textures/{atlasPath}/";
public DLCTexturePackage ImportAsTexturePack(ZipArchive zip)
{
ImportResult<Atlas, ImportStats> blocks = ImportAtlas(ResourceLocations.GetFromCategory(AtlasResource.GetId(AtlasResource.AtlasType.BlockAtlas)) as AtlasResource);
ImportResult<Atlas, ImportStats> items = ImportAtlas(ResourceLocations.GetFromCategory(AtlasResource.GetId(AtlasResource.AtlasType.ItemAtlas)) as AtlasResource);
IReadOnlyDictionary<string, string> lookUpTable = GetVersionLookUpTable(format, atlasResource.Type);
return null;
}
public ImportResult<Atlas, ImportStats> ImportAtlas(AtlasResource atlasResource)
{
Atlas atlas = Atlas.CreateDefault(atlasResource, _gameVersion);
string atlasPath = GetAtlasPathFromFormat(packMeta.Format, atlasResource.Type);
string path = Path.Combine(JAVA_RESOURCE_PACK_PATH, atlasPath).Replace('\\', '/');
IReadOnlyDictionary<string, string> lookUpTable = GetVersionLookUpTable(atlasResource.Type);
IReadOnlyDictionary<string, int> map =
atlasResource.TilesInfo.enumerate()
.ToDictionary(tileInfo => string.IsNullOrEmpty(tileInfo.value.InternalName) ? $"{Guid.NewGuid()}.{tileInfo.index}" : tileInfo.value.InternalName, it => it.index);
IEnumerable<ZipArchiveEntry> entries = zip.Entries.Where(e => e.FullName.StartsWith(path) && !e.FullName.EndsWith("/"));
IEnumerable<ZipArchiveEntry> entries = _zip.Entries.Where(e => e.FullName.StartsWith(path) && !e.FullName.EndsWith("/"));
IReadOnlyDictionary<string, ZipArchiveEntry> javaAnimations = entries.Where(e => e.FullName.EndsWith(".mcmeta")).ToDictionary(entry => entry.FullName);
ImportResult<Atlas> result = new ImportResult<Atlas>(atlas, atlas.TileCount);
Size maxTileSize = Size.Empty;
int maxWidth = 0;
ImportStats stats = new ImportStats(atlas.TileCount);
IDictionary<string, Animation> animations = new Dictionary<string, Animation>();
foreach (ZipArchiveEntry t in entries)
{
if (!t.FullName.EndsWith(".png"))
@@ -89,28 +121,41 @@ namespace PckStudio.Core.IO.Java
string name = Path.GetFileNameWithoutExtension(t.FullName);
if (!map.TryGetValue(name, out int i) && !(lookUpTable.TryGetValue(name, out string lceKey) && map.TryGetValue(lceKey, out i)))
continue;
if (i >= atlas.TileCount)
continue;
Image img = Image.FromStream(t.Open());
if (javaAnimations.ContainsKey(t.FullName + ".mcmeta"))
bool isAnimation = false;
if ((isAnimation = javaAnimations.TryGetValue(t.FullName + ".mcmeta", out ZipArchiveEntry animationEntry)))
{
string jsonData = animationEntry.ReadAllText();
animations.Add(name, AnimationDeserializer.DefaultDeserializer.DeserializeJavaAnimation(JObject.Parse(jsonData), img));
stats.animations++;
img = img.GetArea(new Rectangle(Point.Empty, new Size(img.Width, img.Width)));
}
if (img.Size.Width > maxTileSize.Width)
maxTileSize = new Size(img.Size.Width, img.Size.Width);
if (img.Width > maxWidth)
maxWidth = img.Width;
atlas[i].Texture = img;
result.SetMarked(i);
stats.textures++;
}
atlas.SetTileSize(new Size(maxWidth, maxWidth));
ImportResult<Atlas, ImportStats> result = new ImportResult<Atlas, ImportStats>(atlas, stats);
Debug.WriteLine("Import Stats");
Debug.WriteLine($"Textures: {stats.Textures}/{stats.MaxTextures}({stats.MissingTextures} missing)");
Debug.WriteLine($"Animations: {stats.Animations}");
foreach (string item in animations.Keys)
{
Debug.WriteLine(item);
}
atlas.SetTileSize(maxTileSize);
return result;
}
static readonly IReadOnlyDictionary<string, string> latest2lce_blocks = JsonConvert.DeserializeObject<Dictionary<string, string>>(Resources.latest2lce_blocks);
static readonly IReadOnlyDictionary<string, string> latest2lce_items = JsonConvert.DeserializeObject<Dictionary<string, string>>(Resources.latest2lce_items);
private static IReadOnlyDictionary<string, string> GetVersionLookUpTable(int format, AtlasResource.AtlasType atlasType)
private static IReadOnlyDictionary<string, string> GetVersionLookUpTable(AtlasResource.AtlasType atlasType)
{
_ = format;
return atlasType switch
{
AtlasResource.AtlasType.BlockAtlas => latest2lce_blocks,
@@ -119,7 +164,7 @@ namespace PckStudio.Core.IO.Java
};
}
private static string GetVersionFromFormat(int format) => _formatToVersion.TryGetValue(format, out string version) ? version : "unknown";
private static string GetVersionFromFormat(int format) => _formatToVersion.TryGetValue(format, out IVersion versionRange) ? versionRange.ToString(" - ") : "unknown";
private static string GetAtlasPathFromFormat(int format, AtlasResource.AtlasType type)
{
@@ -140,20 +185,5 @@ namespace PckStudio.Core.IO.Java
_ => throw new Exception()
};
}
private static int GetColumnsFromGameVersion(LCEGameVersion gameVersion, AtlasResource.AtlasType atlasType)
{
return gameVersion switch
{
LCEGameVersion._1_13 when atlasType == AtlasResource.AtlasType.BlockAtlas => 34,
LCEGameVersion._1_13 when atlasType == AtlasResource.AtlasType.ItemAtlas => 17,
LCEGameVersion._1_14 when atlasType == AtlasResource.AtlasType.BlockAtlas => 39,
LCEGameVersion._1_14 when atlasType == AtlasResource.AtlasType.ItemAtlas => 18,
_ when atlasType == AtlasResource.AtlasType.PaintingAtlas => 16,
_ => throw new ArgumentException(nameof(gameVersion))
};
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
namespace PckStudio.Core.IO.Java
{
class SingleVersion(Version version) : IVersion
{
private readonly Version _version = version;
public SingleVersion(string version) : this(new Version(version)) { }
public bool Equals(Version other) => _version.Equals(other);
public string ToString(string _) => _version.ToString();
}
}

View File

@@ -0,0 +1,14 @@
using System;
using PckStudio.Core.Extensions;
namespace PckStudio.Core.IO.Java
{
class SpecificVerions(params Version[] versions) : IVersion
{
private readonly Version[] _versions = versions;
public bool Equals(Version other) => other?.EqualsAny(_versions) ?? default;
public string ToString(string seperator) => _versions.ToString(seperator);
}
}

View File

@@ -0,0 +1,16 @@
using System;
namespace PckStudio.Core.IO.Java
{
class VersionRange(Version min, Version max) : IVersion
{
private readonly Version _min = min;
private readonly Version _max = max;
public VersionRange(string min, string max) : this(new Version(min), new Version(max)) { }
public bool Equals(Version other) => _min <= other && other <= _max;
public string ToString(string seperator) => $"{_min}{seperator}{_max}";
}
}

View File

@@ -2,7 +2,97 @@
{
public enum LCEGameVersion
{
// latest on most old gen consoles
_1_13,
_1_14,
TU0,
TU1,
TU2,
TU3,
TU4,
TU5,
TU6,
TU7,
TU8,
TU9,
TU10,
TU11,
TU12,
TU13,
TU14,
TU15,
TU16,
TU17,
TU18,
TU19,
TU20,
TU21,
TU22,
TU23,
TU24,
TU25,
TU26,
TU27,
TU28,
TU29,
TU30,
TU31,
TU32,
TU33,
TU34,
TU35,
TU36,
TU37,
TU38,
TU39,
TU41,
TU42,
TU43,
TU44,
TU45,
TU46,
TU47,
TU48,
TU49,
TU50,
TU51,
TU52,
TU53,
TU54,
TU55,
TU56,
TU57,
TU58,
TU59,
TU60,
TU61,
TU62,
TU63,
TU64,
TU65,
TU66,
TU67,
TU68,
TU69,
TU70,
TU71,
TU72,
TU73,
TU74,
TU75,
//Build_0054,
//GDC_Build,
//Build_0035,
//Build_0033,
//Build_0016,
}
}

View File

@@ -50,7 +50,12 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Extensions\ZipArchiveEntryExtensions.cs" />
<Compile Include="IO\Java\ImportResult.cs" />
<Compile Include="IO\Java\IVersion.cs" />
<Compile Include="IO\Java\SingleVersion.cs" />
<Compile Include="IO\Java\SpecificVerions.cs" />
<Compile Include="IO\Java\VersionRange.cs" />
<Compile Include="LCEGameVersion.cs" />
<Compile Include="IO\Java\McPackmeta.cs" />
<Compile Include="IO\Java\ResourcePackImporter.cs" />

View File

@@ -267,7 +267,7 @@
"displayName": "Book"
},
{
"internalName": "map",
"internalName": "map_filled",
"displayName": "Map"
},
{

View File

@@ -257,7 +257,7 @@
"sugar_cane": "reeds",
"repeater": "repeater_off",
"chiseled_sandstone": "sandstone_carved",
"sandstone": "sandstone_normal",
"sandstone": "sandstone_side",
"cut_sandstone": "sandstone_smooth",
"acacia_sapling": "sapling_acacia",
"birch_sapling": "sapling_birch",

View File

@@ -10,6 +10,13 @@
"book_normal": "book",
"chainmail_boots": "bootsChain",
"leather_boots": "bootsCloth",
"crossbow_standby": "crossbow",
"crossbow_pulling_0": "crossbow_pull_0",
"crossbow_pulling_1": "crossbow_pull_1",
"crossbow_pulling_2": "crossbow_pull_2",
"campfire": "campfire_carried",
"leather_boots_overlay": "bootsCloth_overlay",
"diamond_boots": "bootsDiamond",
"golden_boots": "bootsGold",
@@ -52,6 +59,7 @@
"armor_stand": "wooden_armorstand",
"oak_sign": "sign",
"clock_00": "clock",
"clock_0000": "clock",
"compass_00": "compass",
"dye_powder_black": "dyePowder_black",
"dye_powder_blue": "dyePowder_blue",
@@ -125,7 +133,9 @@
"cod_bucket": "bucketFish",
"salmon_bucket": "bucketSalmon",
"pufferfish": "fish_pufferfish_raw",
"pufferfish_bucket": "bucketPuffer",
"tropical_fish": "fish_clownfish_raw",
"tropical_fish_bucket": "bucketTropical",
"fish_cod_raw": "fishRaw",
"cooked_cod": "fishCooked",
"minecart_normal": "minecart",
@@ -153,7 +163,7 @@
"porkchop": "porkchopRaw",
"map": "map_empty",
"filled_map_markings": "map_filled_markings",
"filled_map": "map",
"filled_map": "map_filled",
"glass_bottle": "glassBottle",
"dragon_breath": "dragonFireball",
"glistering_melon_slice": "speckledMelon",

View File

@@ -169,8 +169,7 @@ namespace PckStudio.Core.Skin
const string sep = " ";
string fstr = Enumerable.Range(0, values.Length)
.Select(i => string.Concat("{", i, "}"))
.Aggregate((sum, next) => string.IsNullOrWhiteSpace(next) ? sum : sum + sep + next)
.ToString();
.ToString(sep);
return string.Format(CultureInfo.InvariantCulture, fstr, values);
}