mirror of
https://github.com/KillzXGaming/Switch-Toolbox
synced 2024-11-14 00:47:37 +00:00
Update DDS and nutexb with fixes
This commit is contained in:
parent
f51dd17f94
commit
0c9bafe67f
5 changed files with 133 additions and 133 deletions
|
@ -1252,71 +1252,14 @@ namespace FirstPlugin
|
|||
dds.header.width = Texture.Width;
|
||||
dds.header.height = Texture.Height;
|
||||
dds.header.mipmapCount = (uint)mipmaps.Count;
|
||||
dds.header.pitchOrLinearSize = (uint)mipmaps[0][0].Length;
|
||||
|
||||
bool IsDX10 = false;
|
||||
if (IsCompressedFormat(Texture.Format))
|
||||
dds.SetFlags(GetCompressedDXGI_FORMAT(Texture.Format));
|
||||
else
|
||||
dds.SetFlags(GetUncompressedDXGI_FORMAT(Texture.Format));
|
||||
|
||||
switch (Texture.Format)
|
||||
{
|
||||
case SurfaceFormat.BC1_UNORM:
|
||||
case SurfaceFormat.BC1_SRGB:
|
||||
dds.header.ddspf.fourCC = "DXT1";
|
||||
break;
|
||||
case SurfaceFormat.BC2_UNORM:
|
||||
case SurfaceFormat.BC2_SRGB:
|
||||
dds.header.ddspf.fourCC = "DXT3";
|
||||
break;
|
||||
case SurfaceFormat.BC3_UNORM:
|
||||
case SurfaceFormat.BC3_SRGB:
|
||||
dds.header.ddspf.fourCC = "DXT5";
|
||||
break;
|
||||
case SurfaceFormat.BC4_UNORM:
|
||||
IsDX10 = true;
|
||||
dds.DX10header = new DDS.DX10Header();
|
||||
dds.DX10header.DXGI_Format = DDS.DXGI_FORMAT.DXGI_FORMAT_BC4_UNORM;
|
||||
break;
|
||||
case SurfaceFormat.BC4_SNORM:
|
||||
IsDX10 = true;
|
||||
dds.DX10header = new DDS.DX10Header();
|
||||
dds.DX10header.DXGI_Format = DDS.DXGI_FORMAT.DXGI_FORMAT_BC4_SNORM;
|
||||
break;
|
||||
case SurfaceFormat.BC5_UNORM:
|
||||
IsDX10 = true;
|
||||
dds.DX10header = new DDS.DX10Header();
|
||||
dds.DX10header.DXGI_Format = DDS.DXGI_FORMAT.DXGI_FORMAT_BC5_UNORM;
|
||||
break;
|
||||
case SurfaceFormat.BC5_SNORM:
|
||||
IsDX10 = true;
|
||||
dds.DX10header = new DDS.DX10Header();
|
||||
dds.DX10header.DXGI_Format = DDS.DXGI_FORMAT.DXGI_FORMAT_BC5_SNORM;
|
||||
break;
|
||||
case SurfaceFormat.BC6_FLOAT:
|
||||
IsDX10 = true;
|
||||
dds.DX10header = new DDS.DX10Header();
|
||||
dds.DX10header.DXGI_Format = DDS.DXGI_FORMAT.DXGI_FORMAT_BC6H_SF16;
|
||||
break;
|
||||
case SurfaceFormat.BC6_UFLOAT:
|
||||
IsDX10 = true;
|
||||
dds.DX10header = new DDS.DX10Header();
|
||||
dds.DX10header.DXGI_Format = DDS.DXGI_FORMAT.DXGI_FORMAT_BC6H_UF16;
|
||||
break;
|
||||
case SurfaceFormat.BC7_UNORM:
|
||||
IsDX10 = true;
|
||||
dds.DX10header = new DDS.DX10Header();
|
||||
dds.DX10header.DXGI_Format = DDS.DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM_SRGB;
|
||||
break;
|
||||
case SurfaceFormat.BC7_SRGB:
|
||||
IsDX10 = true;
|
||||
dds.DX10header = new DDS.DX10Header();
|
||||
dds.DX10header.DXGI_Format = DDS.DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM;
|
||||
break;
|
||||
default:
|
||||
throw new Exception($"Format {Texture.Format} not supported!");
|
||||
}
|
||||
|
||||
if (IsDX10)
|
||||
dds.header.ddspf.fourCC = "DX10";
|
||||
|
||||
dds.Save(dds, FileName, IsDX10, mipmaps);
|
||||
dds.Save(dds, FileName, mipmaps);
|
||||
}
|
||||
public void LoadTexture(Texture tex, int target = 1)
|
||||
{
|
||||
|
@ -1331,11 +1274,9 @@ namespace FirstPlugin
|
|||
int linesPerBlockHeight = (1 << (int)tex.BlockHeightLog2) * 8;
|
||||
|
||||
uint bpp = Formats.bpps((uint)((int)tex.Format >> 8));
|
||||
|
||||
for (int arrayLevel = 0; arrayLevel < tex.ArrayLength; arrayLevel++)
|
||||
{
|
||||
int blockHeightShift = 0;
|
||||
|
||||
List<byte[]> mips = new List<byte[]>();
|
||||
for (int mipLevel = 0; mipLevel < tex.TextureData[arrayLevel].Count; mipLevel++)
|
||||
{
|
||||
|
|
|
@ -219,50 +219,14 @@ namespace FirstPlugin
|
|||
dds.header.height = (uint)renderedTex.width;
|
||||
dds.header.mipmapCount = (uint)renderedTex.mipmaps[0].Count;
|
||||
|
||||
bool IsDX10 = false;
|
||||
dds.header.pitchOrLinearSize = (uint)renderedTex.mipmaps[0][0].Length;
|
||||
|
||||
switch (format)
|
||||
{
|
||||
case ((int)GTX.GX2SurfaceFormat.GX2_SURFACE_FORMAT_T_BC1_UNORM):
|
||||
case ((int)GTX.GX2SurfaceFormat.GX2_SURFACE_FORMAT_T_BC1_SRGB):
|
||||
dds.header.ddspf.fourCC = "DXT1";
|
||||
break;
|
||||
case ((int)GTX.GX2SurfaceFormat.GX2_SURFACE_FORMAT_T_BC2_UNORM):
|
||||
case ((int)GTX.GX2SurfaceFormat.GX2_SURFACE_FORMAT_T_BC2_SRGB):
|
||||
dds.header.ddspf.fourCC = "DXT3";
|
||||
break;
|
||||
case ((int)GTX.GX2SurfaceFormat.GX2_SURFACE_FORMAT_T_BC3_UNORM):
|
||||
case ((int)GTX.GX2SurfaceFormat.GX2_SURFACE_FORMAT_T_BC3_SRGB):
|
||||
dds.header.ddspf.fourCC = "DXT5";
|
||||
break;
|
||||
case ((int)GTX.GX2SurfaceFormat.GX2_SURFACE_FORMAT_T_BC4_UNORM):
|
||||
IsDX10 = true;
|
||||
dds.DX10header = new DDS.DX10Header();
|
||||
dds.DX10header.DXGI_Format = DDS.DXGI_FORMAT.DXGI_FORMAT_BC4_UNORM;
|
||||
break;
|
||||
case ((int)GTX.GX2SurfaceFormat.GX2_SURFACE_FORMAT_T_BC4_SNORM):
|
||||
IsDX10 = true;
|
||||
dds.DX10header = new DDS.DX10Header();
|
||||
dds.DX10header.DXGI_Format = DDS.DXGI_FORMAT.DXGI_FORMAT_BC4_SNORM;
|
||||
break;
|
||||
case ((int)GTX.GX2SurfaceFormat.GX2_SURFACE_FORMAT_T_BC5_UNORM):
|
||||
IsDX10 = true;
|
||||
dds.DX10header = new DDS.DX10Header();
|
||||
dds.DX10header.DXGI_Format = DDS.DXGI_FORMAT.DXGI_FORMAT_BC5_UNORM;
|
||||
break;
|
||||
case ((int)GTX.GX2SurfaceFormat.GX2_SURFACE_FORMAT_T_BC5_SNORM):
|
||||
IsDX10 = true;
|
||||
dds.DX10header = new DDS.DX10Header();
|
||||
dds.DX10header.DXGI_Format = DDS.DXGI_FORMAT.DXGI_FORMAT_BC5_SNORM;
|
||||
break;
|
||||
default:
|
||||
throw new Exception($"Format {(GTX.GX2SurfaceFormat)format} not supported!");
|
||||
}
|
||||
if (IsCompressedFormat((GX2SurfaceFormat)format))
|
||||
dds.SetFlags(GetCompressedDXGI_FORMAT((GX2SurfaceFormat)format));
|
||||
else
|
||||
dds.SetFlags(GetUncompressedDXGI_FORMAT((GX2SurfaceFormat)format));
|
||||
|
||||
if (IsDX10)
|
||||
dds.header.ddspf.fourCC = "DX10";
|
||||
|
||||
dds.Save(dds, FileName, IsDX10, renderedTex.mipmaps);
|
||||
dds.Save(dds, FileName, renderedTex.mipmaps);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -32,6 +32,8 @@ namespace FirstPlugin
|
|||
{
|
||||
R8G8B8A8_UNORM = 0x00,
|
||||
R8G8B8A8_SRGB = 0x05,
|
||||
B8G8R8A8_UNORM = 0x50,
|
||||
B8G8R8A8_SRGB = 0x55,
|
||||
BC1_UNORM = 0x80,
|
||||
BC1_SRGB = 0x85,
|
||||
BC2_UNORM = 0x90,
|
||||
|
@ -74,6 +76,8 @@ namespace FirstPlugin
|
|||
{
|
||||
switch (format)
|
||||
{
|
||||
case (byte)NUTEXImageFormat.B8G8R8A8_UNORM:
|
||||
case (byte)NUTEXImageFormat.B8G8R8A8_SRGB:
|
||||
case (byte)NUTEXImageFormat.R8G8B8A8_UNORM:
|
||||
case (byte)NUTEXImageFormat.R8G8B8A8_SRGB:
|
||||
return 4;
|
||||
|
@ -134,14 +138,12 @@ namespace FirstPlugin
|
|||
texture.Read(new FileReader(file));
|
||||
|
||||
Console.WriteLine(texture.Format.ToString("x") + " " + file + " " + texture.Text);
|
||||
|
||||
/*
|
||||
try
|
||||
{
|
||||
Bitmap bitmap = texture.DisplayImage();
|
||||
Bitmap bitmap = texture.DisplayTexture();
|
||||
|
||||
if (bitmap != null)
|
||||
bitmap.Save(System.IO.Path.GetDirectoryName(ofd.FileName) + texture.Text + ".png");
|
||||
bitmap.Save(System.IO.Path.GetFullPath(file) + texture.ArcOffset + texture.Text + ".png");
|
||||
else
|
||||
Console.WriteLine(" Not supported Format! " + texture.Format);
|
||||
|
||||
|
@ -150,16 +152,21 @@ namespace FirstPlugin
|
|||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Somethign went wrong??");
|
||||
}
|
||||
texture.blocksCompressed.Clear();
|
||||
texture.mipmaps.Clear();
|
||||
|
||||
|
||||
texture = null;
|
||||
GC.Collect();*/
|
||||
GC.Collect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public class NuTex : TreeNodeFile
|
||||
{
|
||||
public bool BadSwizzle;
|
||||
public uint Width;
|
||||
public uint Height;
|
||||
public byte Format;
|
||||
|
@ -168,6 +175,7 @@ namespace FirstPlugin
|
|||
public List<List<byte[]>> mipmaps = new List<List<byte[]>>();
|
||||
public List<List<byte[]>> blocksCompressed = new List<List<byte[]>>();
|
||||
bool IsSwizzled = true;
|
||||
public string ArcOffset; //Temp for exporting in batch
|
||||
|
||||
MenuItem export = new MenuItem("Export");
|
||||
|
||||
|
@ -227,17 +235,15 @@ namespace FirstPlugin
|
|||
dds.header.width = Width;
|
||||
dds.header.height = Height;
|
||||
dds.header.mipmapCount = (uint)mipmaps.Count;
|
||||
dds.header.pitchOrLinearSize = (uint)mipmaps[0][0].Length;
|
||||
|
||||
bool IsDX10 = false;
|
||||
if (IsCompressedFormat((NUTEXImageFormat)Format))
|
||||
dds.SetFlags(GetCompressedDXGI_FORMAT((NUTEXImageFormat)Format));
|
||||
else
|
||||
dds.SetFlags(GetUncompressedDXGI_FORMAT((NUTEXImageFormat)Format));
|
||||
|
||||
IsDX10 = true;
|
||||
dds.DX10header = new DDS.DX10Header();
|
||||
dds.DX10header.DXGI_Format = DDS.DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM;
|
||||
|
||||
if (IsDX10)
|
||||
dds.header.ddspf.fourCC = "DX10";
|
||||
|
||||
dds.Save(dds, FileName, IsDX10, mipmaps);
|
||||
dds.Save(dds, FileName, mipmaps);
|
||||
}
|
||||
public void Read(FileReader reader)
|
||||
{
|
||||
|
@ -285,8 +291,8 @@ namespace FirstPlugin
|
|||
{
|
||||
for (int mipLevel = 0; mipLevel < mipCount; mipLevel++)
|
||||
{
|
||||
mips.Add(reader.ReadBytes((int)mipSizes[mipLevel]));
|
||||
break; //Don't load mip maps yet. They break for some reason????
|
||||
mips.Add(reader.ReadBytes((int)Width * (int)Height * (int)bpps(Format)));
|
||||
break;
|
||||
}
|
||||
blocksCompressed.Add(mips);
|
||||
}
|
||||
|
@ -300,20 +306,26 @@ namespace FirstPlugin
|
|||
|
||||
public Bitmap DisplayTexture(int DisplayMipIndex = 0, int ArrayIndex = 0)
|
||||
{
|
||||
if (BadSwizzle)
|
||||
return BitmapExtension.GetBitmap(Properties.Resources.Black, 32, 32);
|
||||
|
||||
if (IsSwizzled)
|
||||
LoadTexture();
|
||||
else
|
||||
mipmaps.Add(blocksCompressed[0]);
|
||||
|
||||
if (mipmaps.Count <= 0)
|
||||
if (mipmaps[0].Count <= 0)
|
||||
{
|
||||
throw new Exception("No texture data found");
|
||||
return BitmapExtension.GetBitmap(Properties.Resources.Black, 32, 32);
|
||||
}
|
||||
|
||||
uint width = (uint)Math.Max(1, Width >> DisplayMipIndex);
|
||||
uint height = (uint)Math.Max(1, Height >> DisplayMipIndex);
|
||||
|
||||
byte[] data = mipmaps[ArrayIndex][DisplayMipIndex];
|
||||
|
||||
|
||||
|
||||
return DecodeBlock(data, width, height, (NUTEXImageFormat)Format);
|
||||
}
|
||||
public static Bitmap DecodeBlock(byte[] data, uint Width, uint Height, NUTEXImageFormat Format)
|
||||
|
@ -342,6 +354,8 @@ namespace FirstPlugin
|
|||
{
|
||||
case NUTEXImageFormat.R8G8B8A8_UNORM: return DDS.DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||
case NUTEXImageFormat.R8G8B8A8_SRGB: return DDS.DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB;
|
||||
case NUTEXImageFormat.B8G8R8A8_UNORM: return DDS.DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM;
|
||||
case NUTEXImageFormat.B8G8R8A8_SRGB: return DDS.DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM_SRGB;
|
||||
default:
|
||||
throw new Exception($"Cannot convert format {Format}");
|
||||
}
|
||||
|
@ -423,12 +437,22 @@ namespace FirstPlugin
|
|||
|
||||
Console.WriteLine($"{blk_dim.ToString("x")} {bpp} {width} {height} {linesPerBlockHeight} {blkWidth} {blkHeight} {size} { blocksCompressed[arrayLevel][mipLevel].Length}");
|
||||
|
||||
byte[] result = TegraX1Swizzle.deswizzle(width, height, blkWidth, blkHeight, target, bpp, tileMode, (int)Math.Max(0, BlockHeightLog2 - blockHeightShift), blocksCompressed[arrayLevel][mipLevel]);
|
||||
//Create a copy and use that to remove uneeded data
|
||||
byte[] result_ = new byte[size];
|
||||
Array.Copy(result, 0, result_, 0, size);
|
||||
try
|
||||
{
|
||||
byte[] result = TegraX1Swizzle.deswizzle(width, height, blkWidth, blkHeight, target, bpp, tileMode, (int)Math.Max(0, BlockHeightLog2 - blockHeightShift), blocksCompressed[arrayLevel][mipLevel]);
|
||||
//Create a copy and use that to remove uneeded data
|
||||
byte[] result_ = new byte[size];
|
||||
Array.Copy(result, 0, result_, 0, size);
|
||||
|
||||
mips.Add(result_);
|
||||
mips.Add(result_);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
System.Windows.Forms.MessageBox.Show("Failed to swizzle texture!");
|
||||
Console.WriteLine(e);
|
||||
BadSwizzle = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
mipmaps.Add(mips);
|
||||
}
|
||||
|
@ -456,6 +480,7 @@ namespace FirstPlugin
|
|||
IsActive = true;
|
||||
EditorRoot = new NuTex();
|
||||
((NuTex)EditorRoot).FileHandler = this;
|
||||
((NuTex)EditorRoot).ArcOffset = System.IO.Path.GetFileNameWithoutExtension(FileName);
|
||||
((NuTex)EditorRoot).Read(new FileReader(Data));
|
||||
}
|
||||
public void Unload()
|
||||
|
|
|
@ -76,7 +76,7 @@ namespace FirstPlugin
|
|||
{
|
||||
uint block_height = (uint)(1 << blockHeightLog2);
|
||||
|
||||
// Console.WriteLine($"Swizzle {width} {height} {blkWidth} {blkHeight} {roundPitch} {bpp} {tileMode} {blockHeightLog2} {data.Length} {toSwizzle}");
|
||||
Console.WriteLine($"Swizzle {width} {height} {blkWidth} {blkHeight} {roundPitch} {bpp} {tileMode} {blockHeightLog2} {data.Length} {toSwizzle}");
|
||||
|
||||
width = DIV_ROUND_UP(width, blkWidth);
|
||||
height = DIV_ROUND_UP(height, blkHeight);
|
||||
|
|
|
@ -95,7 +95,25 @@ namespace Switch_Toolbox.Library
|
|||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetFourCC(DXGI_FORMAT Format)
|
||||
{
|
||||
switch (Format)
|
||||
{
|
||||
case DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM:
|
||||
case DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM_SRGB:
|
||||
header.ddspf.fourCC = "DXT1";
|
||||
break;
|
||||
case DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM:
|
||||
case DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM_SRGB:
|
||||
header.ddspf.fourCC = "DXT3";
|
||||
break;
|
||||
case DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM:
|
||||
case DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM_SRGB:
|
||||
header.ddspf.fourCC = "DXT5";
|
||||
break;
|
||||
}
|
||||
}
|
||||
public bool IsDX10;
|
||||
public Header header;
|
||||
public DX10Header DX10header;
|
||||
public class Header
|
||||
|
@ -318,6 +336,8 @@ namespace Switch_Toolbox.Library
|
|||
int DX10HeaderSize = 0;
|
||||
if (header.ddspf.fourCC == "DX10")
|
||||
{
|
||||
IsDX10 = true;
|
||||
|
||||
DX10HeaderSize = 20;
|
||||
ReadDX10Header(reader);
|
||||
}
|
||||
|
@ -357,7 +377,57 @@ namespace Switch_Toolbox.Library
|
|||
return array;
|
||||
}
|
||||
}
|
||||
public void Save(DDS dds, string FileName, bool IsDX10 = false, List<List<byte[]>> data = null)
|
||||
public void SetFlags(DXGI_FORMAT Format)
|
||||
{
|
||||
header.flags = (uint)(DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT | DDSD.MIPMAPCOUNT | DDSD.LINEARSIZE);
|
||||
header.caps = (uint)DDSCAPS.TEXTURE;
|
||||
if (header.mipmapCount > 1)
|
||||
header.caps |= (uint)(DDSCAPS.COMPLEX | DDSCAPS.MIPMAP);
|
||||
|
||||
switch (Format)
|
||||
{
|
||||
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM:
|
||||
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB:
|
||||
header.ddspf.flags = (uint)(DDPF.RGB | DDPF.ALPHAPIXELS);
|
||||
header.ddspf.RGBBitCount = 0x8 * 4;
|
||||
header.ddspf.RBitMask = 0x000000FF;
|
||||
header.ddspf.GBitMask = 0x0000FF00;
|
||||
header.ddspf.BBitMask = 0x00FF0000;
|
||||
header.ddspf.ABitMask = 0xFF000000;
|
||||
break;
|
||||
case DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM_SRGB:
|
||||
case DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM:
|
||||
header.ddspf.flags = (uint)DDPF.FOURCC;
|
||||
header.ddspf.fourCC = "DXT1";
|
||||
break;
|
||||
case DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM_SRGB:
|
||||
case DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM:
|
||||
header.ddspf.flags = (uint)DDPF.FOURCC;
|
||||
header.ddspf.fourCC = "DXT3";
|
||||
break;
|
||||
case DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM_SRGB:
|
||||
case DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM:
|
||||
header.ddspf.flags = (uint)DDPF.FOURCC;
|
||||
header.ddspf.fourCC = "DXT5";
|
||||
break;
|
||||
case DXGI_FORMAT.DXGI_FORMAT_BC4_UNORM:
|
||||
case DXGI_FORMAT.DXGI_FORMAT_BC4_SNORM:
|
||||
case DXGI_FORMAT.DXGI_FORMAT_BC5_UNORM:
|
||||
case DXGI_FORMAT.DXGI_FORMAT_BC5_SNORM:
|
||||
case DXGI_FORMAT.DXGI_FORMAT_BC6H_UF16:
|
||||
case DXGI_FORMAT.DXGI_FORMAT_BC6H_SF16:
|
||||
case DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM:
|
||||
case DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM_SRGB:
|
||||
header.ddspf.fourCC = "DX10";
|
||||
if (DX10header == null)
|
||||
DX10header = new DX10Header();
|
||||
|
||||
IsDX10 = true;
|
||||
DX10header.DXGI_Format = Format;
|
||||
break;
|
||||
}
|
||||
}
|
||||
public void Save(DDS dds, string FileName, List<List<byte[]>> data = null)
|
||||
{
|
||||
FileWriter writer = new FileWriter(new FileStream(FileName, FileMode.Create, FileAccess.Write, FileShare.Write));
|
||||
writer.Write(Encoding.ASCII.GetBytes("DDS "));
|
||||
|
|
Loading…
Reference in a new issue