diff --git a/src/main/java/net/minecraftforge/client/ForgeHooksClient.java b/src/main/java/net/minecraftforge/client/ForgeHooksClient.java index 5dc2e81c6..e29d8fc3c 100644 --- a/src/main/java/net/minecraftforge/client/ForgeHooksClient.java +++ b/src/main/java/net/minecraftforge/client/ForgeHooksClient.java @@ -478,6 +478,7 @@ public class ForgeHooksClient case GENERIC: glEnableVertexAttribArray(attr.getIndex()); glVertexAttribPointer(attr.getIndex(), count, constant, false, stride, buffer); + break; default: LOGGER.fatal("Unimplemented vanilla attribute upload: {}", attrType.getDisplayName()); } @@ -508,6 +509,7 @@ public class ForgeHooksClient break; case GENERIC: glDisableVertexAttribArray(attr.getIndex()); + break; default: LOGGER.fatal("Unimplemented vanilla attribute upload: {}", attrType.getDisplayName()); } diff --git a/src/main/java/net/minecraftforge/client/model/ItemLayerModel.java b/src/main/java/net/minecraftforge/client/model/ItemLayerModel.java index 3cf46078b..fc613556a 100644 --- a/src/main/java/net/minecraftforge/client/model/ItemLayerModel.java +++ b/src/main/java/net/minecraftforge/client/model/ItemLayerModel.java @@ -34,6 +34,8 @@ import net.minecraft.client.renderer.vertex.VertexFormat; import net.minecraft.resources.IResourceManager; import net.minecraft.util.EnumFacing; import net.minecraft.util.ResourceLocation; +import net.minecraftforge.client.model.pipeline.IVertexConsumer; +import net.minecraftforge.client.model.pipeline.TRSRTransformer; import net.minecraftforge.client.model.pipeline.UnpackedBakedQuad; import net.minecraftforge.common.model.IModelState; import net.minecraftforge.common.model.TRSRTransformation; @@ -399,53 +401,49 @@ public final class ItemLayerModel implements IUnbakedModel float x3, float y3, float z3, float u3, float v3) { UnpackedBakedQuad.Builder builder = new UnpackedBakedQuad.Builder(format); + builder.setQuadTint(tint); builder.setQuadOrientation(side); builder.setTexture(sprite); - putVertex(builder, format, transform, side, x0, y0, z0, u0, v0); - putVertex(builder, format, transform, side, x1, y1, z1, u1, v1); - putVertex(builder, format, transform, side, x2, y2, z2, u2, v2); - putVertex(builder, format, transform, side, x3, y3, z3, u3, v3); + + boolean hasTransform = transform.isPresent() && !transform.get().isIdentity(); + IVertexConsumer consumer = hasTransform ? new TRSRTransformer(builder, transform.get()) : builder; + + putVertex(consumer, format, side, x0, y0, z0, u0, v0); + putVertex(consumer, format, side, x1, y1, z1, u1, v1); + putVertex(consumer, format, side, x2, y2, z2, u2, v2); + putVertex(consumer, format, side, x3, y3, z3, u3, v3); + return builder.build(); } - private static void putVertex(UnpackedBakedQuad.Builder builder, VertexFormat format, Optional transform, EnumFacing side, float x, float y, float z, float u, float v) + private static void putVertex(IVertexConsumer consumer, VertexFormat format, EnumFacing side, float x, float y, float z, float u, float v) { - Vector4f vec = new Vector4f(); - boolean hasTransform = transform.isPresent() && !transform.get().isIdentity(); - for(int e = 0; e < format.getElementCount(); e++) { switch(format.getElement(e).getUsage()) { case POSITION: - if(hasTransform) - { - vec.x = x; - vec.y = y; - vec.z = z; - vec.w = 1; - transform.get().getMatrixVec().transform(vec); - builder.put(e, vec.x, vec.y, vec.z, vec.w); - } - else - { - builder.put(e, x, y, z, 1); - } + consumer.put(e, x, y, z, 1f); break; case COLOR: - builder.put(e, 1f, 1f, 1f, 1f); + consumer.put(e, 1f, 1f, 1f, 1f); break; - case UV: if(format.getElement(e).getIndex() == 0) - { - builder.put(e, u, v, 0f, 1f); - break; - } case NORMAL: - builder.put(e, (float)side.getXOffset(), (float)side.getYOffset(), (float)side.getZOffset(), 0f); + float offX = (float) side.getXOffset(); + float offY = (float) side.getYOffset(); + float offZ = (float) side.getZOffset(); + consumer.put(e, offX, offY, offZ, 0f); break; + case UV: + if(format.getElement(e).getIndex() == 0) + { + consumer.put(e, u, v, 0f, 1f); + break; + } + // else fallthrough to default default: - builder.put(e); + consumer.put(e); break; } } diff --git a/src/main/java/net/minecraftforge/client/model/ItemTextureQuadConverter.java b/src/main/java/net/minecraftforge/client/model/ItemTextureQuadConverter.java index 3da32a828..212e8fe76 100644 --- a/src/main/java/net/minecraftforge/client/model/ItemTextureQuadConverter.java +++ b/src/main/java/net/minecraftforge/client/model/ItemTextureQuadConverter.java @@ -25,11 +25,11 @@ import net.minecraft.client.renderer.texture.NativeImage; import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.client.renderer.vertex.VertexFormat; import net.minecraft.util.EnumFacing; +import net.minecraftforge.client.model.pipeline.IVertexConsumer; +import net.minecraftforge.client.model.pipeline.TRSRTransformer; import net.minecraftforge.client.model.pipeline.UnpackedBakedQuad; import net.minecraftforge.common.model.TRSRTransformation; -import javax.vecmath.Vector4f; - import java.util.List; public final class ItemTextureQuadConverter @@ -240,65 +240,59 @@ public final class ItemTextureQuadConverter builder.setQuadOrientation(side); builder.setTexture(sprite); + // only apply the transform if it's not identity + boolean hasTransform = !transform.isIdentity(); + IVertexConsumer consumer = hasTransform ? new TRSRTransformer(builder, transform) : builder; + if (side == EnumFacing.SOUTH) { - putVertex(builder, format, transform, side, x1, y1, z, u1, v2, color); - putVertex(builder, format, transform, side, x2, y1, z, u2, v2, color); - putVertex(builder, format, transform, side, x2, y2, z, u2, v1, color); - putVertex(builder, format, transform, side, x1, y2, z, u1, v1, color); + putVertex(consumer, format, side, x1, y1, z, u1, v2, color); + putVertex(consumer, format, side, x2, y1, z, u2, v2, color); + putVertex(consumer, format, side, x2, y2, z, u2, v1, color); + putVertex(consumer, format, side, x1, y2, z, u1, v1, color); } else { - putVertex(builder, format, transform, side, x1, y1, z, u1, v2, color); - putVertex(builder, format, transform, side, x1, y2, z, u1, v1, color); - putVertex(builder, format, transform, side, x2, y2, z, u2, v1, color); - putVertex(builder, format, transform, side, x2, y1, z, u2, v2, color); + putVertex(consumer, format, side, x1, y1, z, u1, v2, color); + putVertex(consumer, format, side, x1, y2, z, u1, v1, color); + putVertex(consumer, format, side, x2, y2, z, u2, v1, color); + putVertex(consumer, format, side, x2, y1, z, u2, v2, color); } return builder.build(); } - private static void putVertex(UnpackedBakedQuad.Builder builder, VertexFormat format, TRSRTransformation transform, EnumFacing side, + private static void putVertex(IVertexConsumer consumer, VertexFormat format, EnumFacing side, float x, float y, float z, float u, float v, int color) { - Vector4f vec = new Vector4f(); for (int e = 0; e < format.getElementCount(); e++) { switch (format.getElement(e).getUsage()) { case POSITION: - if (transform.isIdentity()) - { - builder.put(e, x, y, z, 1); - } - // only apply the transform if it's not identity - else - { - vec.x = x; - vec.y = y; - vec.z = z; - vec.w = 1; - transform.getMatrixVec().transform(vec); - builder.put(e, vec.x, vec.y, vec.z, vec.w); - } + consumer.put(e, x, y, z, 1f); break; case COLOR: float r = ((color >> 16) & 0xFF) / 255f; // red - float g = ((color >> 8) & 0xFF) / 255f; // green - float b = ((color >> 0) & 0xFF) / 255f; // blue + float g = ((color >> 8) & 0xFF) / 255f; // green + float b = ((color >> 0) & 0xFF) / 255f; // blue float a = ((color >> 24) & 0xFF) / 255f; // alpha - builder.put(e, r, g, b, a); + consumer.put(e, r, g, b, a); + break; + case NORMAL: + float offX = (float) side.getXOffset(); + float offY = (float) side.getYOffset(); + float offZ = (float) side.getZOffset(); + consumer.put(e, offX, offY, offZ, 0f); break; case UV: if (format.getElement(e).getIndex() == 0) { - builder.put(e, u, v, 0f, 1f); + consumer.put(e, u, v, 0f, 1f); break; } - case NORMAL: - builder.put(e, (float) side.getXOffset(), (float) side.getYOffset(), (float) side.getZOffset(), 0f); - break; + // else fallthrough to default default: - builder.put(e); + consumer.put(e); break; } } diff --git a/src/main/java/net/minecraftforge/client/model/ModelFluid.java b/src/main/java/net/minecraftforge/client/model/ModelFluid.java index 73c461fee..11310e5a3 100644 --- a/src/main/java/net/minecraftforge/client/model/ModelFluid.java +++ b/src/main/java/net/minecraftforge/client/model/ModelFluid.java @@ -30,7 +30,6 @@ import java.util.Set; import javax.annotation.Nullable; import javax.vecmath.Matrix4f; -import javax.vecmath.Vector4f; import net.minecraft.block.state.IBlockState; import net.minecraft.client.renderer.model.BakedQuad; @@ -44,6 +43,8 @@ import net.minecraft.resources.IResourceManager; import net.minecraft.util.EnumFacing; import net.minecraft.util.ResourceLocation; import net.minecraft.util.math.MathHelper; +import net.minecraftforge.client.model.pipeline.IVertexConsumer; +import net.minecraftforge.client.model.pipeline.TRSRTransformer; import net.minecraftforge.client.model.pipeline.UnpackedBakedQuad; import net.minecraftforge.versions.forge.ForgeVersion; import net.minecraftforge.common.model.IModelState; @@ -399,15 +400,19 @@ public final class ModelFluid implements IUnbakedModel private BakedQuad buildQuad(EnumFacing side, TextureAtlasSprite texture, boolean flip, boolean offset, VertexParameter x, VertexParameter y, VertexParameter z, VertexParameter u, VertexParameter v) { UnpackedBakedQuad.Builder builder = new UnpackedBakedQuad.Builder(format); + builder.setQuadOrientation(side); builder.setTexture(texture); builder.setQuadTint(0); + boolean hasTransform = transformation.isPresent() && !transformation.get().isIdentity(); + IVertexConsumer consumer = hasTransform ? new TRSRTransformer(builder, transformation.get()) : builder; + for (int i = 0; i < 4; i++) { int vertex = flip ? 3 - i : i; putVertex( - builder, side, offset, + consumer, side, offset, x.get(vertex), y.get(vertex), z.get(vertex), texture.getInterpolatedU(u.get(vertex)), texture.getInterpolatedV(v.get(vertex)) @@ -417,7 +422,7 @@ public final class ModelFluid implements IUnbakedModel return builder.build(); } - private void putVertex(UnpackedBakedQuad.Builder builder, EnumFacing side, boolean offset, float x, float y, float z, float u, float v) + private void putVertex(IVertexConsumer consumer, EnumFacing side, boolean offset, float x, float y, float z, float u, float v) { for(int e = 0; e < format.getElementCount(); e++) { @@ -427,32 +432,30 @@ public final class ModelFluid implements IUnbakedModel float dx = offset ? side.getDirectionVec().getX() * eps : 0f; float dy = offset ? side.getDirectionVec().getY() * eps : 0f; float dz = offset ? side.getDirectionVec().getZ() * eps : 0f; - float[] data = { x - dx, y - dy, z - dz, 1f }; - if(transformation.isPresent() && !transformation.get().isIdentity()) - { - Vector4f vec = new Vector4f(data); - transformation.get().getMatrixVec().transform(vec); - vec.get(data); - } - builder.put(e, data); + consumer.put(e, x - dx, y - dy, z - dz, 1f); break; case COLOR: - builder.put(e, - ((color >> 16) & 0xFF) / 255f, - ((color >> 8) & 0xFF) / 255f, - (color & 0xFF) / 255f, - ((color >> 24) & 0xFF) / 255f); + float r = ((color >> 16) & 0xFF) / 255f; + float g = ((color >> 8) & 0xFF) / 255f; + float b = ( color & 0xFF) / 255f; + float a = ((color >> 24) & 0xFF) / 255f; + consumer.put(e, r, g, b, a); break; - case UV: if(format.getElement(e).getIndex() == 0) - { - builder.put(e, u, v, 0f, 1f); - break; - } case NORMAL: - builder.put(e, (float)side.getXOffset(), (float)side.getYOffset(), (float)side.getZOffset(), 0f); + float offX = (float) side.getXOffset(); + float offY = (float) side.getYOffset(); + float offZ = (float) side.getZOffset(); + consumer.put(e, offX, offY, offZ, 0f); break; + case UV: + if(format.getElement(e).getIndex() == 0) + { + consumer.put(e, u, v, 0f, 1f); + break; + } + // else fallthrough to default default: - builder.put(e); + consumer.put(e); break; } } diff --git a/src/main/java/net/minecraftforge/client/model/SimpleModelFontRenderer.java b/src/main/java/net/minecraftforge/client/model/SimpleModelFontRenderer.java index 7852f5c5d..62f7b4c45 100644 --- a/src/main/java/net/minecraftforge/client/model/SimpleModelFontRenderer.java +++ b/src/main/java/net/minecraftforge/client/model/SimpleModelFontRenderer.java @@ -19,12 +19,12 @@ package net.minecraftforge.client.model; -import javax.vecmath.Matrix3f; import javax.vecmath.Matrix4f; import javax.vecmath.Vector3f; import javax.vecmath.Vector4f; import net.minecraftforge.client.model.pipeline.UnpackedBakedQuad; +import net.minecraftforge.common.model.TRSRTransformation; import com.google.common.collect.ImmutableList; @@ -41,7 +41,7 @@ import net.minecraft.util.ResourceLocation; public abstract class SimpleModelFontRenderer extends FontRenderer { private float r, g, b, a; - private final Matrix4f matrix; + private final TRSRTransformation transform; private ImmutableList.Builder builder = ImmutableList.builder(); private final VertexFormat format; private final Vector3f normal = new Vector3f(0, 0, 1); @@ -52,16 +52,10 @@ public abstract class SimpleModelFontRenderer extends FontRenderer { public SimpleModelFontRenderer(GameSettings settings, ResourceLocation font, TextureManager manager, boolean isUnicode, Matrix4f matrix, VertexFormat format) { - super(manager, null); -// super(settings, font, manager, isUnicode); - this.matrix = new Matrix4f(matrix); - Matrix3f nm = new Matrix3f(); - this.matrix.getRotationScale(nm); - nm.invert(); - nm.transpose(); + super(manager, null); + this.transform = new TRSRTransformation(matrix); this.format = format; - nm.transform(normal); - normal.normalize(); + transform.transformNormal(normal); orientation = EnumFacing.getFacingFromVector(normal.x, normal.y, normal.z); } @@ -79,21 +73,15 @@ public abstract class SimpleModelFontRenderer extends FontRenderer { private void addVertex(UnpackedBakedQuad.Builder quadBuilder, float x, float y, float u, float v) { - vec.x = x; - vec.y = y; - vec.z = 0; - vec.w = 1; - matrix.transform(vec); for(int e = 0; e < format.getElementCount(); e++) { switch(format.getElement(e).getUsage()) { case POSITION: + vec.set(x, y, 0f, 1f); + transform.transformPosition(vec); quadBuilder.put(e, vec.x, vec.y, vec.z, vec.w); break; - case UV: - quadBuilder.put(e, sprite.getInterpolatedU(u * 16), sprite.getInterpolatedV(v * 16), 0, 1); - break; case COLOR: quadBuilder.put(e, r, g, b, a); break; @@ -101,6 +89,13 @@ public abstract class SimpleModelFontRenderer extends FontRenderer { //quadBuilder.put(e, normal.x, normal.y, normal.z, 1); quadBuilder.put(e, 0, 0, 1, 1); break; + case UV: + if(format.getElement(e).getIndex() == 0) + { + quadBuilder.put(e, sprite.getInterpolatedU(u * 16), sprite.getInterpolatedV(v * 16), 0, 1); + break; + } + // else fallthrough to default default: quadBuilder.put(e); break; diff --git a/src/main/java/net/minecraftforge/client/model/b3d/B3DModel.java b/src/main/java/net/minecraftforge/client/model/b3d/B3DModel.java index 0f536a237..371a67713 100644 --- a/src/main/java/net/minecraftforge/client/model/b3d/B3DModel.java +++ b/src/main/java/net/minecraftforge/client/model/b3d/B3DModel.java @@ -37,7 +37,6 @@ import java.util.Map; import java.util.Set; import javax.annotation.Nullable; -import javax.vecmath.Matrix3f; import javax.vecmath.Matrix4f; import javax.vecmath.Quat4f; import javax.vecmath.Vector2f; @@ -45,6 +44,7 @@ import javax.vecmath.Vector3f; import javax.vecmath.Vector4f; import net.minecraftforge.versions.forge.ForgeVersion; +import net.minecraftforge.common.model.TRSRTransformation; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Triple; @@ -681,25 +681,21 @@ public class B3DModel else t.setIdentity(); } + TRSRTransformation trsr = new TRSRTransformation(t); + // pos - Vector4f pos = new Vector4f(this.pos), newPos = new Vector4f(); + Vector4f pos = new Vector4f(this.pos); pos.w = 1; - t.transform(pos, newPos); - Vector3f rPos = new Vector3f(newPos.x / newPos.w, newPos.y / newPos.w, newPos.z / newPos.w); + trsr.transformPosition(pos); + Vector3f rPos = new Vector3f(pos.x / pos.w, pos.y / pos.w, pos.z / pos.w); // normal Vector3f rNormal = null; if(this.normal != null) { - Matrix3f tm = new Matrix3f(); - t.getRotationScale(tm); - tm.invert(); - tm.transpose(); - Vector3f normal = new Vector3f(this.normal); - rNormal = new Vector3f(); - tm.transform(normal, rNormal); - rNormal.normalize(); + rNormal = new Vector3f(this.normal); + trsr.transformNormal(rNormal); } // texCoords TODO diff --git a/src/main/java/net/minecraftforge/client/model/obj/OBJModel.java b/src/main/java/net/minecraftforge/client/model/obj/OBJModel.java index 5016a745c..5fced635d 100644 --- a/src/main/java/net/minecraftforge/client/model/obj/OBJModel.java +++ b/src/main/java/net/minecraftforge/client/model/obj/OBJModel.java @@ -857,8 +857,6 @@ public class OBJModel implements IUnbakedModel public Face bake(TRSRTransformation transform) { - Matrix4f m = transform.getMatrixVec(); - Matrix3f mn = null; Vertex[] vertices = new Vertex[verts.length]; // Normal[] normals = norms != null ? new Normal[norms.length] : null; // TextureCoordinate[] textureCoords = texCoords != null ? new TextureCoordinate[texCoords.length] : null; @@ -869,24 +867,16 @@ public class OBJModel implements IUnbakedModel // Normal n = norms != null ? norms[i] : null; // TextureCoordinate t = texCoords != null ? texCoords[i] : null; - Vector4f pos = new Vector4f(v.getPos()), newPos = new Vector4f(); + Vector4f pos = new Vector4f(v.getPos()); pos.w = 1; - m.transform(pos, newPos); - vertices[i] = new Vertex(newPos, v.getMaterial()); + transform.transformPosition(pos); + vertices[i] = new Vertex(pos, v.getMaterial()); if (v.hasNormal()) { - if(mn == null) - { - mn = new Matrix3f(); - m.getRotationScale(mn); - mn.invert(); - mn.transpose(); - } - Vector3f normal = new Vector3f(v.getNormal().getData()), newNormal = new Vector3f(); - mn.transform(normal, newNormal); - newNormal.normalize(); - vertices[i].setNormal(new Normal(newNormal)); + Vector3f normal = new Vector3f(v.getNormal().getData()); + transform.transformNormal(normal); + vertices[i].setNormal(new Normal(normal)); } if (v.hasTextureCoordinate()) vertices[i].setTextureCoordinate(v.getTextureCoordinate()); diff --git a/src/main/java/net/minecraftforge/client/model/pipeline/TRSRTransformer.java b/src/main/java/net/minecraftforge/client/model/pipeline/TRSRTransformer.java new file mode 100644 index 000000000..4fae14e55 --- /dev/null +++ b/src/main/java/net/minecraftforge/client/model/pipeline/TRSRTransformer.java @@ -0,0 +1,55 @@ +/* + * Minecraft Forge + * Copyright (c) 2016-2018. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package net.minecraftforge.client.model.pipeline; + +import net.minecraftforge.common.model.TRSRTransformation; + +import javax.vecmath.Vector3f; +import javax.vecmath.Vector4f; + +public class TRSRTransformer extends VertexTransformer +{ + private final TRSRTransformation transform; + + public TRSRTransformer(IVertexConsumer parent, TRSRTransformation transform) + { + super(parent); + this.transform = transform; + } + + @Override + public void put(int element, float... data) + { + switch (getVertexFormat().getElement(element).getUsage()) + { + case POSITION: + Vector4f pos = new Vector4f(data); + transform.transformPosition(pos); + pos.get(data); + break; + case NORMAL: + Vector3f normal = new Vector3f(data); + transform.transformNormal(normal); + normal.get(data); + break; + } + super.put(element, data); + } +} diff --git a/src/main/java/net/minecraftforge/client/model/pipeline/VertexLighterFlat.java b/src/main/java/net/minecraftforge/client/model/pipeline/VertexLighterFlat.java index 138655a59..4150e3205 100644 --- a/src/main/java/net/minecraftforge/client/model/pipeline/VertexLighterFlat.java +++ b/src/main/java/net/minecraftforge/client/model/pipeline/VertexLighterFlat.java @@ -216,19 +216,19 @@ public class VertexLighterFlat extends QuadGatheringTransformer pos[2] += blockInfo.getBlockPos().getZ();*/ parent.put(e, position[v]); break; - case NORMAL: if(normalIndex != -1) - { + case NORMAL: parent.put(e, normal[v]); break; - } case COLOR: parent.put(e, color[v]); break; - case UV: if(element.getIndex() == 1) - { - parent.put(e, lightmap[v]); - break; - } + case UV: + if(element.getIndex() == 1) + { + parent.put(e, lightmap[v]); + break; + } + // else fallthrough to default default: parent.put(e, quadData[e][v]); } diff --git a/src/main/java/net/minecraftforge/common/model/TRSRTransformation.java b/src/main/java/net/minecraftforge/common/model/TRSRTransformation.java index 25769e6c6..ed312af09 100644 --- a/src/main/java/net/minecraftforge/common/model/TRSRTransformation.java +++ b/src/main/java/net/minecraftforge/common/model/TRSRTransformation.java @@ -72,6 +72,8 @@ public final class TRSRTransformation implements IModelState, ITransformation private Vector3f scale; private Quat4f rightRot; + private Matrix3f normalTransform; + public TRSRTransformation(@Nullable Matrix4f matrix) { if(matrix == null) @@ -625,6 +627,29 @@ public final class TRSRTransformation implements IModelState, ITransformation return vertexIndex; } + public void transformPosition(Vector4f position) + { + matrix.transform(position); + } + + public void transformNormal(Vector3f normal) + { + checkNormalTransform(); + normalTransform.transform(normal); + normal.normalize(); + } + + private void checkNormalTransform() + { + if (normalTransform == null) + { + normalTransform = new Matrix3f(); + matrix.getRotationScale(normalTransform); + normalTransform.invert(); + normalTransform.transpose(); + } + } + @Override public String toString() {