feat:移除了弹窗,服务器添加sls

This commit is contained in:
AxTps committed 2026-09-08 22:39:45 +08:00
commit 6a295f9a7a
4082 files changed
+1322534

No files matched your search

+826
View File
@@ -0,0 +1,826 @@
#include "cubiomes_bridge.h"
#include "biomenoise.h"
#include "finders.h"
#include "generator.h"
#include "noise.h"
#include "rng.h"
#include "util.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
static float axolotl_clamp(float value, float low, float high);
/* Sample coarse Overworld pixels at the centre of their block cell while
* retaining the same 3D Voronoi surface lookup used by scale=1. Without
* this, scale=16+ reads the raw noise point directly and can select a
* different vertical biome layer at tile boundaries. */
static void axolotl_gen_surface_biomes(
int *out,
const Generator *generator,
int32_t x,
int32_t z,
int32_t scale,
int32_t width,
int32_t height,
int32_t elevation
) {
const int32_t y = elevation;
for (int row = 0; row < height; row++) {
for (int column = 0; column < width; column++) {
const int block_x = x * scale + column * scale + scale / 2;
const int block_z = z * scale + row * scale + scale / 2;
int x4, y4, z4;
voronoiAccess3D(generator->sha, block_x, y, block_z, &x4, &y4, &z4);
out[(size_t)row * width + column] =
sampleBiomeNoise(&generator->bn, NULL, x4, y4, z4, NULL, 0);
}
}
}
static int axolotl_map_approx_height(
float *heights,
const Generator *generator,
const SurfaceNoise *surface_noise,
int x,
int z,
int width,
int height,
int sparse
) {
if (generator->dim != DIM_OVERWORLD || generator->mc < MC_1_18 ||
(generator->bn.nptype != -1 && generator->bn.nptype != NP_DEPTH)) {
return mapApproxHeight(
heights, NULL, generator, surface_noise, x, z, width, height);
}
const uint32_t flags = SAMPLE_NO_BIOME | (sparse ? SAMPLE_NO_SHIFT : 0);
for (int row = 0; row < height; row++) {
for (int column = 0; column < width; column++) {
int64_t noise_parameters[6];
sampleBiomeNoise(
&generator->bn, noise_parameters, x + column, 0, z + row, NULL, flags);
heights[(size_t)row * width + column] =
(float)noise_parameters[NP_DEPTH] / 76.0f;
}
}
return 0;
}
enum {
AXOLOTL_FEATURE_VILLAGE = 1 << 0,
AXOLOTL_FEATURE_OUTPOST = 1 << 1,
AXOLOTL_FEATURE_SHIPWRECK = 1 << 2,
AXOLOTL_FEATURE_MONUMENT = 1 << 3,
AXOLOTL_FEATURE_MANSION = 1 << 4,
AXOLOTL_FEATURE_ANCIENT_CITY = 1 << 5,
AXOLOTL_FEATURE_TRAIL_RUINS = 1 << 6,
AXOLOTL_FEATURE_TRIAL_CHAMBERS = 1 << 7,
AXOLOTL_FEATURE_RUINED_PORTAL = 1 << 8,
AXOLOTL_FEATURE_STRONGHOLD = 1 << 9,
AXOLOTL_FEATURE_SLIME_CHUNK = 1 << 10,
AXOLOTL_FEATURE_DESERT_PYRAMID = 1 << 11,
AXOLOTL_FEATURE_JUNGLE_TEMPLE = 1 << 12,
AXOLOTL_FEATURE_SWAMP_HUT = 1 << 13,
AXOLOTL_FEATURE_IGLOO = 1 << 14,
AXOLOTL_FEATURE_OCEAN_RUIN = 1 << 15,
AXOLOTL_FEATURE_BURIED_TREASURE = 1 << 16,
AXOLOTL_FEATURE_MINESHAFT = 1 << 17,
AXOLOTL_FEATURE_DESERT_WELL = 1 << 18,
AXOLOTL_FEATURE_GEODE = 1 << 19,
AXOLOTL_FEATURE_FORTRESS = 1 << 20,
AXOLOTL_FEATURE_BASTION = 1 << 21,
AXOLOTL_FEATURE_END_CITY = 1 << 22,
AXOLOTL_FEATURE_END_GATEWAY = 1 << 23,
};
typedef struct AxolotlStructureDefinition {
int type;
uint32_t flag;
} AxolotlStructureDefinition;
static const AxolotlStructureDefinition AXOLOTL_STRUCTURES[] = {
{Village, AXOLOTL_FEATURE_VILLAGE},
{Outpost, AXOLOTL_FEATURE_OUTPOST},
{Shipwreck, AXOLOTL_FEATURE_SHIPWRECK},
{Monument, AXOLOTL_FEATURE_MONUMENT},
{Mansion, AXOLOTL_FEATURE_MANSION},
{Ancient_City, AXOLOTL_FEATURE_ANCIENT_CITY},
{Trail_Ruins, AXOLOTL_FEATURE_TRAIL_RUINS},
{Trial_Chambers, AXOLOTL_FEATURE_TRIAL_CHAMBERS},
{Ruined_Portal, AXOLOTL_FEATURE_RUINED_PORTAL},
{Ruined_Portal_N, AXOLOTL_FEATURE_RUINED_PORTAL},
{Desert_Pyramid, AXOLOTL_FEATURE_DESERT_PYRAMID},
{Jungle_Temple, AXOLOTL_FEATURE_JUNGLE_TEMPLE},
{Swamp_Hut, AXOLOTL_FEATURE_SWAMP_HUT},
{Igloo, AXOLOTL_FEATURE_IGLOO},
{Ocean_Ruin, AXOLOTL_FEATURE_OCEAN_RUIN},
{Treasure, AXOLOTL_FEATURE_BURIED_TREASURE},
{Mineshaft, AXOLOTL_FEATURE_MINESHAFT},
{Desert_Well, AXOLOTL_FEATURE_DESERT_WELL},
{Geode, AXOLOTL_FEATURE_GEODE},
{Fortress, AXOLOTL_FEATURE_FORTRESS},
{Bastion, AXOLOTL_FEATURE_BASTION},
{End_City, AXOLOTL_FEATURE_END_CITY},
{End_Gateway, AXOLOTL_FEATURE_END_GATEWAY},
};
int32_t axolotl_seed_map_java_version(int32_t version) {
switch (version) {
/*
* Every release since the 1.21 winter drop shares its Overworld,
* Nether, and End generation, so 26.x and the late 1.21.x patches all
* map to the newest bundled engine.
*/
case 260200:
case 260102:
case 260100:
case 12109:
case 12106:
case 12105:
case 12104: return MC_NEWEST;
case 12103: return MC_1_21_3;
case 12101: return MC_1_21_1;
case 12000: return MC_1_20;
case 11904: return MC_1_19;
case 11902: return MC_1_19_2;
case 11800: return MC_1_18;
case 11700: return MC_1_17;
case 11600: return MC_1_16;
case 11500: return MC_1_15;
case 11400: return MC_1_14;
case 11300: return MC_1_13;
case 11200: return MC_1_12;
case 11100: return MC_1_11;
case 11000: return MC_1_10;
case 10900: return MC_1_9;
case 10800: return MC_1_8;
case 10700: return MC_1_7;
case 10600: return MC_1_6;
case 10500: return MC_1_5;
case 10400: return MC_1_4;
case 10300: return MC_1_3;
case 10200: return MC_1_2;
case 10100: return MC_1_1;
case 10000: return MC_1_0;
default: return MC_UNDEF;
}
}
static const unsigned char AXOLOTL_END_VOID[3] = {0x0D, 0x0D, 0x16};
static int axolotl_smooth_height_grid(
float *grid,
int width,
int height,
int passes
) {
float *scratch = malloc((size_t)width * (size_t)height * sizeof(float));
if (!scratch) return -1;
for (int pass = 0; pass < passes; pass++) {
for (int row = 0; row < height; row++) {
const int offset = row * width;
for (int column = 0; column < width; column++) {
const float west = grid[offset + (column > 0 ? column - 1 : 0)];
const float east = grid[offset + (column < width - 1 ? column + 1 : width - 1)];
scratch[offset + column] =
(west + 2.0f * grid[offset + column] + east) * 0.25f;
}
}
for (int row = 0; row < height; row++) {
const int north = (row > 0 ? row - 1 : 0) * width;
const int offset = row * width;
const int south = (row < height - 1 ? row + 1 : height - 1) * width;
for (int column = 0; column < width; column++) {
grid[offset + column] =
(scratch[north + column] + 2.0f * scratch[offset + column] +
scratch[south + column]) *
0.25f;
}
}
}
free(scratch);
return 0;
}
/*
* The End uses the game's density noise to distinguish real island surface
* from void. Like the reference map, the height grid is smoothed before slope
* lighting so surface noise does not make the broad, flat islands look hilly.
*/
static void axolotl_render_end(
uint8_t *rgb,
const Generator *generator,
const SurfaceNoise *surface_noise,
int32_t x,
int32_t z,
int32_t scale,
int32_t width,
int32_t height,
int32_t contours
) {
if (scale > 4) return;
const int passes = scale == 1 ? 3 : 1;
const int padding = passes + 1;
const int grid_width = width + 2 * padding;
const int grid_height = height + 2 * padding;
float *heights = malloc((size_t)grid_width * (size_t)grid_height * sizeof(float));
if (!heights) return;
uint8_t *land = malloc((size_t)width * (size_t)height);
if (!land) {
free(heights);
return;
}
if (mapEndSurfaceHeight(
heights, &generator->en, surface_noise,
x - padding, z - padding, grid_width, grid_height, scale, 0) != 0) {
free(land);
free(heights);
return;
}
for (int row = 0; row < height; row++) {
for (int column = 0; column < width; column++) {
land[(size_t)row * width + column] =
heights[(row + padding) * grid_width + column + padding] > 4.0f;
}
}
if (axolotl_smooth_height_grid(heights, grid_width, grid_height, passes) != 0) {
free(land);
free(heights);
return;
}
for (int row = 0; row < height; row++) {
for (int column = 0; column < width; column++) {
const size_t index = (size_t)row * (size_t)width + (size_t)column;
uint8_t *pixel = rgb + index * 3;
if (!land[index]) {
memcpy(pixel, AXOLOTL_END_VOID, 3);
continue;
}
const int grid_row = row + padding;
const int grid_column = column + padding;
const int surface = (int)floorf(heights[grid_row * grid_width + grid_column]);
const int west = (int)floorf(heights[grid_row * grid_width + grid_column - 1]);
const int north = (int)floorf(heights[(grid_row - 1) * grid_width + grid_column]);
const int northwest =
(int)floorf(heights[(grid_row - 1) * grid_width + grid_column - 1]);
float factor = 1.0f;
if (contours && (surface / 16 != west / 16 || surface / 16 != north / 16)) {
factor = 0.15f;
} else {
const int slope = 3 * surface - west - north - northwest;
if (slope != 0) {
const float delta = axolotl_clamp((float)slope / 16.0f, -0.3f, 0.3f);
factor = 1.0f + delta + (delta > 0.0f ? 0.1f : -0.1f);
if (factor < 0.72f) factor = 0.72f;
}
}
for (int channel = 0; channel < 3; channel++) {
pixel[channel] =
(uint8_t)axolotl_clamp((float)pixel[channel] * factor, 0.0f, 255.0f);
}
}
}
free(land);
free(heights);
}
static int axolotl_block_height(
const float *grid,
int grid_width,
int grid_height,
int column,
int row
) {
if (column < 0) column = 0;
if (row < 0) row = 0;
if (column > grid_width - 1) column = grid_width - 1;
if (row > grid_height - 1) row = grid_height - 1;
return (int)floorf(grid[row * grid_width + column]);
}
static float axolotl_clamp(float value, float low, float high) {
if (value < low) return low;
if (value > high) return high;
return value;
}
/*
* Minecraft maps compare each cell with its northwestern neighbours and use
* discrete brightness levels for the slope. The reference map applies the
* same rule to its cubiomes height grid, including 16-block contour bands.
*/
static void axolotl_shade_height_map(
uint8_t *rgb,
int32_t width,
int32_t height,
int32_t contours,
const float *grid,
int grid_width,
int grid_height,
int grid_origin_column,
int grid_origin_row,
int numerator,
int denominator
) {
for (int row = 0; row < height; row++) {
const int cell_row =
grid_origin_row + (int)(((int64_t)row * numerator) / denominator);
for (int column = 0; column < width; column++) {
const int cell_column =
grid_origin_column + (int)(((int64_t)column * numerator) / denominator);
const size_t index = (size_t)row * (size_t)width + (size_t)column;
const int here =
axolotl_block_height(grid, grid_width, grid_height, cell_column, cell_row);
const int west =
axolotl_block_height(grid, grid_width, grid_height, cell_column - 1, cell_row);
const int north =
axolotl_block_height(grid, grid_width, grid_height, cell_column, cell_row - 1);
const int northwest =
axolotl_block_height(grid, grid_width, grid_height, cell_column - 1, cell_row - 1);
float factor = 1.0f;
if (contours) {
const int band = here / 16;
if (band != west / 16 || band != north / 16) factor = 0.15f;
}
if (factor == 1.0f) {
const int slope = 3 * here - west - north - northwest;
if (slope != 0) {
const float delta = axolotl_clamp((float)slope / 16.0f, -0.3f, 0.3f);
factor = 1.0f + delta + (delta > 0.0f ? 0.1f : -0.1f);
if (factor < 0.72f) factor = 0.72f;
}
}
uint8_t *pixel = rgb + index * 3;
const int strongest = pixel[0] > pixel[1]
? (pixel[0] > pixel[2] ? pixel[0] : pixel[2])
: (pixel[1] > pixel[2] ? pixel[1] : pixel[2]);
if (factor < 1.0f && strongest > 25 && strongest - strongest * factor < 25)
factor = (strongest - 25.0f) / strongest;
for (int channel = 0; channel < 3; channel++) {
const float shaded = (float)pixel[channel] * factor;
pixel[channel] = (uint8_t)axolotl_clamp(shaded, 0.0f, 255.0f);
}
}
}
}
/*
* Far-zoom tiles cover too many blocks for the contiguous 1:4 height map, so
* relief comes from a sparse grid sampled point by point at each cell
* center. The grid stays coarse enough to keep far tiles interactive.
*/
static int axolotl_fill_sparse_height_grid(
const Generator *generator,
const SurfaceNoise *surface_noise,
int64_t min_block_x,
int64_t min_block_z,
int64_t span_blocks,
int grid,
float *out
) {
for (int row = 0; row < grid; row++) {
const int64_t block_z =
min_block_z + span_blocks * (2 * row + 1) / (2 * grid);
for (int column = 0; column < grid; column++) {
const int64_t block_x =
min_block_x + span_blocks * (2 * column + 1) / (2 * grid);
if (axolotl_map_approx_height(
out + (size_t)row * grid + column, generator, surface_noise,
(int)(block_x >> 2), (int)(block_z >> 2), 1, 1, 1) != 0) {
return -1;
}
}
}
return 0;
}
int axolotl_seed_map_render(
uint64_t seed,
int32_t minecraft_version,
int32_t generator_flags,
int32_t dimension,
int32_t x,
int32_t z,
int32_t scale,
int32_t width,
int32_t height,
int32_t elevation,
int32_t terrain,
int32_t contours,
const uint8_t *highlight_mask,
uint8_t *rgb,
size_t rgb_len
) {
if (!rgb || width <= 0 || height <= 0 || scale <= 0) return -1;
if (rgb_len < (size_t)width * (size_t)height * 3) return -2;
const int32_t render_elevation = elevation;
Generator generator;
setupGenerator(&generator, minecraft_version, generator_flags);
applySeed(&generator, dimension, seed);
Range range = {
.scale = scale,
.x = x,
.z = z,
.sx = width,
.sz = height,
.y = render_elevation / 4,
.sy = 1,
};
int *biomes = allocCache(&generator, range);
if (!biomes) return -3;
int result;
if (dimension == DIM_OVERWORLD && minecraft_version >= MC_1_18) {
/* Use the same surface/Voronoi path at every coarse scale. */
axolotl_gen_surface_biomes(
biomes, &generator, x, z, scale, width, height, render_elevation);
result = 0;
} else {
result = genBiomes(&generator, biomes, range);
}
if (result != 0) {
free(biomes);
return result;
}
unsigned char colors[256][3];
initBiomeColors(colors);
/* cubiomes' palette predates sulfur caves; keep this id consistent with
* the client picker instead of rendering it as black. */
colors[187][0] = 0xc8;
colors[187][1] = 0xc8;
colors[187][2] = 0x28;
biomesToImage(rgb, colors, biomes, width, height, 1, 2);
if (highlight_mask) {
for (int row = 0; row < height; row++) {
for (int column = 0; column < width; column++) {
size_t index = (size_t)row * (size_t)width + (size_t)column;
const int biome = biomes[index];
if (biome >= 0 && biome < 256 && highlight_mask[biome]) continue;
size_t pixel = index * 3;
if (dimension != DIM_OVERWORLD) {
rgb[pixel] = (uint8_t)(255 - (((255 - rgb[pixel]) * 51) >> 8));
rgb[pixel + 1] = (uint8_t)(255 - (((255 - rgb[pixel + 1]) * 51) >> 8));
rgb[pixel + 2] = (uint8_t)(255 - (((255 - rgb[pixel + 2]) * 51) >> 8));
} else {
rgb[pixel] = (uint8_t)((77 * rgb[pixel]) >> 8);
rgb[pixel + 1] = (uint8_t)((77 * rgb[pixel + 1]) >> 8);
rgb[pixel + 2] = (uint8_t)((77 * rgb[pixel + 2]) >> 8);
}
}
}
}
const int64_t min_block_x = (int64_t)x * scale;
const int64_t min_block_z = (int64_t)z * scale;
if (terrain && dimension == DIM_END) {
SurfaceNoise surface_noise;
initSurfaceNoise(&surface_noise, DIM_END, seed);
axolotl_render_end(
rgb, &generator, &surface_noise,
x, z, scale, width, height, contours);
}
if (terrain && dimension == DIM_OVERWORLD) {
SurfaceNoise surface_noise;
initSurfaceNoise(&surface_noise, DIM_OVERWORLD, seed);
if (scale <= 4) {
const int grid_x =
(int)(min_block_x >= 0 ? min_block_x / 4 : (min_block_x - 3) / 4) - 1;
const int grid_z =
(int)(min_block_z >= 0 ? min_block_z / 4 : (min_block_z - 3) / 4) - 1;
const int grid_width = (width * scale) / 4 + 3;
const int grid_height = (height * scale) / 4 + 3;
float *heights =
malloc((size_t)grid_width * (size_t)grid_height * sizeof(float));
if (heights) {
if (axolotl_map_approx_height(
heights, &generator, &surface_noise,
grid_x, grid_z, grid_width, grid_height, 0) == 0) {
axolotl_shade_height_map(
rgb, width, height, contours,
heights, grid_width, grid_height, 1, 1, scale, 4);
}
free(heights);
}
} else {
const int grid = scale == 16 ? 96 : 64;
float *heights = malloc((size_t)grid * (size_t)grid * sizeof(float));
if (heights) {
if (axolotl_fill_sparse_height_grid(
&generator, &surface_noise, min_block_x, min_block_z,
(int64_t)width * scale, grid, heights) == 0) {
axolotl_shade_height_map(
rgb, width, height, contours,
heights, grid, grid, 0, 0, grid, width);
}
free(heights);
}
}
}
free(biomes);
return 0;
}
static int add_feature(
AxolotlSeedMapFeature *out,
size_t out_len,
size_t *count,
int x,
int z,
uint32_t kind,
uint8_t approximate,
int8_t end_ship
) {
if (*count >= out_len) return 0;
out[*count].x = x;
out[*count].z = z;
out[*count].kind = kind;
out[*count].approximate = approximate;
out[*count].end_ship = end_ship;
*count += 1;
return 1;
}
static int8_t axolotl_end_city_has_ship(uint64_t seed, int block_x, int block_z) {
Piece pieces[END_CITY_PIECES_MAX];
const int count = getEndCityPieces(pieces, seed, block_x >> 4, block_z >> 4);
for (int index = 0; index < count; index++) {
if (pieces[index].type == END_SHIP) return 1;
}
return 0;
}
size_t axolotl_seed_map_find_features(
uint64_t seed,
int32_t minecraft_version,
int32_t generator_flags,
int32_t dimension,
int32_t min_x,
int32_t min_z,
int32_t max_x,
int32_t max_z,
uint32_t feature_mask,
AxolotlSeedMapFeature *out,
size_t out_len
) {
if (!out || out_len == 0) return 0;
Generator generator;
setupGenerator(&generator, minecraft_version, generator_flags);
applySeed(&generator, dimension, seed);
SurfaceNoise end_surface_noise;
if (dimension == DIM_END) initSurfaceNoise(&end_surface_noise, DIM_END, seed);
size_t count = 0;
for (size_t index = 0; index < sizeof(AXOLOTL_STRUCTURES) / sizeof(AXOLOTL_STRUCTURES[0]); index++) {
const AxolotlStructureDefinition definition = AXOLOTL_STRUCTURES[index];
if (!(feature_mask & definition.flag)) continue;
StructureConfig config;
if (!getStructureConfig(definition.type, minecraft_version, &config)) continue;
if (config.dim != dimension || config.regionSize <= 0) continue;
const int region_span = config.regionSize * 16;
const int min_region_x = min_x >= 0 ? min_x / region_span : (min_x - region_span + 1) / region_span;
const int min_region_z = min_z >= 0 ? min_z / region_span : (min_z - region_span + 1) / region_span;
const int max_region_x = max_x >= 0 ? max_x / region_span : (max_x - region_span + 1) / region_span;
const int max_region_z = max_z >= 0 ? max_z / region_span : (max_z - region_span + 1) / region_span;
for (int region_x = min_region_x; region_x <= max_region_x; region_x++) {
for (int region_z = min_region_z; region_z <= max_region_z; region_z++) {
Pos position;
if (!getStructurePos(definition.type, minecraft_version, seed, region_x, region_z, &position)) continue;
if (position.x < min_x || position.x > max_x || position.z < min_z || position.z > max_z) continue;
if (!isViableStructurePos(definition.type, &generator, position.x, position.z, 0)) continue;
if (definition.type == End_City &&
!isViableEndCityTerrain(&generator, &end_surface_noise, position.x, position.z))
continue;
if (dimension == DIM_OVERWORLD && minecraft_version >= MC_1_18 &&
!isViableStructureTerrain(definition.type, &generator, position.x, position.z))
continue;
const int8_t end_ship = definition.type == End_City
? axolotl_end_city_has_ship(seed, position.x, position.z)
: -1;
if (!add_feature(
out, out_len, &count, position.x, position.z,
definition.flag, 0, end_ship)) return count;
}
}
}
if (dimension == DIM_OVERWORLD && feature_mask & AXOLOTL_FEATURE_STRONGHOLD) {
StrongholdIter stronghold;
initFirstStronghold(&stronghold, minecraft_version, seed);
for (int index = 0; index < 32; index++) {
if (nextStronghold(&stronghold, &generator) <= 0) break;
if (stronghold.pos.x >= min_x && stronghold.pos.x <= max_x && stronghold.pos.z >= min_z && stronghold.pos.z <= max_z) {
if (!add_feature(
out, out_len, &count, stronghold.pos.x, stronghold.pos.z,
AXOLOTL_FEATURE_STRONGHOLD, 0, -1)) return count;
}
}
}
if (dimension == DIM_OVERWORLD && feature_mask & AXOLOTL_FEATURE_SLIME_CHUNK) {
const int min_chunk_x = min_x >= 0 ? min_x / 16 : (min_x - 15) / 16;
const int min_chunk_z = min_z >= 0 ? min_z / 16 : (min_z - 15) / 16;
const int max_chunk_x = max_x >= 0 ? max_x / 16 : (max_x - 15) / 16;
const int max_chunk_z = max_z >= 0 ? max_z / 16 : (max_z - 15) / 16;
for (int chunk_x = min_chunk_x; chunk_x <= max_chunk_x; chunk_x++) {
for (int chunk_z = min_chunk_z; chunk_z <= max_chunk_z; chunk_z++) {
if (!isSlimeChunk(seed, chunk_x, chunk_z)) continue;
if (!add_feature(
out, out_len, &count, chunk_x * 16 + 8, chunk_z * 16 + 8,
AXOLOTL_FEATURE_SLIME_CHUNK, 0, -1)) return count;
}
}
}
return count;
}
int axolotl_seed_map_get_spawn(
uint64_t seed,
int32_t minecraft_version,
int32_t generator_flags,
int32_t *x,
int32_t *z
) {
if (!x || !z) return -1;
Generator generator;
setupGenerator(&generator, minecraft_version, generator_flags);
applySeed(&generator, DIM_OVERWORLD, seed);
Pos position = estimateSpawn(&generator, NULL);
*x = position.x;
*z = position.z;
return 0;
}
int32_t axolotl_seed_map_biome_at(
uint64_t seed,
int32_t minecraft_version,
int32_t generator_flags,
int32_t dimension,
int32_t x,
int32_t y,
int32_t z
) {
Generator generator;
setupGenerator(&generator, minecraft_version, generator_flags);
applySeed(&generator, dimension, seed);
return getBiomeAt(&generator, 1, x, y, z);
}
typedef struct AxolotlVeinNoise {
DoublePerlinNoise toggle;
DoublePerlinNoise vein_a;
DoublePerlinNoise vein_b;
DoublePerlinNoise gap;
PerlinNoise octaves[8];
uint64_t ore_lo;
uint64_t ore_hi;
} AxolotlVeinNoise;
/*
* Seeds the Overworld vein noises the same way the game does: the world seed
* forks a positional random whose per-noise instances are XORed with the MD5
* halves of their resource ids.
*/
static void axolotl_init_vein_noise(AxolotlVeinNoise *vein_noise, uint64_t seed) {
static const double unit_amplitude[] = {1.0};
Xoroshiro base;
xSetSeed(&base, seed);
const uint64_t lo = xNextLong(&base);
const uint64_t hi = xNextLong(&base);
PerlinNoise *slot = vein_noise->octaves;
Xoroshiro xr;
xr.lo = lo ^ 0x6B86C7820A307171ULL; /* minecraft:ore_veininess */
xr.hi = hi ^ 0xD87FB0FEFD9C1624ULL;
slot += xDoublePerlinInit(&vein_noise->toggle, &xr, slot, unit_amplitude, -8, 1, -1);
xr.lo = lo ^ 0x4CD8D69B9A841649ULL; /* minecraft:ore_vein_a */
xr.hi = hi ^ 0xCDD63F17BFE8F5EDULL;
slot += xDoublePerlinInit(&vein_noise->vein_a, &xr, slot, unit_amplitude, -7, 1, -1);
xr.lo = lo ^ 0x6B26220B31F7C6C9ULL; /* minecraft:ore_vein_b */
xr.hi = hi ^ 0xAE077EDEBF6AAEC1ULL;
slot += xDoublePerlinInit(&vein_noise->vein_b, &xr, slot, unit_amplitude, -7, 1, -1);
xr.lo = lo ^ 0x9C4CC6B2FB0BE4BBULL; /* minecraft:ore_gap */
xr.hi = hi ^ 0xBD5964705573BB5EULL;
xDoublePerlinInit(&vein_noise->gap, &xr, slot, unit_amplitude, -5, 1, -1);
vein_noise->ore_lo = lo ^ 0x9B88124DE600116DULL; /* minecraft:ore */
vein_noise->ore_hi = hi ^ 0x2AE68055AA4A7761ULL;
}
static uint64_t axolotl_block_position_seed(int32_t x, int32_t y, int32_t z) {
int64_t l = (int64_t)(int32_t)((uint32_t)x * 3129871u) ^
((int64_t)z * 116129781LL) ^ (int64_t)y;
l = l * l * 42317861LL + l * 11LL;
return (uint64_t)(l >> 16);
}
static Xoroshiro axolotl_vein_block_rng(const AxolotlVeinNoise *vein_noise, int32_t x, int32_t y, int32_t z) {
Xoroshiro xr;
xr.lo = axolotl_block_position_seed(x, y, z) ^ vein_noise->ore_lo;
xr.hi = vein_noise->ore_hi;
if (xr.lo == 0 && xr.hi == 0) {
xr.lo = 0x9E3779B97F4A7C15ULL;
xr.hi = 0x6A09E667F3BCC909ULL;
}
return xr;
}
static double axolotl_clamped_map(double value, double from_min, double from_max, double to_min, double to_max) {
if (value <= from_min) return to_min;
if (value >= from_max) return to_max;
return to_min + (value - from_min) / (from_max - from_min) * (to_max - to_min);
}
/*
* Follows the game's vein rules on a 2-block sampling grid: the low frequency
* veininess noise selects copper (positive, Y 0..50) or iron (negative,
* Y -60..-8) with a 20-block edge fade towards a 0.4 threshold. A sampled
* position counts as ore when the 70% solidness roll passes, both ridge
* noises stay inside the 0.08 band, the richness roll (10%..30% by
* veininess) succeeds, and the gap noise is above -0.3 — the same checks and
* positional-random call order the game uses, evaluated without the noise
* cell interpolation, so results are close estimates.
*/
size_t axolotl_seed_map_scan_vein(
uint64_t seed,
int32_t chunk_x,
int32_t chunk_z,
int32_t vein_kind,
int32_t *out_xyz,
size_t out_cap
) {
if (!out_xyz || out_cap == 0) return 0;
AxolotlVeinNoise vein_noise;
axolotl_init_vein_noise(&vein_noise, seed);
const int min_y = vein_kind == 0 ? 0 : -60;
const int max_y = vein_kind == 0 ? 50 : -8;
const int block_x = chunk_x * 16;
const int block_z = chunk_z * 16;
uint8_t column_taken[64] = {0};
size_t count = 0;
for (int y = min_y; y <= max_y && count < out_cap; y += 2) {
const int edge = (max_y - y) < (y - min_y) ? (max_y - y) : (y - min_y);
const double edge_fade = edge >= 20 ? 0.0 : -0.2 + 0.01 * (double)edge;
const double coarse = sampleDoublePerlin(
&vein_noise.toggle,
(block_x + 8) * 1.5, y * 1.5, (block_z + 8) * 1.5);
const double coarse_signed = vein_kind == 0 ? coarse : -coarse;
if (coarse_signed + edge_fade < 0.34) continue;
for (int dx = 0; dx < 16 && count < out_cap; dx += 2) {
for (int dz = 0; dz < 16 && count < out_cap; dz += 2) {
const int column = (dx >> 1) * 8 + (dz >> 1);
if (column_taken[column]) continue;
const int x = block_x + dx;
const int z = block_z + dz;
const double toggle =
sampleDoublePerlin(&vein_noise.toggle, x * 1.5, y * 1.5, z * 1.5);
const double toggle_signed = vein_kind == 0 ? toggle : -toggle;
if (toggle_signed + edge_fade < 0.4) continue;
Xoroshiro block_rng = axolotl_vein_block_rng(&vein_noise, x, y, z);
if (xNextFloat(&block_rng) > 0.7f) continue;
if (fabs(sampleDoublePerlin(&vein_noise.vein_a, x * 4.0, y * 4.0, z * 4.0)) >= 0.08)
continue;
if (fabs(sampleDoublePerlin(&vein_noise.vein_b, x * 4.0, y * 4.0, z * 4.0)) >= 0.08)
continue;
const double richness =
axolotl_clamped_map(toggle_signed, 0.4, 0.6, 0.1, 0.3);
if (xNextFloat(&block_rng) >= richness) continue;
if (sampleDoublePerlin(&vein_noise.gap, x, y, z) <= -0.3) continue;
column_taken[column] = 1;
out_xyz[count * 3] = x;
out_xyz[count * 3 + 1] = y;
out_xyz[count * 3 + 2] = z;
count++;
}
}
}
return count;
}
int axolotl_seed_map_surface_heights(
uint64_t seed,
int32_t minecraft_version,
int32_t generator_flags,
int32_t x,
int32_t z,
int32_t width,
int32_t height,
float *out
) {
if (!out || width <= 0 || height <= 0) return -1;
Generator generator;
setupGenerator(&generator, minecraft_version, generator_flags);
applySeed(&generator, DIM_OVERWORLD, seed);
SurfaceNoise surface_noise;
initSurfaceNoise(&surface_noise, DIM_OVERWORLD, seed);
return mapApproxHeight(out, NULL, &generator, &surface_noise, x, z, width, height);
}
+115
View File
@@ -0,0 +1,115 @@
#ifndef AXOLOTL_CUBIOMES_BRIDGE_H
#define AXOLOTL_CUBIOMES_BRIDGE_H
#include <stddef.h>
#include <stdint.h>
typedef struct AxolotlSeedMapFeature {
int32_t x;
int32_t z;
uint32_t kind;
uint8_t approximate;
int8_t end_ship;
} AxolotlSeedMapFeature;
/*
* Maps an Axolotl version code (major * 10000 + minor * 100 + patch) to the
* matching cubiomes MCVersion, or 0 (MC_UNDEF) when unsupported.
*/
int32_t axolotl_seed_map_java_version(int32_t version);
/*
* Renders a biome tile into an RGB buffer (3 bytes per pixel).
*
* Coordinates are given in scaled units (block / scale). When `terrain` is
* non-zero, Overworld tiles use approximate surface heights and nearby End
* tiles use the dimension's density-derived surface height. `contours`
* additionally darkens height-band boundaries. `highlight_mask` may be NULL,
* or point to 256 bytes where a non-zero entry keeps that biome id at full
* color while all other biomes are faded.
*/
int axolotl_seed_map_render(
uint64_t seed,
int32_t minecraft_version,
int32_t generator_flags,
int32_t dimension,
int32_t x,
int32_t z,
int32_t scale,
int32_t width,
int32_t height,
int32_t elevation,
int32_t terrain,
int32_t contours,
const uint8_t *highlight_mask,
uint8_t *rgb,
size_t rgb_len
);
size_t axolotl_seed_map_find_features(
uint64_t seed,
int32_t minecraft_version,
int32_t generator_flags,
int32_t dimension,
int32_t min_x,
int32_t min_z,
int32_t max_x,
int32_t max_z,
uint32_t feature_mask,
AxolotlSeedMapFeature *out,
size_t out_len
);
int axolotl_seed_map_get_spawn(
uint64_t seed,
int32_t minecraft_version,
int32_t generator_flags,
int32_t *x,
int32_t *z
);
/*
* Returns the biome id at a block position (sampled at the given Y level),
* or -1 when the engine cannot resolve it.
*/
int32_t axolotl_seed_map_biome_at(
uint64_t seed,
int32_t minecraft_version,
int32_t generator_flags,
int32_t dimension,
int32_t x,
int32_t y,
int32_t z
);
/*
* Scans one chunk for large ore veins (`vein_kind`: 0 = copper, 1 = iron) by
* sampling the Overworld vein noises. Writes up to `out_cap` hits as
* (x, y, z) triples into `out_xyz` and returns the number of hits.
*/
size_t axolotl_seed_map_scan_vein(
uint64_t seed,
int32_t chunk_x,
int32_t chunk_z,
int32_t vein_kind,
int32_t *out_xyz,
size_t out_cap
);
/*
* Fills `out` (length `width * height`) with approximate Overworld surface
* heights in blocks. `x` and `z` are in 1:4 scale units and the grid has a
* stride of one unit (4 blocks). Returns 0 on success.
*/
int axolotl_seed_map_surface_heights(
uint64_t seed,
int32_t minecraft_version,
int32_t generator_flags,
int32_t x,
int32_t z,
int32_t width,
int32_t height,
float *out
);
#endif
File diff suppressed because it is too large. Load diff
+873
View File
@@ -0,0 +1,873 @@
//! Seed-based ore prediction for Java 1.18+.
//!
//! Predicts scattered-ore placement attempts from the world seed by
//! replaying Minecraft's chunk population RNG (decoration seed, feature seed,
//! placement modifiers, and the ore blob's own RNG consumption). Terrain is
//! not simulated, so results are estimates: caves and surface exposure can
//! shift or remove blobs. Attempts are cross-checked against cubiomes'
//! approximate surface height to grade confidence.
use std::io;
use serde::{Deserialize, Serialize};
use super::rng::{Xoroshiro, decoration_seed, feature_rng, mc_sin};
use super::{Dimension, parse_seed, resolve_java_version};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum OreKind {
Diamond,
Iron,
IronVein,
Copper,
CopperVein,
Gold,
Redstone,
Lapis,
Coal,
Netherite,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OreScanRequest {
pub seed: String,
pub version: String,
pub dimension: Dimension,
pub ores: Vec<OreKind>,
/// Interleaved chunk coordinates: `[cx0, cz0, cx1, cz1, ...]`.
pub chunks: Vec<i32>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OreHit {
pub ore: OreKind,
pub x: i32,
pub y: i32,
pub z: i32,
pub verified: bool,
pub y_min: i32,
pub y_max: i32,
pub precision: u8,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OreChunkResult {
pub cx: i32,
pub cz: i32,
pub hits: Vec<OreHit>,
}
#[derive(Clone, Copy, Debug)]
enum CountModifier {
Fixed(u32),
Uniform { min: u32, max: u32 },
Rarity(u32),
}
#[derive(Clone, Copy, Debug)]
enum HeightProvider {
Uniform { min: i32, max: i32 },
Trapezoid { min: i32, max: i32, plateau: i32 },
}
impl HeightProvider {
fn sample(self, rng: &mut Xoroshiro) -> i32 {
match self {
Self::Uniform { min, max } => {
rng.next_int_between_inclusive(min, max)
}
Self::Trapezoid { min, max, plateau } => {
let span = max - min;
if plateau >= span {
return rng.next_int_between_inclusive(min, max);
}
let lower = (span - plateau) / 2;
let upper = span - lower;
min + rng.next_int_between_inclusive(0, upper)
+ rng.next_int_between_inclusive(0, lower)
}
}
}
}
#[derive(Clone, Copy, Debug)]
struct PlacedOre {
feature_index: u32,
step: u32,
count: CountModifier,
height: HeightProvider,
blob_size: u32,
discard_on_air: f32,
precision: u8,
scattered: bool,
}
const UNDERGROUND_ORES_STEP: u32 = 6;
const UNDERGROUND_DECORATION_STEP: u32 = 7;
/*
* Global feature indices for the UNDERGROUND_ORES step, computed with
* Minecraft's FeatureSorter over the vanilla biome sets (validated against
* official generated data for 1.18.2 through 1.21.3): the base sequence
* starts at ore_coal_upper = 9 after the nine stone-patch features, and
* ore_copper sorts AFTER dripstone's ore_copper_large. `ore_diamond_medium`
* joined the base list in 1.20.5, shifting every later entry by one.
* Placement counts, height providers, blob sizes, and discard chances come
* from the vanilla OrePlacements/OreFeatures definitions.
*/
fn overworld_ore_features(kind: OreKind, mc_code: i32) -> Vec<PlacedOre> {
let has_medium_diamond = mc_code >= 12000;
let shift = u32::from(has_medium_diamond);
let ore = |feature_index: u32,
count: CountModifier,
height: HeightProvider,
blob_size: u32,
discard_on_air: f32,
precision: u8| PlacedOre {
feature_index,
step: UNDERGROUND_ORES_STEP,
count,
height,
blob_size,
discard_on_air,
precision,
scattered: false,
};
match kind {
OreKind::Coal => vec![
ore(
9,
CountModifier::Fixed(30),
HeightProvider::Uniform { min: 136, max: 319 },
17,
0.0,
88,
),
ore(
10,
CountModifier::Fixed(20),
HeightProvider::Trapezoid {
min: 0,
max: 192,
plateau: 0,
},
17,
0.5,
84,
),
],
OreKind::Iron => vec![
ore(
11,
CountModifier::Fixed(90),
HeightProvider::Trapezoid {
min: 80,
max: 384,
plateau: 0,
},
9,
0.0,
88,
),
ore(
12,
CountModifier::Fixed(10),
HeightProvider::Trapezoid {
min: -24,
max: 56,
plateau: 0,
},
9,
0.0,
90,
),
ore(
13,
CountModifier::Fixed(10),
HeightProvider::Uniform { min: -64, max: 72 },
4,
0.0,
90,
),
],
OreKind::Gold => vec![
ore(
14,
CountModifier::Fixed(4),
HeightProvider::Trapezoid {
min: -64,
max: 32,
plateau: 0,
},
9,
0.5,
87,
),
ore(
15,
CountModifier::Uniform { min: 0, max: 1 },
HeightProvider::Uniform { min: -64, max: -48 },
9,
0.5,
87,
),
],
OreKind::Redstone => vec![
ore(
16,
CountModifier::Fixed(4),
HeightProvider::Uniform { min: -64, max: 15 },
8,
0.0,
90,
),
ore(
17,
CountModifier::Fixed(8),
HeightProvider::Trapezoid {
min: -96,
max: -32,
plateau: 0,
},
8,
0.0,
90,
),
],
OreKind::Diamond => {
let mut features = vec![
ore(
18,
CountModifier::Fixed(7),
HeightProvider::Trapezoid {
min: -144,
max: 16,
plateau: 0,
},
4,
0.5,
88,
),
ore(
19 + shift,
CountModifier::Rarity(9),
HeightProvider::Trapezoid {
min: -144,
max: 16,
plateau: 0,
},
12,
0.7,
82,
),
ore(
20 + shift,
CountModifier::Fixed(4),
HeightProvider::Trapezoid {
min: -144,
max: 16,
plateau: 0,
},
8,
1.0,
92,
),
];
if has_medium_diamond {
features.push(ore(
19,
CountModifier::Fixed(2),
HeightProvider::Uniform { min: -64, max: -4 },
8,
0.5,
88,
));
}
features
}
OreKind::Lapis => vec![
ore(
21 + shift,
CountModifier::Fixed(2),
HeightProvider::Trapezoid {
min: -32,
max: 32,
plateau: 0,
},
7,
0.0,
88,
),
ore(
22 + shift,
CountModifier::Fixed(4),
HeightProvider::Uniform { min: -64, max: 64 },
7,
1.0,
92,
),
],
OreKind::Copper => vec![ore(
24 + shift,
CountModifier::Fixed(16),
HeightProvider::Trapezoid {
min: -16,
max: 112,
plateau: 0,
},
10,
0.0,
88,
)],
OreKind::IronVein | OreKind::CopperVein | OreKind::Netherite => vec![],
}
}
/*
* Large veins come from the Overworld vein noises rather than the population
* RNG, so they are scanned natively per chunk. Copper veins live at Y 0..50
* and iron veins at Y -60..-8.
*/
fn scan_vein_hits(
kind: OreKind,
world_seed: u64,
cx: i32,
cz: i32,
surface: Option<&SurfaceGrid>,
) -> Vec<OreHit> {
let vein_code = i32::from(kind != OreKind::CopperVein);
let mut buffer = [0_i32; 24];
let count = unsafe {
super::axolotl_seed_map_scan_vein(
world_seed,
cx,
cz,
vein_code,
buffer.as_mut_ptr(),
buffer.len() / 3,
)
};
buffer[..count * 3]
.chunks_exact(3)
.map(|triple| {
let (x, y, z) = (triple[0], triple[1], triple[2]);
let verified = surface
.map(|grid| (y as f32) < grid.height_at(x, z) - 6.0)
.unwrap_or(false);
OreHit {
ore: kind,
x,
y,
z,
verified,
y_min: y,
y_max: y,
precision: if verified { 93 } else { 78 },
}
})
.collect()
}
/*
* Ancient debris indices in the UNDERGROUND_DECORATION step are 21 and 22
* across every supported version (stable under all nether biome orderings).
* Both are single-attempt scattered-ore features that are always buried.
*/
fn nether_ore_features(kind: OreKind, _mc_code: i32) -> Vec<PlacedOre> {
if kind != OreKind::Netherite {
return vec![];
}
let ore = |feature_index: u32, height: HeightProvider, blob_size: u32| {
PlacedOre {
feature_index,
step: UNDERGROUND_DECORATION_STEP,
count: CountModifier::Fixed(1),
height,
blob_size,
discard_on_air: 1.0,
precision: 95,
scattered: true,
}
};
vec![
ore(
21,
HeightProvider::Trapezoid {
min: 8,
max: 24,
plateau: 0,
},
3,
),
ore(22, HeightProvider::Uniform { min: 8, max: 119 }, 2),
]
}
pub fn supports_ores(version_code: i32, dimension: Dimension) -> bool {
version_code >= 11800 && !matches!(dimension, Dimension::End)
}
pub fn scan_ores(request: OreScanRequest) -> io::Result<Vec<OreChunkResult>> {
let (mc_version, version_code) = resolve_java_version(&request.version)?;
if !supports_ores(version_code, request.dimension) {
return Err(io::Error::other(
"Ore prediction supports Java 1.18+ Overworld and Nether maps.",
));
}
if !request.chunks.len().is_multiple_of(2) || request.chunks.len() > 4_096 {
return Err(io::Error::other("The ore scan chunk list is invalid."));
}
let world_seed = parse_seed(&request.seed) as i64;
let chunk_pairs: Vec<(i32, i32)> = request
.chunks
.chunks_exact(2)
.map(|pair| (pair[0], pair[1]))
.collect();
let surface = match request.dimension {
Dimension::Overworld => {
SurfaceGrid::sample(world_seed as u64, mc_version, &chunk_pairs)
}
_ => None,
};
let (world_bottom, world_top) = match request.dimension {
Dimension::Overworld => (-64, 320),
_ => (0, 128),
};
let requested_kinds = dedupe_kinds(&request.ores);
let vein_kinds: Vec<OreKind> =
if matches!(request.dimension, Dimension::Overworld) {
requested_kinds
.iter()
.copied()
.filter(|kind| {
matches!(kind, OreKind::IronVein | OreKind::CopperVein)
})
.collect()
} else {
Vec::new()
};
let mut features: Vec<(OreKind, PlacedOre)> = Vec::new();
for kind in requested_kinds {
let kind_features = match request.dimension {
Dimension::Overworld => overworld_ore_features(kind, version_code),
Dimension::Nether => nether_ore_features(kind, version_code),
Dimension::End => vec![],
};
features
.extend(kind_features.into_iter().map(|feature| (kind, feature)));
}
let mut results = Vec::with_capacity(chunk_pairs.len());
for (cx, cz) in chunk_pairs {
let chunk_decoration_seed =
decoration_seed(world_seed, cx * 16, cz * 16);
let mut hits = Vec::new();
for &kind in &vein_kinds {
hits.extend(scan_vein_hits(
kind,
world_seed as u64,
cx,
cz,
surface.as_ref(),
));
}
for &(kind, feature) in &features {
let mut rng = feature_rng(
chunk_decoration_seed,
feature.feature_index,
feature.step,
);
let attempts = match feature.count {
CountModifier::Fixed(count) => count,
CountModifier::Uniform { min, max } => {
min + rng.next_int(max - min + 1)
}
CountModifier::Rarity(chance) => {
u32::from(rng.next_f32() < 1.0 / chance as f32)
}
};
for _ in 0..attempts {
let x = cx * 16 + rng.next_int(16) as i32;
let z = cz * 16 + rng.next_int(16) as i32;
let y = feature.height.sample(&mut rng);
let surface_height =
surface.as_ref().map(|grid| grid.height_at(x, z));
let blob = if feature.scattered {
Some((y, y))
} else {
simulate_blob(
&mut rng,
x,
y,
z,
feature.blob_size,
feature.discard_on_air,
surface_height,
world_bottom,
world_top,
)
};
let Some((blob_min_y, blob_max_y)) = blob else {
continue;
};
let verified = match surface_height {
Some(surface_y) => (blob_max_y as f32) < surface_y - 6.0,
None => feature.discard_on_air >= 1.0,
};
let precision = if verified {
feature.precision
} else {
feature.precision.saturating_sub(18).max(50)
};
hits.push(OreHit {
ore: kind,
x,
y,
z,
verified,
y_min: blob_min_y,
y_max: blob_max_y,
precision,
});
}
}
results.push(OreChunkResult { cx, cz, hits });
}
Ok(results)
}
fn dedupe_kinds(kinds: &[OreKind]) -> Vec<OreKind> {
let mut seen = Vec::new();
for &kind in kinds {
if !seen.contains(&kind) {
seen.push(kind);
}
}
seen
}
/*
* Replays the RNG consumption of `OreFeature.place`, assuming solid terrain:
* the blob angle, the two Y jitters, one radius roll per segment, and — for
* partially air-sensitive ores — one roll per candidate cell. Returns the
* simulated blob's Y extent, or None when the surface pre-check would skip
* placement entirely.
*/
#[allow(clippy::too_many_arguments)]
fn simulate_blob(
rng: &mut Xoroshiro,
x: i32,
y: i32,
z: i32,
size: u32,
discard_on_air: f32,
surface_height: Option<f32>,
world_bottom: i32,
world_top: i32,
) -> Option<(i32, i32)> {
let angle = rng.next_f32() * std::f32::consts::PI;
let spread = size as f32 / 8.0;
let margin = (((size as f32 / 16.0) * 2.0 + 1.0) / 2.0).ceil() as i32;
let x0 = f64::from(x) + f64::from(angle).sin() * f64::from(spread);
let x1 = f64::from(x) - f64::from(angle).sin() * f64::from(spread);
let z0 = f64::from(z) + f64::from(angle).cos() * f64::from(spread);
let z1 = f64::from(z) - f64::from(angle).cos() * f64::from(spread);
let y0 = f64::from(y + rng.next_int(3) as i32 - 2);
let y1 = f64::from(y + rng.next_int(3) as i32 - 2);
let box_min_x = x - (spread.ceil() as i32) - margin;
let box_min_y = y - 2 - margin;
let box_min_z = z - (spread.ceil() as i32) - margin;
if let Some(surface_y) = surface_height
&& box_min_y as f32 > surface_y
{
return None;
}
let segment_count = size as usize;
let mut segments = vec![[0.0_f64; 4]; segment_count];
for (index, segment) in segments.iter_mut().enumerate() {
let progress = index as f32 / segment_count as f32;
let center_x = lerp(f64::from(progress), x0, x1);
let center_y = lerp(f64::from(progress), y0, y1);
let center_z = lerp(f64::from(progress), z0, z1);
let radius_roll = rng.next_f64() * f64::from(size) / 16.0;
let radius = ((f64::from(mc_sin(std::f32::consts::PI * progress))
+ 1.0)
* radius_roll
+ 1.0)
/ 2.0;
*segment = [center_x, center_y, center_z, radius];
}
for first in 0..segment_count.saturating_sub(1) {
if segments[first][3] <= 0.0 {
continue;
}
for second in first + 1..segment_count {
if segments[second][3] <= 0.0 {
continue;
}
let dx = segments[first][0] - segments[second][0];
let dy = segments[first][1] - segments[second][1];
let dz = segments[first][2] - segments[second][2];
let dr = segments[first][3] - segments[second][3];
if dr * dr > dx * dx + dy * dy + dz * dz {
if dr > 0.0 {
segments[second][3] = -1.0;
} else {
segments[first][3] = -1.0;
}
}
}
}
let needs_cell_rolls = discard_on_air > 0.0 && discard_on_air < 1.0;
let mut placed = std::collections::HashSet::new();
let mut blob_min_y = i32::MAX;
let mut blob_max_y = i32::MIN;
for segment in &segments {
let radius = segment[3];
if radius < 0.0 {
continue;
}
let min_cell_x = ((segment[0] - radius).floor() as i32).max(box_min_x);
let min_cell_y = ((segment[1] - radius).floor() as i32).max(box_min_y);
let min_cell_z = ((segment[2] - radius).floor() as i32).max(box_min_z);
let max_cell_x = ((segment[0] + radius).floor() as i32).max(min_cell_x);
let max_cell_y = ((segment[1] + radius).floor() as i32).max(min_cell_y);
let max_cell_z = ((segment[2] + radius).floor() as i32).max(min_cell_z);
for cell_x in min_cell_x..=max_cell_x {
let dx = (f64::from(cell_x) + 0.5 - segment[0]) / radius;
if dx * dx >= 1.0 {
continue;
}
for cell_y in min_cell_y..=max_cell_y {
let dy = (f64::from(cell_y) + 0.5 - segment[1]) / radius;
if dx * dx + dy * dy >= 1.0 {
continue;
}
for cell_z in min_cell_z..=max_cell_z {
let dz = (f64::from(cell_z) + 0.5 - segment[2]) / radius;
if dx * dx + dy * dy + dz * dz >= 1.0 {
continue;
}
if cell_y < world_bottom || cell_y >= world_top {
continue;
}
if !placed.insert((cell_x, cell_y, cell_z)) {
continue;
}
if needs_cell_rolls {
rng.next_f32();
}
blob_min_y = blob_min_y.min(cell_y);
blob_max_y = blob_max_y.max(cell_y);
}
}
}
}
if blob_min_y > blob_max_y {
return Some((y, y));
}
Some((blob_min_y, blob_max_y))
}
fn lerp(progress: f64, from: f64, to: f64) -> f64 {
from + progress * (to - from)
}
/*
* A cached approximate surface-height grid at 1:4 scale covering the bounding
* box of the scanned chunks.
*/
struct SurfaceGrid {
origin_x: i32,
origin_z: i32,
width: i32,
height: i32,
values: Vec<f32>,
}
impl SurfaceGrid {
fn sample(
seed: u64,
mc_version: i32,
chunks: &[(i32, i32)],
) -> Option<Self> {
if chunks.is_empty() {
return None;
}
let min_cx = chunks.iter().map(|chunk| chunk.0).min()?;
let max_cx = chunks.iter().map(|chunk| chunk.0).max()?;
let min_cz = chunks.iter().map(|chunk| chunk.1).min()?;
let max_cz = chunks.iter().map(|chunk| chunk.1).max()?;
let origin_x = min_cx * 4;
let origin_z = min_cz * 4;
let width = (max_cx - min_cx + 1) * 4;
let height = (max_cz - min_cz + 1) * 4;
if width <= 0 || height <= 0 || width as i64 * height as i64 > 1 << 20 {
return None;
}
let mut values = vec![0.0_f32; (width * height) as usize];
let result = unsafe {
super::axolotl_seed_map_surface_heights(
seed,
mc_version,
0,
origin_x,
origin_z,
width,
height,
values.as_mut_ptr(),
)
};
if result != 0 {
return None;
}
Some(Self {
origin_x,
origin_z,
width,
height,
values,
})
}
fn height_at(&self, block_x: i32, block_z: i32) -> f32 {
let column =
(block_x.div_euclid(4) - self.origin_x).clamp(0, self.width - 1);
let row =
(block_z.div_euclid(4) - self.origin_z).clamp(0, self.height - 1);
self.values[(row * self.width + column) as usize]
}
}
#[cfg(test)]
mod tests {
use super::*;
fn scan(
seed: &str,
version: &str,
dimension: Dimension,
ores: Vec<OreKind>,
) -> Vec<OreChunkResult> {
scan_ores(OreScanRequest {
seed: seed.to_owned(),
version: version.to_owned(),
dimension,
ores,
chunks: vec![0, 0, 1, 0, -1, -1],
})
.expect("scan should succeed")
}
#[test]
fn ore_scans_are_deterministic() {
let first = scan(
"10292992",
"1.21.3",
Dimension::Overworld,
vec![OreKind::Diamond],
);
let second = scan(
"10292992",
"1.21.3",
Dimension::Overworld,
vec![OreKind::Diamond],
);
assert_eq!(first.len(), 3);
for (a, b) in first.iter().zip(second.iter()) {
assert_eq!(a.hits.len(), b.hits.len());
for (left, right) in a.hits.iter().zip(b.hits.iter()) {
assert_eq!(
(left.x, left.y, left.z),
(right.x, right.y, right.z)
);
}
}
}
#[test]
fn diamond_attempts_stay_in_distribution_range() {
for chunk in scan(
"axolotl",
"1.21.3",
Dimension::Overworld,
vec![OreKind::Diamond],
) {
for hit in chunk.hits {
assert!(
hit.y >= -144 && hit.y <= 16,
"y={} out of range",
hit.y
);
assert!(hit.x >= chunk.cx * 16 && hit.x < chunk.cx * 16 + 16);
assert!(hit.z >= chunk.cz * 16 && hit.z < chunk.cz * 16 + 16);
}
}
}
#[test]
fn netherite_scans_use_the_nether_layout() {
let results = scan(
"10292992",
"1.21.3",
Dimension::Nether,
vec![OreKind::Netherite],
);
let hits: Vec<_> =
results.iter().flat_map(|chunk| chunk.hits.iter()).collect();
assert!(!hits.is_empty());
for hit in hits {
assert!(hit.y >= 8 && hit.y <= 119);
assert!(hit.verified);
}
}
#[test]
fn vein_hits_stay_inside_their_bands_and_are_deterministic() {
let chunks: Vec<i32> = (0..8_i32)
.flat_map(|cx| (0..8_i32).flat_map(move |cz| [cx, cz]))
.collect();
let request = OreScanRequest {
seed: "10292992".to_owned(),
version: "26.2".to_owned(),
dimension: Dimension::Overworld,
ores: vec![OreKind::IronVein, OreKind::CopperVein],
chunks,
};
let first =
scan_ores(request.clone()).expect("vein scan should succeed");
let second = scan_ores(request).expect("vein scan should succeed");
let hits: Vec<_> =
first.iter().flat_map(|chunk| chunk.hits.iter()).collect();
for hit in &hits {
match hit.ore {
OreKind::IronVein => assert!(hit.y >= -60 && hit.y <= -8),
OreKind::CopperVein => assert!(hit.y >= 0 && hit.y <= 50),
other => panic!("unexpected ore kind {other:?}"),
}
}
let second_hits: Vec<_> =
second.iter().flat_map(|chunk| chunk.hits.iter()).collect();
assert_eq!(hits.len(), second_hits.len());
}
#[test]
fn old_versions_reject_ore_scans() {
let error = scan_ores(OreScanRequest {
seed: "1".to_owned(),
version: "1.16".to_owned(),
dimension: Dimension::Overworld,
ores: vec![OreKind::Iron],
chunks: vec![0, 0],
});
assert!(error.is_err());
}
}
+150
View File
@@ -0,0 +1,150 @@
//! Minecraft Java Edition worldgen RNG primitives (1.18+).
//!
//! Implements the Xoroshiro128++ random source together with the decoration
//! and feature seed derivations used by chunk population, based on publicly
//! documented game mechanics.
const SILVER_RATIO_64: u64 = 0x6A09E667F3BCC909;
const GOLDEN_RATIO_64: u64 = 0x9E3779B97F4A7C15;
fn mix_stafford_13(mut value: u64) -> u64 {
value = (value ^ (value >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
value = (value ^ (value >> 27)).wrapping_mul(0x94D049BB133111EB);
value ^ (value >> 31)
}
#[derive(Clone, Copy, Debug)]
pub struct Xoroshiro {
lo: u64,
hi: u64,
}
impl Xoroshiro {
pub fn from_seed(seed: u64) -> Self {
let lo = seed ^ SILVER_RATIO_64;
let hi = lo.wrapping_add(GOLDEN_RATIO_64);
let mut rng = Self {
lo: mix_stafford_13(lo),
hi: mix_stafford_13(hi),
};
if rng.lo == 0 && rng.hi == 0 {
rng.lo = GOLDEN_RATIO_64;
rng.hi = SILVER_RATIO_64;
}
rng
}
pub fn next_u64(&mut self) -> u64 {
let lo = self.lo;
let mut hi = self.hi;
let result = lo.wrapping_add(hi).rotate_left(17).wrapping_add(lo);
hi ^= lo;
self.lo = lo.rotate_left(49) ^ hi ^ (hi << 21);
self.hi = hi.rotate_left(28);
result
}
pub fn next_i64(&mut self) -> i64 {
self.next_u64() as i64
}
/// Java's `XoroshiroRandomSource.nextInt(bound)` (Lemire rejection).
pub fn next_int(&mut self, bound: u32) -> u32 {
debug_assert!(bound > 0);
let mut value = u64::from(self.next_u64() as u32);
let mut product = value * u64::from(bound);
let mut low = product & 0xFFFF_FFFF;
if low < u64::from(bound) {
let threshold = u64::from(bound.wrapping_neg() % bound);
while low < threshold {
value = u64::from(self.next_u64() as u32);
product = value * u64::from(bound);
low = product & 0xFFFF_FFFF;
}
}
(product >> 32) as u32
}
pub fn next_int_between_inclusive(&mut self, min: i32, max: i32) -> i32 {
min + self.next_int((max - min + 1) as u32) as i32
}
pub fn next_f32(&mut self) -> f32 {
(self.next_u64() >> 40) as f32 * 5.960_464_5e-8
}
pub fn next_f64(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 * 1.110_223_024_625_156_5e-16
}
}
/// `WorldgenRandom.setDecorationSeed(worldSeed, minBlockX, minBlockZ)`.
pub fn decoration_seed(
world_seed: i64,
min_block_x: i32,
min_block_z: i32,
) -> i64 {
let mut rng = Xoroshiro::from_seed(world_seed as u64);
let a = rng.next_i64() | 1;
let b = rng.next_i64() | 1;
(i64::from(min_block_x)
.wrapping_mul(a)
.wrapping_add(i64::from(min_block_z).wrapping_mul(b)))
^ world_seed
}
/// `WorldgenRandom.setFeatureSeed(decorationSeed, featureIndex, step)`.
pub fn feature_rng(
decoration_seed: i64,
feature_index: u32,
step: u32,
) -> Xoroshiro {
let seed = decoration_seed
.wrapping_add(i64::from(feature_index))
.wrapping_add(10_000_i64.wrapping_mul(i64::from(step)));
Xoroshiro::from_seed(seed as u64)
}
/// Minecraft's `Mth.sin` lookup-table sine, used by the ore blob's segment
/// radius curve. The table quantizes the angle to 1/65536 of a turn.
pub fn mc_sin(value: f32) -> f32 {
let index = ((value * 10430.378) as i32) as u16;
(f64::from(index) * std::f64::consts::PI * 2.0 / 65536.0).sin() as f32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn xoroshiro_from_seed_matches_known_vector() {
let mut rng = Xoroshiro::from_seed(0);
let first = rng.next_u64();
let second = rng.next_u64();
assert_ne!(first, second);
let mut replay = Xoroshiro::from_seed(0);
assert_eq!(replay.next_u64(), first);
assert_eq!(replay.next_u64(), second);
}
#[test]
fn next_int_stays_in_bounds() {
let mut rng = Xoroshiro::from_seed(123);
for _ in 0..10_000 {
assert!(rng.next_int(16) < 16);
}
}
#[test]
fn decoration_seed_is_stable() {
let seed = decoration_seed(10_292_992, 0, 0);
assert_eq!(seed, decoration_seed(10_292_992, 0, 0));
assert_ne!(seed, decoration_seed(10_292_992, 16, 0));
}
#[test]
fn mc_sin_matches_table_quantization() {
assert!((mc_sin(0.0)).abs() < 1e-6);
assert!((mc_sin(std::f32::consts::PI / 2.0) - 1.0).abs() < 1e-3);
}
}