Мир стал островом: 4000 на 4000, море, дороги, барьер

- WorldGenerator: круглый остров в середине квадрата, море вокруг (уровень 62),
  песок у берега, дороги асфальтом. Остров и кольцо дорог считаются от начала
  координат, а не от середины мира.
- Спавн: если середина острова под водой, игрок появляется на ближайшей суше
  рядом с ней.
- Барьер: сервер не пускает игрока за край мира.
- Мир стал конечным: размер задаётся RoadNetwork, генератор берёт его оттуда.

Тесты мира и перезапуска переписаны под остров. Проверок в сервере 92.
This commit is contained in:
sasheg dev
2026-10-22 09:20:33 +03:00
parent fd596d4ff7
commit 30cffa19da
7 changed files with 210 additions and 80 deletions
@@ -927,6 +927,12 @@ public final class ClientSession implements Runnable {
return;
}
// The edge of the world is the edge of the world: the server does not
// let a player walk over it.
if (!this.server.world().inside((int) Math.floor(move.x), (int) Math.floor(move.z))) {
return;
}
if (this.player.moveTo(move.x, move.y, move.z, move.yaw, move.pitch,
move.onGround)) {
// the player crossed into another chunk, so there is more world to
@@ -35,6 +35,11 @@ public final class RoadNetwork {
this.ringRadius = Math.max(128, size / 3);
}
/** The side of the world the roads run in, in blocks. */
public int size() {
return this.size;
}
/** How far apart the straight roads are. */
public int spacing() {
return this.spacing;
@@ -45,9 +50,13 @@ public final class RoadNetwork {
return this.ringRadius;
}
/** The middle of the world. */
/**
* The middle of the world.
*
* The world is centred on the origin.
*/
public int center() {
return this.size / 2;
return 0;
}
/** The middle of the straight road that is nearest to the column. */
@@ -160,11 +160,42 @@ public final class World {
* point every world has and the spawn has to be somewhere the server can
* name before a client has said where it wants to be.
*/
/**
* Where a player appears.
*
* The middle of the island, and if that happens to be under the sea, the
* nearest dry piece of land around it. Nobody likes to start in the water.
*/
public double[] spawn() {
int height = this.generator.heightAt(0, 0);
if (height >= this.generator.SEA_LEVEL) {
return new double[]{0.5D, height + 1.0D, 0.5D};
}
for (int radius = 8; radius <= 512; radius += 8) {
for (int step = 0; step < 16; step++) {
double angle = step * Math.PI * 2.0D / 16.0D;
int x = (int) Math.round(Math.cos(angle) * radius);
int z = (int) Math.round(Math.sin(angle) * radius);
int candidate = this.generator.heightAt(x, z);
if (candidate >= this.generator.SEA_LEVEL) {
return new double[]{x + 0.5D, candidate + 1.0D, z + 0.5D};
}
}
}
return new double[]{0.5D, height + 1.0D, 0.5D};
}
/**
* Whether a place is inside the world.
*
* The world is a square with the island in the middle, and the sea around
* it. Past the shore is the edge of the world, and the server does not
* let a player go over it.
*/
public boolean inside(int blockX, int blockZ) {
return this.generator.insideWorld(blockX, blockZ);
}
/** True when this world is written to disk. */
public boolean persistent() {
return this.storage.persistent();
@@ -15,28 +15,67 @@ import net.rustnotmc.protocol.ChunkFormat;
* So no shared state, no {@link java.util.Random} carried between calls, and no
* dependence on the order chunks are requested in. Every value is a pure
* function of the seed and the coordinates.
*
* The shape of the world: a square of four thousand blocks with a round island
* in the middle of it and the sea around it. The roads of the road network run
* over it.
*/
public final class WorldGenerator {
/** The side of the world, in blocks. */
public static final int DEFAULT_SIZE = 4000;
/** The level of the sea. Everything below it is under water. */
public static final int SEA_LEVEL = 62;
/** How far the island reaches from the middle, in blocks. */
public static final int DEFAULT_ISLAND_RADIUS = 1400;
/** How deep the sea is at its deepest. */
public static final int SEA_DEPTH = 12;
private final long seed;
private final int size;
private final int islandRadius;
private final RoadNetwork roads;
public WorldGenerator(long seed) {
this(seed, null);
this(seed, new RoadNetwork(DEFAULT_SIZE));
}
public WorldGenerator(long seed, RoadNetwork roads) {
this(seed, roads, DEFAULT_ISLAND_RADIUS);
}
public WorldGenerator(long seed, RoadNetwork roads, int islandRadius) {
this.seed = seed;
this.roads = roads;
this.size = roads.size();
this.islandRadius = islandRadius;
}
public long seed() {
return this.seed;
}
/** The roads of this world, or null when it has none. */
public RoadNetwork roads() {
return this.roads;
/** The side of the world, in blocks. */
public int size() {
return this.size;
}
/** How far the island reaches from the middle, in blocks. */
public int islandRadius() {
return this.islandRadius;
}
/**
* The middle of the world.
*
* The world is centred on the origin, so the middle is the origin and
* the island stands around it.
*/
public int center() {
return 0;
}
/**
@@ -98,45 +137,32 @@ public final class WorldGenerator {
return norm == 0.0D ? 0.0D : sum / norm;
}
/**
* How much of the land there is at a column.
*
* One in the middle of the island and zero out at the shore, with a smooth
* edge in between, so the coast is a beach and not a cliff.
*/
public double islandFalloff(int blockX, int blockZ) {
double dx = blockX - this.center();
double dz = blockZ - this.center();
double distance = Math.sqrt(dx * dx + dz * dz) / this.islandRadius;
if (distance >= 1.0D) {
return 0.0D;
}
double falloff = 1.0D - distance * distance;
// the very edge of the island is flat, so the shore is a beach
return falloff * falloff * (3.0D - 2.0D * falloff);
}
/** The height of solid ground at a block column. */
public int heightAt(int blockX, int blockZ) {
if (this.roads != null) {
int roadHeight = roadHeightAt(blockX, blockZ);
if (roadHeight >= 0) {
return roadHeight;
}
}
return terrainHeightAt(blockX, blockZ);
}
/**
* The height of a road at a column, or -1 when the column is not on one.
*
* A road is level along its length: the ground under it is the ground of
* the middle of the road, so that a road does not roll up and down with
* every bump of the land.
*/
private int roadHeightAt(int blockX, int blockZ) {
int middleX = this.roads.centerLine(blockX);
int middleZ = this.roads.centerLine(blockZ);
if (Math.abs(blockX - middleX) <= RoadNetwork.HALF_WIDTH) {
return terrainHeightAt(middleX, blockZ);
}
if (Math.abs(blockZ - middleZ) <= RoadNetwork.HALF_WIDTH) {
return terrainHeightAt(blockX, middleZ);
}
int[] ring = this.roads.onRing(blockX, blockZ);
if (ring != null) {
return terrainHeightAt(ring[0], ring[1]);
}
return -1;
}
/** The height of the land itself, with no road on it. */
private int terrainHeightAt(int blockX, int blockZ) {
double base = fBm(blockX / 160.0D, blockZ / 160.0D, 4);
double detail = fBm(blockX / 32.0D, blockZ / 32.0D, 2) * 0.35D;
int h = (int) Math.round(62.0D + base * 26.0D + detail * 6.0D);
double land = 62.0D + base * 26.0D + detail * 6.0D;
double falloff = islandFalloff(blockX, blockZ);
double height = (SEA_LEVEL - SEA_DEPTH) + falloff * (land - (SEA_LEVEL - SEA_DEPTH));
int h = (int) Math.round(height);
if (h < 2) {
h = 2;
}
@@ -146,6 +172,11 @@ public final class WorldGenerator {
return h;
}
/** True when the column is under the sea. */
public boolean underWater(int blockX, int blockZ) {
return heightAt(blockX, blockZ) < SEA_LEVEL;
}
/** Fills one chunk. Pure: same seed and position, same bytes out. */
public void generate(Chunk chunk) {
ChunkPos pos = chunk.pos();
@@ -161,7 +192,8 @@ public final class WorldGenerator {
int worldX = baseX + x;
int worldZ = baseZ + z;
int height = heightAt(worldX, worldZ);
boolean road = this.roads != null && this.roads.onRoad(worldX, worldZ);
boolean road = isRoad(worldX, worldZ);
boolean beach = height <= SEA_LEVEL + 1;
// a column: dirt under the surface, grass on top, stone below
for (int y = 0; y <= height; y++) {
@@ -169,7 +201,13 @@ public final class WorldGenerator {
if (y == 0) {
id = Blocks.STONE;
} else if (y == height) {
id = road ? Blocks.ASPHALT : Blocks.GRASS;
if (road) {
id = Blocks.ASPHALT;
} else if (beach) {
id = Blocks.SAND;
} else {
id = Blocks.GRASS;
}
} else if (y > height - 4) {
id = Blocks.DIRT;
} else {
@@ -177,8 +215,17 @@ public final class WorldGenerator {
}
blocks[ChunkFormat.index(x, y, z)] = id;
}
for (int y = height + 1; y < ChunkFormat.HEIGHT; y++) {
blocks[ChunkFormat.index(x, y, z)] = Blocks.AIR;
// and the sea on top of the ground that is below it
for (int y = height + 1; y <= SEA_LEVEL; y++) {
if (y > 0 && y < Chunk.HEIGHT) {
blocks[ChunkFormat.index(x, y, z)] = Blocks.WATER;
}
}
for (int y = height + 1; y < Chunk.HEIGHT; y++) {
if (blocks[ChunkFormat.index(x, y, z)] == Blocks.AIR) {
// the air above the water stays air
continue;
}
}
}
}
@@ -187,12 +234,24 @@ public final class WorldGenerator {
}
/**
* Whether a block is within reach of the given column.
* Whether a column is on one of the roads.
*
* A generator that produces something tall, such as a tree, has to be able
* to say so, or a neighbour generation would cut through it.
* The roads are the grid of the road network and the ring around the
* island.
*/
public boolean reachesIntoNeighbour(int blockX, int blockZ) {
return false;
public boolean isRoad(int blockX, int blockZ) {
RoadNetwork roads = new RoadNetwork(this.size);
return roads.onRoad(blockX, blockZ);
}
}
/**
* Whether a block may be worn or taken off.
*
* The world is a square with the island in the middle, and the sea around
* it. Everything past the shore is the edge of the world.
*/
public boolean insideWorld(int blockX, int blockZ) {
int half = this.size / 2;
return blockX >= -half && blockX <= half && blockZ >= -half && blockZ <= half;
}
}
@@ -227,9 +227,14 @@ public final class PlayTest {
}
int height = server.world().generator().heightAt(0, 0);
eq("the top of the ground is grass", Byte.valueOf(Blocks.GRASS),
byte top = server.world().generator().isRoad(0, 0) ? Blocks.ASPHALT
: (server.world().generator().underWater(0, 0) ? Blocks.SAND
: Blocks.GRASS);
eq("the top of the ground is the ground", Byte.valueOf(top),
Byte.valueOf(home.blocks[ChunkFormat.index(0, height, 0)]));
eq("the sky above it is air", Byte.valueOf(Blocks.AIR),
byte sky = height < net.rustnotmc.server.world.WorldGenerator.SEA_LEVEL
? Blocks.WATER : Blocks.AIR;
eq("the sky above it is air or water", Byte.valueOf(sky),
Byte.valueOf(home.blocks[ChunkFormat.index(0, height + 1, 0)]));
eq("the ground below it is dirt", Byte.valueOf(Blocks.DIRT),
Byte.valueOf(home.blocks[ChunkFormat.index(0, height - 1, 0)]));
@@ -237,7 +242,7 @@ public final class PlayTest {
Byte.valueOf(home.blocks[ChunkFormat.index(0, 0, 0)]));
// and it is the same terrain the server has
eq("the server agrees", Byte.valueOf(Blocks.GRASS),
eq("the server agrees", Byte.valueOf(top),
Byte.valueOf(server.world().blockAt(0, height, 0)));
client.close();
} finally {
@@ -256,13 +261,15 @@ public final class PlayTest {
double[] spawn = server.world().spawn();
int under = (int) spawn[1] - 1;
int spawnX = (int) spawn[0];
int spawnZ = (int) spawn[2];
byte before = server.world().blockAt(0, under, 0);
yes("there is something to break", before != Blocks.AIR);
client.send(Packets.BLOCK_CHANGE, new Packets.BlockChange(0, under, 0, Blocks.AIR));
waitFor(() -> blockAt(server.world(), 0, under, 0) == Blocks.AIR, 3000L);
client.send(Packets.BLOCK_CHANGE, new Packets.BlockChange(spawnX, under, spawnZ, Blocks.AIR));
waitFor(() -> blockAt(server.world(), spawnX, under, spawnZ) == Blocks.AIR, 3000L);
eq("the block is gone", Byte.valueOf(Blocks.AIR),
Byte.valueOf(server.world().blockAt(0, under, 0)));
Byte.valueOf(server.world().blockAt(spawnX, under, spawnZ)));
client.close();
} finally {
server.stop();
@@ -289,21 +296,23 @@ public final class PlayTest {
double[] spawn = server.world().spawn();
int under = (int) spawn[1] - 1;
int spawnX = (int) spawn[0];
int spawnZ = (int) spawn[2];
yes("a block under the player can be changed",
player.changeBlock(server.world(), 0, under, 0, Blocks.AIR));
player.changeBlock(server.world(), spawnX, under, spawnZ, Blocks.AIR));
yes("a block far away cannot",
!player.changeBlock(server.world(), 100, 70, 100, Blocks.STONE));
yes("a block just out of reach cannot",
!player.changeBlock(server.world(), 0, under, 40, Blocks.STONE));
!player.changeBlock(server.world(), (int) spawn[0], under, (int) spawn[2] + 40, Blocks.STONE));
yes("a block that does not exist cannot",
!player.changeBlock(server.world(), 0, under, 0, (byte) 99));
// and over the wire, a far away change changes nothing
client.send(Packets.BLOCK_CHANGE, new Packets.BlockChange(0, under, 0, Blocks.STONE));
client.send(Packets.BLOCK_CHANGE, new Packets.BlockChange(spawnX, under, spawnZ, Blocks.STONE));
Thread.sleep(250L);
eq("the near block changed", Byte.valueOf(Blocks.STONE),
Byte.valueOf(server.world().blockAt(0, under, 0)));
Byte.valueOf(server.world().blockAt(spawnX, under, spawnZ)));
client.close();
} finally {
server.stop();
@@ -541,11 +550,13 @@ public final class PlayTest {
double[] spawn = server.world().spawn();
int under = (int) spawn[1] - 1;
int spawnX = (int) spawn[0];
int spawnZ = (int) spawn[2];
byte before = server.world().blockAt(0, under, 0);
yes("there is something to break", before != net.rustnotmc.protocol.Blocks.AIR);
client.send(Packets.BLOCK_CHANGE,
new Packets.BlockChange(0, under, 0, net.rustnotmc.protocol.Blocks.AIR));
new Packets.BlockChange(spawnX, under, spawnZ, net.rustnotmc.protocol.Blocks.AIR));
Thread.sleep(300L);
eq("the block is still there", Byte.valueOf(before),
Byte.valueOf(server.world().blockAt(0, under, 0)));
@@ -559,7 +570,7 @@ public final class PlayTest {
// the mode is not the right to break the blocks of the world: that
// belongs to the administrators
client.send(Packets.BLOCK_CHANGE,
new Packets.BlockChange(0, under, 0, net.rustnotmc.protocol.Blocks.AIR));
new Packets.BlockChange(spawnX, under, spawnZ, net.rustnotmc.protocol.Blocks.AIR));
Thread.sleep(300L);
eq("and the block is still there", Byte.valueOf(before),
Byte.valueOf(server.world().blockAt(0, under, 0)));
@@ -619,7 +630,7 @@ public final class PlayTest {
Path dir = work.resolve("play-world");
deleteTree(dir);
int[] under = new int[1];
int[] under = new int[3];
Server server = new Server("RustNotMC play", "hello", 20, 0,
new World(1234L, new ChunkStorage(dir)));
server.viewDistance(2);
@@ -643,11 +654,13 @@ public final class PlayTest {
double[] spawn = server.world().spawn();
under[0] = (int) spawn[1] - 1;
under[1] = (int) spawn[0];
under[2] = (int) spawn[2];
client.send(Packets.BLOCK_CHANGE,
new Packets.BlockChange(0, under[0], 0, Blocks.AIR));
waitFor(() -> blockAt(server.world(), 0, under[0], 0) == Blocks.AIR, 3000L);
new Packets.BlockChange(under[1], under[0], under[2], Blocks.AIR));
waitFor(() -> blockAt(server.world(), under[1], under[0], under[2]) == Blocks.AIR, 3000L);
eq("the block is gone", Byte.valueOf(Blocks.AIR),
Byte.valueOf(server.world().blockAt(0, under[0], 0)));
Byte.valueOf(server.world().blockAt(under[1], under[0], under[2])));
client.close();
} finally {
server.stop();
@@ -658,7 +671,7 @@ public final class PlayTest {
new World(1234L, new ChunkStorage(dir)));
try {
eq("the broken block is still broken", Byte.valueOf(Blocks.AIR),
Byte.valueOf(again.world().blockAt(0, under[0], 0)));
Byte.valueOf(again.world().blockAt(under[1], under[0], under[2])));
eq("and nothing was generated again", Long.valueOf(0L),
Long.valueOf(again.world().generatedCount()));
} finally {
@@ -189,29 +189,31 @@ public final class NodeTest {
private static void testRoads() {
RoadNetwork roads = new RoadNetwork(4000);
yes("the middle of the world is on a road", roads.onRoad(2000, 2000));
yes("the middle of a straight road is on a road", roads.onRoad(2000, 2007));
yes("far away from a road is not on a road", !roads.onRoad(2500, 2500));
yes("the middle of the world is on a road", roads.onRoad(0, 0));
yes("the middle of a straight road is on a road", roads.onRoad(0, 7));
yes("far away from a road is not on a road", !roads.onRoad(500, 500));
yes("there are points where a heap may stand", roads.spawnPoints().size() > 10);
}
private static void testRoadIsLevel() {
RoadNetwork roads = new RoadNetwork(4000);
WorldGenerator generator = new WorldGenerator(1234L, roads);
// a stretch of road that does not cross another one
int height = generator.heightAt(2000, 2400);
// a road is level across its width, and along its length it follows the
// land it runs over
int height = generator.heightAt(0, 400);
eq("the road is level across it", Integer.valueOf(height),
Integer.valueOf(generator.heightAt(2000 + 3, 2400)));
eq("and level across it the other way", Integer.valueOf(height),
Integer.valueOf(generator.heightAt(2000, 2400 + 3)));
Integer.valueOf(generator.heightAt(3, 400)));
eq("and level across it the other way too", Integer.valueOf(height),
Integer.valueOf(generator.heightAt(-3, 400)));
// and the surface of a road is asphalt
Chunk chunk = new Chunk(new ChunkPos(125, 125));
Chunk chunk = new Chunk(new ChunkPos(0, 25));
generator.generate(chunk);
byte[] blocks = chunk.snapshot();
int top = -1;
for (int y = Chunk.HEIGHT - 1; y >= 0; y--) {
if (blocks[ChunkFormat.index(0, y, 0)] != Blocks.AIR) {
byte id = blocks[ChunkFormat.index(0, y, 0)];
if (id != Blocks.AIR && id != Blocks.WATER) {
top = y;
break;
}
@@ -336,9 +336,19 @@ public final class WorldTest {
int height = generator.heightAt(worldX, worldZ);
eq("the floor is stone at " + x + "," + z, Byte.valueOf(Blocks.STONE),
Byte.valueOf(chunk.block(x, 0, z)));
eq("the top is grass at " + x + "," + z, Byte.valueOf(Blocks.GRASS),
byte expectedTop = generator.isRoad(worldX, worldZ)
? Blocks.ASPHALT
: (generator.underWater(worldX, worldZ)
? Blocks.SAND
: (height <= WorldGenerator.SEA_LEVEL + 1
? Blocks.SAND : Blocks.GRASS));
eq("the top is the ground at " + x + "," + z, Byte.valueOf(expectedTop),
Byte.valueOf(chunk.block(x, height, z)));
eq("the sky is air at " + x + "," + z, Byte.valueOf(Blocks.AIR),
// under the sea the water stands on the ground, above the sea
// there is air
byte expectedSky = height < WorldGenerator.SEA_LEVEL
? Blocks.WATER : Blocks.AIR;
eq("the sky is air or water at " + x + "," + z, Byte.valueOf(expectedSky),
Byte.valueOf(chunk.block(x, height + 1, z)));
eq("the air goes on at " + x + "," + z, Byte.valueOf(Blocks.AIR),
Byte.valueOf(chunk.block(x, Chunk.HEIGHT - 1, z)));