using System; using System.Collections.Generic; using System.Drawing; using System.Text; using System.Threading.Tasks; using Toolbox.Library.IO; using Toolbox.Library; using System.Windows.Forms; using OpenTK.Graphics.OpenGL; using Toolbox.Library.Forms; using Bfres.Structs; using System.IO; using System.Linq; namespace FirstPlugin { public enum BlockType : uint { Invalid = 0x00, EndOfFile = 0x01, AlignData = 0x02, VertexShaderHeader = 0x03, VertexShaderProgram = 0x05, PixelShaderHeader = 0x06, PixelShaderProgram = 0x07, GeometryShaderHeader = 0x08, GeometryShaderProgram = 0x09, GeometryShaderProgram2 = 0x10, ImageInfo = 0x11, ImageData = 0x12, MipData = 0x13, ComputeShaderHeader = 0x14, ComputeShader = 0x15, UserBlock = 0x16, } public class GTXFile : TreeNodeFile, IFileFormat, IContextMenuNode, ITextureContainer { public FileType FileType { get; set; } = FileType.Image; public bool CanSave { get; set; } public string[] Description { get; set; } = new string[] { "GTX" }; public string[] Extension { get; set; } = new string[] { "*.gtx" }; public string FileName { get; set; } public string FilePath { get; set; } public IFileInfo IFileInfo { get; set; } public bool Identify(System.IO.Stream stream) { using (var reader = new Toolbox.Library.IO.FileReader(stream, true)) { return reader.CheckSignature(4, "Gfx2"); } } public Type[] Types { get { List types = new List(); return types.ToArray(); } } public bool DisplayIcons => false; public List TextureList { get { List texList = new List(); foreach (STGenericTexture node in textures) texList.Add(node); return texList; } set { } } private GTXHeader header; public List data = new List(); public List mipMaps = new List(); public List textures = new List(); public List blocks = new List(); public override UserControl GetEditor() { STPropertyGrid editor = new STPropertyGrid(); editor.Text = Text; editor.Dock = DockStyle.Fill; return editor; } public override void OnAfterAdded() { if (textures.Count > 0 && this.TreeView != null) this.TreeView.SelectedNode = textures[0]; } public override void FillEditor(UserControl control) { ((STPropertyGrid)control).LoadProperty(header); } public void Load(System.IO.Stream stream) { CanSave = true; Text = FileName; ReadGx2(new FileReader(stream)); string name = System.IO.Path.GetFileNameWithoutExtension(Text); foreach (var image in textures) { if (Nodes.Count == 1) image.Text = $"{name}"; else image.Text = $"{name}_{textures.IndexOf(image)}"; } } public ToolStripItem[] GetContextMenuItems() { return new ToolStripItem[] { new ToolStripMenuItem("Save", null, Save, Keys.Control | Keys.S), new ToolStripMenuItem("Export All", null, ExportAllAction, Keys.Control | Keys.E), }; } private void Save(object sender, EventArgs args) { SaveFileDialog sfd = new SaveFileDialog(); sfd.DefaultExt = "gtx"; sfd.Filter = "Supported Formats|*.gtx;"; sfd.FileName = FileName; if (sfd.ShowDialog() == DialogResult.OK) { STFileSaver.SaveFileFormat(this, sfd.FileName); } } protected void ExportAllAction(object sender, EventArgs e) { if (Nodes.Count <= 0) return; string formats = FileFilters.GTX; string[] forms = formats.Split('|'); List Formats = new List(); for (int i = 0; i < forms.Length; i++) { if (i > 1 || i == (forms.Length - 1)) //Skip lines with all extensions { if (!forms[i].StartsWith("*")) Formats.Add(forms[i]); } } FolderSelectDialog sfd = new FolderSelectDialog(); if (sfd.ShowDialog() == DialogResult.OK) { string folderPath = sfd.SelectedPath; BatchFormatExport form = new BatchFormatExport(Formats); if (form.ShowDialog() == DialogResult.OK) { string extension = form.GetSelectedExtension(); extension.Replace(" ", string.Empty); foreach (STGenericTexture node in Nodes) { ((STGenericTexture)node).Export($"{folderPath}\\{node.Text}{extension}"); } } } } public void Unload() { } public void Save(System.IO.Stream stream) { using (FileWriter writer = new FileWriter(stream, true)) { writer.ByteOrder = Syroot.BinaryData.ByteOrder.BigEndian; header.Write(writer); uint surfBlockType; uint dataBlockType; uint mipBlockType; if (header.MajorVersion == 6 && header.MinorVersion == 0) { surfBlockType = 0x0A; dataBlockType = 0x0B; mipBlockType = 0x0C; } else if (header.MajorVersion == 6 || header.MajorVersion == 7) { surfBlockType = 0x0B; dataBlockType = 0x0C; mipBlockType = 0x0D; } else throw new Exception($"Unsupported GTX version {header.MajorVersion}"); int imageInfoIndex = -1; int imageBlockIndex = -1; int imageMipBlockIndex = -1; writer.Seek(header.HeaderSize, System.IO.SeekOrigin.Begin); foreach (var block in blocks) { if ((uint)block.BlockType == surfBlockType) { imageInfoIndex++; imageBlockIndex++; imageMipBlockIndex++; block.data = textures[imageInfoIndex].surface.Write(); block.Write(writer); } else if ((uint)block.BlockType == dataBlockType) { var tex = textures[imageBlockIndex]; var pos = writer.Position; uint Alignment = tex.surface.alignment; //Create alignment block first uint dataAlignment = GetAlignBlockSize((uint)pos + 32, Alignment); GTXDataBlock dataAlignBlock = new GTXDataBlock(BlockType.AlignData, dataAlignment, 0, 0); dataAlignBlock.Write(writer); block.data = tex.surface.data; block.Write(writer); } else if ((uint)block.BlockType == mipBlockType) { var tex = textures[imageMipBlockIndex]; var pos = writer.Position; uint Alignment = tex.surface.alignment; //Create alignment block first uint dataAlignment = GetAlignBlockSize((uint)pos + 32, Alignment); GTXDataBlock dataAlignBlock = new GTXDataBlock(BlockType.AlignData, dataAlignment, 0, 0); dataAlignBlock.Write(writer); if (tex.surface.mipData == null || tex.surface.mipData.Length <= 0) throw new Exception("Invalid mip data!"); block.data = tex.surface.mipData; block.Write(writer); } else if (block.BlockType != BlockType.AlignData) { block.Write(writer); } } } } private static uint GetAlignBlockSize(uint DataOffset, uint Alignment) { uint alignSize = RoundUp(DataOffset, Alignment) - DataOffset - 32; uint z = 1; while (alignSize < 0) alignSize = RoundUp(DataOffset + (Alignment * z), Alignment) - DataOffset - 32; z += 1; return alignSize; } private static uint RoundUp(uint X, uint Y) { return((X - 1) | (Y - 1)) + 1; } private void ReadGx2(FileReader reader) { reader.ByteOrder = Syroot.BinaryData.ByteOrder.BigEndian; header = new GTXHeader(); header.Read(reader); Console.WriteLine("header size " + header.HeaderSize); uint surfBlockType; uint dataBlockType; uint mipBlockType; uint vertexShaderHeader = 0x03; uint vertexShaderProgram = 0x05; uint pixelShaderHeader = 0x06; uint pixelShaderProgram = 0x07; uint geometryShaderHeader = 0x08; uint geometryShaderProgram = 0x09; uint userDataBlock = 0x10; if (header.MajorVersion == 6 && header.MinorVersion == 0) { surfBlockType = 0x0A; dataBlockType = 0x0B; mipBlockType = 0x0C; } else if (header.MajorVersion == 6 || header.MajorVersion == 7) { surfBlockType = 0x0B; dataBlockType = 0x0C; mipBlockType = 0x0D; } else throw new Exception($"Unsupported GTX version {header.MajorVersion}"); if (header.GpuVersion != 2) throw new Exception($"Unsupported GPU version {header.GpuVersion}"); reader.Position = header.HeaderSize; bool blockB = false; bool blockC = false; uint ImageInfo = 0; uint images = 0; while (reader.Position < reader.BaseStream.Length) { Console.WriteLine("BLOCK POS " + reader.Position + " " + reader.BaseStream.Length); GTXDataBlock block = new GTXDataBlock(); block.Read(reader); blocks.Add(block); bool BlockIsEmpty = block.BlockType == BlockType.AlignData || block.BlockType == BlockType.EndOfFile; //Here we use "if" instead of "case" statements as types vary between versions if ((uint)block.BlockType == surfBlockType) { ImageInfo += 1; blockB = true; var surface = new SurfaceInfoParse(); surface.Read(new FileReader(block.data)); if (surface.tileMode == 0 || surface.tileMode > 16) throw new Exception($"Invalid tileMode {surface.tileMode}!"); if (surface.numMips > 14) throw new Exception($"Invalid number of mip maps {surface.numMips}!"); TextureData textureData = new TextureData(); textureData.surface = surface; textureData.MipCount = surface.numMips; textureData.ArrayCount = surface.depth; textureData.Text = "Texture" + ImageInfo; Nodes.Add(textureData); textures.Add(textureData); } else if ((uint)block.BlockType == dataBlockType) { images += 1; blockC = true; data.Add(block.data); } else if ((uint)block.BlockType == mipBlockType) { mipMaps.Add(block.data); } else if ((uint)block.BlockType == vertexShaderHeader) Nodes.Add(new BlockDisplay(block.data) { Text = "Vertex Shader Header" }); else if ((uint)block.BlockType == vertexShaderProgram) Nodes.Add(new BlockDisplay(block.data) { Text = "Vertex Shader Program" }); else if ((uint)block.BlockType == pixelShaderHeader) Nodes.Add(new BlockDisplay(block.data) { Text = "Pixel Shader Header" }); else if ((uint)block.BlockType == pixelShaderProgram) Nodes.Add(new BlockDisplay(block.data) { Text = "Pixel Shader Program" }); else if ((uint)block.BlockType == geometryShaderHeader) Nodes.Add(new BlockDisplay(block.data) { Text = "Geometry Shader Header" }); else if ((uint)block.BlockType == geometryShaderProgram) Nodes.Add(new BlockDisplay(block.data) { Text = "Geometry Shader Program" }); else if (!BlockIsEmpty) Nodes.Add(new BlockDisplay(block.data) { Text = $"Block Type {block.BlockType.ToString("X")}" }); } if (textures.Count != data.Count) throw new Exception($"Bad size! {textures.Count} {data.Count}"); int curTex = 0; int curMip = 0; foreach (var node in Nodes) { if (node is TextureData) { TextureData tex = (TextureData)node; tex.surface.data = data[curTex]; tex.surface.bpp = GX2.surfaceGetBitsPerPixel(tex.surface.format) >> 3; tex.Format = FTEX.ConvertFromGx2Format((Syroot.NintenTools.Bfres.GX2.GX2SurfaceFormat)tex.surface.format); tex.Width = tex.surface.width; tex.Height = tex.surface.height; if (tex.surface.numMips > 1) tex.surface.mipData = mipMaps[curMip++]; else tex.surface.mipData = new byte[0]; if (tex.surface.mipData == null) tex.surface.numMips = 1; curTex++; } } } public class BlockDisplay : TreeNodeCustom { public byte[] BlockData; public BlockDisplay(byte[] data) { BlockData = data; } public override void OnClick(TreeView treeview) { HexEditor editor = (HexEditor)LibraryGUI.GetActiveContent(typeof(HexEditor)); if (editor == null) { editor = new HexEditor(); LibraryGUI.LoadEditor(editor); } editor.Text = Text; editor.Dock = DockStyle.Fill; editor.LoadData(BlockData); } } public class GTXHeader { readonly string Magic = "Gfx2"; public uint HeaderSize; public uint MajorVersion; public uint MinorVersion; public uint GpuVersion; public uint AlignMode; public void Read(FileReader reader) { string Signature = reader.ReadString(4, Encoding.ASCII); if (Signature != Magic) throw new Exception($"Invalid signature {Signature}! Expected Gfx2."); HeaderSize = reader.ReadUInt32(); MajorVersion = reader.ReadUInt32(); MinorVersion = reader.ReadUInt32(); GpuVersion = reader.ReadUInt32(); //Ignored in 6.0 AlignMode = reader.ReadUInt32(); } public void Write(FileWriter writer) { writer.WriteSignature(Magic); writer.Write(HeaderSize); writer.Write(MajorVersion); writer.Write(MinorVersion); writer.Write(GpuVersion); writer.Write(AlignMode); } } public class GTXDataBlock { readonly string Magic = "BLK{"; public uint HeaderSize; public uint MajorVersion; public uint MinorVersion; public BlockType BlockType; public uint Identifier; public uint Index; public uint DataSize; public byte[] data; public GTXDataBlock() { } public GTXDataBlock(BlockType blockType, uint dataSize, uint identifier, uint index) { HeaderSize = 32; MajorVersion = 1; MinorVersion = 0; BlockType = blockType; DataSize = dataSize; Identifier = identifier; Index = index; data = new byte[dataSize]; } public void Read(FileReader reader) { long blockStart = reader.Position; string Signature = reader.ReadString(4, Encoding.ASCII); if (Signature != Magic) throw new Exception($"Invalid signature {Signature}! Expected BLK."); HeaderSize = reader.ReadUInt32(); MajorVersion = reader.ReadUInt32(); //Must be 0x01 for 6.x MinorVersion = reader.ReadUInt32(); //Must be 0x00 for 6.x BlockType = reader.ReadEnum(false); DataSize = reader.ReadUInt32(); Identifier = reader.ReadUInt32(); Index = reader.ReadUInt32(); reader.Seek(blockStart + HeaderSize, System.IO.SeekOrigin.Begin); data = reader.ReadBytes((int)DataSize); } public void Write(FileWriter writer) { long blockStart = writer.Position; writer.WriteSignature(Magic); writer.Write(HeaderSize); writer.Write(MajorVersion); writer.Write(MinorVersion); writer.Write(BlockType, false); writer.Write(data.Length); writer.Write(Identifier); writer.Write(Index); writer.Seek(blockStart + HeaderSize, System.IO.SeekOrigin.Begin); writer.Write(data); } } public class TextureData : STGenericTexture { public override TEX_FORMAT[] SupportedFormats { get { return new TEX_FORMAT[] { TEX_FORMAT.BC1_UNORM, TEX_FORMAT.BC1_UNORM_SRGB, TEX_FORMAT.BC2_UNORM, TEX_FORMAT.BC2_UNORM_SRGB, TEX_FORMAT.BC3_UNORM, TEX_FORMAT.BC3_UNORM_SRGB, TEX_FORMAT.BC4_UNORM, TEX_FORMAT.BC4_SNORM, TEX_FORMAT.BC5_UNORM, TEX_FORMAT.BC5_SNORM, TEX_FORMAT.B5G5R5A1_UNORM, TEX_FORMAT.B5G6R5_UNORM, TEX_FORMAT.B8G8R8A8_UNORM_SRGB, TEX_FORMAT.B8G8R8A8_UNORM, TEX_FORMAT.R10G10B10A2_UNORM, TEX_FORMAT.R16_UNORM, TEX_FORMAT.B4G4R4A4_UNORM, TEX_FORMAT.R8G8B8A8_UNORM_SRGB, TEX_FORMAT.R8G8B8A8_UNORM, TEX_FORMAT.R8_UNORM, TEX_FORMAT.R8G8_UNORM, TEX_FORMAT.R32G8X24_FLOAT, }; } } public override bool CanEdit { get; set; } = true; public SurfaceInfoParse surface; public TextureData() { ImageKey = "Texture"; SelectedImageKey = "Texture"; CanDelete = false; CanReplace = true; CanRename = false; } public override string ExportFilter => FileFilters.GTX; public override string ReplaceFilter => FileFilters.GTX; private void ApplySurface(GX2.GX2Surface NewSurface) { surface.aa = NewSurface.aa; surface.alignment = NewSurface.alignment; surface.bpp = NewSurface.bpp; surface.compSel = NewSurface.compSel; surface.data = NewSurface.data; surface.depth = NewSurface.depth; surface.dim = NewSurface.dim; surface.firstMip = NewSurface.firstMip; surface.firstSlice = NewSurface.firstSlice; surface.format = NewSurface.format; surface.height = NewSurface.height; surface.imageCount = NewSurface.imageCount; surface.imageSize = NewSurface.imageSize; surface.mipData = NewSurface.mipData; surface.mipSize = NewSurface.mipSize; surface.mipOffset = NewSurface.mipOffset; surface.numArray = NewSurface.numArray; surface.numMips = NewSurface.numMips; surface.pitch = NewSurface.pitch; surface.resourceFlags = NewSurface.resourceFlags; surface.swizzle = NewSurface.swizzle; surface.tileMode = NewSurface.tileMode; surface.use = NewSurface.use; surface.width = NewSurface.width; surface.texRegs = NewSurface.texRegs; SetChannelComponents(); } private STChannelType SetChannel(byte compSel) { if (compSel == 0) return STChannelType.Red; else if (compSel == 1) return STChannelType.Green; else if (compSel == 2) return STChannelType.Blue; else if (compSel == 3) return STChannelType.Alpha; else if (compSel == 4) return STChannelType.Zero; else return STChannelType.One; } private void SetChannelComponents() { surface.compSel = new byte[4] { 0, 1, 2, 3 }; } public override void SetImageData(Bitmap bitmap, int ArrayLevel) { if (bitmap == null) return; //Image is likely disposed and not needed to be applied RedChannel = SetChannel(surface.compSel[0]); GreenChannel = SetChannel(surface.compSel[1]); BlueChannel = SetChannel(surface.compSel[2]); AlphaChannel = SetChannel(surface.compSel[3]); surface.format = (uint)FTEX.ConvertToGx2Format(Format); surface.width = (uint)bitmap.Width; surface.height = (uint)bitmap.Height; if (MipCount != 1) { MipCount = GenerateMipCount(bitmap.Width, bitmap.Height); if (MipCount == 0) MipCount = 1; } surface.numMips = MipCount; surface.mipOffset = new uint[MipCount]; //Create image block from bitmap first var data = GenerateMipsAndCompress(bitmap, MipCount, Format); //Swizzle and create surface var NewSurface = GX2.CreateGx2Texture(data, Text, (uint)surface.tileMode, (uint)surface.aa, (uint)surface.width, (uint)surface.height, (uint)surface.depth, (uint)surface.format, (uint)0, (uint)surface.dim, (uint)surface.numMips ); ApplySurface(NewSurface); IsEdited = true; LoadOpenGLTexture(); LibraryGUI.UpdateViewport(); } public override byte[] GetImageData(int ArrayLevel = 0, int MipLevel = 0, int DepthLevel = 0) { RedChannel = SetChannel(surface.compSel[0]); GreenChannel = SetChannel(surface.compSel[1]); BlueChannel = SetChannel(surface.compSel[2]); AlphaChannel = SetChannel(surface.compSel[3]); Console.WriteLine(""); Console.WriteLine("// ----- GX2Surface Info ----- "); Console.WriteLine(" dim = " + surface.dim); Console.WriteLine(" width = " + surface.width); Console.WriteLine(" height = " + surface.height); Console.WriteLine(" depth = " + surface.depth); Console.WriteLine(" numMips = " + surface.numMips); Console.WriteLine(" format = " + surface.format); Console.WriteLine(" aa = " + surface.aa); Console.WriteLine(" use = " + surface.use); Console.WriteLine(" imageSize = " + surface.imageSize); Console.WriteLine(" mipSize = " + surface.mipSize); Console.WriteLine(" tileMode = " + surface.tileMode); Console.WriteLine(" swizzle = " + surface.swizzle); Console.WriteLine(" alignment = " + surface.alignment); Console.WriteLine(" pitch = " + surface.pitch); Console.WriteLine(" bits per pixel = " + (surface.bpp << 3)); Console.WriteLine(" bytes per pixel = " + surface.bpp); Console.WriteLine(" data size = " + surface.data.Length); Console.WriteLine(" mip size = " + surface.mipData.Length); Console.WriteLine(" realSize = " + surface.imageSize); return GX2.Decode(surface, ArrayLevel, MipLevel); } private void Remove(object sender, EventArgs args) { ((GTXFile)Parent).Nodes.Remove(this); } public override void Export(string FileName) { Export(FileName); } public override void Replace(string FileName) { FTEX ftex = new FTEX(); ftex.ReplaceTexture(FileName, Format, (uint)(surface.mipSize <= 1 ? 1 : 0)); if (ftex.texture != null) { surface.swizzle = ftex.texture.Swizzle; surface.tileMode = (uint)ftex.texture.TileMode; surface.format = (uint)ftex.texture.Format; surface.aa = (uint)ftex.texture.AAMode; surface.use = (uint)ftex.texture.Use; surface.alignment = (uint)ftex.texture.Alignment; surface.dim = (uint)ftex.texture.Dim; surface.width = (uint)ftex.texture.Width; surface.height = (uint)ftex.texture.Height; surface.depth = (uint)ftex.texture.Depth; surface.numMips = (uint)ftex.texture.MipCount; surface.imageSize = (uint)ftex.texture.Data.Length; surface.mipSize = (uint)ftex.texture.MipData.Length; surface.data = ftex.texture.Data; surface.mipData = ftex.texture.MipData; surface.mipOffset = ftex.texture.MipOffsets; surface.firstMip = ftex.texture.ViewMipFirst; surface.firstSlice = 0; surface.numSlices = ftex.texture.ArrayLength; surface.imageCount = ftex.texture.MipCount; surface.pitch = ftex.texture.Pitch; surface.texRegs = GX2.CreateRegisters(surface); Format = FTEX.ConvertFromGx2Format((Syroot.NintenTools.Bfres.GX2.GX2SurfaceFormat)surface.format); Width = surface.width; Height = surface.height; MipCount = surface.numMips; ArrayCount = surface.depth; ImageEditorBase editor = (ImageEditorBase)LibraryGUI.GetActiveContent(typeof(ImageEditorBase)); if (editor != null) UpdateEditor(); } } public override void OnClick(TreeView treeView) { UpdateEditor(); } public void UpdateEditor() { ImageEditorBase editor = (ImageEditorBase)LibraryGUI.GetActiveContent(typeof(ImageEditorBase)); if (editor == null) { editor = new ImageEditorBase(); editor.Dock = DockStyle.Fill; LibraryGUI.LoadEditor(editor); } editor.Text = Text; var tex = FTEX.FromGx2Surface(surface, Text); tex.MipCount = MipCount; editor.LoadProperties(tex, () => { surface.compSel[0] = (byte)tex.CompSelR; surface.compSel[1] = (byte)tex.CompSelG; surface.compSel[2] = (byte)tex.CompSelB; surface.compSel[3] = (byte)tex.CompSelA; }); editor.LoadImage(this); } } public class SurfaceInfoParse : GX2.GX2Surface { public void Read(FileReader reader) { reader.ByteOrder = Syroot.BinaryData.ByteOrder.BigEndian; dim = reader.ReadUInt32(); width = reader.ReadUInt32(); height = reader.ReadUInt32(); depth = reader.ReadUInt32(); numMips = reader.ReadUInt32(); format = reader.ReadUInt32(); aa = reader.ReadUInt32(); use = reader.ReadUInt32(); imageSize = reader.ReadUInt32(); imagePtr = reader.ReadUInt32(); mipSize = reader.ReadUInt32(); mipPtr = reader.ReadUInt32(); tileMode = reader.ReadUInt32(); swizzle = reader.ReadUInt32(); alignment = reader.ReadUInt32(); pitch = reader.ReadUInt32(); mipOffset = reader.ReadUInt32s(13); firstMip = reader.ReadUInt32(); imageCount = reader.ReadUInt32(); firstSlice = reader.ReadUInt32(); numSlices = reader.ReadUInt32(); compSel = reader.ReadBytes(4); texRegs = reader.ReadUInt32s(5); } public byte[] Write() { MemoryStream mem = new MemoryStream(); FileWriter writer = new FileWriter(mem); writer.ByteOrder = Syroot.BinaryData.ByteOrder.BigEndian; writer.Write(dim); writer.Write(width); writer.Write(height); writer.Write(depth); writer.Write(numMips); writer.Write(format); writer.Write(aa); writer.Write(use); writer.Write(imageSize); writer.Write(imagePtr); writer.Write(mipSize); writer.Write(mipPtr); writer.Write(tileMode); writer.Write(swizzle); writer.Write(alignment); writer.Write(pitch); for (int i = 0; i < 13; i++) { if (mipOffset.Length > i) writer.Write(mipOffset[i]); else writer.Write(0); } writer.Write(firstMip); writer.Write(imageCount); writer.Write(firstSlice); writer.Write(numSlices); for (int i = 0; i < 4; i++) { if (compSel != null && compSel.Length > i) writer.Write(compSel[i]); else writer.Write((byte)0); } for (int i = 0; i < 5; i++) { if (texRegs != null && texRegs.Length > i) writer.Write(texRegs[i]); else writer.Write(0); } return mem.ToArray(); } } } }