using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Toolbox; using System.Windows.Forms; using Toolbox.Library; using Toolbox.Library.IO; using Toolbox.Library.Forms; using System.Runtime.InteropServices; using System.Drawing; using System.IO; using static Toolbox.Library.STGenericTexture; using LibHac; using System.ComponentModel; using VGAudio.Utilities; namespace FirstPlugin { public class TXTG : STGenericTexture, IFileFormat, ILeaveOpenOnLoad, IDisposable { public FileType FileType { get; set; } = FileType.Image; public bool CanSave { get; set; } = true; public string[] Description { get; set; } = new string[] { "Texture To Go" }; public string[] Extension { get; set; } = new string[] { "*.txtg" }; public string FileName { get; set; } public string FilePath { get; set; } public IFileInfo IFileInfo { get; set; } public TXTG() { } public bool Identify(System.IO.Stream stream) { using (var reader = new Toolbox.Library.IO.FileReader(stream, true)) { return reader.CheckSignature(4, "6PK0", 4); } } public Type[] Types { get { List types = new List(); return types.ToArray(); } } public override bool CanEdit { get; set; } = true; public override TEX_FORMAT[] SupportedFormats { get { return new TEX_FORMAT[] { TEX_FORMAT.BC1_UNORM, TEX_FORMAT.BC2_UNORM, TEX_FORMAT.BC3_UNORM, TEX_FORMAT.BC4_UNORM, TEX_FORMAT.BC5_UNORM, TEX_FORMAT.R8_UNORM, TEX_FORMAT.R8G8_UNORM, TEX_FORMAT.R8G8_UNORM, TEX_FORMAT.R10G10B10A2_UNORM, TEX_FORMAT.B5G6R5_UNORM, TEX_FORMAT.B5G5R5A1_UNORM, TEX_FORMAT.B4G4R4A4_UNORM, TEX_FORMAT.R8G8B8A8_UNORM, TEX_FORMAT.R8G8B8A8_UNORM_SRGB, }; } } public override void OnClick(TreeView treeview) { UpdateEditor(); } private void UpdateEditor() { ImageEditorBase editor = (ImageEditorBase)LibraryGUI.GetActiveContent(typeof(ImageEditorBase)); if (editor == null) { editor = new ImageEditorBase(); editor.Dock = DockStyle.Fill; LibraryGUI.LoadEditor(editor); } DisplayProperties prop = new DisplayProperties(); prop.Width = Width; prop.Height = Height; prop.MipCount = MipCount; prop.ArrayCount = ArrayCount; prop.Format = this.Format; prop.Hash = String.Join(String.Empty, Array.ConvertAll(this.HeaderInfo.Hash, x => x.ToString("X2"))); editor.Text = Text; editor.LoadProperties(prop); editor.LoadImage(this); } [StructLayout(LayoutKind.Sequential, Pack = 1)] public class Header { public ushort HeaderSize = 0x50; public ushort Version = 0x11; public Magic Magic = "6PK0"; public ushort Width; public ushort Height; public ushort Depth = 1; public byte MipCount; public byte Unknown1 = 2; public byte Unknown2 = 1; public ushort Padding = 0; public byte FormatFlag = 0; //Unsure how this value works public uint FormatSetting = 0; //Varies by format flag. Sometimes a second channel layout public byte CompSelectR = 0; public byte CompSelectG = 1; public byte CompSelectB = 2; public byte CompSelectA = 3; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] public byte[] Hash; //32 byte hash, likely made off file path. Files with identical data still have unique hashes public ushort Format; public ushort Unknown3 = 0x300; //Always 0x300? public uint TextureSetting1 = 1116471296; //00 00 8C 42 public uint TextureSetting2 = 32563; //Likey some setting for astc. Terrain and tree files use different values for their astc formats. public uint TextureSetting3 = 33554944; //00 02 00 02 public uint TextureSetting4 = 67330; //02 05 01 00 Varies sometimes with second byte } private Header HeaderInfo; //Image data is properly loaded afterwards private List> ImageList = new List>(); public override ToolStripItem[] GetContextMenuItems() { List items = new List(); items.Add(new ToolStripMenuItem("Save File", null, (o, e) => { STFileSaver.SaveFileFormat(this, FilePath); })); items.AddRange(base.GetContextMenuItems()); return items.ToArray(); } public void Load(Stream stream) { Tag = this; CanReplace = true; ImageKey = "Texture"; SelectedImageKey = "Texture"; string name = Path.GetFileNameWithoutExtension(FileName); Text = name; //cache for loading file if (PluginRuntime.TextureCache.ContainsKey(name)) PluginRuntime.TextureCache.Remove(name); PluginRuntime.TextureCache.Add(name, this); using (var reader = new FileReader(stream, true)) { reader.SetByteOrder(false); HeaderInfo = reader.ReadStruct
(); Width = HeaderInfo.Width; Height = HeaderInfo.Height; ArrayCount = HeaderInfo.Depth; MipCount = HeaderInfo.MipCount; RedChannel = ChannelList[HeaderInfo.CompSelectR]; GreenChannel = ChannelList[HeaderInfo.CompSelectG]; BlueChannel = ChannelList[HeaderInfo.CompSelectB]; AlphaChannel = ChannelList[HeaderInfo.CompSelectA]; SurfaceInfo[] surfaces = new SurfaceInfo[MipCount * ArrayCount]; reader.SeekBegin(HeaderInfo.HeaderSize); for (int i = 0; i < MipCount * ArrayCount; i++) { surfaces[i] = new SurfaceInfo(); surfaces[i].ArrayLevel = reader.ReadUInt16(); surfaces[i].MipLevel = reader.ReadByte(); reader.ReadByte(); //Always 1 } for (int i = 0; i < MipCount * ArrayCount; i++) { surfaces[i].Size = reader.ReadUInt32(); reader.ReadUInt32(); //Always 6 } long pos = reader.Position; if (FormatList.ContainsKey(HeaderInfo.Format)) this.Format = FormatList[HeaderInfo.Format]; else throw new Exception($"Unsupported format! {HeaderInfo.Format.ToString("X")}"); //Dumb hack. Terrain is oddly 8x8 astc, but the format seems to be 0x101 //Use some of the different texture settings, as they likely configure the astc blocks in some way if (this.HeaderInfo.TextureSetting2 == 32628) { this.Format = TEX_FORMAT.ASTC_8x5_UNORM; } if (this.HeaderInfo.TextureSetting2 == 32631) { this.Format = TEX_FORMAT.ASTC_8x8_UNORM; } //Image data is properly loaded afterwards List> data = new List>(); //Combine each mip and array for (int i = 0; i < MipCount * ArrayCount; i++) { var imageData = reader.ReadBytes((int)(surfaces[i].Size)); //Array level if (data.Count <= surfaces[i].ArrayLevel) data.Add(new List()); data[surfaces[i].ArrayLevel].Add(Zstb.SDecompress(imageData)); } ImageList = data; } } public void Save(Stream stream) { //Apply generic properties HeaderInfo.Format = FormatList.FirstOrDefault(x => x.Value == Format).Key; HeaderInfo.Width = (ushort)this.Width; HeaderInfo.Height = (ushort)this.Height; HeaderInfo.Depth = (ushort)this.ArrayCount; HeaderInfo.MipCount = (byte)this.MipCount; using (var writer = new FileWriter(stream)) { writer.WriteStruct(this.HeaderInfo); writer.SeekBegin(this.HeaderInfo.HeaderSize); List surfaceSizes = new List(); List surfaceData = new List(); //Surface index list for (int mip = 0; mip < this.MipCount; mip++) { for (int array = 0; array < this.ArrayCount; array++) { writer.Write((ushort)array); writer.Write((byte)mip); writer.Write((byte)1); var surface = Zstb.SCompress(ImageList[array][mip], 20); surfaceSizes.Add((uint)surface.Length); surfaceData.Add(surface); } } //Surface sizes foreach (var surface in surfaceSizes) { writer.Write(surface); writer.Write(6); } //Surface data foreach (var data in surfaceData) { writer.Write(data); } } } public void Dispose() { if (PluginRuntime.TextureCache.ContainsKey(FileName)) PluginRuntime.TextureCache.Remove(FileName); } public override byte[] GetImageData(int ArrayLevel = 0, int MipLevel = 0, int DepthLevel = 0) { var data = ImageList[ArrayLevel][MipLevel]; return TegraX1Swizzle.GetDirectImageData(this, data, MipLevel); } public override void SetImageData(Bitmap bitmap, int ArrayLevel) { //Set the data using an instance of a switch texture var tex = new TextureData(); tex.Texture = new Syroot.NintenTools.NSW.Bntx.Texture(); tex.Format = this.Format; tex.Width = this.Width; tex.Height = this.Height; tex.MipCount = this.MipCount; tex.ArrayCount = this.ArrayCount; tex.Texture.TextureData = new List>(); tex.Texture.TextureData.Add(new List()); tex.SetImageData(bitmap, ArrayLevel); SetImage(tex, ArrayLevel); } public override void Replace(string FileName) { //Replace the data using an instance of a switch texture var tex = new TextureData(); tex.Replace(FileName, MipCount, 0, Format, Syroot.NintenTools.NSW.Bntx.GFX.SurfaceDim.Dim2D, 1); //Get swappable array level ImageEditorBase editor = (ImageEditorBase)LibraryGUI.GetActiveContent(typeof(ImageEditorBase)); int targetArray = 0; if (editor != null) targetArray = editor.GetArrayDisplayLevel(); SetImage(tex, targetArray); } private void SetImage(TextureData tex, int targetArray) { //If it's null, the operation is cancelled if (tex.Texture == null) return; for (int i = 0; i < ImageList[0].Count; i++) Console.WriteLine($"SIZE 1 mip{i} {ImageList[0][i].Length}"); //Ensure the format matches if image requires multiple surface levels if (ImageList.Count > 1 && this.Format != tex.Format) throw new Exception($"Imported texture must use the original format for surface injecting! Expected {this.Format} but got {tex.Format}! If you need ASTC, use an astc encoder with .astc file format."); //Swap individual image if (tex.Texture.TextureData.Count == 1) { ImageList[targetArray] = tex.Texture.TextureData[0]; } else //Swap all surfaces if multiple are imported { ImageList.Clear(); foreach (var surface in tex.Texture.TextureData) ImageList.Add(surface); } for (int i = 0; i < ImageList[0].Count; i++) Console.WriteLine($"SIZE 2 mip{i} {ImageList[0][i].Length}"); Width = tex.Texture.Width; Height = tex.Texture.Height; MipCount = tex.Texture.MipCount; ArrayCount = (uint)ImageList.Count; Format = tex.Format; IsEdited = true; UpdateEditor(); this.LoadOpenGLTexture(); } class SurfaceInfo { public byte MipLevel; public ushort ArrayLevel; public byte SurfaceCount; //Always 1 public uint Size; } static Dictionary ChannelList = new Dictionary() { { 0, STChannelType.Red }, { 1, STChannelType.Green }, { 2, STChannelType.Blue }, { 3, STChannelType.Alpha }, { 4, STChannelType.Zero }, { 5, STChannelType.One }, }; static Dictionary FormatList = new Dictionary() { { 0x101, TEX_FORMAT.ASTC_4x4_UNORM }, { 0x102, TEX_FORMAT.ASTC_8x8_UNORM }, { 0x105, TEX_FORMAT.ASTC_8x8_SRGB }, { 0x109, TEX_FORMAT.ASTC_4x4_SRGB }, { 0x202, TEX_FORMAT.BC1_UNORM }, { 0x203, TEX_FORMAT.BC1_UNORM_SRGB }, { 0x302, TEX_FORMAT.BC1_UNORM }, { 0x505, TEX_FORMAT.BC3_UNORM_SRGB }, { 0x602, TEX_FORMAT.BC4_UNORM }, { 0x606, TEX_FORMAT.BC4_UNORM }, { 0x607, TEX_FORMAT.BC4_UNORM }, { 0x702, TEX_FORMAT.BC5_UNORM }, { 0x703, TEX_FORMAT.BC5_UNORM }, { 0x707, TEX_FORMAT.BC5_UNORM }, { 0x901, TEX_FORMAT.BC7_UNORM }, }; public class DisplayProperties { [Browsable(true)] [ReadOnly(true)] [Description("Height of the image.")] [Category("Image Info")] public uint Height { get; set; } [Browsable(true)] [ReadOnly(true)] [Description("Width of the image.")] [Category("Image Info")] public uint Width { get; set; } [Browsable(true)] [ReadOnly(true)] [Description("Format of the image.")] [Category("Image Info")] public TEX_FORMAT Format { get; set; } [Browsable(true)] [ReadOnly(true)] [Description("Mip map count of the image.")] [Category("Image Info")] public uint MipCount { get; set; } [Browsable(true)] [ReadOnly(true)] [Description("Array count of the image for multiple surfaces.")] [Category("Image Info")] public uint ArrayCount { get; set; } [Browsable(true)] [ReadOnly(true)] [Description("The hash value.")] [Category("Image Info")] public string Hash { get; set; } } } }