/* * This file is part of Cubic Chunks Mod, licensed under the MIT License (MIT). * * Copyright (c) 2015 contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package cubicchunks.world; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.util.Arrays; import cubicchunks.util.Bits; import cubicchunks.util.Coords; import cubicchunks.world.cube.Cube; public class ServerHeightMap implements IHeightMap { /** * Special value to indicate the absence of a segment in the segments arrays. * Since all integer values might represent a valid segment, an unlikely value to occur has been chosen. It * logically represents a 1-block segment at the very top of the world with opacity zero. */ private static final int NONE_SEGMENT = packSegment(0x7fffff, 0); /** * Array containing the y-coordinates of the lowest segment in each block column. The value {@link #NONE} is used if * a given block column does not contain any segments. */ private final int[] ymin; /** * Array containing the y-coordinate of the highest segment in each block column. The value {@link #NONE} is used if * a given block column does not contain any segments. */ private final int[] ymax; /** * Array containing an array of segments for each x/z position in a column. */ private final int[][] segments; private int heightMapLowest; private int hash; private boolean needsHash; public ServerHeightMap() { this.ymin = new int[Cube.SIZE*Cube.SIZE]; this.ymax = new int[Cube.SIZE*Cube.SIZE]; this.segments = new int[Cube.SIZE*Cube.SIZE][]; // init to empty for (int i = 0; i < Cube.SIZE*Cube.SIZE; i++) { this.ymin[i] = Coords.NO_HEIGHT; this.ymax[i] = Coords.NO_HEIGHT; } this.heightMapLowest = Coords.NO_HEIGHT; this.hash = 0; this.needsHash = true; } private static int packSegment(int pos, int opacity) { return Bits.packUnsignedToInt(opacity, 8, 24) | Bits.packSignedToInt(pos, 24, 0); } private static int unpackOpacity(int packed) { return Bits.unpackUnsigned(packed, 8, 24); } private static int unpackPosition(int packed) { return Bits.unpackSigned(packed, 24, 0); } private static int getLastSegmentIndex(int[] segments) { for (int i = segments.length - 1; i >= 0; i--) { if (segments[i] != NONE_SEGMENT) { return i; } } throw new Error("Invalid segments state"); } // Interface: IHeightMap ---------------------------------------------------------------------------------------- @Override public void onOpacityChange(int localX, int blockY, int localZ, int opacity) { int xzIndex = getIndex(localX, localZ); // try to stay in no-segments mode as long as we can, this is the simple case boolean isOpaque = opacity != 0; if (this.segments[xzIndex] == null) { this.setNoSegments(xzIndex, blockY, isOpaque); } else { this.setOpacityWithSegments(xzIndex, blockY, isOpaque); } this.heightMapLowest = Coords.NO_HEIGHT; this.needsHash = true; } @Override public boolean isOccluded(int localX, int blockY, int localZ) { return blockY <= this.getTopBlockY(localX, localZ); } @Override public int getTopBlockY(int localX, int localZ) { return this.ymax[getIndex(localX, localZ)]; } @Override public int getTopBlockYBelow(int localX, int localZ, int blockY) { // within the highest segment or there exists no segment for this block column int i = getIndex(localX, localZ); if (blockY > this.ymax[i]) { return this.getTopBlockY(localX, localZ); } // below or at the minimum height, thus there are no blocks below if (blockY <= this.ymin[i]) { return Coords.NO_HEIGHT; } // There are no opacity changes, everything is opaque from ymin to ymax. blockY is between ymin and ymax, thus // the next opaque block below blockY is blockY - 1. int[] segments = this.segments[i]; if (segments == null) { return blockY - 1; } // binary search for the segment containing blockY int mini = 0; int maxi = getLastSegmentIndex(segments); while (mini <= maxi) { int midi = (mini + maxi) >>> 1; int midPos = unpackPosition(segments[midi]); if (midPos < blockY) { mini = midi + 1; } else if (midPos > blockY) { maxi = midi - 1; } else { // hit a segment start exactly mini = midi + 1; break; } } assert (mini > 0) : String.format("can't find %d in %s", blockY, dump(localX, localZ)); // The binary search ends on answer + 1, so subtract 1. The result is the index of the segment containing // blockY. int segmentIndex = mini - 1; int blockYSegment = segments[segmentIndex]; int blockYSegmentOpacity = unpackOpacity(blockYSegment); // The lowest segment is always opaque. Thus, if blockY is in the lowest segment, the next opaque block is // at blockY - 1. if (segmentIndex == 0) { assert blockYSegmentOpacity != 0 : "The bottom opacity segment is transparent!"; return blockY - 1; } // Otherwise, there exists a segment underneath the segment of blockY. int blockYSegmentHeight = unpackPosition(blockYSegment); // If the segment of blockY is transparent, the next opaque block is at the top of the segment underneath. if (blockYSegmentOpacity == 0) { return blockYSegmentHeight - 1; } // The segment of blockY is opaque, thus, if blockY is not at the bottom of its segment, the next opaque block // is at blockY - 1. if (blockY != blockYSegmentHeight) { return blockY - 1; } // If blockY is the lowest block in its segment, the next opaque block is the highest block in the next opaque // segment. int belowYSegment = segments[segmentIndex - 1]; int belowYSegmentHeight = unpackPosition(belowYSegment); return belowYSegmentHeight - 1; } @Override public int getBottomBlockY(int localX, int localZ) { return this.ymin[getIndex(localX, localZ)]; } @Override public int[] getHeightmap() { return Arrays.copyOf(this.ymax, this.ymax.length); } @Override public int getLowestTopBlockY() { if (this.heightMapLowest == Coords.NO_HEIGHT) { this.heightMapLowest = Integer.MAX_VALUE; for (int i = 0; i < this.ymax.length; i++) { if (this.ymax[i] < this.heightMapLowest) { this.heightMapLowest = this.ymax[i]; } } if (this.heightMapLowest == Coords.NO_HEIGHT) { this.heightMapLowest--; // don't recalculate this on every call } } return this.heightMapLowest; } // Helper ---------------------------------------------------------------------------------------------------------- private void setNoSegments(int xzIndex, int blockY, boolean isOpaque) { if (isOpaque) { this.setNoSegmentsOpaque(xzIndex, blockY); } else { this.setNoSegmentsTransparent(xzIndex, blockY); } } private void setNoSegmentsOpaque(int xzIndex, int blockY) { // something from nothing? if (this.ymin[xzIndex] == Coords.NO_HEIGHT && this.ymax[xzIndex] == Coords.NO_HEIGHT) { this.ymin[xzIndex] = blockY; this.ymax[xzIndex] = blockY; return; } // extending the range? if (blockY == this.ymin[xzIndex] - 1) { this.ymin[xzIndex]--; return; } else if (blockY == this.ymax[xzIndex] + 1) { this.ymax[xzIndex]++; return; } // making a new section //more than one block above ymax? if (blockY > this.ymax[xzIndex] + 1) { /* A visualization of what happens: X - already opaque, within min-max # - newly set opaque [ ] --- --> no segment here, new_ymax=blockY [#] (block at blockY) --- --> segment=2, height=blockY, isOpaque=1 [ ] --- [ ] --- --> segment=1, height=old_ymax + 1, isOpaque=0 [X] (block at old_ymax) --- [X] --- [X] (block at ymin) --- --> segment=0, height=ymin, isOpaque=1 [ ] ^ going up from there */ this.segments[xzIndex] = new int[]{ packSegment(this.ymin[xzIndex], 1), packSegment(this.ymax[xzIndex] + 1, 0), packSegment(blockY, 1) }; this.ymax[xzIndex] = blockY; return; //more than one block below ymin? } else if (blockY < this.ymin[xzIndex] - 1) { /* X - already opaque, within min-max # - newly set opaque --- [ ] --- --> no segment, limited by ymax [X] (block at ymax) --- [X] --- [X] (block at old_ymin) --- --> segment=2, height=old_ymin, isOpaque=1 [ ] --- [ ] --- --> segment=1, height=blockY + 1, isOpaque=0 [#] (block at blockY) --- --> segment=0, height=blockY, isOpaque=1 [ ] ^ going up from there */ this.segments[xzIndex] = new int[]{ packSegment(blockY, 1), packSegment(blockY + 1, 0), packSegment(this.ymin[xzIndex], 1) }; this.ymin[xzIndex] = blockY; return; } // must already be in range assert (blockY >= this.ymin[xzIndex] && blockY <= this.ymax[xzIndex]); } private void setNoSegmentsTransparent(int xzIndex, int blockY) { // nothing into nothing? if (this.ymin[xzIndex] == Coords.NO_HEIGHT && this.ymax[xzIndex] == Coords.NO_HEIGHT) { return; } assert !(this.ymin[xzIndex] == Coords.NO_HEIGHT || this.ymax[xzIndex] == Coords.NO_HEIGHT) : "Only one of ymin and ymax is NONE! This is not possible"; // only one block left? if (this.ymax[xzIndex] == this.ymin[xzIndex]) { // something into nothing? if (blockY == this.ymin[xzIndex]) { this.ymin[xzIndex] = Coords.NO_HEIGHT; this.ymax[xzIndex] = Coords.NO_HEIGHT; } // if setting to transparent somewhere else - nothing changes return; } // out of range? if (blockY < this.ymin[xzIndex] || blockY > this.ymax[xzIndex]) { return; } // shrinking the range? if (blockY == this.ymin[xzIndex]) { this.ymin[xzIndex]++; return; } else if (blockY == this.ymax[xzIndex]) { this.ymax[xzIndex]--; return; } // we must be bisecting the range, need to make segments assert (blockY > this.ymin[xzIndex] && blockY < this.ymax[xzIndex]) : String.format("blockY outside of ymin/ymax range: %d -> [%d,%d]", blockY, this.ymin[xzIndex], this.ymax[xzIndex]); /* Example: --- [ ] --- --> no segment, limited by ymax [X] (block at ymax) --- [X] --- --> segment=2, height=blockY + 1, isOpaque=1 [-] <--removing this, at y=blockY --- --> segment=1, height=blockY, isOpaque=0 [X] --- [X] (block ay ymin) --- --> segment=0, height=ymin, isOpaque=1 [ ] ^ going up */ this.segments[xzIndex] = new int[]{ packSegment(this.ymin[xzIndex], 1), packSegment(blockY, 0), packSegment(blockY + 1, 1) }; } private void setOpacityWithSegments(int xzIndex, int blockY, boolean isOpaque) { // binary search to find the insertion point int[] segments = this.segments[xzIndex]; int minj = 0; int maxj = getLastSegmentIndex(segments); while (minj <= maxj) { int midj = (minj + maxj) >>> 1; int midPos = unpackPosition(segments[midj]); if (midPos < blockY) { minj = midj + 1; } else if (midPos > blockY) { maxj = midj - 1; } else { minj = midj + 1; break; } } // minj-1 is the containing segment, or -1 if we're off the bottom int j = minj - 1; if (j < 0) { setOpacityWithSegmentsBelowBottom(xzIndex, blockY, isOpaque); } else if (blockY > this.ymax[xzIndex]) { setOpacityWithSegmentsAboveTop(xzIndex, blockY, isOpaque); } else { // j is the containing segment, blockY may be at the start setOpacityWithSegmentsFor(xzIndex, blockY, j, isOpaque); } } private void setOpacityWithSegmentsBelowBottom(int xzIndex, int blockY, boolean isOpaque) { // will the opacity even change? if (!isOpaque) { return; } int[] segments = this.segments[xzIndex]; boolean extendsBottomSegmentByOne = blockY == this.ymin[xzIndex] - 1; if (extendsBottomSegmentByOne) { /* --- [X] --- [X] --- <-- the current bottom segment starts here [#] <-- inserting here --- <-- new bottom segment start [ ] ^ going up */ assert unpackOpacity(segments[0]) == 1 : "The bottom segment is transparent!"; moveSegmentStartDownAndUpdateMinY(xzIndex, 0); } else { /* --- [X] --- [X] --- <-- the current bottom segment starts here, now segment 2 [ ] --- <-- new segment 1, height=blockY + 1, isOpaque=0 [#] <-- inserting here --- <-- new segment 0, height=blockY, isOpaque=1 [ ] ^ going up */ int segment0 = packSegment(blockY, 1); int segment1 = packSegment(blockY + 1, 0); insertSegmentsBelow(xzIndex, 0, segment0, segment1); this.ymin[xzIndex] = blockY; } } private void setOpacityWithSegmentsAboveTop(int xzIndex, int blockY, boolean isOpaque) { // will the opacity even change? if (!isOpaque) { return; } int[] segments = this.segments[xzIndex]; int lastIndex = getLastSegmentIndex(segments); boolean extendsTopSegmentByOne = blockY == this.ymax[xzIndex] + 1; if (extendsTopSegmentByOne) { /* [ ] --- <-- new ymax [#] <-- inserting here --- <-- current top segment ends here, limited by ymax [X] --- [X] ^ going up */ assert unpackOpacity(segments[lastIndex]) == 1 : "The top segment is transparent!"; this.ymax[xzIndex] = blockY; } else { /* [ ] --- <-- limited by newMaxY [#] <-- inserting here --- <-- new segment [previousLastSegment+2], height=blockY, isOpaque=1 [ ] <-- possibly many blocks here --- [ ] <-- block at prevMaxY+1 --- <-- previously limited by ymax, add segment=[previousLastSegment+1], height=prevMaxY+1, isOpaque=0 [X] <-- block at prevMaxY --- [X] ^ going up */ int segmentPrevLastPlus1 = packSegment(this.ymax[xzIndex] + 1, 0); int segmentPrevLastPlus2 = packSegment(blockY, 1); //insert below the segment above the last segment, so above the last segment insertSegmentsBelow(xzIndex, lastIndex + 1, segmentPrevLastPlus1, segmentPrevLastPlus2); this.ymax[xzIndex] = blockY; } } private void setOpacityWithSegmentsFor(int xzIndex, int blockY, int segmentIndexWithBlockY, boolean isOpaque) { int[] segments = this.segments[xzIndex]; int isOpaqueInt = isOpaque ? 1 : 0; int segmentWithBlockY = segments[segmentIndexWithBlockY]; //does it even change anything? if (unpackOpacity(segmentWithBlockY) == isOpaqueInt) { return; } int segmentBottom = unpackPosition(segmentWithBlockY); int segmentTop = getSegmentTopBlockY(xzIndex, segmentIndexWithBlockY); if (segmentTop == segmentBottom) { assert segmentBottom == blockY; negateOneBlockSegment(xzIndex, segmentIndexWithBlockY); return; } /* 3 possible cases: * change at the top of segment * change at the bottom of segment * change in the middle of segment */ int lastSegment = getLastSegmentIndex(segments); if (blockY == segmentTop) { //if it's the top of the top segment - just change ymax if (segmentIndexWithBlockY == lastSegment) { assert unpackOpacity(segments[lastSegment]) == 1 : "The top segment is transparent!"; this.ymax[xzIndex]--; return; } /* [-] --- [#] <-- changing this from [X] to [-] --- [X] <-- segmentWithBlockY ^ going up */ moveSegmentStartDownAndUpdateMinY(xzIndex, segmentIndexWithBlockY + 1); return; } if (blockY == segmentBottom) { moveSegmentStartUpAndUpdateMinY(xzIndex, segmentIndexWithBlockY); return; } /* --- [X] --- [X] --- <-- insert this (newSegment2), height=blockY + 1, opacity=!isOpaque [#] <-- changing this --- <-- insert this (newSegment1), height=blockY, opacity=isOpaque [X] --- [X] --- <-- segmentWithBlockY [-] */ int newSegment1 = packSegment(blockY, isOpaqueInt); int newSegment2 = packSegment(blockY + 1, 1 - isOpaqueInt); insertSegmentsBelow(xzIndex, segmentIndexWithBlockY + 1, newSegment1, newSegment2); } private void negateOneBlockSegment(int xzIndex, int segmentIndexWithBlockY) { int[] segments = this.segments[xzIndex]; int lastSegmentIndex = getLastSegmentIndex(segments); assert lastSegmentIndex >= 2 : "Less than 3 segments in array!"; if (segmentIndexWithBlockY == lastSegmentIndex) { assert unpackOpacity(segments[segmentIndexWithBlockY]) == 1 : "The top segment is transparent!"; //the top segment must be opaque, so we set it to transparent //and the segment below it is also transparent. //set both of them to NONE and decrease maxY int segmentBelow = segments[segmentIndexWithBlockY - 1]; this.ymax[xzIndex] = unpackPosition(segmentBelow) - 1; if (segmentIndexWithBlockY == 2) { //after removing top 2 segments we will be left with 1 segment //remove them entirely to guarantee at least 3 segments and use min/maxY this.segments[xzIndex] = null; return; } segments[segmentIndexWithBlockY] = NONE_SEGMENT; segments[segmentIndexWithBlockY - 1] = NONE_SEGMENT; return; } if (segmentIndexWithBlockY == 0) { assert unpackOpacity(segments[segmentIndexWithBlockY]) == 1 : "The top segment is transparent!"; //same logic as for top segment applies int segmentAbove = segments[1]; this.ymin[xzIndex] = unpackPosition(segments[2]); if (lastSegmentIndex == 2) { this.segments[xzIndex] = null; return; } removeTwoSegments(xzIndex, 0); return; } /* The situation: # - opacity to set - - opposite opacity --- [#] --- [#] --- <-- old segment=segmentIndexWithBlockY+1, height=blockY+1 [-] --- <-- old segment=segmentIndexWithBlockY, height=blockY, opacity=(-) [#] --- [#] ^ going up Since this is not the top/bottom segment - we can remove it. And to avoid 2 identical segments in a row - remove the one above too */ removeTwoSegments(xzIndex, segmentIndexWithBlockY); //but in case after the removal there are less than 3 segments //remove them entirely and rely only on min/maxY if (lastSegmentIndex == 2) { this.segments[xzIndex] = null; return; } } private void moveSegmentStartUpAndUpdateMinY(int xzIndex, int segmentIndex) { int segment = this.segments[xzIndex][segmentIndex]; int pos = unpackPosition(segment); int opacity = unpackOpacity(segment); // move the segment this.segments[xzIndex][segmentIndex] = packSegment(pos + 1, opacity); // move the bottom if needed if (segmentIndex == 0) { this.ymin[xzIndex]++; } } private void moveSegmentStartDownAndUpdateMinY(int xzIndex, int segmentIndex) { int segment = this.segments[xzIndex][segmentIndex]; int pos = unpackPosition(segment); int opacity = unpackOpacity(segment); // move the segment this.segments[xzIndex][segmentIndex] = packSegment(pos - 1, opacity); // move the bottom if needed if (segmentIndex == 0) { this.ymin[xzIndex]--; } } private void removeTwoSegments(int xzIndex, int firstSegmentToRemove) { int[] segments = this.segments[xzIndex]; int jmax = getLastSegmentIndex(segments); // remove the segment System.arraycopy(segments, firstSegmentToRemove + 2, segments, firstSegmentToRemove, jmax - 1 - firstSegmentToRemove); segments[jmax] = NONE_SEGMENT; segments[jmax - 1] = NONE_SEGMENT; if (segments[0] == NONE_SEGMENT) { this.segments[xzIndex] = null; } } //is theIndex = lastSegmentIndex+1, it will be inserted after last segment private void insertSegmentsBelow(int xzIndex, int theIndex, int... newSegments) { int lastIndex = getLastSegmentIndex(this.segments[xzIndex]); int expandSize = newSegments.length; //will it fit in current array? if (this.segments[xzIndex].length >= lastIndex + expandSize) { //shift all segments up System.arraycopy(this.segments[xzIndex], theIndex, this.segments[xzIndex], theIndex + expandSize, lastIndex + 1 - theIndex); System.arraycopy(newSegments, 0, this.segments[xzIndex], theIndex + 0, expandSize); } else { //need to expand the array int[] newSegmentArr = new int[(lastIndex + 1) + expandSize]; int newArrIndex = 0; int oldArrIndex = 0; //copy all index up to before theIndex for (int i = 0; i < theIndex; i++) { newSegmentArr[newArrIndex] = this.segments[xzIndex][oldArrIndex]; newArrIndex++; oldArrIndex++; } //copy new elements for (int i = 0; i < expandSize; i++) { newSegmentArr[newArrIndex] = newSegments[i]; newArrIndex++; } //copy everything else while (newArrIndex < newSegmentArr.length) { newSegmentArr[newArrIndex] = this.segments[xzIndex][oldArrIndex]; newArrIndex++; oldArrIndex++; } this.segments[xzIndex] = newSegmentArr; } } private int getSegmentTopBlockY(int xzIndex, int segmentIndex) { int[] segments = this.segments[xzIndex]; //if it's the last segment in the array, or the one above is NoneSegment if (segments.length - 1 == segmentIndex || segments[segmentIndex + 1] == NONE_SEGMENT) { return this.ymax[xzIndex]; } return unpackPosition(segments[segmentIndex + 1]) - 1; } private static int getIndex(int localX, int localZ) { return (localZ << 4) | localX; } @Override public int hashCode() { if (this.needsHash) { this.hash = computeHash(); this.needsHash = false; } return hash; } private int computeHash() { final int MyFavoritePrime = 37; int hash = 1; for (int i = 0; i < this.segments.length; i++) { hash *= MyFavoritePrime; hash += this.ymin[i]; hash *= MyFavoritePrime; hash += this.ymax[i]; if (this.segments[i] == null) { hash *= MyFavoritePrime; } else { for (int n : this.segments[i]) { hash *= MyFavoritePrime; hash += n; } } } return hash; } // Serialization / NBT --------------------------------------------------------------------------------------------- public byte[] getData() { try { ByteArrayOutputStream buf = new ByteArrayOutputStream(); DataOutputStream out = new DataOutputStream(buf); writeData(out); out.close(); return buf.toByteArray(); } catch (IOException ex) { throw new Error(ex); } } public byte[] getDataForClient() { try { ByteArrayOutputStream buf = new ByteArrayOutputStream(); DataOutputStream out = new DataOutputStream(buf); for (int v : this.ymin) { out.writeInt(v); } for (int v : this.ymax) { out.writeInt(v); } out.close(); return buf.toByteArray(); } catch (IOException e) { throw new Error(e); } } public void readData(byte[] data) { try { ByteArrayInputStream buf = new ByteArrayInputStream(data); DataInputStream in = new DataInputStream(buf); readData(in); in.close(); } catch (IOException ex) { throw new Error(ex); } } public void readData(DataInputStream in) throws IOException { for (int i = 0; i < this.segments.length; i++) { this.ymin[i] = in.readInt(); this.ymax[i] = in.readInt(); int[] segments = new int[in.readUnsignedShort()]; if (segments.length == 0) { continue; } for (int j = 0; j < segments.length; j++) { segments[j] = in.readInt(); } this.segments[i] = segments; } } public void writeData(DataOutputStream out) throws IOException { for (int i = 0; i < this.segments.length; i++) { out.writeInt(this.ymin[i]); out.writeInt(ymax[i]); int[] segments = this.segments[i]; if (segments == null || segments.length == 0) { out.writeShort(0); } else { int lastSegmentIndex = getLastSegmentIndex(segments); out.writeShort(lastSegmentIndex + 1); for (int j = 0; j <= lastSegmentIndex; j++) { out.writeInt(segments[j]); } } } } // Debug ----------------------------------------------------------------------------------------------------------- public String dump(int localX, int localZ) { int i = getIndex(localX, localZ); StringBuilder buf = new StringBuilder(); buf.append("range=["); buf.append(this.ymin[i]); buf.append(","); buf.append(this.ymax[i]); buf.append("], segments(p,o)="); if (this.segments[i] != null) { for (int packed : this.segments[i]) { int pos = unpackPosition(packed); int opacity = unpackOpacity(packed); buf.append("("); buf.append(pos); buf.append(","); buf.append(opacity); buf.append(")"); } } return buf.toString(); } }