343 lines
8.8 KiB
Go
343 lines
8.8 KiB
Go
package data
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"sync"
|
|
)
|
|
|
|
// MultiNoisePreset names the two built-in multi-noise parameter lists.
|
|
const (
|
|
MultiNoiseOverworld = "minecraft:overworld"
|
|
MultiNoiseNether = "minecraft:nether"
|
|
)
|
|
|
|
// ParamRange is a single quantized parameter range [Min, Max].
|
|
type ParamRange struct {
|
|
Min, Max int64
|
|
}
|
|
|
|
// ParameterPoint is a 6-D parameter box plus an offset used by Minecraft's
|
|
// climate distance function.
|
|
type ParameterPoint struct {
|
|
Biome string
|
|
Temperature ParamRange
|
|
Humidity ParamRange
|
|
Continentalness ParamRange
|
|
Erosion ParamRange
|
|
Depth ParamRange
|
|
Weirdness ParamRange
|
|
Offset int64
|
|
}
|
|
|
|
// MultiNoiseData holds the parameter lists for each preset.
|
|
type MultiNoiseData struct {
|
|
Presets map[string][]ParameterPoint
|
|
|
|
bvls map[string]*bvhNode
|
|
bvhOnce sync.Once
|
|
bvhErr error
|
|
}
|
|
|
|
// raw JSON shapes.
|
|
type rawParameter struct {
|
|
Temperature [2]int64 `json:"temperature"`
|
|
Humidity [2]int64 `json:"humidity"`
|
|
Continentalness [2]int64 `json:"continentalness"`
|
|
Erosion [2]int64 `json:"erosion"`
|
|
Depth [2]int64 `json:"depth"`
|
|
Weirdness [2]int64 `json:"weirdness"`
|
|
Offset float64 `json:"offset"`
|
|
}
|
|
|
|
type rawEntry struct {
|
|
Biome string `json:"biome"`
|
|
Parameters rawParameter `json:"parameters"`
|
|
}
|
|
|
|
type rawMultiNoise map[string][]rawEntry
|
|
|
|
// LoadMultiNoise loads the expanded multi-noise parameter list for a Minecraft
|
|
// version. The data is expected to be present under the
|
|
// "multi_noise_biome_source_parameter_list" key in minecraft-data.
|
|
func LoadMultiNoise(version string) (*MultiNoiseData, error) {
|
|
var raw rawMultiNoise
|
|
if err := LoadVersionedJson(version, "multi_noise_biome_source_parameter_list", &raw); err != nil {
|
|
return nil, fmt.Errorf("load multi_noise for %s: %w", version, err)
|
|
}
|
|
|
|
out := &MultiNoiseData{Presets: make(map[string][]ParameterPoint, len(raw))}
|
|
for preset, entries := range raw {
|
|
points := make([]ParameterPoint, len(entries))
|
|
for i, e := range entries {
|
|
points[i] = ParameterPoint{
|
|
Biome: e.Biome,
|
|
Temperature: ParamRange{Min: e.Parameters.Temperature[0], Max: e.Parameters.Temperature[1]},
|
|
Humidity: ParamRange{Min: e.Parameters.Humidity[0], Max: e.Parameters.Humidity[1]},
|
|
Continentalness: ParamRange{Min: e.Parameters.Continentalness[0], Max: e.Parameters.Continentalness[1]},
|
|
Erosion: ParamRange{Min: e.Parameters.Erosion[0], Max: e.Parameters.Erosion[1]},
|
|
Depth: ParamRange{Min: e.Parameters.Depth[0], Max: e.Parameters.Depth[1]},
|
|
Weirdness: ParamRange{Min: e.Parameters.Weirdness[0], Max: e.Parameters.Weirdness[1]},
|
|
Offset: int64(math.Round(e.Parameters.Offset * 10000.0)),
|
|
}
|
|
}
|
|
out.Presets[preset] = points
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
// Lookup finds the biome whose parameter box is closest to the supplied
|
|
// quantized climate parameters. The preset argument should be one of the
|
|
// MultiNoise* constants.
|
|
func (m *MultiNoiseData) Lookup(preset string, t, h, c, e, d, w int64) (string, error) {
|
|
points, ok := m.Presets[preset]
|
|
if !ok {
|
|
return "", fmt.Errorf("unknown multi-noise preset: %s", preset)
|
|
}
|
|
|
|
if len(points) == 0 {
|
|
return "", fmt.Errorf("multi-noise preset %s has no entries", preset)
|
|
}
|
|
|
|
m.bvhOnce.Do(func() { m.bvhErr = m.buildBVHs() })
|
|
if m.bvhErr != nil {
|
|
return "", m.bvhErr
|
|
}
|
|
|
|
root := m.bvls[preset]
|
|
if root == nil {
|
|
return "", fmt.Errorf("no BVH for preset: %s", preset)
|
|
}
|
|
|
|
q := point6{t, h, c, e, d, w}
|
|
best := result{dist: int64(math.MaxInt64)}
|
|
root.lookup(q, points, &best)
|
|
return best.biome, nil
|
|
}
|
|
|
|
func distanceToRange(value int64, r ParamRange) int64 {
|
|
if value < r.Min {
|
|
d := r.Min - value
|
|
return d * d
|
|
}
|
|
if value > r.Max {
|
|
d := value - r.Max
|
|
return d * d
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// point6 is a quantized climate sample in six dimensions.
|
|
type point6 struct{ t, h, c, e, d, w int64 }
|
|
|
|
func (q point6) dist(p ParameterPoint) int64 {
|
|
return distanceToRange(q.t, p.Temperature) +
|
|
distanceToRange(q.h, p.Humidity) +
|
|
distanceToRange(q.c, p.Continentalness) +
|
|
distanceToRange(q.e, p.Erosion) +
|
|
distanceToRange(q.d, p.Depth) +
|
|
distanceToRange(q.w, p.Weirdness) +
|
|
p.Offset
|
|
}
|
|
|
|
type result struct {
|
|
dist int64
|
|
biome string
|
|
}
|
|
|
|
// bvhNode is a bounding-volume-hierarchy node for fast nearest-box search.
|
|
type bvhNode struct {
|
|
bounds ParameterPoint
|
|
left *bvhNode
|
|
right *bvhNode
|
|
leaf []int // indices into original slice; non-nil only for leaves
|
|
}
|
|
|
|
func (m *MultiNoiseData) buildBVHs() error {
|
|
m.bvls = make(map[string]*bvhNode, len(m.Presets))
|
|
for preset, points := range m.Presets {
|
|
indices := make([]int, len(points))
|
|
for i := range indices {
|
|
indices[i] = i
|
|
}
|
|
m.bvls[preset] = buildBVH(points, indices, 0)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func buildBVH(points []ParameterPoint, indices []int, depth int) *bvhNode {
|
|
node := &bvhNode{}
|
|
node.bounds = boundsOf(points, indices)
|
|
|
|
// Small leaves avoid BVH overhead for tiny lists.
|
|
if len(indices) <= 16 {
|
|
node.leaf = make([]int, len(indices))
|
|
copy(node.leaf, indices)
|
|
return node
|
|
}
|
|
|
|
// Pick axis by largest variance.
|
|
axis := depth % 6
|
|
if axis < 0 {
|
|
axis = 0
|
|
}
|
|
|
|
// Partition by median of the chosen axis' center.
|
|
mid := len(indices) / 2
|
|
quickSelectByAxis(points, indices, axis, mid)
|
|
|
|
leftIdx := indices[:mid]
|
|
rightIdx := indices[mid:]
|
|
|
|
// Degenerate split: fall back to leaf to avoid infinite recursion.
|
|
if len(leftIdx) == 0 || len(rightIdx) == 0 {
|
|
node.leaf = make([]int, len(indices))
|
|
copy(node.leaf, indices)
|
|
return node
|
|
}
|
|
|
|
node.left = buildBVH(points, leftIdx, depth+1)
|
|
node.right = buildBVH(points, rightIdx, depth+1)
|
|
return node
|
|
}
|
|
|
|
func boundsOf(points []ParameterPoint, indices []int) ParameterPoint {
|
|
if len(indices) == 0 {
|
|
return ParameterPoint{}
|
|
}
|
|
b := points[indices[0]]
|
|
for _, i := range indices[1:] {
|
|
p := points[i]
|
|
if p.Temperature.Min < b.Temperature.Min {
|
|
b.Temperature.Min = p.Temperature.Min
|
|
}
|
|
if p.Temperature.Max > b.Temperature.Max {
|
|
b.Temperature.Max = p.Temperature.Max
|
|
}
|
|
if p.Humidity.Min < b.Humidity.Min {
|
|
b.Humidity.Min = p.Humidity.Min
|
|
}
|
|
if p.Humidity.Max > b.Humidity.Max {
|
|
b.Humidity.Max = p.Humidity.Max
|
|
}
|
|
if p.Continentalness.Min < b.Continentalness.Min {
|
|
b.Continentalness.Min = p.Continentalness.Min
|
|
}
|
|
if p.Continentalness.Max > b.Continentalness.Max {
|
|
b.Continentalness.Max = p.Continentalness.Max
|
|
}
|
|
if p.Erosion.Min < b.Erosion.Min {
|
|
b.Erosion.Min = p.Erosion.Min
|
|
}
|
|
if p.Erosion.Max > b.Erosion.Max {
|
|
b.Erosion.Max = p.Erosion.Max
|
|
}
|
|
if p.Depth.Min < b.Depth.Min {
|
|
b.Depth.Min = p.Depth.Min
|
|
}
|
|
if p.Depth.Max > b.Depth.Max {
|
|
b.Depth.Max = p.Depth.Max
|
|
}
|
|
if p.Weirdness.Min < b.Weirdness.Min {
|
|
b.Weirdness.Min = p.Weirdness.Min
|
|
}
|
|
if p.Weirdness.Max > b.Weirdness.Max {
|
|
b.Weirdness.Max = p.Weirdness.Max
|
|
}
|
|
if p.Offset < b.Offset {
|
|
b.Offset = p.Offset
|
|
}
|
|
}
|
|
return b
|
|
}
|
|
|
|
func (n *bvhNode) lowerBound(q point6) int64 {
|
|
return distanceToRange(q.t, n.bounds.Temperature) +
|
|
distanceToRange(q.h, n.bounds.Humidity) +
|
|
distanceToRange(q.c, n.bounds.Continentalness) +
|
|
distanceToRange(q.e, n.bounds.Erosion) +
|
|
distanceToRange(q.d, n.bounds.Depth) +
|
|
distanceToRange(q.w, n.bounds.Weirdness) +
|
|
n.bounds.Offset
|
|
}
|
|
|
|
func (n *bvhNode) lookup(q point6, points []ParameterPoint, best *result) {
|
|
if n.lowerBound(q) >= best.dist {
|
|
return
|
|
}
|
|
|
|
if n.leaf != nil {
|
|
for _, idx := range n.leaf {
|
|
p := points[idx]
|
|
d := q.dist(p)
|
|
if d < best.dist {
|
|
best.dist = d
|
|
best.biome = p.Biome
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Visit the child with the smaller lower bound first for better pruning.
|
|
lbL := n.left.lowerBound(q)
|
|
lbR := n.right.lowerBound(q)
|
|
if lbL < lbR {
|
|
n.left.lookup(q, points, best)
|
|
n.right.lookup(q, points, best)
|
|
} else {
|
|
n.right.lookup(q, points, best)
|
|
n.left.lookup(q, points, best)
|
|
}
|
|
}
|
|
|
|
func quickSelectByAxis(points []ParameterPoint, indices []int, axis, k int) {
|
|
if len(indices) <= 1 {
|
|
return
|
|
}
|
|
for {
|
|
pivot := points[indices[len(indices)/2]]
|
|
pivotVal := axisCenter(pivot, axis)
|
|
left, right := 0, len(indices)-1
|
|
for left <= right {
|
|
for axisCenter(points[indices[left]], axis) < pivotVal {
|
|
left++
|
|
}
|
|
for axisCenter(points[indices[right]], axis) > pivotVal {
|
|
right--
|
|
}
|
|
if left <= right {
|
|
indices[left], indices[right] = indices[right], indices[left]
|
|
left++
|
|
right--
|
|
}
|
|
}
|
|
if k <= right {
|
|
indices = indices[:right+1]
|
|
} else if k >= left {
|
|
indices = indices[left:]
|
|
k -= left
|
|
} else {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func axisCenter(p ParameterPoint, axis int) int64 {
|
|
switch axis {
|
|
case 0:
|
|
return (p.Temperature.Min + p.Temperature.Max) / 2
|
|
case 1:
|
|
return (p.Humidity.Min + p.Humidity.Max) / 2
|
|
case 2:
|
|
return (p.Continentalness.Min + p.Continentalness.Max) / 2
|
|
case 3:
|
|
return (p.Erosion.Min + p.Erosion.Max) / 2
|
|
case 4:
|
|
return (p.Depth.Min + p.Depth.Max) / 2
|
|
case 5:
|
|
return (p.Weirdness.Min + p.Weirdness.Max) / 2
|
|
}
|
|
return 0
|
|
}
|