More modsupport updates for improved DynmapBlockScan
This commit is contained in:
parent
376ef17f88
commit
d41a85ea56
6 changed files with 98 additions and 159 deletions
|
|
@ -317,6 +317,7 @@ public class HDBlockModels {
|
||||||
double[] to = new double[3];
|
double[] to = new double[3];
|
||||||
double xrot = 0, yrot = 0, zrot = 0;
|
double xrot = 0, yrot = 0, zrot = 0;
|
||||||
double xrotorig = 8, yrotorig = 8, zrotorig = 8;
|
double xrotorig = 8, yrotorig = 8, zrotorig = 8;
|
||||||
|
int modrotx = 0, modroty = 0, modrotz = 0; // Model level rotation
|
||||||
boolean shade = true;
|
boolean shade = true;
|
||||||
ArrayList<ModelBoxSide> sides = new ArrayList<ModelBoxSide>();
|
ArrayList<ModelBoxSide> sides = new ArrayList<ModelBoxSide>();
|
||||||
};
|
};
|
||||||
|
|
@ -953,9 +954,17 @@ public class HDBlockModels {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Rest are faces (<side - upnsew>/<txtidx>/umin/vmin/umax/vmax> or <<side - upnsew>/<txtidx>)
|
// Rest are faces (<side - upnsew>/<txtidx>/umin/vmin/umax/vmax> or <<side - upnsew>/<txtidx>)
|
||||||
|
// OR R/mrx/mry/mrz for model rotation
|
||||||
for (int faceidx = 2; faceidx < prms.length; faceidx++) {
|
for (int faceidx = 2; faceidx < prms.length; faceidx++) {
|
||||||
String v = prms[faceidx];
|
String v = prms[faceidx];
|
||||||
String[] flds = v.split("/");
|
String[] flds = v.split("/");
|
||||||
|
// If rotation
|
||||||
|
if (flds[0].equals("R") && (flds.length == 4)) {
|
||||||
|
box.modrotx = Integer.parseInt(flds[1]);
|
||||||
|
box.modroty = Integer.parseInt(flds[2]);
|
||||||
|
box.modrotz = Integer.parseInt(flds[3]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
ModelBoxSide side = new ModelBoxSide();
|
ModelBoxSide side = new ModelBoxSide();
|
||||||
side.rot = null;
|
side.rot = null;
|
||||||
if ((flds.length != 2) && (flds.length != 6)) {
|
if ((flds.length != 2) && (flds.length != 6)) {
|
||||||
|
|
@ -1016,6 +1025,10 @@ public class HDBlockModels {
|
||||||
new Vector3D(bl.xrotorig / 16, bl.yrotorig / 16, bl.zrotorig / 16),
|
new Vector3D(bl.xrotorig / 16, bl.yrotorig / 16, bl.zrotorig / 16),
|
||||||
patch.textureindex);
|
patch.textureindex);
|
||||||
}
|
}
|
||||||
|
// If model rotation, apply too
|
||||||
|
if ((bl.modrotx != 0) || (bl.modroty != 0) || (bl.modrotz != 0)) {
|
||||||
|
patch = pdf.getPatch(patch, bl.modrotx, bl.modroty, bl.modrotz, patch.textureindex);
|
||||||
|
}
|
||||||
pd.add(patch);
|
pd.add(patch);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
@ -1025,7 +1038,7 @@ public class HDBlockModels {
|
||||||
}
|
}
|
||||||
PatchDefinition[] patcharray = new PatchDefinition[pd.size()];
|
PatchDefinition[] patcharray = new PatchDefinition[pd.size()];
|
||||||
for (int i = 0; i < patcharray.length; i++) {
|
for (int i = 0; i < patcharray.length; i++) {
|
||||||
patcharray[i] = pd.get(i);
|
patcharray[i] = pd.get(i);
|
||||||
}
|
}
|
||||||
if (patcharray.length > max_patches)
|
if (patcharray.length > max_patches)
|
||||||
max_patches = patcharray.length;
|
max_patches = patcharray.length;
|
||||||
|
|
|
||||||
|
|
@ -210,12 +210,6 @@ public class ModModelDefinitionImpl implements ModModelDefinition {
|
||||||
blkModel.add(mod);
|
blkModel.add(mod);
|
||||||
return mod;
|
return mod;
|
||||||
}
|
}
|
||||||
@Override
|
|
||||||
public ModelBlockModel addRotatedModelBlockModel(String blockname, ModelBlockModel model, int xrot, int yrot, int zrot) {
|
|
||||||
ModelBlockModelImpl mod = new ModelBlockModelImpl(blockname, this, model, xrot, yrot, zrot);
|
|
||||||
blkModel.add(mod);
|
|
||||||
return mod;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPatchID(double x0, double y0, double z0, double xu,
|
public String getPatchID(double x0, double y0, double z0, double xu,
|
||||||
double yu, double zu, double xv, double yv, double zv, double umin,
|
double yu, double zu, double xv, double yv, double zv, double umin,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ public class ModelBlockModelImpl extends BlockModelImpl implements ModelBlockMod
|
||||||
private double xrot = 0, yrot = 0, zrot = 0;
|
private double xrot = 0, yrot = 0, zrot = 0;
|
||||||
private boolean shade;
|
private boolean shade;
|
||||||
private double[] rotorigin;
|
private double[] rotorigin;
|
||||||
|
private int modrotx = 0, modroty = 0, modrotz = 0;
|
||||||
@Override
|
@Override
|
||||||
public void addBlockSide(BlockSide side, double[] uv, SideRotation rot, int textureid, int tintidx) {
|
public void addBlockSide(BlockSide side, double[] uv, SideRotation rot, int textureid, int tintidx) {
|
||||||
ModelSide ms = new ModelSide();
|
ModelSide ms = new ModelSide();
|
||||||
|
|
@ -48,21 +49,10 @@ public class ModelBlockModelImpl extends BlockModelImpl implements ModelBlockMod
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private ArrayList<ModelBlockImpl> boxes = new ArrayList<ModelBlockImpl>();
|
private ArrayList<ModelBlockImpl> boxes = new ArrayList<ModelBlockImpl>();
|
||||||
private String rotsourceblockname;
|
|
||||||
private Map<String, String> rotsourcestatemap;
|
|
||||||
private int xrot, yrot, zrot;
|
|
||||||
|
|
||||||
public ModelBlockModelImpl(String blkname, ModModelDefinitionImpl mdf) {
|
public ModelBlockModelImpl(String blkname, ModModelDefinitionImpl mdf) {
|
||||||
super(blkname, mdf);
|
super(blkname, mdf);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ModelBlockModelImpl(String blkname, ModModelDefinitionImpl mdf, ModelBlockModel mod, int xrot, int yrot, int zrot) {
|
|
||||||
super(blkname, mdf);
|
|
||||||
this.rotsourceblockname = mod.getBlockNames()[0];
|
|
||||||
this.rotsourcestatemap = new HashMap<String, String>();
|
|
||||||
this.rotsourcestatemap.putAll(mod.getBlockStateMappings().get(0));
|
|
||||||
this.xrot = xrot; this.yrot = yrot; this.zrot = zrot;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static HashMap<BlockSide, String> fromBlockSide = new HashMap<BlockSide, String>();
|
private static HashMap<BlockSide, String> fromBlockSide = new HashMap<BlockSide, String>();
|
||||||
static {
|
static {
|
||||||
|
|
@ -79,76 +69,50 @@ public class ModelBlockModelImpl extends BlockModelImpl implements ModelBlockMod
|
||||||
String ids = this.getIDsAndMeta();
|
String ids = this.getIDsAndMeta();
|
||||||
if (ids == null) return null;
|
if (ids == null) return null;
|
||||||
String line;
|
String line;
|
||||||
// If rotating another model
|
line = String.format("modellist:%s", ids);
|
||||||
if (rotsourceblockname != null) {
|
for (ModelBlockImpl mb: boxes) {
|
||||||
line = String.format("patchblock:%s", ids);
|
line += String.format(",box=%f/%f/%f", mb.from[0], mb.from[1], mb.from[2]);
|
||||||
line += "\npatchrotate:id=" + rotsourceblockname;
|
if (!mb.shade) { // if shade=false
|
||||||
if (rotsourcestatemap != null) {
|
line += "/false";
|
||||||
line += ",state=";
|
}
|
||||||
boolean first = true;
|
line += String.format(":%f/%f/%f", mb.to[0], mb.to[1], mb.to[2]);
|
||||||
for (Entry<String, String> r : rotsourcestatemap.entrySet()) {
|
if ((mb.xrot != 0) || (mb.yrot != 0) || (mb.zrot != 0)) { // If needed, add rotation
|
||||||
if (first) {
|
line += String.format("/%f/%f/%f", mb.xrot, mb.yrot, mb.zrot);
|
||||||
first = false;
|
// If origin also defined, add it
|
||||||
}
|
if (mb.rotorigin != null) {
|
||||||
else {
|
line += String.format("/%f/%f/%f", mb.rotorigin[0], mb.rotorigin[1], mb.rotorigin[2]);
|
||||||
line += '/';
|
}
|
||||||
}
|
}
|
||||||
line += r.getKey() + ":" + r.getValue();
|
for (BlockSide bs : fromBlockSide.keySet()) {
|
||||||
}
|
String side = fromBlockSide.get(bs);
|
||||||
}
|
ModelSide mside = mb.sides.get(bs);
|
||||||
if (xrot != 0) {
|
if (mside != null) {
|
||||||
line += ",rotx=" + xrot;
|
String rval = side;
|
||||||
}
|
switch (mside.rot) {
|
||||||
if (yrot != 0) {
|
case DEG0:
|
||||||
line += ",roty=" + yrot;
|
default:
|
||||||
}
|
break;
|
||||||
if (zrot != 0) {
|
case DEG90:
|
||||||
line += ",rotz=" + yrot;
|
rval += "90";
|
||||||
}
|
break;
|
||||||
}
|
case DEG180:
|
||||||
else {
|
rval += "180";
|
||||||
line = String.format("modellist:%s", ids);
|
break;
|
||||||
for (ModelBlockImpl mb: boxes) {
|
case DEG270:
|
||||||
line += String.format(",box=%f/%f/%f", mb.from[0], mb.from[1], mb.from[2]);
|
rval += "270";
|
||||||
if (!mb.shade) { // if shade=false
|
break;
|
||||||
line += "/false";
|
}
|
||||||
}
|
if (mside.uv != null) {
|
||||||
line += String.format(":%f/%f/%f", mb.to[0], mb.to[1], mb.to[2]);
|
line += String.format(":%s/%d/%f/%f/%f/%f", rval, mside.textureid, mside.uv[0], mside.uv[1], mside.uv[2], mside.uv[3]);
|
||||||
if ((mb.xrot != 0) || (mb.yrot != 0) || (mb.zrot != 0)) { // If needed, add rotation
|
}
|
||||||
line += String.format("/%f/%f/%f", mb.xrot, mb.yrot, mb.zrot);
|
else {
|
||||||
// If origin also defined, add it
|
line += String.format(":%s/%d", rval, mside.textureid);
|
||||||
if (mb.rotorigin != null) {
|
}
|
||||||
line += String.format("/%f/%f/%f", mb.rotorigin[0], mb.rotorigin[1], mb.rotorigin[2]);
|
}
|
||||||
}
|
}
|
||||||
}
|
if ((mb.modrotx != 0) || (mb.modroty != 0) || (mb.modrotz != 0)) {
|
||||||
for (BlockSide bs : fromBlockSide.keySet()) {
|
line += String.format(":R/%d/%d/%d", mb.modrotx, mb.modroty, mb.modrotz);
|
||||||
String side = fromBlockSide.get(bs);
|
}
|
||||||
ModelSide mside = mb.sides.get(bs);
|
|
||||||
if (mside != null) {
|
|
||||||
String rval = side;
|
|
||||||
switch (mside.rot) {
|
|
||||||
case DEG0:
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
case DEG90:
|
|
||||||
rval += "90";
|
|
||||||
break;
|
|
||||||
case DEG180:
|
|
||||||
rval += "180";
|
|
||||||
break;
|
|
||||||
case DEG270:
|
|
||||||
rval += "270";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (mside.uv != null) {
|
|
||||||
line += String.format(":%s/%d/%f/%f/%f/%f", rval, mside.textureid, mside.uv[0], mside.uv[1], mside.uv[2], mside.uv[3]);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
line += String.format(":%s/%d", rval, mside.textureid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return line;
|
return line;
|
||||||
}
|
}
|
||||||
|
|
@ -168,11 +132,14 @@ public class ModelBlockModelImpl extends BlockModelImpl implements ModelBlockMod
|
||||||
* @param zrot - degrees of rotation of block around Z
|
* @param zrot - degrees of rotation of block around Z
|
||||||
* @param shade - shade setting for model
|
* @param shade - shade setting for model
|
||||||
* @param rotorigin - rotation origin, if any (default [ 8, 8, 8 ](
|
* @param rotorigin - rotation origin, if any (default [ 8, 8, 8 ](
|
||||||
|
* @param modrotx - model level rotation in degrees (0, 90, 180, 270)
|
||||||
|
* @param modroty - model level rotation in degrees (0, 90, 180, 270)
|
||||||
|
* @param modrotz - model level rotation in degrees (0, 90, 180, 270)
|
||||||
* @return model block to add faces to
|
* @return model block to add faces to
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public ModelBlock addModelBlock(double[] from, double[] to, double xrot, double yrot, double zrot,
|
public ModelBlock addModelBlock(double[] from, double[] to, double xrot, double yrot, double zrot,
|
||||||
boolean shade, double[] rotorigin) {
|
boolean shade, double[] rotorigin, int modrotx, int modroty, int modrotz) {
|
||||||
ModelBlockImpl mbi = new ModelBlockImpl();
|
ModelBlockImpl mbi = new ModelBlockImpl();
|
||||||
if (from != null) { mbi.from[0] = from[0]; mbi.from[1] = from[1]; mbi.from[2] = from[2]; }
|
if (from != null) { mbi.from[0] = from[0]; mbi.from[1] = from[1]; mbi.from[2] = from[2]; }
|
||||||
if (to != null) { mbi.to[0] = to[0]; mbi.to[1] = to[1]; mbi.to[2] = to[2]; }
|
if (to != null) { mbi.to[0] = to[0]; mbi.to[1] = to[1]; mbi.to[2] = to[2]; }
|
||||||
|
|
@ -181,6 +148,9 @@ public class ModelBlockModelImpl extends BlockModelImpl implements ModelBlockMod
|
||||||
if (rotorigin != null) {
|
if (rotorigin != null) {
|
||||||
mbi.rotorigin = Arrays.copyOf(rotorigin, 3);
|
mbi.rotorigin = Arrays.copyOf(rotorigin, 3);
|
||||||
}
|
}
|
||||||
|
mbi.modrotx = modrotx;
|
||||||
|
mbi.modroty = modroty;
|
||||||
|
mbi.modrotz = modrotz;
|
||||||
boxes.add(mbi);
|
boxes.add(mbi);
|
||||||
return mbi;
|
return mbi;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -215,67 +215,35 @@ public class PatchDefinition implements RenderPatch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private boolean outOfRange(double v) {
|
||||||
|
return (v < -1.0) || (v > 2.0);
|
||||||
|
}
|
||||||
public boolean validate() {
|
public boolean validate() {
|
||||||
boolean good = true;
|
boolean good = true;
|
||||||
if((x0 < -1.0) || (x0 > 2.0)) {
|
// Compute visible corners to see if we're inside cube
|
||||||
Log.severe("Invalid x0=" + x0);
|
double xx0 = x0 + (xu - x0) * umin;
|
||||||
good = false;
|
double xx1 = x0 + (xv - x0) * vmin;
|
||||||
|
double xx2 = x0 + (xu - x0) * umax;
|
||||||
|
double xx3 = x0 + (xv - x0) * vmax;
|
||||||
|
if (outOfRange(xx0) || outOfRange(xx1) || outOfRange(xx2) || outOfRange(xx3)) {
|
||||||
|
Log.severe(String.format("Invalid visible range xu=[%f:%f], xv=[%f:%f]", xx0, xx2, xx1, xx3));
|
||||||
|
good = false;
|
||||||
}
|
}
|
||||||
if((y0 < -1.0) || (y0 > 2.0)) {
|
double yy0 = y0 + (yu - y0) * umin;
|
||||||
Log.severe("Invalid y0=" + y0);
|
double yy1 = y0 + (yv - y0) * vmin;
|
||||||
good = false;
|
double yy2 = y0 + (yu - y0) * umax;
|
||||||
|
double yy3 = y0 + (yv - y0) * vmax;
|
||||||
|
if (outOfRange(yy0) || outOfRange(yy1) || outOfRange(yy2) || outOfRange(yy3)) {
|
||||||
|
Log.severe(String.format("Invalid visible range yu=[%f:%f], yv=[%f:%f]", yy0, yy2, yy1, yy3));
|
||||||
|
good = false;
|
||||||
}
|
}
|
||||||
if((z0 < -1.0) || (z0 > 2.0)) {
|
double zz0 = z0 + (zu - z0) * umin;
|
||||||
Log.severe("Invalid z0=" + z0);
|
double zz1 = z0 + (zv - z0) * vmin;
|
||||||
good = false;
|
double zz2 = z0 + (zu - z0) * umax;
|
||||||
}
|
double zz3 = z0 + (zv - z0) * vmax;
|
||||||
if((xu < -1.0) || (xu > 2.0)) {
|
if (outOfRange(zz0) || outOfRange(zz1) || outOfRange(zz2) || outOfRange(zz3)) {
|
||||||
Log.severe("Invalid xu=" + xu);
|
Log.severe(String.format("Invalid visible range zu=[%f:%f], zv=[%f:%f]", zz0, zz2, zz1, zz3));
|
||||||
good = false;
|
good = false;
|
||||||
}
|
|
||||||
if((yu < -1.0) || (yu > 2.0)) {
|
|
||||||
Log.severe("Invalid yu=" + yu);
|
|
||||||
good = false;
|
|
||||||
}
|
|
||||||
if((zu < -1.0) || (zu > 2.0)) {
|
|
||||||
Log.severe("Invalid zu=" + zu);
|
|
||||||
good = false;
|
|
||||||
}
|
|
||||||
if((xv < -1.0) || (xv > 2.0)) {
|
|
||||||
Log.severe("Invalid xv=" + xv);
|
|
||||||
good = false;
|
|
||||||
}
|
|
||||||
if((yv < -1.0) || (yv > 2.0)) {
|
|
||||||
Log.severe("Invalid yv=" + yv);
|
|
||||||
good = false;
|
|
||||||
}
|
|
||||||
if((zv < -1.0) || (zv > 2.0)) {
|
|
||||||
Log.severe("Invalid zv=" + zv);
|
|
||||||
good = false;
|
|
||||||
}
|
|
||||||
if((umin < 0.0) || (umin > umax)) {
|
|
||||||
Log.severe("Invalid umin=" + umin);
|
|
||||||
good = false;
|
|
||||||
}
|
|
||||||
if((vmin < 0.0) || (vmin > vmax)) {
|
|
||||||
Log.severe("Invalid vmin=" + vmin);
|
|
||||||
good = false;
|
|
||||||
}
|
|
||||||
if(umax > 1.0) {
|
|
||||||
Log.severe("Invalid umax=" + umax);
|
|
||||||
good = false;
|
|
||||||
}
|
|
||||||
if(vmax > 1.0) {
|
|
||||||
Log.severe("Invalid vmax=" + vmax);
|
|
||||||
good = false;
|
|
||||||
}
|
|
||||||
if ((vminatumax < 0.0) || (vminatumax > vmaxatumax)) {
|
|
||||||
Log.severe("Invalid vminatumax=" + vminatumax);
|
|
||||||
good = false;
|
|
||||||
}
|
|
||||||
if(vmaxatumax > 1.0) {
|
|
||||||
Log.severe("Invalid vmaxatumax=" + vmaxatumax);
|
|
||||||
good = false;
|
|
||||||
}
|
}
|
||||||
if (!good) {
|
if (!good) {
|
||||||
Log.warning("Patch not valid: " + toString());
|
Log.warning("Patch not valid: " + toString());
|
||||||
|
|
|
||||||
|
|
@ -168,16 +168,6 @@ public interface ModModelDefinition {
|
||||||
* @return block model record
|
* @return block model record
|
||||||
*/
|
*/
|
||||||
public ModelBlockModel addModelBlockModel(String blockname);
|
public ModelBlockModel addModelBlockModel(String blockname);
|
||||||
/**
|
|
||||||
* Add rotated model block model, based on existing model : default assumes all metadata values are matching
|
|
||||||
* @param blockname - block name
|
|
||||||
* @param model - existing model to be rotated
|
|
||||||
* @param xrot - x rotation in degrees (0, 90, 180, 270)
|
|
||||||
* @param yrot - y rotation in degrees (0, 90, 180, 270)
|
|
||||||
* @param zrot - z rotation in degrees (0, 90, 180, 270)
|
|
||||||
* @return block model record
|
|
||||||
*/
|
|
||||||
public ModelBlockModel addRotatedModelBlockModel(String blockname, ModelBlockModel model, int xrot, int yrot, int zrot);
|
|
||||||
/**
|
/**
|
||||||
* Final call for model definition: publishes definiiton to Dynmap to be used for the mod
|
* Final call for model definition: publishes definiiton to Dynmap to be used for the mod
|
||||||
* @return true if successful, false if error
|
* @return true if successful, false if error
|
||||||
|
|
|
||||||
|
|
@ -39,13 +39,17 @@ public interface ModelBlockModel extends BlockModel {
|
||||||
* @param zrot - degrees of rotation of block around Z
|
* @param zrot - degrees of rotation of block around Z
|
||||||
* @param shade - shade setting for model
|
* @param shade - shade setting for model
|
||||||
* @param rotorigin = rotation origin [x, y, z] (if null, [ 8,8,8 ] is assumed
|
* @param rotorigin = rotation origin [x, y, z] (if null, [ 8,8,8 ] is assumed
|
||||||
|
* @param modrotx - model level rotation in degrees (0, 90, 180, 270)
|
||||||
|
* @param modroty - model level rotation in degrees (0, 90, 180, 270)
|
||||||
|
* @param modrotz - model level rotation in degrees (0, 90, 180, 270)
|
||||||
* @return model block to add faces to
|
* @return model block to add faces to
|
||||||
*/
|
*/
|
||||||
public ModelBlock addModelBlock(double[] from, double[] to, double xrot, double yrot, double zrot, boolean shade, double[] rotorigin);
|
public ModelBlock addModelBlock(double[] from, double[] to, double xrot, double yrot, double zrot,
|
||||||
|
boolean shade, double[] rotorigin, int modrotx, int modroty, int modrotz);
|
||||||
default public ModelBlock addModelBlock(double[] from, double[] to, double xrot, double yrot, double zrot, boolean shade) {
|
default public ModelBlock addModelBlock(double[] from, double[] to, double xrot, double yrot, double zrot, boolean shade) {
|
||||||
return addModelBlock(from, to, xrot, yrot, zrot, shade, null);
|
return addModelBlock(from, to, xrot, yrot, zrot, shade, null, 0, 0, 0);
|
||||||
}
|
}
|
||||||
default public ModelBlock addModelBlock(double[] from, double[] to, double xrot, double yrot, double zrot) {
|
default public ModelBlock addModelBlock(double[] from, double[] to, double xrot, double yrot, double zrot) {
|
||||||
return addModelBlock(from, to, xrot, yrot, zrot, true, null);
|
return addModelBlock(from, to, xrot, yrot, zrot, true, null, 0, 0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue