orefinder-estimate 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/DISCLAIMER.md +19 -0
- data/LICENSE +21 -0
- data/README.md +52 -0
- data/exe/orefinder-estimate +104 -0
- data/lib/orefinder_estimate/cluster.rb +166 -0
- data/lib/orefinder_estimate/data.rb +37 -0
- data/lib/orefinder_estimate/estimate.rb +99 -0
- data/lib/orefinder_estimate/ore-data.json +316 -0
- data/lib/orefinder_estimate/probability.rb +90 -0
- data/lib/orefinder_estimate/rng.rb +46 -0
- data/lib/orefinder_estimate/version.rb +43 -0
- data/lib/orefinder_estimate.rb +13 -0
- metadata +58 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 241ee2fe99c357eb6a52fe274ab907fb5ddf48379eee6bf0a7f572ca6b795a30
|
|
4
|
+
data.tar.gz: '095401a530362c9a0ac3c58d271a3886fe514db5a2554df68b89344abb8eba63'
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 170e51a5dea283bfe8c1321cfd793436d5c8f04fc9cb7c17ef4ee1b08a60f073d730b7f02957f0c0bcb85334c0a06118bfcbb75cf44c106dce7e9acf3f2b5672
|
|
7
|
+
data.tar.gz: 8b79f27cee69fbf594702fc3f6f6fa3cf8a1bf2630c474cca13ae775601a2f0d44ad2f03bf483c31efa063263c0d16583854680425a8a4b9fd7a77bf0d9543a8
|
data/DISCLAIMER.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# About these estimates
|
|
2
|
+
|
|
3
|
+
These packages give you a fast, offline **estimate** of where an ore is most
|
|
4
|
+
likely to concentrate in Minecraft - the best mining Y level and the nearby
|
|
5
|
+
areas worth digging - using published Y-level distribution ranges and
|
|
6
|
+
statistical weighting.
|
|
7
|
+
|
|
8
|
+
Think of it as a smart heads-up for planning your next mining trip: quick to
|
|
9
|
+
run, no world load, no network calls, and easy to build on in your own projects.
|
|
10
|
+
|
|
11
|
+
For pinpoint, seed-exact block coordinates (Java and Bedrock), pair it with the
|
|
12
|
+
full tool at:
|
|
13
|
+
|
|
14
|
+
**https://orefinder.io**
|
|
15
|
+
|
|
16
|
+
As with any planning tool, treat the results as guidance and confirm important
|
|
17
|
+
spots in-game. Minecraft, the Minecraft name, and associated assets are property
|
|
18
|
+
of Mojang/Microsoft. These packages are an unofficial, fan-made utility and are
|
|
19
|
+
not affiliated with or endorsed by Mojang.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ore Finder (https://orefinder.io)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# orefinder-estimate (RubyGems)
|
|
2
|
+
|
|
3
|
+
A fast, offline, dependency-free Minecraft ore **estimator** gem. Feed it a
|
|
4
|
+
seed label, version, and player position; get likely ore coordinates and the
|
|
5
|
+
best mining Y for Java or Bedrock.
|
|
6
|
+
|
|
7
|
+
> Fast, offline estimates of the best mining Y and where an ore is likely to
|
|
8
|
+
> concentrate. For pinpoint, seed-exact coordinates (Java & Bedrock), pair it with
|
|
9
|
+
> **[Ore Finder](https://orefinder.io)**.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
gem install orefinder-estimate
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Ruby
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
require 'orefinder_estimate'
|
|
21
|
+
|
|
22
|
+
result = OrefinderEstimate.estimate(
|
|
23
|
+
'seed' => '42',
|
|
24
|
+
'version' => 'java_1_21',
|
|
25
|
+
'ore' => 'diamond',
|
|
26
|
+
'x' => 0, 'y' => 64, 'z' => 0,
|
|
27
|
+
'biome' => 'plains',
|
|
28
|
+
'searchRadius' => 220
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
puts result['metadata']['miningPeakY']
|
|
32
|
+
p result['bestCluster'].first
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## CLI
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
orefinder-estimate --version java_1_21 --ore diamond -x 0 -y 64 -z 0
|
|
39
|
+
orefinder-estimate --ore ancient_debris --version java_1_21 --biome crimson_forest --json
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Supported ores
|
|
43
|
+
|
|
44
|
+
diamond, iron, gold, coal, copper, emerald, redstone, lapis, ancient_debris
|
|
45
|
+
(plus netherite aliases), nether_gold, nether_quartz, gilded_blackstone,
|
|
46
|
+
blackstone.
|
|
47
|
+
|
|
48
|
+
Deterministic and parity-tested against the sibling packages. No world-gen
|
|
49
|
+
simulation, no structures, no network calls. Seed-accurate placement lives at
|
|
50
|
+
[orefinder.io](https://orefinder.io).
|
|
51
|
+
|
|
52
|
+
MIT (c) [Ore Finder](https://orefinder.io).
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Ore Finder Estimate CLI. Approximate distribution estimate; NOT seed-accurate.
|
|
5
|
+
# For exact, seed-based results use https://orefinder.io
|
|
6
|
+
require 'json'
|
|
7
|
+
|
|
8
|
+
begin
|
|
9
|
+
require 'orefinder_estimate'
|
|
10
|
+
rescue LoadError
|
|
11
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
|
12
|
+
require 'orefinder_estimate'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
HELP = <<~TXT
|
|
16
|
+
orefinder-estimate - approximate Minecraft ore locator (offline)
|
|
17
|
+
|
|
18
|
+
USAGE:
|
|
19
|
+
orefinder-estimate --version <ver> --ore <ore> [options]
|
|
20
|
+
|
|
21
|
+
OPTIONS:
|
|
22
|
+
--seed <s> World seed label. Default: 42
|
|
23
|
+
--version <v> e.g. java_1_21, 1.20.1 (required)
|
|
24
|
+
--ore <o> e.g. diamond, ancient_debris (required)
|
|
25
|
+
-x -y -z <n> Player position. Default: 0 64 0
|
|
26
|
+
--biome <b> Biome bonus. Default: plains
|
|
27
|
+
--radius <n> Search radius 32-512. Default: 220
|
|
28
|
+
--json Print raw JSON
|
|
29
|
+
--ores List supported ores
|
|
30
|
+
-h, --help Show this help
|
|
31
|
+
|
|
32
|
+
Approximate estimate, not seed-accurate. For exact coords use https://orefinder.io
|
|
33
|
+
TXT
|
|
34
|
+
|
|
35
|
+
opts = {}
|
|
36
|
+
flags = []
|
|
37
|
+
i = 0
|
|
38
|
+
while i < ARGV.length
|
|
39
|
+
a = ARGV[i]
|
|
40
|
+
case a
|
|
41
|
+
when '-h', '--help' then flags << 'help'
|
|
42
|
+
when '--json' then flags << 'json'
|
|
43
|
+
when '--ores' then flags << 'ores'
|
|
44
|
+
else
|
|
45
|
+
if a.start_with?('-') && i + 1 < ARGV.length
|
|
46
|
+
opts[a] = ARGV[i + 1]
|
|
47
|
+
i += 1
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
i += 1
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if flags.include?('help') || ARGV.empty?
|
|
54
|
+
puts HELP
|
|
55
|
+
exit 0
|
|
56
|
+
end
|
|
57
|
+
if flags.include?('ores')
|
|
58
|
+
puts OrefinderEstimate::Data.known_ore_keys.sort.join("\n")
|
|
59
|
+
exit 0
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
version = opts['--version']
|
|
63
|
+
ore = opts['--ore']
|
|
64
|
+
if version.nil? || ore.nil?
|
|
65
|
+
warn 'Error: --version and --ore are required. Use --help.'
|
|
66
|
+
exit 2
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
res = OrefinderEstimate.estimate(
|
|
70
|
+
'seed' => opts['--seed'] || '42',
|
|
71
|
+
'version' => version,
|
|
72
|
+
'ore' => ore,
|
|
73
|
+
'x' => (opts['-x'] || '0').to_f,
|
|
74
|
+
'y' => (opts['-y'] || '64').to_f,
|
|
75
|
+
'z' => (opts['-z'] || '0').to_f,
|
|
76
|
+
'biome' => opts['--biome'] || 'plains',
|
|
77
|
+
'searchRadius' => (opts['--radius'] || '220').to_f
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
unless res['success']
|
|
81
|
+
warn "Error (#{res['error']}): #{res['message']}"
|
|
82
|
+
exit 1
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
if flags.include?('json')
|
|
86
|
+
puts JSON.pretty_generate(res)
|
|
87
|
+
exit 0
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
m = res['metadata']
|
|
91
|
+
puts 'Ore Finder Estimate (approximate - verify at https://orefinder.io)'
|
|
92
|
+
puts "#{m['ore']} #{m['versionLabel']} [#{m['edition']}/#{m['era']}] best Y ~ #{m['miningPeakY']}"
|
|
93
|
+
puts "biome #{m['biomeEffective']} (x#{m['biomeEffect']}) dimension #{m['dimension']}"
|
|
94
|
+
puts ''
|
|
95
|
+
puts 'Best mining area:'
|
|
96
|
+
res['bestCluster'].each do |p|
|
|
97
|
+
puts " X #{p['x']} Y #{p['y']} Z #{p['z']} #{p['distance']}m #{p['probabilityPct']}% (conf #{p['confidence']})"
|
|
98
|
+
end
|
|
99
|
+
res['secondaryClusters'].each_with_index do |group, gi|
|
|
100
|
+
puts "Nearby area #{gi + 1}:"
|
|
101
|
+
group.each do |p|
|
|
102
|
+
puts " X #{p['x']} Y #{p['y']} Z #{p['z']} #{p['distance']}m #{p['probabilityPct']}%"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'rng'
|
|
4
|
+
require_relative 'probability'
|
|
5
|
+
|
|
6
|
+
# Cluster-based coordinate generator (statistical, not chunk-exact). See ../../../SPEC.md section 6.
|
|
7
|
+
module OrefinderEstimate
|
|
8
|
+
module Cluster
|
|
9
|
+
SECONDARY_XZ_MIN = 10
|
|
10
|
+
SECONDARY_XZ_MAX = 30
|
|
11
|
+
PRIMARY_XZ_MIN = 8
|
|
12
|
+
PRIMARY_XZ_MAX = 15
|
|
13
|
+
SECONDARY_Y_MAX = 3
|
|
14
|
+
PRIMARY_Y_MAX = 2
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def num_str(v)
|
|
19
|
+
v == v.to_i ? v.to_i.to_s : v.to_s
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def dist2(x1, z1, x2, z2)
|
|
23
|
+
dx = x1 - x2
|
|
24
|
+
dz = z1 - z2
|
|
25
|
+
Math.sqrt(dx * dx + dz * dz)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def adaptive_center_band(cfg, search_radius)
|
|
29
|
+
f = cfg['frequency'].to_s
|
|
30
|
+
lo = 50
|
|
31
|
+
hi = 150
|
|
32
|
+
if %w[very_rare rare].include?(f)
|
|
33
|
+
lo = 40
|
|
34
|
+
hi = 90
|
|
35
|
+
elsif %w[very_common common].include?(f)
|
|
36
|
+
lo = 60
|
|
37
|
+
hi = 140
|
|
38
|
+
end
|
|
39
|
+
band_hi = [hi, search_radius].min
|
|
40
|
+
band_lo = [lo, band_hi].min
|
|
41
|
+
[band_lo, band_hi]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def norm_distance(d, search_radius)
|
|
45
|
+
cap = [search_radius, 1].max
|
|
46
|
+
Probability.clamp(d / cap.to_f, 0, 1)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def point_ux_score(prob_pct, distance, search_radius)
|
|
50
|
+
(prob_pct / 100.0) * 0.8 - norm_distance(distance, search_radius) * 0.2
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def cluster_rank_score(max_prob, mean_prob, mean_norm_dist)
|
|
54
|
+
(max_prob * 0.7 + mean_prob * 0.3) - mean_norm_dist * 12
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def generate_clusters(seed:, ore:, era:, cfg:, px:, pz:, biome:, search_radius: 220)
|
|
58
|
+
peak_y = Probability.optimal_y_for_mining(cfg)
|
|
59
|
+
band_lo, band_hi = adaptive_center_band(cfg, search_radius)
|
|
60
|
+
num_centers = search_radius < 48 ? 2 : 3
|
|
61
|
+
points_per_cluster = [4, 3, 3]
|
|
62
|
+
min_y = cfg['minY']
|
|
63
|
+
max_y = cfg['maxY']
|
|
64
|
+
|
|
65
|
+
expand = lambda do |cx, cz, n, rng, confidence_bonus, primary|
|
|
66
|
+
xz_min = primary ? PRIMARY_XZ_MIN : SECONDARY_XZ_MIN
|
|
67
|
+
xz_max = primary ? PRIMARY_XZ_MAX : SECONDARY_XZ_MAX
|
|
68
|
+
y_max = primary ? PRIMARY_Y_MAX : SECONDARY_Y_MAX
|
|
69
|
+
pts = []
|
|
70
|
+
|
|
71
|
+
push = lambda do |x, y, z|
|
|
72
|
+
prob = Probability.calculate_ore_probability(cfg, y, biome)
|
|
73
|
+
d = dist2(x, z, px, pz)
|
|
74
|
+
conf = Probability.clamp(Probability.calculate_confidence(prob, d, y, peak_y) + confidence_bonus, 0, 100)
|
|
75
|
+
pts << {
|
|
76
|
+
'x' => x, 'y' => y, 'z' => z,
|
|
77
|
+
'confidence' => Rng.round_to_tenth(conf),
|
|
78
|
+
'probabilityPct' => Rng.round_half_up(prob * 1000) / 10.0,
|
|
79
|
+
'distance' => Rng.round_to_tenth(d)
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
if primary && n > 0
|
|
84
|
+
center_y = Probability.clamp(Rng.round_half_up(peak_y + (rng.call - 0.5) * 2), min_y, max_y)
|
|
85
|
+
push.call(Rng.round_half_up(cx), center_y, Rng.round_half_up(cz))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
start_i = (primary && n > 0) ? 1 : 0
|
|
89
|
+
(start_i...n).each do
|
|
90
|
+
mag_x = xz_min + rng.call * (xz_max - xz_min)
|
|
91
|
+
mag_z = xz_min + rng.call * (xz_max - xz_min)
|
|
92
|
+
sx = rng.call < 0.5 ? -1 : 1
|
|
93
|
+
sz = rng.call < 0.5 ? -1 : 1
|
|
94
|
+
x = Rng.round_half_up(cx + sx * mag_x)
|
|
95
|
+
z = Rng.round_half_up(cz + sz * mag_z)
|
|
96
|
+
dy = if primary
|
|
97
|
+
Probability.clamp(Rng.round_half_up((rng.call + rng.call - 1) * y_max), -y_max, y_max)
|
|
98
|
+
else
|
|
99
|
+
Probability.clamp(Rng.round_half_up((rng.call + rng.call + rng.call - 1.5) * 2), -y_max, y_max)
|
|
100
|
+
end
|
|
101
|
+
y = Probability.clamp(Rng.round_half_up(peak_y + dy), min_y, max_y)
|
|
102
|
+
push.call(x, y, z)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if primary && !pts.empty?
|
|
106
|
+
max_probs = pts.map { |p| p['probabilityPct'] }
|
|
107
|
+
max_p = max_probs.max
|
|
108
|
+
if max_p < 75
|
|
109
|
+
k = max_probs.index(max_p)
|
|
110
|
+
y_best = Probability.clamp(Rng.round_half_up(peak_y), min_y, max_y)
|
|
111
|
+
p0 = pts[k]
|
|
112
|
+
prob = Probability.calculate_ore_probability(cfg, y_best, biome)
|
|
113
|
+
d = dist2(p0['x'], p0['z'], px, pz)
|
|
114
|
+
conf = Probability.clamp(Probability.calculate_confidence(prob, d, y_best, peak_y) + confidence_bonus, 0, 100)
|
|
115
|
+
pts[k] = {
|
|
116
|
+
'x' => p0['x'], 'y' => y_best, 'z' => p0['z'],
|
|
117
|
+
'confidence' => Rng.round_to_tenth(conf),
|
|
118
|
+
'probabilityPct' => Rng.round_half_up(prob * 1000) / 10.0,
|
|
119
|
+
'distance' => Rng.round_to_tenth(d)
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Stable sort: pointUxScore desc, then confidence desc.
|
|
125
|
+
pts.each_with_index.sort_by do |p, i|
|
|
126
|
+
[-point_ux_score(p['probabilityPct'], p['distance'], search_radius), -p['confidence'], i]
|
|
127
|
+
end.map(&:first)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
base_rng = Rng.make_rng([seed.to_s, ore, era, 'centers', num_str(Rng.round_half_up(px)), num_str(Rng.round_half_up(pz))])
|
|
131
|
+
base_angle = base_rng.call * Math::PI * 2
|
|
132
|
+
|
|
133
|
+
built = []
|
|
134
|
+
(0...num_centers).each do |c|
|
|
135
|
+
r = Rng.make_rng([seed.to_s, ore, era, 'c', c.to_s, num_str(band_lo), num_str(band_hi)])
|
|
136
|
+
angle = base_angle + c * ((2 * Math::PI) / num_centers) + (r.call - 0.5) * 0.45
|
|
137
|
+
rad = band_lo + r.call * (band_hi - band_lo)
|
|
138
|
+
cx = px + Math.cos(angle) * rad
|
|
139
|
+
cz = pz + Math.sin(angle) * rad
|
|
140
|
+
|
|
141
|
+
n = points_per_cluster[c] || 3
|
|
142
|
+
primary = c.zero?
|
|
143
|
+
bonus = primary ? 5 : 0
|
|
144
|
+
pts = expand.call(cx, cz, n, r, bonus, primary)
|
|
145
|
+
|
|
146
|
+
if pts.empty?
|
|
147
|
+
max_prob = 0
|
|
148
|
+
mean_prob = 0
|
|
149
|
+
mean_norm_dist = 1
|
|
150
|
+
else
|
|
151
|
+
max_prob = pts.map { |p| p['probabilityPct'] }.max
|
|
152
|
+
mean_prob = pts.sum { |p| p['probabilityPct'] } / pts.length.to_f
|
|
153
|
+
mean_norm_dist = pts.sum { |p| norm_distance(p['distance'], search_radius) } / pts.length.to_f
|
|
154
|
+
end
|
|
155
|
+
rank = cluster_rank_score(max_prob, mean_prob, mean_norm_dist)
|
|
156
|
+
rank += 22 if c.zero?
|
|
157
|
+
built << { 'pts' => pts, 'rank' => rank }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
sorted = built.each_with_index.sort_by { |b, i| [-b['rank'], i] }.map(&:first)
|
|
161
|
+
best = sorted.empty? ? [] : sorted[0]['pts']
|
|
162
|
+
secondary = sorted[1..].map { |b| b['pts'] }
|
|
163
|
+
{ 'bestCluster' => best, 'secondaryClusters' => secondary }
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'set'
|
|
5
|
+
|
|
6
|
+
# Loads the embedded ore-distribution tables. Fully offline; no network.
|
|
7
|
+
module OrefinderEstimate
|
|
8
|
+
module Data
|
|
9
|
+
ORE_DATA = JSON.parse(File.read(File.join(__dir__, 'ore-data.json'))).freeze
|
|
10
|
+
NETHER_BIOMES = ORE_DATA['netherBiomes'].to_set
|
|
11
|
+
ORE_ALIASES = ORE_DATA['oreAliases']
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def known_ore_keys
|
|
16
|
+
ORE_DATA['ores'].keys + ORE_ALIASES.keys
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def resolve_ore_keys(raw)
|
|
20
|
+
display_label = raw.to_s.strip.downcase
|
|
21
|
+
compute_key = ORE_ALIASES[display_label] || display_label
|
|
22
|
+
[compute_key, display_label]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def known_ore?(ore_key)
|
|
26
|
+
k = ore_key.to_s.strip.downcase
|
|
27
|
+
ORE_ALIASES.key?(k) || ORE_DATA['ores'].key?(k)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def era_config(era, compute_key)
|
|
31
|
+
entry = ORE_DATA['ores'][compute_key]
|
|
32
|
+
return nil unless entry
|
|
33
|
+
|
|
34
|
+
entry[era]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'data'
|
|
4
|
+
require_relative 'version'
|
|
5
|
+
require_relative 'probability'
|
|
6
|
+
require_relative 'cluster'
|
|
7
|
+
require_relative 'rng'
|
|
8
|
+
|
|
9
|
+
# Public estimate API. See ../../../SPEC.md section 7. Offline only; never calls a network.
|
|
10
|
+
module OrefinderEstimate
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def error(error, message, extra = {})
|
|
14
|
+
{ 'success' => false, 'error' => error, 'message' => message }.merge(extra)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def estimate(input)
|
|
18
|
+
return error('invalid_body', 'Input must be a hash.') unless input.is_a?(Hash)
|
|
19
|
+
|
|
20
|
+
ore_in = input['ore'].is_a?(String) ? input['ore'].strip.downcase : ''
|
|
21
|
+
compute_key, display_label = Data.resolve_ore_keys(ore_in)
|
|
22
|
+
return error('invalid_ore', 'Unknown ore for estimator.') if ore_in.empty? || !Data.known_ore?(ore_in)
|
|
23
|
+
|
|
24
|
+
px = to_num(input['x'])
|
|
25
|
+
pz = to_num(input['z'])
|
|
26
|
+
py = input['y'].nil? ? 64.0 : to_num(input['y'])
|
|
27
|
+
if px.nil? || pz.nil? || py.nil? || !px.finite? || !pz.finite? || !py.finite?
|
|
28
|
+
return error('invalid_position', 'x, y and z must be finite numbers.')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
version_field = input['version'] || input['platform'] || ''
|
|
32
|
+
parsed = Version.parse_version(version_field.to_s)
|
|
33
|
+
return error('invalid_version', 'Pass a version like "java_1_21" or "1.20.1".') if parsed.nil?
|
|
34
|
+
|
|
35
|
+
cfg = Data.era_config(parsed['era'], compute_key)
|
|
36
|
+
if cfg.nil?
|
|
37
|
+
msg = compute_key == 'copper' && parsed['era'] == 'legacy' ? 'Copper is modern-only (1.18+).' : 'This ore/version combination has no dataset row.'
|
|
38
|
+
return error('ore_not_in_era', msg)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
need_mc = cfg['specialRules'] ? cfg['specialRules']['minMcMinor'] : nil
|
|
42
|
+
if need_mc.is_a?(Integer) && parsed['mcMinor'] < need_mc
|
|
43
|
+
return error('version_too_old_for_ore',
|
|
44
|
+
"This target needs Minecraft 1.#{need_mc}+ (you selected 1.#{parsed['mcMinor']}).",
|
|
45
|
+
'minMcMinor' => need_mc, 'selectedMcMinor' => parsed['mcMinor'])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
biome = input['biome']
|
|
49
|
+
raw_biome = biome.is_a?(String) && !biome.strip.empty? ? biome.strip.downcase.gsub(/\s+/, '_') : 'plains'
|
|
50
|
+
biome_effective = Probability.normalize_biome(cfg, raw_biome)
|
|
51
|
+
|
|
52
|
+
radius_val = input['searchRadius'].nil? ? 220.0 : to_num(input['searchRadius'])
|
|
53
|
+
search_radius = [[radius_val, 32].max, 512].min.to_i
|
|
54
|
+
seed = input['seed'].nil? ? 'numeric' : input['seed']
|
|
55
|
+
|
|
56
|
+
clusters = Cluster.generate_clusters(
|
|
57
|
+
seed: seed, ore: compute_key, era: parsed['era'], cfg: cfg,
|
|
58
|
+
px: px, pz: pz, biome: raw_biome, search_radius: search_radius
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
peak_y = Probability.optimal_y_for_mining(cfg)
|
|
62
|
+
effect = Probability.biome_multiplier(cfg, biome_effective)
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
'success' => true,
|
|
66
|
+
'ore' => display_label,
|
|
67
|
+
'computeKey' => compute_key,
|
|
68
|
+
'bestCluster' => clusters['bestCluster'],
|
|
69
|
+
'secondaryClusters' => clusters['secondaryClusters'],
|
|
70
|
+
'metadata' => {
|
|
71
|
+
'ore' => display_label,
|
|
72
|
+
'computeKey' => compute_key,
|
|
73
|
+
'edition' => parsed['edition'],
|
|
74
|
+
'era' => parsed['era'],
|
|
75
|
+
'mcMinor' => parsed['mcMinor'],
|
|
76
|
+
'versionLabel' => parsed['raw'],
|
|
77
|
+
'optimalY' => cfg['optimalY'],
|
|
78
|
+
'practicalY' => cfg['practicalY'],
|
|
79
|
+
'actualPeakY' => cfg['actualPeakY'],
|
|
80
|
+
'yRange' => [cfg['minY'], cfg['maxY']],
|
|
81
|
+
'biomeRequested' => raw_biome,
|
|
82
|
+
'biomeEffective' => biome_effective,
|
|
83
|
+
'dimension' => cfg['dimension'] || 'overworld',
|
|
84
|
+
'biomeEffect' => Rng.round_half_up(effect * 1000) / 1000.0,
|
|
85
|
+
'miningPeakY' => peak_y,
|
|
86
|
+
'generation' => 'distribution_cluster_estimate',
|
|
87
|
+
'approximate' => true
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def to_num(v)
|
|
93
|
+
return nil if v.nil?
|
|
94
|
+
|
|
95
|
+
Float(v)
|
|
96
|
+
rescue ArgumentError, TypeError
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"note": "Approximate Minecraft ore Y-distribution tables. Not seed-accurate. See https://orefinder.io",
|
|
4
|
+
"netherBiomes": [
|
|
5
|
+
"nether_wastes",
|
|
6
|
+
"crimson_forest",
|
|
7
|
+
"warped_forest",
|
|
8
|
+
"soul_sand_valley",
|
|
9
|
+
"basalt_deltas"
|
|
10
|
+
],
|
|
11
|
+
"oreAliases": {
|
|
12
|
+
"netherite_block": "ancient_debris",
|
|
13
|
+
"netherite_scrap": "ancient_debris",
|
|
14
|
+
"netherite_ingot": "ancient_debris"
|
|
15
|
+
},
|
|
16
|
+
"ores": {
|
|
17
|
+
"coal": {
|
|
18
|
+
"modern": {
|
|
19
|
+
"minY": 0, "maxY": 256, "optimalY": 96, "practicalY": null, "actualPeakY": null,
|
|
20
|
+
"frequency": "very_common", "dimension": "overworld",
|
|
21
|
+
"distributions": [
|
|
22
|
+
{ "type": "uniform", "minY": 136, "maxY": 256, "peakY": null, "weight": 0.5 },
|
|
23
|
+
{ "type": "triangular", "minY": 0, "maxY": 192, "peakY": 96, "weight": 0.5 }
|
|
24
|
+
],
|
|
25
|
+
"biomeModifiers": { "stony_peaks": 1.5, "windswept_hills": 1.3 },
|
|
26
|
+
"specialRules": {}
|
|
27
|
+
},
|
|
28
|
+
"legacy": {
|
|
29
|
+
"minY": 0, "maxY": 127, "optimalY": 64, "practicalY": null, "actualPeakY": null,
|
|
30
|
+
"frequency": "very_common", "dimension": "overworld",
|
|
31
|
+
"distributions": [
|
|
32
|
+
{ "type": "uniform", "minY": 0, "maxY": 127, "peakY": null, "weight": 1.0 }
|
|
33
|
+
],
|
|
34
|
+
"biomeModifiers": {},
|
|
35
|
+
"specialRules": {}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"iron": {
|
|
39
|
+
"modern": {
|
|
40
|
+
"minY": -64, "maxY": 256, "optimalY": 16, "practicalY": null, "actualPeakY": null,
|
|
41
|
+
"frequency": "very_common", "dimension": "overworld",
|
|
42
|
+
"distributions": [
|
|
43
|
+
{ "type": "uniform", "minY": -64, "maxY": 72, "peakY": null, "weight": 0.33 },
|
|
44
|
+
{ "type": "triangular", "minY": -24, "maxY": 56, "peakY": 16, "weight": 0.33 },
|
|
45
|
+
{ "type": "triangular", "minY": 80, "maxY": 256, "peakY": 232, "weight": 0.34 }
|
|
46
|
+
],
|
|
47
|
+
"biomeModifiers": { "jagged_peaks": 2.0, "snowy_slopes": 1.8, "stony_peaks": 1.7 },
|
|
48
|
+
"specialRules": {}
|
|
49
|
+
},
|
|
50
|
+
"legacy": {
|
|
51
|
+
"minY": 0, "maxY": 63, "optimalY": 32, "practicalY": null, "actualPeakY": null,
|
|
52
|
+
"frequency": "very_common", "dimension": "overworld",
|
|
53
|
+
"distributions": [
|
|
54
|
+
{ "type": "uniform", "minY": 0, "maxY": 63, "peakY": null, "weight": 1.0 }
|
|
55
|
+
],
|
|
56
|
+
"biomeModifiers": {},
|
|
57
|
+
"specialRules": {}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"copper": {
|
|
61
|
+
"modern": {
|
|
62
|
+
"minY": -16, "maxY": 112, "optimalY": 48, "practicalY": null, "actualPeakY": null,
|
|
63
|
+
"frequency": "common", "dimension": "overworld",
|
|
64
|
+
"distributions": [
|
|
65
|
+
{ "type": "triangular", "minY": -16, "maxY": 112, "peakY": 48, "weight": 1.0 }
|
|
66
|
+
],
|
|
67
|
+
"biomeModifiers": { "dripstone_caves": 3.0 },
|
|
68
|
+
"specialRules": {}
|
|
69
|
+
},
|
|
70
|
+
"legacy": null
|
|
71
|
+
},
|
|
72
|
+
"gold": {
|
|
73
|
+
"modern": {
|
|
74
|
+
"minY": -64, "maxY": 32, "optimalY": -16, "practicalY": null, "actualPeakY": null,
|
|
75
|
+
"frequency": "rare", "dimension": "overworld",
|
|
76
|
+
"distributions": [
|
|
77
|
+
{ "type": "triangular", "minY": -64, "maxY": 32, "peakY": -16, "weight": 0.5 },
|
|
78
|
+
{ "type": "uniform", "minY": -64, "maxY": -48, "peakY": null, "weight": 0.5 }
|
|
79
|
+
],
|
|
80
|
+
"biomeModifiers": { "badlands": 5.0, "eroded_badlands": 5.0, "wooded_badlands": 5.0 },
|
|
81
|
+
"specialRules": {}
|
|
82
|
+
},
|
|
83
|
+
"legacy": {
|
|
84
|
+
"minY": 0, "maxY": 31, "optimalY": 16, "practicalY": null, "actualPeakY": null,
|
|
85
|
+
"frequency": "rare", "dimension": "overworld",
|
|
86
|
+
"distributions": [
|
|
87
|
+
{ "type": "uniform", "minY": 0, "maxY": 31, "peakY": null, "weight": 1.0 }
|
|
88
|
+
],
|
|
89
|
+
"biomeModifiers": {},
|
|
90
|
+
"specialRules": {}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"diamond": {
|
|
94
|
+
"modern": {
|
|
95
|
+
"minY": -64, "maxY": 16, "optimalY": -59, "practicalY": -53, "actualPeakY": null,
|
|
96
|
+
"frequency": "very_rare", "dimension": "overworld",
|
|
97
|
+
"distributions": [
|
|
98
|
+
{ "type": "triangular", "minY": -64, "maxY": 16, "peakY": -59, "weight": 1.0 }
|
|
99
|
+
],
|
|
100
|
+
"biomeModifiers": {},
|
|
101
|
+
"specialRules": {}
|
|
102
|
+
},
|
|
103
|
+
"legacy": {
|
|
104
|
+
"minY": 1, "maxY": 15, "optimalY": 11, "practicalY": null, "actualPeakY": null,
|
|
105
|
+
"frequency": "very_rare", "dimension": "overworld",
|
|
106
|
+
"distributions": [
|
|
107
|
+
{ "type": "uniform", "minY": 1, "maxY": 15, "peakY": null, "weight": 1.0 }
|
|
108
|
+
],
|
|
109
|
+
"biomeModifiers": {},
|
|
110
|
+
"specialRules": {}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"lapis": {
|
|
114
|
+
"modern": {
|
|
115
|
+
"minY": -64, "maxY": 64, "optimalY": 0, "practicalY": null, "actualPeakY": null,
|
|
116
|
+
"frequency": "uncommon", "dimension": "overworld",
|
|
117
|
+
"distributions": [
|
|
118
|
+
{ "type": "triangular", "minY": -32, "maxY": 32, "peakY": 0, "weight": 0.5 },
|
|
119
|
+
{ "type": "uniform", "minY": -64, "maxY": 64, "peakY": null, "weight": 0.5 }
|
|
120
|
+
],
|
|
121
|
+
"biomeModifiers": {},
|
|
122
|
+
"specialRules": {}
|
|
123
|
+
},
|
|
124
|
+
"legacy": {
|
|
125
|
+
"minY": 0, "maxY": 30, "optimalY": 15, "practicalY": null, "actualPeakY": null,
|
|
126
|
+
"frequency": "uncommon", "dimension": "overworld",
|
|
127
|
+
"distributions": [
|
|
128
|
+
{ "type": "triangular", "minY": 0, "maxY": 30, "peakY": 15, "weight": 1.0 }
|
|
129
|
+
],
|
|
130
|
+
"biomeModifiers": {},
|
|
131
|
+
"specialRules": {}
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
"redstone": {
|
|
135
|
+
"modern": {
|
|
136
|
+
"minY": -64, "maxY": 16, "optimalY": -59, "practicalY": -53, "actualPeakY": null,
|
|
137
|
+
"frequency": "common", "dimension": "overworld",
|
|
138
|
+
"distributions": [
|
|
139
|
+
{ "type": "uniform", "minY": -64, "maxY": 16, "peakY": null, "weight": 0.5 },
|
|
140
|
+
{ "type": "triangular", "minY": -64, "maxY": -32, "peakY": -59, "weight": 0.5 }
|
|
141
|
+
],
|
|
142
|
+
"biomeModifiers": {},
|
|
143
|
+
"specialRules": {}
|
|
144
|
+
},
|
|
145
|
+
"legacy": {
|
|
146
|
+
"minY": 0, "maxY": 15, "optimalY": 8, "practicalY": null, "actualPeakY": null,
|
|
147
|
+
"frequency": "common", "dimension": "overworld",
|
|
148
|
+
"distributions": [
|
|
149
|
+
{ "type": "uniform", "minY": 0, "maxY": 15, "peakY": null, "weight": 1.0 }
|
|
150
|
+
],
|
|
151
|
+
"biomeModifiers": {},
|
|
152
|
+
"specialRules": {}
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
"emerald": {
|
|
156
|
+
"modern": {
|
|
157
|
+
"minY": -16, "maxY": 320, "optimalY": 232, "practicalY": null, "actualPeakY": 90,
|
|
158
|
+
"frequency": "very_rare", "dimension": "overworld",
|
|
159
|
+
"distributions": [
|
|
160
|
+
{ "type": "triangular", "minY": -16, "maxY": 320, "peakY": 232, "weight": 1.0 }
|
|
161
|
+
],
|
|
162
|
+
"biomeModifiers": {
|
|
163
|
+
"stony_peaks": 1.0, "jagged_peaks": 1.0, "frozen_peaks": 1.0, "meadow": 1.0,
|
|
164
|
+
"grove": 1.0, "cherry_grove": 1.0, "snowy_slopes": 1.0, "windswept_hills": 1.0,
|
|
165
|
+
"windswept_forest": 1.0, "windswept_gravelly_hills": 1.0
|
|
166
|
+
},
|
|
167
|
+
"specialRules": { "biomeExclusive": true }
|
|
168
|
+
},
|
|
169
|
+
"legacy": {
|
|
170
|
+
"minY": 4, "maxY": 32, "optimalY": 18, "practicalY": null, "actualPeakY": null,
|
|
171
|
+
"frequency": "very_rare", "dimension": "overworld",
|
|
172
|
+
"distributions": [
|
|
173
|
+
{ "type": "uniform", "minY": 4, "maxY": 32, "peakY": null, "weight": 1.0 }
|
|
174
|
+
],
|
|
175
|
+
"biomeModifiers": { "extreme_hills": 1.0 },
|
|
176
|
+
"specialRules": { "biomeExclusive": true }
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
"ancient_debris": {
|
|
180
|
+
"modern": {
|
|
181
|
+
"minY": 8, "maxY": 119, "optimalY": 15, "practicalY": 14, "actualPeakY": null,
|
|
182
|
+
"frequency": "very_rare", "dimension": "nether",
|
|
183
|
+
"distributions": [
|
|
184
|
+
{ "type": "triangular", "minY": 8, "maxY": 24, "peakY": 16, "weight": 0.45 },
|
|
185
|
+
{ "type": "uniform", "minY": 8, "maxY": 119, "peakY": null, "weight": 0.55 }
|
|
186
|
+
],
|
|
187
|
+
"biomeModifiers": {
|
|
188
|
+
"nether_wastes": 1.0, "crimson_forest": 1.02, "warped_forest": 1.02,
|
|
189
|
+
"soul_sand_valley": 1.0, "basalt_deltas": 0.94
|
|
190
|
+
},
|
|
191
|
+
"specialRules": { "minMcMinor": 16 }
|
|
192
|
+
},
|
|
193
|
+
"legacy": {
|
|
194
|
+
"minY": 8, "maxY": 119, "optimalY": 15, "practicalY": 14, "actualPeakY": null,
|
|
195
|
+
"frequency": "very_rare", "dimension": "nether",
|
|
196
|
+
"distributions": [
|
|
197
|
+
{ "type": "triangular", "minY": 8, "maxY": 24, "peakY": 16, "weight": 0.45 },
|
|
198
|
+
{ "type": "uniform", "minY": 8, "maxY": 119, "peakY": null, "weight": 0.55 }
|
|
199
|
+
],
|
|
200
|
+
"biomeModifiers": {
|
|
201
|
+
"nether_wastes": 1.0, "crimson_forest": 1.02, "warped_forest": 1.02,
|
|
202
|
+
"soul_sand_valley": 1.0, "basalt_deltas": 0.94
|
|
203
|
+
},
|
|
204
|
+
"specialRules": { "minMcMinor": 16 }
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
"nether_gold": {
|
|
208
|
+
"modern": {
|
|
209
|
+
"minY": 10, "maxY": 114, "optimalY": 48, "practicalY": 45, "actualPeakY": null,
|
|
210
|
+
"frequency": "uncommon", "dimension": "nether",
|
|
211
|
+
"distributions": [
|
|
212
|
+
{ "type": "triangular", "minY": 20, "maxY": 96, "peakY": 48, "weight": 0.55 },
|
|
213
|
+
{ "type": "uniform", "minY": 10, "maxY": 114, "peakY": null, "weight": 0.45 }
|
|
214
|
+
],
|
|
215
|
+
"biomeModifiers": {
|
|
216
|
+
"nether_wastes": 1.0, "crimson_forest": 1.05, "warped_forest": 1.05,
|
|
217
|
+
"soul_sand_valley": 0.98, "basalt_deltas": 1.04
|
|
218
|
+
},
|
|
219
|
+
"specialRules": { "minMcMinor": 16 }
|
|
220
|
+
},
|
|
221
|
+
"legacy": {
|
|
222
|
+
"minY": 10, "maxY": 114, "optimalY": 48, "practicalY": 45, "actualPeakY": null,
|
|
223
|
+
"frequency": "uncommon", "dimension": "nether",
|
|
224
|
+
"distributions": [
|
|
225
|
+
{ "type": "triangular", "minY": 20, "maxY": 96, "peakY": 48, "weight": 0.55 },
|
|
226
|
+
{ "type": "uniform", "minY": 10, "maxY": 114, "peakY": null, "weight": 0.45 }
|
|
227
|
+
],
|
|
228
|
+
"biomeModifiers": {
|
|
229
|
+
"nether_wastes": 1.0, "crimson_forest": 1.05, "warped_forest": 1.05,
|
|
230
|
+
"soul_sand_valley": 0.98, "basalt_deltas": 1.04
|
|
231
|
+
},
|
|
232
|
+
"specialRules": { "minMcMinor": 16 }
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
"nether_quartz": {
|
|
236
|
+
"modern": {
|
|
237
|
+
"minY": 10, "maxY": 114, "optimalY": 42, "practicalY": null, "actualPeakY": null,
|
|
238
|
+
"frequency": "common", "dimension": "nether",
|
|
239
|
+
"distributions": [
|
|
240
|
+
{ "type": "triangular", "minY": 10, "maxY": 114, "peakY": 42, "weight": 1.0 }
|
|
241
|
+
],
|
|
242
|
+
"biomeModifiers": {
|
|
243
|
+
"nether_wastes": 1.0, "crimson_forest": 1.0, "warped_forest": 1.0,
|
|
244
|
+
"soul_sand_valley": 1.0, "basalt_deltas": 1.0
|
|
245
|
+
},
|
|
246
|
+
"specialRules": { "minMcMinor": 8 }
|
|
247
|
+
},
|
|
248
|
+
"legacy": {
|
|
249
|
+
"minY": 10, "maxY": 114, "optimalY": 42, "practicalY": null, "actualPeakY": null,
|
|
250
|
+
"frequency": "common", "dimension": "nether",
|
|
251
|
+
"distributions": [
|
|
252
|
+
{ "type": "triangular", "minY": 10, "maxY": 114, "peakY": 42, "weight": 1.0 }
|
|
253
|
+
],
|
|
254
|
+
"biomeModifiers": {
|
|
255
|
+
"nether_wastes": 1.0, "crimson_forest": 1.0, "warped_forest": 1.0,
|
|
256
|
+
"soul_sand_valley": 1.0, "basalt_deltas": 1.0
|
|
257
|
+
},
|
|
258
|
+
"specialRules": { "minMcMinor": 8 }
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
"gilded_blackstone": {
|
|
262
|
+
"modern": {
|
|
263
|
+
"minY": 62, "maxY": 88, "optimalY": 75, "practicalY": null, "actualPeakY": null,
|
|
264
|
+
"frequency": "rare", "dimension": "nether",
|
|
265
|
+
"distributions": [
|
|
266
|
+
{ "type": "uniform", "minY": 62, "maxY": 88, "peakY": null, "weight": 1.0 }
|
|
267
|
+
],
|
|
268
|
+
"biomeModifiers": {
|
|
269
|
+
"nether_wastes": 0.85, "crimson_forest": 0.9, "warped_forest": 0.9,
|
|
270
|
+
"soul_sand_valley": 0.95, "basalt_deltas": 1.1
|
|
271
|
+
},
|
|
272
|
+
"specialRules": { "minMcMinor": 16 }
|
|
273
|
+
},
|
|
274
|
+
"legacy": {
|
|
275
|
+
"minY": 62, "maxY": 88, "optimalY": 75, "practicalY": null, "actualPeakY": null,
|
|
276
|
+
"frequency": "rare", "dimension": "nether",
|
|
277
|
+
"distributions": [
|
|
278
|
+
{ "type": "uniform", "minY": 62, "maxY": 88, "peakY": null, "weight": 1.0 }
|
|
279
|
+
],
|
|
280
|
+
"biomeModifiers": {
|
|
281
|
+
"nether_wastes": 0.85, "crimson_forest": 0.9, "warped_forest": 0.9,
|
|
282
|
+
"soul_sand_valley": 0.95, "basalt_deltas": 1.1
|
|
283
|
+
},
|
|
284
|
+
"specialRules": { "minMcMinor": 16 }
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
"blackstone": {
|
|
288
|
+
"modern": {
|
|
289
|
+
"minY": 58, "maxY": 92, "optimalY": 74, "practicalY": null, "actualPeakY": null,
|
|
290
|
+
"frequency": "uncommon", "dimension": "nether",
|
|
291
|
+
"distributions": [
|
|
292
|
+
{ "type": "uniform", "minY": 58, "maxY": 92, "peakY": null, "weight": 0.55 },
|
|
293
|
+
{ "type": "triangular", "minY": 62, "maxY": 85, "peakY": 74, "weight": 0.45 }
|
|
294
|
+
],
|
|
295
|
+
"biomeModifiers": {
|
|
296
|
+
"nether_wastes": 0.88, "crimson_forest": 0.92, "warped_forest": 0.92,
|
|
297
|
+
"soul_sand_valley": 0.96, "basalt_deltas": 1.12
|
|
298
|
+
},
|
|
299
|
+
"specialRules": { "minMcMinor": 16 }
|
|
300
|
+
},
|
|
301
|
+
"legacy": {
|
|
302
|
+
"minY": 58, "maxY": 92, "optimalY": 74, "practicalY": null, "actualPeakY": null,
|
|
303
|
+
"frequency": "uncommon", "dimension": "nether",
|
|
304
|
+
"distributions": [
|
|
305
|
+
{ "type": "uniform", "minY": 58, "maxY": 92, "peakY": null, "weight": 0.55 },
|
|
306
|
+
{ "type": "triangular", "minY": 62, "maxY": 85, "peakY": 74, "weight": 0.45 }
|
|
307
|
+
],
|
|
308
|
+
"biomeModifiers": {
|
|
309
|
+
"nether_wastes": 0.88, "crimson_forest": 0.92, "warped_forest": 0.92,
|
|
310
|
+
"soul_sand_valley": 0.96, "basalt_deltas": 1.12
|
|
311
|
+
},
|
|
312
|
+
"specialRules": { "minMcMinor": 16 }
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'data'
|
|
4
|
+
require_relative 'rng'
|
|
5
|
+
|
|
6
|
+
# Distribution + confidence model. See ../../../SPEC.md sections 4-5.
|
|
7
|
+
module OrefinderEstimate
|
|
8
|
+
module Probability
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def clamp(n, lo, hi)
|
|
12
|
+
[[n, lo].max, hi].min
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def triangular_probability(y, min_y, max_y, peak_y)
|
|
16
|
+
return 0.0 if y < min_y || y > max_y
|
|
17
|
+
|
|
18
|
+
range = max_y - min_y
|
|
19
|
+
return 0.0 if range <= 0
|
|
20
|
+
|
|
21
|
+
peak = [[peak_y, min_y].max, max_y].min
|
|
22
|
+
peak_offset = peak - min_y
|
|
23
|
+
fall_len = max_y - peak
|
|
24
|
+
return 0.0 if peak_offset <= 0 && fall_len <= 0
|
|
25
|
+
|
|
26
|
+
if y <= peak
|
|
27
|
+
return (y == min_y ? 1.0 : 0.0) if peak_offset <= 0
|
|
28
|
+
|
|
29
|
+
return (y - min_y).to_f / peak_offset
|
|
30
|
+
end
|
|
31
|
+
return 0.0 if fall_len <= 0
|
|
32
|
+
|
|
33
|
+
(max_y - y).to_f / fall_len
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def uniform_probability(y, min_y, max_y)
|
|
37
|
+
y >= min_y && y <= max_y ? 1.0 : 0.0
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def normalize_biome(cfg, biome)
|
|
41
|
+
dimension = cfg['dimension'] || 'overworld'
|
|
42
|
+
b = biome.to_s.strip.downcase.gsub(/\s+/, '_')
|
|
43
|
+
if dimension == 'nether'
|
|
44
|
+
return Data::NETHER_BIOMES.include?(b) ? b : 'nether_wastes'
|
|
45
|
+
end
|
|
46
|
+
return 'plains' if Data::NETHER_BIOMES.include?(b)
|
|
47
|
+
|
|
48
|
+
b
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def biome_multiplier(cfg, normalized_biome)
|
|
52
|
+
m = cfg['biomeModifiers'][normalized_biome]
|
|
53
|
+
return m unless m.nil?
|
|
54
|
+
return 0.12 if cfg['specialRules'] && cfg['specialRules']['biomeExclusive']
|
|
55
|
+
|
|
56
|
+
1.0
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def calculate_ore_probability(cfg, y, raw_biome)
|
|
60
|
+
nb = normalize_biome(cfg, raw_biome)
|
|
61
|
+
total = 0.0
|
|
62
|
+
cfg['distributions'].each do |dist|
|
|
63
|
+
prob = if dist['type'] == 'triangular' && !dist['peakY'].nil?
|
|
64
|
+
triangular_probability(y, dist['minY'], dist['maxY'], dist['peakY'])
|
|
65
|
+
elsif dist['type'] == 'uniform'
|
|
66
|
+
uniform_probability(y, dist['minY'], dist['maxY'])
|
|
67
|
+
else
|
|
68
|
+
0.0
|
|
69
|
+
end
|
|
70
|
+
total += prob * dist['weight']
|
|
71
|
+
end
|
|
72
|
+
total *= biome_multiplier(cfg, nb)
|
|
73
|
+
[total, 1.0].min
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def optimal_y_for_mining(cfg)
|
|
77
|
+
return cfg['practicalY'] unless cfg['practicalY'].nil?
|
|
78
|
+
return cfg['actualPeakY'] unless cfg['actualPeakY'].nil?
|
|
79
|
+
|
|
80
|
+
cfg['optimalY']
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def calculate_confidence(probability, distance, y_level, optimal_y)
|
|
84
|
+
confidence = probability * 100
|
|
85
|
+
confidence -= [(y_level - optimal_y).abs * 2, 30].min
|
|
86
|
+
confidence -= [distance / 100.0, 20].min
|
|
87
|
+
clamp(Rng.round_to_tenth(confidence), 0, 100)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Deterministic RNG shared by every Ore Finder Estimate port. See ../../../SPEC.md section 2.
|
|
4
|
+
module OrefinderEstimate
|
|
5
|
+
module Rng
|
|
6
|
+
MASK = 0xFFFFFFFF
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# floor(x + 0.5); matches JS Math.round (not banker's rounding).
|
|
11
|
+
def round_half_up(x)
|
|
12
|
+
(x + 0.5).floor
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def round_to_tenth(x)
|
|
16
|
+
round_half_up(x * 10) / 10.0
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def imul(a, b)
|
|
20
|
+
((a & MASK) * (b & MASK)) & MASK
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Returns a lambda producing doubles in [0, 1).
|
|
24
|
+
def mulberry32(seed)
|
|
25
|
+
state = seed & MASK
|
|
26
|
+
lambda do
|
|
27
|
+
state = (state + 0x6D2B79F5) & MASK
|
|
28
|
+
t = state
|
|
29
|
+
t = imul(t ^ (t >> 15), t | 1)
|
|
30
|
+
t = (t ^ ((t + imul(t ^ (t >> 7), t | 61)) & MASK)) & MASK
|
|
31
|
+
(t ^ (t >> 14)) / 4_294_967_296.0
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# FNV-1a hash of parts.join('|') -> mulberry32 generator (ASCII inputs).
|
|
36
|
+
def make_rng(parts)
|
|
37
|
+
h = 2_166_136_261
|
|
38
|
+
s = parts.join('|')
|
|
39
|
+
s.each_char do |ch|
|
|
40
|
+
h ^= ch.ord
|
|
41
|
+
h = imul(h, 16_777_619)
|
|
42
|
+
end
|
|
43
|
+
mulberry32(h & MASK)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Minecraft version string -> era (modern >= 1.18, else legacy). See ../../../SPEC.md section 3.
|
|
4
|
+
module OrefinderEstimate
|
|
5
|
+
module Version
|
|
6
|
+
UNDERSCORE = /\A(?:java|bedrock)_(\d+)_(\d+)(?:_(\d+))?\z/.freeze
|
|
7
|
+
DOTTED = /(\d+)\.(\d+)(?:\.(\d+))?/.freeze
|
|
8
|
+
LOOSE = /(\d{1,2})[^0-9]+(\d{1,2})/.freeze
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def era(mc_minor)
|
|
13
|
+
mc_minor >= 18 ? 'modern' : 'legacy'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def parse_version(raw)
|
|
17
|
+
return nil unless raw.is_a?(String)
|
|
18
|
+
|
|
19
|
+
trimmed = raw.strip
|
|
20
|
+
return nil if trimmed.empty?
|
|
21
|
+
|
|
22
|
+
s = trimmed.downcase
|
|
23
|
+
edition = s.include?('bedrock') ? 'bedrock' : 'java'
|
|
24
|
+
|
|
25
|
+
if (m = UNDERSCORE.match(s))
|
|
26
|
+
mc_minor = m[2].to_i
|
|
27
|
+
patch = m[3] ? m[3].to_i : 0
|
|
28
|
+
return { 'edition' => edition, 'era' => era(mc_minor), 'mcMinor' => mc_minor, 'patch' => patch, 'raw' => trimmed }
|
|
29
|
+
end
|
|
30
|
+
if (m = DOTTED.match(s))
|
|
31
|
+
mc_minor = m[2].to_i
|
|
32
|
+
patch = m[3] ? m[3].to_i : 0
|
|
33
|
+
return { 'edition' => edition, 'era' => era(mc_minor), 'mcMinor' => mc_minor, 'patch' => patch, 'raw' => trimmed }
|
|
34
|
+
end
|
|
35
|
+
if (m = LOOSE.match(s))
|
|
36
|
+
mc_minor = m[2].to_i
|
|
37
|
+
return { 'edition' => edition, 'era' => era(mc_minor), 'mcMinor' => mc_minor, 'patch' => 0, 'raw' => trimmed }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Ore Finder Estimate - offline, approximate Minecraft ore-distribution estimator.
|
|
4
|
+
# Approximate only; for seed-accurate results use https://orefinder.io
|
|
5
|
+
require_relative 'orefinder_estimate/version'
|
|
6
|
+
require_relative 'orefinder_estimate/data'
|
|
7
|
+
require_relative 'orefinder_estimate/probability'
|
|
8
|
+
require_relative 'orefinder_estimate/cluster'
|
|
9
|
+
require_relative 'orefinder_estimate/estimate'
|
|
10
|
+
|
|
11
|
+
module OrefinderEstimate
|
|
12
|
+
VERSION = '1.0.0'
|
|
13
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: orefinder-estimate
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Ore Finder
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: A fast, dependency-free, offline estimator for the best mining Y and
|
|
13
|
+
the areas where ores are most likely to concentrate across Java and Bedrock. Pairs
|
|
14
|
+
with orefinder.io for seed-exact coordinates.
|
|
15
|
+
email:
|
|
16
|
+
- hello@orefinder.io
|
|
17
|
+
executables:
|
|
18
|
+
- orefinder-estimate
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- DISCLAIMER.md
|
|
23
|
+
- LICENSE
|
|
24
|
+
- README.md
|
|
25
|
+
- exe/orefinder-estimate
|
|
26
|
+
- lib/orefinder_estimate.rb
|
|
27
|
+
- lib/orefinder_estimate/cluster.rb
|
|
28
|
+
- lib/orefinder_estimate/data.rb
|
|
29
|
+
- lib/orefinder_estimate/estimate.rb
|
|
30
|
+
- lib/orefinder_estimate/ore-data.json
|
|
31
|
+
- lib/orefinder_estimate/probability.rb
|
|
32
|
+
- lib/orefinder_estimate/rng.rb
|
|
33
|
+
- lib/orefinder_estimate/version.rb
|
|
34
|
+
homepage: https://orefinder.io
|
|
35
|
+
licenses:
|
|
36
|
+
- MIT
|
|
37
|
+
metadata:
|
|
38
|
+
homepage_uri: https://orefinder.io
|
|
39
|
+
source_code_uri: https://github.com/nazzal5448/ore-finder-pkgs
|
|
40
|
+
rubygems_mfa_required: 'true'
|
|
41
|
+
rdoc_options: []
|
|
42
|
+
require_paths:
|
|
43
|
+
- lib
|
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: 2.7.0
|
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
requirements: []
|
|
55
|
+
rubygems_version: 3.6.7
|
|
56
|
+
specification_version: 4
|
|
57
|
+
summary: Fast offline Minecraft ore estimator (best mining Y and likely ore areas).
|
|
58
|
+
test_files: []
|