feat:移除了弹窗,服务器添加sls
This commit is contained in:
commit
6a295f9a7a
4082 files changed
+1322534
No files matched your search
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user