Improved riverbed generation

- Pond levels follow intersecting lower channels
- Pond sampling covers requested flood margins
- Water steps align with sea level
- River crests grade into surrounding beds
- Riverbeds include varied cross sections and finer noise
- Submerged soils use stable block types
- Hydrology probes inspect wet bed geometry
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-26 18:01:26 -04:00
1 parent 494cdb1a5c
commit 75837d757b
25 files changed
+1566 -144

No files matched your search

@@ -0,0 +1,92 @@
package art.arcane.iris.probe;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
final class HydrologyBlockMaps {
private HydrologyBlockMaps() {
}
static List<String> write(HydrologyBlockProbe.Configuration configuration,
Map<Long, HydrologyBlockProbe.PlannedColumn> columns, HydrologyVoxels.Volume volume,
int worldHeight) throws IOException {
int minimumX = configuration.minimumChunkX() << 4;
int minimumZ = configuration.minimumChunkZ() << 4;
int width = (configuration.maximumChunkX() - configuration.minimumChunkX() + 1) << 4;
int depth = (configuration.maximumChunkZ() - configuration.minimumChunkZ() + 1) << 4;
BufferedImage raw = new BufferedImage(width, depth, BufferedImage.TYPE_INT_RGB);
BufferedImage effective = new BufferedImage(width, depth, BufferedImage.TYPE_INT_RGB);
BufferedImage loss = new BufferedImage(width, depth, BufferedImage.TYPE_INT_RGB);
BufferedImage fill = new BufferedImage(width, depth, BufferedImage.TYPE_INT_RGB);
File csv = new File(configuration.output(), "columns.csv");
try (BufferedWriter writer = Files.newBufferedWriter(csv.toPath(), StandardCharsets.UTF_8)) {
writer.write("x,z,role,natural_height,raster_terrain,effective_terrain,raster_head,effective_head,water_lost,terrain_delta,wet_bed_raise,generated_fill_solids,lowest_neighbor_head,known_neighbors\n");
for (int z = 0; z < depth; z++) {
for (int x = 0; x < width; x++) {
int blockX = minimumX + x;
int blockZ = minimumZ + z;
HydrologyBlockProbe.PlannedColumn column = columns.get(HydrologyVoxels.columnKey(blockX, blockZ));
boolean lost = column.rawHead() != HydrologyVoxels.NO_WATER && column.head() == HydrologyVoxels.NO_WATER;
int raised = column.wetBedRaise();
HydrologyBlockProbe.BedNeighbours neighbours = HydrologyBlockProbe.bedNeighbours(columns, blockX, blockZ);
raw.setRGB(x, z, relief(columns, column, blockX, blockZ, false, worldHeight));
effective.setRGB(x, z, relief(columns, column, blockX, blockZ, true, worldHeight));
loss.setRGB(x, z, lost ? 0xD33B31 : column.head() != HydrologyVoxels.NO_WATER ? 0x247AC4 : 0xC4C4C4);
fill.setRGB(x, z, raised > 0 ? (0xFF0000 | (Math.max(0, 224 - 28 * raised) << 8) | 0x20)
: column.head() != HydrologyVoxels.NO_WATER ? 0x247AC4 : 0xC4C4C4);
writer.write(blockX + "," + blockZ + "," + column.role() + "," + column.natural() + "," + column.terrain() + ","
+ column.effectiveTerrain() + "," + waterHead(column.rawHead()) + "," + waterHead(column.head()) + "," + lost + ","
+ (column.effectiveTerrain() - column.terrain()) + "," + raised + ","
+ HydrologyBlockProbe.raisedBedSolids(column, volume, blockX, blockZ) + ","
+ waterHead(neighbours.lowestHead()) + "," + neighbours.known() + "\n");
}
}
}
List<String> artifacts = new ArrayList<>();
artifacts.add(csv.getAbsolutePath());
artifacts.add(writeImage(configuration.output(), "raster-bed.png", raw));
artifacts.add(writeImage(configuration.output(), "effective-bed.png", effective));
artifacts.add(writeImage(configuration.output(), "water-loss.png", loss));
artifacts.add(writeImage(configuration.output(), "bed-fill.png", fill));
return List.copyOf(artifacts);
}
private static String waterHead(int head) {
return head == HydrologyVoxels.NO_WATER ? "" : Integer.toString(head);
}
private static String writeImage(File output, String name, BufferedImage image) throws IOException {
File file = new File(output, name);
ImageIO.write(image, "png", file);
return file.getAbsolutePath();
}
private static int relief(Map<Long, HydrologyBlockProbe.PlannedColumn> columns, HydrologyBlockProbe.PlannedColumn column,
int x, int z, boolean effective, int worldHeight) {
int height = effective ? column.effectiveTerrain() : column.terrain();
int head = effective ? column.head() : column.rawHead();
double dx = height(columns, x + 1, z, effective, height) - height(columns, x - 1, z, effective, height);
double dz = height(columns, x, z + 1, effective, height) - height(columns, x, z - 1, effective, height);
double light = Math.clamp((dx + dz + 4D) / Math.sqrt(6D * (dx * dx + dz * dz + 4D)), 0D, 1D);
double shade = (0.5D + 0.5D * light) * (Math.floorMod(height, 4) == 0 ? 0.82D : 1D);
double altitude = Math.clamp(height / (double) Math.max(1, worldHeight - 1), 0D, 1D);
boolean wet = head != HydrologyVoxels.NO_WATER;
int red = (int) Math.round(shade * (wet ? 50D + 60D * altitude : 160D + 50D * altitude));
int green = (int) Math.round(shade * (wet ? 130D + 65D * altitude : 145D + 60D * altitude));
int blue = (int) Math.round(shade * (wet ? 190D + 50D * altitude : 110D + 80D * altitude));
return (red << 16) | (green << 8) | blue;
}
private static int height(Map<Long, HydrologyBlockProbe.PlannedColumn> columns, int x, int z, boolean effective, int fallback) {
HydrologyBlockProbe.PlannedColumn column = columns.get(HydrologyVoxels.columnKey(x, z));
return column == null ? fallback : effective ? column.effectiveTerrain() : column.terrain();
}
}
@@ -66,6 +66,14 @@ public final class HydrologyBlockProbe {
static final class Evidence {
long plannedWetColumns;
long effectiveWetColumns;
long lostWetColumns;
long raisedWetBedColumns;
long raisedWetBedBlocks;
int maximumWetBedRaise;
long solidRaisedBedBlocks;
long dropAdjacentRaisedWetBedColumns;
long censoredRaisedWetBedColumns;
long channelColumns;
long pondColumns;
long bankColumns;
@@ -88,7 +96,16 @@ public final class HydrologyBlockProbe {
List<String> artifacts) {
}
record PlannedColumn(WaterRole role, int terrain, int natural, int head) {
record PlannedColumn(WaterRole role, int terrain, int natural, int rawHead, int effectiveTerrain, int head) {
int wetBedRaise() {
return rawHead == HydrologyVoxels.NO_WATER || head == HydrologyVoxels.NO_WATER ? 0 : Math.max(0, effectiveTerrain - terrain);
}
}
record BedNeighbours(int lowestHead, int known) {
boolean dropBelow(int head) {
return lowestHead != HydrologyVoxels.NO_WATER && lowestHead <= head - 2;
}
}
public static void main(String[] args) {
@@ -96,8 +113,10 @@ public final class HydrologyBlockProbe {
try {
Result result = run(Configuration.parse(args));
Evidence evidence = result.evidence();
System.out.println("IRIS_HYDROLOGY_BLOCK_RESULT version=2 status=" + result.status() + " seed=" + result.configuration().seed()
System.out.println("IRIS_HYDROLOGY_BLOCK_RESULT version=4 status=" + result.status() + " seed=" + result.configuration().seed()
+ " plannedWetColumns=" + evidence.plannedWetColumns + " missingSurfaceColumns=" + evidence.scan.missingSurfaces
+ " effectiveWetColumns=" + evidence.effectiveWetColumns + " lostWetColumns=" + evidence.lostWetColumns
+ " raisedWetBedColumns=" + evidence.raisedWetBedColumns + " raisedWetBedBlocks=" + evidence.raisedWetBedBlocks
+ " containmentLeaks=" + evidence.scan.containmentLeaks + " tickConversionCells=" + evidence.scan.tickConversionCells
+ " cane=" + evidence.validCaneRoots + " failures=" + result.failures().size()
+ " output=" + result.configuration().output().getAbsolutePath());
@@ -135,7 +154,8 @@ public final class HydrologyBlockProbe {
failures.add("The engine reported " + reports.size() + " generation errors.");
}
Files.createDirectories(configuration.output().toPath());
List<String> artifacts = writeImages(configuration, volume, engine);
List<String> artifacts = new ArrayList<>(writeImages(configuration, volume, engine));
artifacts.addAll(HydrologyBlockMaps.write(configuration, planned, volume, engine.getHeight()));
Result result = new Result(failures.isEmpty() ? "PASS" : "FAIL", configuration, seaLevel, evidence, List.copyOf(failures), artifacts);
Files.writeString(new File(configuration.output(), "hydrology-blocks.json").toPath(), json(result), StandardCharsets.UTF_8);
return result;
@@ -155,7 +175,9 @@ public final class HydrologyBlockProbe {
int head = heads[(lz << 4) | lx];
int natural = complex.naturalTrueHeight(x, z);
int terrain = own.empty() ? natural : own.terrainHeight(lx, lz);
planned.put(HydrologyVoxels.columnKey(x, z), new PlannedColumn(own.role(lx, lz), terrain, natural, head));
int effectiveTerrain = (int) Math.round(complex.getRawHeightStream().getDouble(x, z));
planned.put(HydrologyVoxels.columnKey(x, z), new PlannedColumn(own.role(lx, lz), terrain, natural,
own.waterHead(lx, lz), effectiveTerrain, head));
}
}
}
@@ -203,8 +225,15 @@ public final class HydrologyBlockProbe {
default -> {
}
}
if (column.head() != HydrologyVoxels.NO_WATER) {
evidence.plannedWetColumns++;
inspectWater(column, x, z, evidence);
if (column.wetBedRaise() > 0) {
evidence.solidRaisedBedBlocks += raisedBedSolids(column, volume, x, z);
BedNeighbours neighbours = bedNeighbours(planned, x, z);
if (neighbours.dropBelow(column.head())) {
evidence.dropAdjacentRaisedWetBedColumns++;
} else if (neighbours.known() < CARDINALS.length) {
evidence.censoredRaisedWetBedColumns++;
}
}
if (HydrologyBlockMetrics.scanned(columns, x, z)) {
evidence.scan.column(volume, x, z, HydrologyBlockMetrics.scanFloor(columns, seaLevel, x, z), height - 1);
@@ -214,6 +243,58 @@ public final class HydrologyBlockProbe {
return evidence;
}
static void inspectWater(PlannedColumn column, int x, int z, Evidence evidence) {
if (column.head() != HydrologyVoxels.NO_WATER) {
evidence.effectiveWetColumns++;
}
if (column.rawHead() == HydrologyVoxels.NO_WATER) {
return;
}
evidence.plannedWetColumns++;
if (column.head() == HydrologyVoxels.NO_WATER) {
evidence.lostWetColumns++;
evidence.example("lost planned water at " + x + "," + column.rawHead() + "," + z + " role=" + column.role()
+ " bed=" + column.terrain() + " effectiveGround=" + column.effectiveTerrain());
} else if (column.wetBedRaise() > 0) {
int raised = column.wetBedRaise();
evidence.raisedWetBedColumns++;
evidence.raisedWetBedBlocks += raised;
evidence.maximumWetBedRaise = Math.max(evidence.maximumWetBedRaise, raised);
evidence.example("raised wet bed at " + x + "," + z + " role=" + column.role() + " rasterGround=" + column.terrain()
+ " effectiveGround=" + column.effectiveTerrain() + " fill=" + raised + " remainingDepth="
+ (column.head() - column.effectiveTerrain()));
}
}
static int raisedBedSolids(PlannedColumn column, HydrologyVoxels.Volume volume, int x, int z) {
if (column.wetBedRaise() == 0) {
return 0;
}
int solids = 0;
for (int y = column.terrain() + 1; y <= column.effectiveTerrain(); y++) {
if (volume.at(x, y, z) == HydrologyVoxels.SOLID) {
solids++;
}
}
return solids;
}
static BedNeighbours bedNeighbours(Map<Long, PlannedColumn> columns, int x, int z) {
int lowest = Integer.MAX_VALUE;
int known = 0;
for (int[] side : CARDINALS) {
PlannedColumn neighbour = columns.get(HydrologyVoxels.columnKey(x + side[0], z + side[1]));
if (neighbour == null) {
continue;
}
known++;
if (neighbour.head() != HydrologyVoxels.NO_WATER) {
lowest = Math.min(lowest, neighbour.head());
}
}
return new BedNeighbours(lowest == Integer.MAX_VALUE ? HydrologyVoxels.NO_WATER : lowest, known);
}
private static void inspectCane(HydrologyVoxels.Volume volume, int x, int z, int height, Evidence evidence) {
for (int y = 1; y < height; y++) {
if (volume.at(x, y, z) != HydrologyVoxels.CANE || volume.at(x, y - 1, z) == HydrologyVoxels.CANE) {
@@ -249,6 +330,13 @@ public final class HydrologyBlockProbe {
if (evidence.invalidCaneSubstrates > 0 || evidence.dryCaneRoots > 0) {
failures.add("Generated sugar cane has invalid substrate or no adjacent water.");
}
if (evidence.lostWetColumns > 0) {
failures.add("Water settling removes planned water from " + evidence.lostWetColumns + " columns in fresh generation.");
}
if (evidence.raisedWetBedColumns > 0) {
failures.add("Water settling fills " + evidence.raisedWetBedBlocks + " blocks above the raster bed in "
+ evidence.raisedWetBedColumns + " wet columns in fresh generation.");
}
if (evidence.scan.missingSurfaces > 0) {
failures.add("Generated blocks miss the planned water surface in " + evidence.scan.missingSurfaces + " columns.");
}
@@ -288,6 +376,14 @@ public final class HydrologyBlockProbe {
Evidence evidence = result.evidence();
Map<String, Object> counts = new LinkedHashMap<>();
counts.put("plannedWetColumns", evidence.plannedWetColumns);
counts.put("effectiveWetColumns", evidence.effectiveWetColumns);
counts.put("lostWetColumns", evidence.lostWetColumns);
counts.put("raisedWetBedColumns", evidence.raisedWetBedColumns);
counts.put("raisedWetBedBlocks", evidence.raisedWetBedBlocks);
counts.put("maximumWetBedRaise", evidence.maximumWetBedRaise);
counts.put("solidRaisedBedBlocks", evidence.solidRaisedBedBlocks);
counts.put("dropAdjacentRaisedWetBedColumns", evidence.dropAdjacentRaisedWetBedColumns);
counts.put("censoredRaisedWetBedColumns", evidence.censoredRaisedWetBedColumns);
counts.put("missingSurfaceColumns", evidence.scan.missingSurfaces);
counts.put("channelColumns", evidence.channelColumns);
counts.put("pondColumns", evidence.pondColumns);
@@ -313,6 +409,9 @@ public final class HydrologyBlockProbe {
report.put("artifacts", result.artifacts());
report.put("coordinateConvention", "Block X/Z are world coordinates; Y is relative to the world minimum height.");
report.put("imageLegend", "open=light blue,water=blue,lava=orange,cane=lime,solid=gray");
report.put("bedMapLegend", "raster-bed and effective-bed show elevation, slope shading and four-block contours on the same world-height scale; water-loss: red=planned wet made dry, blue=retained water, gray=dry");
report.put("bedFillLegend", "bed-fill: yellow to red=one to eight or more blocks of wet bed fill, blue=wet bed retained, gray=dry; dry bank and dam fill is excluded");
report.put("columnData", "columns.csv is ordered by Z then X; blank water heads mean dry; heights are relative to the world minimum height");
return new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n";
}
@@ -1,13 +1,28 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.hydrology.raster.WaterRole;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
public class HydrologyBlockProbeTest {
@Rule
public TemporaryFolder folder = new TemporaryFolder();
@Test
public void failuresNameMissingSurfaceLeaksTicksAndCoverage() {
HydrologyBlockProbe.Evidence evidence = new HydrologyBlockProbe.Evidence();
@@ -21,4 +36,110 @@ public class HydrologyBlockProbeTest {
"Fluid ticks would move generated water in 4 cells.",
"Required generated coverage is absent: pond"), HydrologyBlockProbe.failures(evidence, Set.of("channel", "pond")));
}
@Test
public void dryRidgesFailEvenWhenTheRemainingWaterIsSealed() {
HydrologyBlockProbe.Evidence evidence = new HydrologyBlockProbe.Evidence();
HydrologyBlockProbe.inspectWater(new HydrologyBlockProbe.PlannedColumn(WaterRole.CHANNEL, 77, 82, 79, 80,
HydrologyVoxels.NO_WATER), -103, -1391, evidence);
HydrologyBlockProbe.inspectWater(new HydrologyBlockProbe.PlannedColumn(WaterRole.WATERFALL, 70, 82, 79, 70, 79),
-102, -1391, evidence);
HydrologyBlockProbe.inspectWater(new HydrologyBlockProbe.PlannedColumn(WaterRole.BANK, 82, 82, HydrologyVoxels.NO_WATER,
82, HydrologyVoxels.NO_WATER), -104, -1391, evidence);
assertEquals(2L, evidence.plannedWetColumns);
assertEquals(1L, evidence.effectiveWetColumns);
assertEquals(1L, evidence.lostWetColumns);
assertEquals(List.of("Water settling removes planned water from 1 columns in fresh generation."),
HydrologyBlockProbe.failures(evidence, Set.of()));
assertEquals(List.of("lost planned water at -103,79,-1391 role=CHANNEL bed=77 effectiveGround=80"), evidence.examples);
}
@Test
public void submergedChannelAndPondFillFailsWithoutAnyLostSurfaceWater() {
HydrologyBlockProbe.PlannedColumn channel = new HydrologyBlockProbe.PlannedColumn(WaterRole.CHANNEL, 318, 330, 326, 324, 326);
HydrologyBlockProbe.PlannedColumn pond = new HydrologyBlockProbe.PlannedColumn(WaterRole.POND, 316, 330, 326, 324, 326);
HydrologyBlockProbe.Evidence evidence = new HydrologyBlockProbe.Evidence();
HydrologyBlockProbe.inspectWater(channel, -372, -441, evidence);
HydrologyBlockProbe.inspectWater(pond, -371, -441, evidence);
HydrologyBlockProbe.inspectWater(new HydrologyBlockProbe.PlannedColumn(WaterRole.DAM, 318, 318, HydrologyVoxels.NO_WATER,
330, HydrologyVoxels.NO_WATER), -373, -441, evidence);
assertEquals(0L, evidence.lostWetColumns);
assertEquals(2L, evidence.effectiveWetColumns);
assertEquals(2L, evidence.raisedWetBedColumns);
assertEquals(14L, evidence.raisedWetBedBlocks);
assertEquals(8, evidence.maximumWetBedRaise);
assertEquals(List.of("Water settling fills 14 blocks above the raster bed in 2 wet columns in fresh generation."),
HydrologyBlockProbe.failures(evidence, Set.of()));
assertEquals("raised wet bed at -372,-441 role=CHANNEL rasterGround=318 effectiveGround=324 fill=6 remainingDepth=2",
evidence.examples.getFirst());
HydrologyVoxels.ChunkBlocks chunk = HydrologyVoxels.ChunkBlocks.empty(-24, -28, 384);
for (int y = 319; y <= 324; y++) {
chunk.set(12, y, 7, HydrologyVoxels.SOLID);
}
chunk.set(12, 325, 7, HydrologyVoxels.WATER);
HydrologyVoxels.Volume volume = new HydrologyVoxels.Volume();
volume.put(chunk);
assertEquals(6, HydrologyBlockProbe.raisedBedSolids(channel, volume, -372, -441));
assertEquals(0, HydrologyBlockProbe.raisedBedSolids(pond, volume, -371, -441));
HydrologyBlockProbe.BedNeighbours neighbours = HydrologyBlockProbe.bedNeighbours(Map.of(
HydrologyVoxels.columnKey(-373, -441), new HydrologyBlockProbe.PlannedColumn(WaterRole.CHANNEL, 316, 330, 324, 316, 324)),
-372, -441);
assertTrue(neighbours.dropBelow(channel.head()));
assertEquals(1, neighbours.known());
assertEquals(324, neighbours.lowestHead());
}
@Test
public void columnDataAndBedImagesExposeHeightChangesBeneathOneWaterSurface() throws Exception {
File output = folder.newFolder("columns");
HydrologyBlockProbe.Configuration configuration = new HydrologyBlockProbe.Configuration(new File("/synthetic-pack"), "overworld",
1337L, -1, -1, -1, -1, Set.of(), output, false);
Map<Long, HydrologyBlockProbe.PlannedColumn> columns = new HashMap<>();
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
boolean lost = x == 4 && z == 5;
boolean dry = x == 0 && z == 0;
boolean raised = x == 6 && z == 5;
columns.put(HydrologyVoxels.columnKey(x - 16, z - 16), new HydrologyBlockProbe.PlannedColumn(
dry ? WaterRole.BANK : WaterRole.CHANNEL, 60 + x, 120, dry ? HydrologyVoxels.NO_WATER : 100,
lost ? 101 : raised ? 72 : 60 + x, lost || dry ? HydrologyVoxels.NO_WATER : 100));
}
}
HydrologyVoxels.ChunkBlocks chunk = HydrologyVoxels.ChunkBlocks.empty(-1, -1, 128);
for (int y = 67; y <= 72; y++) {
chunk.set(6, y, 5, HydrologyVoxels.SOLID);
}
HydrologyVoxels.Volume volume = new HydrologyVoxels.Volume();
volume.put(chunk);
List<String> artifacts = HydrologyBlockMaps.write(configuration, columns, volume, 320);
assertEquals(5, artifacts.size());
List<String> csv = Files.readAllLines(new File(output, "columns.csv").toPath());
assertEquals(257, csv.size());
assertEquals("x,z,role,natural_height,raster_terrain,effective_terrain,raster_head,effective_head,water_lost,terrain_delta,wet_bed_raise,generated_fill_solids,lowest_neighbor_head,known_neighbors", csv.getFirst());
assertEquals("-16,-16,BANK,120,60,60,,,false,0,0,0,100,2", csv.get(1));
assertTrue(csv.contains("-12,-11,CHANNEL,120,64,101,100,,true,37,0,0,100,4"));
assertTrue(csv.contains("-10,-11,CHANNEL,120,66,72,100,100,false,6,6,6,100,4"));
assertFalse(String.join("\n", csv).contains(Integer.toString(HydrologyVoxels.NO_WATER)));
BufferedImage raw = ImageIO.read(new File(output, "raster-bed.png"));
BufferedImage effective = ImageIO.read(new File(output, "effective-bed.png"));
BufferedImage loss = ImageIO.read(new File(output, "water-loss.png"));
BufferedImage fill = ImageIO.read(new File(output, "bed-fill.png"));
assertEquals(16, raw.getWidth());
assertEquals(16, raw.getHeight());
assertNotEquals(raw.getRGB(1, 10), raw.getRGB(13, 10));
assertNotEquals(raw.getRGB(4, 5), effective.getRGB(4, 5));
assertEquals(raw.getRGB(10, 10), effective.getRGB(10, 10));
assertEquals(0xD33B31, loss.getRGB(4, 5) & 0xFFFFFF);
assertEquals(0x247AC4, loss.getRGB(10, 10) & 0xFFFFFF);
assertEquals(0xC4C4C4, loss.getRGB(0, 0) & 0xFFFFFF);
assertEquals(0xFF3820, fill.getRGB(6, 5) & 0xFFFFFF);
assertEquals(0x247AC4, fill.getRGB(10, 10) & 0xFFFFFF);
assertEquals(0xC4C4C4, fill.getRGB(0, 0) & 0xFFFFFF);
}
}