Implement explosion optimisations

This commit is contained in:
Spottedleaf 2024-10-24 10:16:56 -07:00
parent e9c58f5451
commit 2a95ad1df3
2 changed files with 439 additions and 1 deletions

View File

@ -17,7 +17,6 @@ todo:
- implement chunk_system.SectionStorageMixin diff from reference - implement chunk_system.SectionStorageMixin diff from reference
- implement chunk_system.SerializableChunkDataMixin diff from reference - implement chunk_system.SerializableChunkDataMixin diff from reference
- implement chunk_system.ServerLevelMixin diff from reference - implement chunk_system.ServerLevelMixin diff from reference
- implement collisions.ServerExplosionMixin diff from reference
- implement starlight.LevelLightEngineMixin diff from reference - implement starlight.LevelLightEngineMixin diff from reference
- implement starlight.ThreadedLevelLightEngineMixin diff from reference - implement starlight.ThreadedLevelLightEngineMixin diff from reference
- implement starlight.ChunkSerializerMixin diff from reference - implement starlight.ChunkSerializerMixin diff from reference

View File

@ -30275,6 +30275,445 @@ index 5eb8982678110fabb82a93c5ec67c666b7fde017..ade435de0af4ee3566fa4a490df53cdd
@Nullable @Nullable
ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create); ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create);
diff --git a/src/main/java/net/minecraft/world/level/ServerExplosion.java b/src/main/java/net/minecraft/world/level/ServerExplosion.java
index 86656de31b1e33381eddd3ef210122118b31e620..c7fd8ce0dae838d91915a1c7a34152bed3ac7682 100644
--- a/src/main/java/net/minecraft/world/level/ServerExplosion.java
+++ b/src/main/java/net/minecraft/world/level/ServerExplosion.java
@@ -64,6 +64,249 @@ public class ServerExplosion implements Explosion {
public float yield;
// CraftBukkit end
public boolean excludeSourceFromDamage = true; // Paper - Allow explosions to damage source
+ // Paper start - collisions optimisations
+ private static final double[] CACHED_RAYS;
+ static {
+ final it.unimi.dsi.fastutil.doubles.DoubleArrayList rayCoords = new it.unimi.dsi.fastutil.doubles.DoubleArrayList();
+
+ for (int x = 0; x <= 15; ++x) {
+ for (int y = 0; y <= 15; ++y) {
+ for (int z = 0; z <= 15; ++z) {
+ if ((x == 0 || x == 15) || (y == 0 || y == 15) || (z == 0 || z == 15)) {
+ double xDir = (double)((float)x / 15.0F * 2.0F - 1.0F);
+ double yDir = (double)((float)y / 15.0F * 2.0F - 1.0F);
+ double zDir = (double)((float)z / 15.0F * 2.0F - 1.0F);
+
+ double mag = Math.sqrt(
+ xDir * xDir + yDir * yDir + zDir * zDir
+ );
+
+ rayCoords.add((xDir / mag) * (double)0.3F);
+ rayCoords.add((yDir / mag) * (double)0.3F);
+ rayCoords.add((zDir / mag) * (double)0.3F);
+ }
+ }
+ }
+ }
+
+ CACHED_RAYS = rayCoords.toDoubleArray();
+ }
+
+ private static final int CHUNK_CACHE_SHIFT = 2;
+ private static final int CHUNK_CACHE_MASK = (1 << CHUNK_CACHE_SHIFT) - 1;
+ private static final int CHUNK_CACHE_WIDTH = 1 << CHUNK_CACHE_SHIFT;
+
+ private static final int BLOCK_EXPLOSION_CACHE_SHIFT = 3;
+ private static final int BLOCK_EXPLOSION_CACHE_MASK = (1 << BLOCK_EXPLOSION_CACHE_SHIFT) - 1;
+ private static final int BLOCK_EXPLOSION_CACHE_WIDTH = 1 << BLOCK_EXPLOSION_CACHE_SHIFT;
+
+ // resistance = (res + 0.3F) * 0.3F;
+ // so for resistance = 0, we need res = -0.3F
+ private static final Float ZERO_RESISTANCE = Float.valueOf(-0.3f);
+ private it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache> blockCache = null;
+ private long[] chunkPosCache = null;
+ private net.minecraft.world.level.chunk.LevelChunk[] chunkCache = null;
+ private ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] directMappedBlockCache;
+ private BlockPos.MutableBlockPos mutablePos;
+
+ private ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache getOrCacheExplosionBlock(final int x, final int y, final int z,
+ final long key, final boolean calculateResistance) {
+ ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache ret = this.blockCache.get(key);
+ if (ret != null) {
+ return ret;
+ }
+
+ BlockPos pos = new BlockPos(x, y, z);
+
+ if (!this.level.isInWorldBounds(pos)) {
+ ret = new ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache(key, pos, null, null, 0.0f, true);
+ } else {
+ net.minecraft.world.level.chunk.LevelChunk chunk;
+ long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(x >> 4, z >> 4);
+ int chunkCacheKey = ((x >> 4) & CHUNK_CACHE_MASK) | (((z >> 4) << CHUNK_CACHE_SHIFT) & (CHUNK_CACHE_MASK << CHUNK_CACHE_SHIFT));
+ if (this.chunkPosCache[chunkCacheKey] == chunkKey) {
+ chunk = this.chunkCache[chunkCacheKey];
+ } else {
+ this.chunkPosCache[chunkCacheKey] = chunkKey;
+ this.chunkCache[chunkCacheKey] = chunk = this.level.getChunk(x >> 4, z >> 4);
+ }
+
+ BlockState blockState = ((ca.spottedleaf.moonrise.patches.getblock.GetBlockChunk)chunk).moonrise$getBlock(x, y, z);
+ FluidState fluidState = blockState.getFluidState();
+
+ Optional<Float> resistance = !calculateResistance ? Optional.empty() : this.damageCalculator.getBlockExplosionResistance((Explosion)(Object)this, this.level, pos, blockState, fluidState);
+
+ ret = new ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache(
+ key, pos, blockState, fluidState,
+ (resistance.orElse(ZERO_RESISTANCE).floatValue() + 0.3f) * 0.3f,
+ false
+ );
+ }
+
+ this.blockCache.put(key, ret);
+
+ return ret;
+ }
+
+ private boolean clipsAnything(final Vec3 from, final Vec3 to,
+ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext context,
+ final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] blockCache,
+ final BlockPos.MutableBlockPos currPos) {
+ // assume that context.delegated = false
+ final double adjX = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.x - to.x);
+ final double adjY = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.y - to.y);
+ final double adjZ = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.z - to.z);
+
+ if (adjX == 0.0 && adjY == 0.0 && adjZ == 0.0) {
+ return false;
+ }
+
+ final double toXAdj = to.x - adjX;
+ final double toYAdj = to.y - adjY;
+ final double toZAdj = to.z - adjZ;
+ final double fromXAdj = from.x + adjX;
+ final double fromYAdj = from.y + adjY;
+ final double fromZAdj = from.z + adjZ;
+
+ int currX = Mth.floor(fromXAdj);
+ int currY = Mth.floor(fromYAdj);
+ int currZ = Mth.floor(fromZAdj);
+
+ final double diffX = toXAdj - fromXAdj;
+ final double diffY = toYAdj - fromYAdj;
+ final double diffZ = toZAdj - fromZAdj;
+
+ final double dxDouble = Math.signum(diffX);
+ final double dyDouble = Math.signum(diffY);
+ final double dzDouble = Math.signum(diffZ);
+
+ final int dx = (int)dxDouble;
+ final int dy = (int)dyDouble;
+ final int dz = (int)dzDouble;
+
+ final double normalizedDiffX = diffX == 0.0 ? Double.MAX_VALUE : dxDouble / diffX;
+ final double normalizedDiffY = diffY == 0.0 ? Double.MAX_VALUE : dyDouble / diffY;
+ final double normalizedDiffZ = diffZ == 0.0 ? Double.MAX_VALUE : dzDouble / diffZ;
+
+ double normalizedCurrX = normalizedDiffX * (diffX > 0.0 ? (1.0 - Mth.frac(fromXAdj)) : Mth.frac(fromXAdj));
+ double normalizedCurrY = normalizedDiffY * (diffY > 0.0 ? (1.0 - Mth.frac(fromYAdj)) : Mth.frac(fromYAdj));
+ double normalizedCurrZ = normalizedDiffZ * (diffZ > 0.0 ? (1.0 - Mth.frac(fromZAdj)) : Mth.frac(fromZAdj));
+
+ for (;;) {
+ currPos.set(currX, currY, currZ);
+
+ // ClipContext.Block.COLLIDER -> BlockBehaviour.BlockStateBase::getCollisionShape
+ // ClipContext.Fluid.NONE -> ignore fluids
+
+ // read block from cache
+ final long key = BlockPos.asLong(currX, currY, currZ);
+
+ final int cacheKey =
+ (currX & BLOCK_EXPLOSION_CACHE_MASK) |
+ (currY & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT) |
+ (currZ & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT + BLOCK_EXPLOSION_CACHE_SHIFT);
+ ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache cachedBlock = blockCache[cacheKey];
+ if (cachedBlock == null || cachedBlock.key != key) {
+ blockCache[cacheKey] = cachedBlock = this.getOrCacheExplosionBlock(currX, currY, currZ, key, false);
+ }
+
+ final BlockState blockState = cachedBlock.blockState;
+ if (blockState != null && !((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockState).moonrise$emptyContextCollisionShape()) {
+ net.minecraft.world.phys.shapes.VoxelShape collision = cachedBlock.cachedCollisionShape;
+ if (collision == null) {
+ collision = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockState).moonrise$getConstantContextCollisionShape();
+ if (collision == null) {
+ collision = blockState.getCollisionShape(this.level, currPos, context);
+ if (!context.isDelegated()) {
+ // if it was not delegated during this call, assume that for any future ones it will not be delegated
+ // again, and cache the result
+ cachedBlock.cachedCollisionShape = collision;
+ }
+ } else {
+ cachedBlock.cachedCollisionShape = collision;
+ }
+ }
+
+ if (!collision.isEmpty() && collision.clip(from, to, currPos) != null) {
+ return true;
+ }
+ }
+
+ if (normalizedCurrX > 1.0 && normalizedCurrY > 1.0 && normalizedCurrZ > 1.0) {
+ return false;
+ }
+
+ // inc the smallest normalized coordinate
+
+ if (normalizedCurrX < normalizedCurrY) {
+ if (normalizedCurrX < normalizedCurrZ) {
+ currX += dx;
+ normalizedCurrX += normalizedDiffX;
+ } else {
+ // x < y && x >= z <--> z < y && z <= x
+ currZ += dz;
+ normalizedCurrZ += normalizedDiffZ;
+ }
+ } else if (normalizedCurrY < normalizedCurrZ) {
+ // y <= x && y < z
+ currY += dy;
+ normalizedCurrY += normalizedDiffY;
+ } else {
+ // y <= x && z <= y <--> z <= y && z <= x
+ currZ += dz;
+ normalizedCurrZ += normalizedDiffZ;
+ }
+ }
+ }
+
+ private float getSeenFraction(final Vec3 source, final Entity target,
+ final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] blockCache,
+ final BlockPos.MutableBlockPos blockPos) {
+ final AABB boundingBox = target.getBoundingBox();
+ final double diffX = boundingBox.maxX - boundingBox.minX;
+ final double diffY = boundingBox.maxY - boundingBox.minY;
+ final double diffZ = boundingBox.maxZ - boundingBox.minZ;
+
+ final double incX = 1.0 / (diffX * 2.0 + 1.0);
+ final double incY = 1.0 / (diffY * 2.0 + 1.0);
+ final double incZ = 1.0 / (diffZ * 2.0 + 1.0);
+
+ if (incX < 0.0 || incY < 0.0 || incZ < 0.0) {
+ return 0.0f;
+ }
+
+ final double offX = (1.0 - Math.floor(1.0 / incX) * incX) * 0.5 + boundingBox.minX;
+ final double offY = boundingBox.minY;
+ final double offZ = (1.0 - Math.floor(1.0 / incZ) * incZ) * 0.5 + boundingBox.minZ;
+
+ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext context = new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext(target);
+
+ int totalRays = 0;
+ int missedRays = 0;
+
+ for (double dx = 0.0; dx <= 1.0; dx += incX) {
+ final double fromX = Math.fma(dx, diffX, offX);
+ for (double dy = 0.0; dy <= 1.0; dy += incY) {
+ final double fromY = Math.fma(dy, diffY, offY);
+ for (double dz = 0.0; dz <= 1.0; dz += incZ) {
+ ++totalRays;
+
+ final Vec3 from = new Vec3(
+ fromX,
+ fromY,
+ Math.fma(dz, diffZ, offZ)
+ );
+
+ if (!this.clipsAnything(from, source, context, blockCache, blockPos)) {
+ ++missedRays;
+ }
+ }
+ }
+ }
+
+ return (float)missedRays / (float)totalRays;
+ }
+ // Paper end - collisions optimisations
public ServerExplosion(ServerLevel world, @Nullable Entity entity, @Nullable DamageSource damageSource, @Nullable ExplosionDamageCalculator behavior, Vec3 pos, float power, boolean createFire, Explosion.BlockInteraction destructionType) {
this.level = world;
@@ -127,64 +370,91 @@ public class ServerExplosion implements Explosion {
}
private List<BlockPos> calculateExplodedPositions() {
- Set<BlockPos> set = new HashSet();
- boolean flag = true;
-
- for (int i = 0; i < 16; ++i) {
- for (int j = 0; j < 16; ++j) {
- for (int k = 0; k < 16; ++k) {
- if (i == 0 || i == 15 || j == 0 || j == 15 || k == 0 || k == 15) {
- double d0 = (double) ((float) i / 15.0F * 2.0F - 1.0F);
- double d1 = (double) ((float) j / 15.0F * 2.0F - 1.0F);
- double d2 = (double) ((float) k / 15.0F * 2.0F - 1.0F);
- double d3 = Math.sqrt(d0 * d0 + d1 * d1 + d2 * d2);
-
- d0 /= d3;
- d1 /= d3;
- d2 /= d3;
- float f = this.radius * (0.7F + this.level.random.nextFloat() * 0.6F);
- double d4 = this.center.x;
- double d5 = this.center.y;
- double d6 = this.center.z;
-
- for (float f1 = 0.3F; f > 0.0F; f -= 0.22500001F) {
- BlockPos blockposition = BlockPos.containing(d4, d5, d6);
- BlockState iblockdata = this.level.getBlockState(blockposition);
- FluidState fluid = iblockdata.getFluidState(); // Paper - Perf: Optimize call to getFluid for explosions
-
- if (!this.level.isInWorldBounds(blockposition)) {
- break;
- }
+ // Paper start - collision optimisations
+ final ObjectArrayList<BlockPos> ret = new ObjectArrayList<>();
- Optional<Float> optional = this.damageCalculator.getBlockExplosionResistance(this, this.level, blockposition, iblockdata, fluid);
+ final Vec3 center = this.center;
- if (optional.isPresent()) {
- f -= ((Float) optional.get() + 0.3F) * 0.3F;
- }
+ final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] blockCache = this.directMappedBlockCache;
- if (f > 0.0F && this.damageCalculator.shouldBlockExplode(this, this.level, blockposition, iblockdata, f)) {
- set.add(blockposition);
- // Paper start - prevent headless pistons from forming
- if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowHeadlessPistons && iblockdata.getBlock() == Blocks.MOVING_PISTON) {
- net.minecraft.world.level.block.entity.BlockEntity extension = this.level.getBlockEntity(blockposition);
- if (extension instanceof net.minecraft.world.level.block.piston.PistonMovingBlockEntity blockEntity && blockEntity.isSourcePiston()) {
- net.minecraft.core.Direction direction = iblockdata.getValue(net.minecraft.world.level.block.piston.PistonHeadBlock.FACING);
- set.add(blockposition.relative(direction.getOpposite()));
- }
- }
- // Paper end - prevent headless pistons from forming
- }
+ // use initial cache value that is most likely to be used: the source position
+ final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache initialCache;
+ {
+ final int blockX = Mth.floor(center.x);
+ final int blockY = Mth.floor(center.y);
+ final int blockZ = Mth.floor(center.z);
+
+ final long key = BlockPos.asLong(blockX, blockY, blockZ);
+
+ initialCache = this.getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true);
+ }
- d4 += d0 * 0.30000001192092896D;
- d5 += d1 * 0.30000001192092896D;
- d6 += d2 * 0.30000001192092896D;
+ // only ~1/3rd of the loop iterations in vanilla will result in a ray, as it is iterating the perimeter of
+ // a 16x16x16 cube
+ // we can cache the rays and their normals as well, so that we eliminate the excess iterations / checks and
+ // calculations in one go
+ // additional aggressive caching of block retrieval is very significant, as at low power (i.e tnt) most
+ // block retrievals are not unique
+ for (int ray = 0, len = CACHED_RAYS.length; ray < len;) {
+ ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache cachedBlock = initialCache;
+
+ double currX = center.x;
+ double currY = center.y;
+ double currZ = center.z;
+
+ final double incX = CACHED_RAYS[ray];
+ final double incY = CACHED_RAYS[ray + 1];
+ final double incZ = CACHED_RAYS[ray + 2];
+
+ ray += 3;
+
+ float power = this.radius * (0.7F + this.level.random.nextFloat() * 0.6F);
+
+ do {
+ final int blockX = Mth.floor(currX);
+ final int blockY = Mth.floor(currY);
+ final int blockZ = Mth.floor(currZ);
+
+ final long key = BlockPos.asLong(blockX, blockY, blockZ);
+
+ if (cachedBlock.key != key) {
+ final int cacheKey =
+ (blockX & BLOCK_EXPLOSION_CACHE_MASK) |
+ (blockY & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT) |
+ (blockZ & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT + BLOCK_EXPLOSION_CACHE_SHIFT);
+ cachedBlock = blockCache[cacheKey];
+ if (cachedBlock == null || cachedBlock.key != key) {
+ blockCache[cacheKey] = cachedBlock = this.getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true);
+ }
+ }
+
+ if (cachedBlock.outOfWorld) {
+ break;
+ }
+
+ power -= cachedBlock.resistance;
+
+ if (power > 0.0f && cachedBlock.shouldExplode == null) {
+ // note: we expect shouldBlockExplode to be pure with respect to power, as Vanilla currently is.
+ // basically, it is unused, which allows us to cache the result
+ final boolean shouldExplode = this.damageCalculator.shouldBlockExplode((Explosion)(Object)this, this.level, cachedBlock.immutablePos, cachedBlock.blockState, power);
+ cachedBlock.shouldExplode = shouldExplode ? Boolean.TRUE : Boolean.FALSE;
+ if (shouldExplode) {
+ if (this.fire || !cachedBlock.blockState.isAir()) {
+ ret.add(cachedBlock.immutablePos);
}
}
}
- }
+
+ power -= 0.22500001F;
+ currX += incX;
+ currY += incY;
+ currZ += incZ;
+ } while (power > 0.0f);
}
- return new ObjectArrayList(set);
+ return ret;
+ // Paper end - collision optimisations
}
private void hurtEntities() {
@@ -390,6 +660,14 @@ public class ServerExplosion implements Explosion {
return;
}
// CraftBukkit end
+ // Paper start - collision optimisations
+ this.blockCache = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>();
+ this.chunkPosCache = new long[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH];
+ java.util.Arrays.fill(this.chunkPosCache, ChunkPos.INVALID_CHUNK_POS);
+ this.chunkCache = new net.minecraft.world.level.chunk.LevelChunk[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH];
+ this.directMappedBlockCache = new ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH];
+ this.mutablePos = new BlockPos.MutableBlockPos();
+ // Paper end - collision optimisations
this.level.gameEvent(this.source, (Holder) GameEvent.EXPLODE, this.center);
List<BlockPos> list = this.calculateExplodedPositions();
@@ -405,6 +683,13 @@ public class ServerExplosion implements Explosion {
if (this.fire) {
this.createFire(list);
}
+ // Paper start - collision optimisations
+ this.blockCache = null;
+ this.chunkPosCache = null;
+ this.chunkCache = null;
+ this.directMappedBlockCache = null;
+ this.mutablePos = null;
+ // Paper end - collision optimisations
}
@@ -494,12 +779,12 @@ public class ServerExplosion implements Explosion {
// Paper start - Optimize explosions
private float getBlockDensity(Vec3 vec3d, Entity entity) {
if (!this.level.paperConfig().environment.optimizeExplosions) {
- return getSeenPercent(vec3d, entity);
+ return this.getSeenFraction(vec3d, entity, this.directMappedBlockCache, this.mutablePos); // Paper - collision optimisations
}
CacheKey key = new CacheKey(this, entity.getBoundingBox());
Float blockDensity = this.level.explosionDensityCache.get(key);
if (blockDensity == null) {
- blockDensity = getSeenPercent(vec3d, entity);
+ blockDensity = this.getSeenFraction(vec3d, entity, this.directMappedBlockCache, this.mutablePos); // Paper - collision optimisations
this.level.explosionDensityCache.put(key, blockDensity);
}
diff --git a/src/main/java/net/minecraft/world/level/biome/Biome.java b/src/main/java/net/minecraft/world/level/biome/Biome.java diff --git a/src/main/java/net/minecraft/world/level/biome/Biome.java b/src/main/java/net/minecraft/world/level/biome/Biome.java
index 8590de51b572c0f73d45aee60313d466e4671da5..b725eea9d3ca81d2ef7802f5d0346d924aa1f808 100644 index 8590de51b572c0f73d45aee60313d466e4671da5..b725eea9d3ca81d2ef7802f5d0346d924aa1f808 100644
--- a/src/main/java/net/minecraft/world/level/biome/Biome.java --- a/src/main/java/net/minecraft/world/level/biome/Biome.java