Phase 4: regenerating ore veins, each tier in one zone
Regenerating Ore Veins 1.4.0 (CurseForge only, MIT). tools/zone_ores.py writes its config from world/zones.json: surface zones as boxes from the zone grid, cave tiers by cave biome, 21 tiers including Cobblemon's evolution stones (their natural ores are off). The world report now allows ore only in tracked veins inside their tier's zone. Self-tests: a mined vein regrows after its chunk unloads and after a restart (new after_restart pass in selftest.py and quick_check.py). make_world places showcase veins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
82f540aa51
commit
5d275e4e14
19 files changed
+10843
-37
No files matched your search
+30
-3
@@ -7,9 +7,11 @@
|
||||
type and gamerules, and pregenerate a circle of `pregen_radius` around
|
||||
0,0 (the island plus a strip of sea; the sea corners inside the square
|
||||
border generate when someone gets there).
|
||||
3. In the same session: save, measure where the land ends
|
||||
(tools/world_report.py) and set a square world border `border_margin`
|
||||
blocks outside the outermost coast.
|
||||
3. In the same session: place the showcase ore veins (world/zones.json →
|
||||
"ores", tiers with "showcase": one vein sticking out of the ground near
|
||||
its zone's sample point, so players see what a regenerating vein looks
|
||||
like), save, measure where the land ends (tools/world_report.py) and set
|
||||
a square world border `border_margin` blocks outside the outermost coast.
|
||||
4. Copy the world to build/world-template without player data, then run
|
||||
the world checks and render the map (build/island-map.png).
|
||||
|
||||
@@ -29,10 +31,34 @@ from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from mcserver import BUILD, ROOT, SERVER, Server, chunky, eula_accepted, prepare, scan_log # noqa: E402
|
||||
import world_report # noqa: E402
|
||||
import zone_grid # noqa: E402
|
||||
from numpy import hypot as np_hypot # noqa: E402
|
||||
|
||||
TEMPLATE = BUILD / "world-template"
|
||||
SKIP = shutil.ignore_patterns("playerdata", "advancements", "stats", "session.lock")
|
||||
# A showcase sits this far from its zone's sample point (where guardians and the self-tests stand), first offset
|
||||
# that is still inside the zone wins.
|
||||
SHOWCASE_OFFSETS = [(16, 0), (0, 16), (-16, 0), (0, -16), (24, 24), (-24, -24)]
|
||||
|
||||
|
||||
def place_showcases(server):
|
||||
"""One hand-placed vein per showcase tier, half sunk into the ground near the zone's sample point."""
|
||||
plan = json.loads((ROOT / "world/zones.json").read_text())
|
||||
grid = zone_grid.load()
|
||||
samples = json.loads(zone_grid.ISLAND.read_text())["samples"]
|
||||
for tier, spec in plan["ores"].items():
|
||||
if not spec.get("showcase"):
|
||||
continue
|
||||
sx, _, sz = samples[spec["zone"]]
|
||||
spot = next(((sx + dx, sz + dz) for dx, dz in SHOWCASE_OFFSETS if grid.zone_at(sx + dx, sz + dz) == spec["zone"]), None)
|
||||
if spot is None:
|
||||
raise RuntimeError(f"no showcase spot inside {spec['zone']} near its sample point {sx},{sz}")
|
||||
x, z = spot
|
||||
server.command(f"forceload add {x} {z}")
|
||||
reply = server.command(f"execute in minecraft:overworld positioned {x} 0 {z} positioned over world_surface "
|
||||
f"positioned ~ ~-1 ~ run regenerating_ore_veins place {tier}").strip()
|
||||
server.command(f"forceload remove {x} {z}")
|
||||
print(f" showcase {tier} at {x},{z}: {reply[:100]}")
|
||||
|
||||
|
||||
def border_for(world, recipe):
|
||||
@@ -75,6 +101,7 @@ def main():
|
||||
print(f" {server.command(f'gamerule {rule} {str(value).lower()}').strip()}")
|
||||
seconds = server.pregenerate(0, 0, recipe["pregen_radius"], shape="circle")
|
||||
print(f"pregeneration finished in {seconds:.0f}s")
|
||||
place_showcases(server)
|
||||
server.command("save-all flush")
|
||||
cx, cz, size = border_for(world, recipe)
|
||||
print(f" {server.command(f'worldborder center {cx} {cz}').strip()}")
|
||||
|
||||
+6
-3
@@ -227,9 +227,12 @@ class Server:
|
||||
failures.append(f"{label}: {check['command']!r} did not match {check['expect']!r}")
|
||||
return failures
|
||||
|
||||
def run_selftest(self, timeout=300):
|
||||
"""Run the companion mod's in-game tests (needs SELFTEST_JVM_ARGS); returns the failures."""
|
||||
reply = self.command("cobblesurvival selftest run").strip()
|
||||
def run_selftest(self, timeout=300, after_restart=False):
|
||||
"""Run the companion mod's in-game tests (needs SELFTEST_JVM_ARGS); returns the failures.
|
||||
|
||||
after_restart runs the second halves of the cases that span a restart (stop, start again, then call this).
|
||||
"""
|
||||
reply = self.command("cobblesurvival selftest run" + (" after_restart" if after_restart else "")).strip()
|
||||
if "started" not in reply:
|
||||
return [f"selftest did not start: {reply!r}"]
|
||||
print(f" {reply}")
|
||||
|
||||
+23
-3
@@ -12,9 +12,13 @@
|
||||
square of structure_radius around each, plus spawn_radius around spawn.
|
||||
5. Force-load the chunks around the sampled village for a few seconds, as a
|
||||
nearby player would, so Lootr converts its loot chests.
|
||||
6. Stop, scan the server log, and run tools/world_report.py --sample on the
|
||||
sampled world: no ore, no vanilla mobs or mob sources, only listed biomes,
|
||||
gamerules as in the recipe, and no vanilla loot chest left near the village.
|
||||
6. Stop, scan the server log, boot the same world again and run the second
|
||||
halves of the self-tests that span a restart (`selftest run after_restart`,
|
||||
e.g. a mined ore vein regrows), stop and scan that log too.
|
||||
7. Run tools/world_report.py --sample on the sampled world: ore only in
|
||||
regenerating veins inside their tier's zone, no vanilla mobs or mob sources,
|
||||
only listed biomes, gamerules as in the recipe, and no vanilla loot chest
|
||||
left near the village.
|
||||
|
||||
The border, strongholds-inside-border and the map need the whole island:
|
||||
tools/make_world.py checks those when the template is rebuilt.
|
||||
@@ -93,6 +97,7 @@ def main():
|
||||
failures.append(f"{type(e).__name__}: {e}")
|
||||
|
||||
failures += [f"log: {line}" for line in scan_log(server.log)]
|
||||
failures += restart_selftest(args_file, props)
|
||||
report = subprocess.run([sys.executable, "-I", str(ROOT / "tools/world_report.py"), str(SERVER / LEVEL),
|
||||
"--sample", "--json", str(ROOT / "build/quick-check-report.json")])
|
||||
if report.returncode:
|
||||
@@ -114,5 +119,20 @@ def main():
|
||||
print("QUICK CHECK PASSED")
|
||||
|
||||
|
||||
def restart_selftest(args_file, props):
|
||||
"""Boot the quick-check world again and run the self-tests' after-restart halves; returns the failures."""
|
||||
failures = []
|
||||
with Server(args_file, props, heap="8G", stdout_name="quick-check-restart-stdout.log",
|
||||
jvm_args=SELFTEST_JVM_ARGS) as server:
|
||||
try:
|
||||
print(f"server restarted in {server.wait_ready():.0f}s")
|
||||
failures += server.run_selftest(after_restart=True)
|
||||
server.command("save-all flush")
|
||||
server.stop()
|
||||
except Exception as e: # report; the context manager still kills the process
|
||||
failures.append(f"{type(e).__name__}: {e}")
|
||||
return failures + [f"log after restart: {line}" for line in scan_log(server.log)]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+11
-1
@@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the companion mod's in-game tests on their own: one server boot on a fresh flat
|
||||
world, `/cobblesurvival selftest run`, wait for the results, stop, scan the log.
|
||||
world, `/cobblesurvival selftest run`, wait for the results, stop, scan the log; then boot
|
||||
the world again for the second halves of the tests that span a restart
|
||||
(`/cobblesurvival selftest run after_restart`).
|
||||
|
||||
The tests (mod/src/main/kotlin/dev/cobblesurvival/selftest/) play real situations on the
|
||||
live server with every mod and config of the pack, e.g. the permadeath matrix: Pokémon
|
||||
@@ -47,6 +49,14 @@ def main():
|
||||
except Exception as e: # report; the context manager still kills the process
|
||||
failures.append(f"{type(e).__name__}: {e}")
|
||||
failures += [f"log: {line}" for line in scan_log(server.log)]
|
||||
with Server(args_file, props, stdout_name="selftest-restart-stdout.log", jvm_args=SELFTEST_JVM_ARGS) as server:
|
||||
try:
|
||||
print(f"server restarted in {server.wait_ready():.0f}s")
|
||||
failures += server.run_selftest(after_restart=True)
|
||||
server.stop()
|
||||
except Exception as e: # report; the context manager still kills the process
|
||||
failures.append(f"{type(e).__name__}: {e}")
|
||||
failures += [f"log after restart: {line}" for line in scan_log(server.log)]
|
||||
print(f"\nselftest took {time.time() - started:.0f}s")
|
||||
if failures:
|
||||
print("SELFTEST FAILED")
|
||||
|
||||
+96
-12
@@ -1,8 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check and picture a generated world (default: build/world-template).
|
||||
|
||||
* Ore scan: every block in every stored chunk is checked; any ore block
|
||||
("*_ore", ancient debris, raw ore blocks from large veins) fails the check.
|
||||
* Ore scan: every block in every stored chunk is checked. Ore ("*_ore",
|
||||
ancient debris, raw ore blocks from large veins) is only allowed as part of
|
||||
a regenerating vein (Regenerating Ore Veins' saved data), and each vein's
|
||||
blocks must lie in the zone of its ore tier (world/zones.json → "ores"),
|
||||
or at most two radii from a block of the same vein that does: the mod
|
||||
checks the biome only at a vein's centre, so its edge can reach over the
|
||||
border of a cave zone. Untracked ore fails the check, and so (except with
|
||||
--sample) does an ore tier without any vein.
|
||||
* Biome set: every biome stored in any chunk section (surface and caves)
|
||||
must be listed in world/biomes.json ("allowed" or "water").
|
||||
* End access: at least one stronghold must start inside the world border.
|
||||
@@ -38,9 +44,13 @@ import numpy as np
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import anvil # noqa: E402
|
||||
import zone_grid # noqa: E402
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
ORE = re.compile(r"(_ore|:ancient_debris|:raw_(iron|copper|gold)_block)$")
|
||||
# Blocks the regenerating veins are made of (amethyst isn't "ore" by name but is a tier).
|
||||
VEIN_BLOCKS = {b for v in json.loads((ROOT / "pack/config/regenerating_ore_veins/veins.json").read_text())
|
||||
for b in v["blocks"]}
|
||||
MOB_BLOCK = re.compile(r":(spawner|trial_spawner|infested_\w+)$")
|
||||
LOOT_CONTAINERS = {"minecraft:chest", "minecraft:trapped_chest", "minecraft:barrel", "minecraft:shulker_box"}
|
||||
SEA_LEVEL = 63
|
||||
@@ -53,7 +63,7 @@ def scan_region(path):
|
||||
floor = np.full((512, 512), -10_000, dtype=np.int16)
|
||||
top = np.full((512, 512), -10_000, dtype=np.int16)
|
||||
biome = np.full((128, 128), -1, dtype=np.int16)
|
||||
names, ores, chunks, present, strongholds = {}, {}, 0, {}, []
|
||||
names, ores, chunks, present, strongholds, ore_at = {}, {}, 0, {}, [], []
|
||||
loot = {"lootr": 0, "unconverted": 0, "unconverted_at": []}
|
||||
for cx, cz, chunk in anvil.region_chunks(path):
|
||||
for be in chunk.get("block_entities", []):
|
||||
@@ -80,13 +90,20 @@ def scan_region(path):
|
||||
present[name] = present.get(name, 0) + 1
|
||||
palette, indices = anvil.section_blocks(section)
|
||||
entries = section.get("block_states", {}).get("palette", [])
|
||||
hits = {i: name for i, name in enumerate(palette) if ORE.search(name) or MOB_BLOCK.search(name)}
|
||||
hits = {i: name for i, name in enumerate(palette)
|
||||
if ORE.search(name) or MOB_BLOCK.search(name) or name in VEIN_BLOCKS}
|
||||
hits.update({i: "minecraft:sculk_shrieker[can_summon=true]" for i, e in enumerate(entries)
|
||||
if e["Name"] == "minecraft:sculk_shrieker" and e.get("Properties", {}).get("can_summon") == "true"})
|
||||
for i, name in hits.items():
|
||||
count = int(np.count_nonzero(indices == i))
|
||||
if count:
|
||||
ores[name] = ores.get(name, 0) + count
|
||||
if count and (ORE.search(name) or name in VEIN_BLOCKS):
|
||||
biome_palette, biome_indices = anvil.section_biomes(section)
|
||||
for idx in np.flatnonzero(indices == i).tolist():
|
||||
y, z, x = idx // 256, (idx // 16) % 16, idx % 16
|
||||
biome_name = biome_palette[int(biome_indices[(y // 4) * 16 + (z // 4) * 4 + x // 4])]
|
||||
ore_at.append((cx * 16 + x, section["Y"] * 16 + y, cz * 16 + z, name, biome_name))
|
||||
decoded = {}
|
||||
for qz in range(4):
|
||||
for qx in range(4):
|
||||
@@ -99,7 +116,7 @@ def scan_region(path):
|
||||
palette, indices = decoded[y // 16]
|
||||
name = palette[int(indices[((y % 16) // 4) * 16 + qz * 4 + qx])]
|
||||
biome[oz // 4 + qz, ox // 4 + qx] = names.setdefault(name, len(names))
|
||||
return rx, rz, floor, top, biome, list(names), ores, chunks, present, strongholds, loot
|
||||
return rx, rz, floor, top, biome, list(names), ores, chunks, present, strongholds, loot, ore_at
|
||||
|
||||
|
||||
# Entities that aren't mobs: decorations, vehicles, and projectiles (a trial chamber's dispenser trap fires
|
||||
@@ -130,10 +147,11 @@ def analyse(world):
|
||||
floor = np.full((h, w), -10_000, dtype=np.int16)
|
||||
top = np.full((h, w), -10_000, dtype=np.int16)
|
||||
biome = np.full((h // 4, w // 4), -1, dtype=np.int16)
|
||||
names, ores, chunks, present, strongholds = [], {}, 0, {}, []
|
||||
names, ores, chunks, present, strongholds, ore_at = [], {}, 0, {}, [], []
|
||||
loot = {"lootr": 0, "unconverted": 0, "unconverted_at": []}
|
||||
with ProcessPoolExecutor(max_workers=os.cpu_count()) as pool:
|
||||
for rx, rz, f, t, b, local, o, n, pr, sh, lt in pool.map(scan_region, regions):
|
||||
for rx, rz, f, t, b, local, o, n, pr, sh, lt, oa in pool.map(scan_region, regions):
|
||||
ore_at += oa
|
||||
for k, v in lt.items():
|
||||
loot[k] += v
|
||||
strongholds += sh
|
||||
@@ -153,10 +171,71 @@ def analyse(world):
|
||||
for k, v in counts.items():
|
||||
entities[k] = entities.get(k, 0) + v
|
||||
return {"origin": (x0 * 512, z0 * 512), "floor": floor, "top": top, "biome": biome,
|
||||
"biome_names": names, "ores": ores, "chunks": chunks, "sections_per_biome": present,
|
||||
"biome_names": names, "ores": ores, "ore_at": ore_at, "chunks": chunks, "sections_per_biome": present,
|
||||
"strongholds": strongholds, "entities": entities, "loot": loot}
|
||||
|
||||
|
||||
def read_veins(world):
|
||||
"""{(x, y, z): vein id} from Regenerating Ore Veins' saved data (empty if the mod never ran)."""
|
||||
path = Path(world, "data/regenerating_ore_veins_veins.dat")
|
||||
if not path.exists():
|
||||
return {}
|
||||
import gzip
|
||||
data = anvil.parse_nbt(gzip.decompress(path.read_bytes()))
|
||||
veins = {}
|
||||
for entry in data.get("data", data).get("Entries", []):
|
||||
pos = entry["Pos"]
|
||||
if isinstance(pos, dict):
|
||||
key = (pos["X"], pos["Y"], pos["Z"])
|
||||
elif isinstance(pos, int): # BlockPos.asLong
|
||||
x, z, y = pos >> 38, (pos << 26) >> 38, ((pos & 0xFFF) ^ 0x800) - 0x800
|
||||
key = (x, y, ((z + (1 << 25)) % (1 << 26)) - (1 << 25))
|
||||
else:
|
||||
key = tuple(int(v) for v in pos)
|
||||
veins[key] = entry.get("VeinId", "")
|
||||
return veins
|
||||
|
||||
|
||||
def check_veins(world, ore_at, sample):
|
||||
"""Every ore block belongs to a tracked vein in its tier's zone; returns (blocks per tier and zone, problems)."""
|
||||
plan = json.loads((ROOT / "world/zones.json").read_text())
|
||||
grid = zone_grid.load()
|
||||
tier_zone = {}
|
||||
for tier, spec in plan["ores"].items():
|
||||
tier_zone[tier] = tier_zone[f"{tier}_deep"] = (spec["zone"], spec["radius"][1])
|
||||
veins = read_veins(world)
|
||||
per_tier, untracked, stray = {}, {}, []
|
||||
blocks = {(x, y, z): (name, biome) for x, y, z, name, biome in ore_at}
|
||||
zone_of = {pos: grid.zone_at(pos[0], pos[2], biome) for pos, (_, biome) in blocks.items()}
|
||||
for pos, (name, _) in blocks.items():
|
||||
vein = veins.get(pos)
|
||||
if vein is None:
|
||||
if ORE.search(name): # amethyst outside a vein is a natural geode
|
||||
untracked[name] = untracked.get(name, 0) + 1
|
||||
continue
|
||||
tier = vein[:-5] if vein.endswith("_deep") else vein
|
||||
counts = per_tier.setdefault(tier, {})
|
||||
counts[zone_of[pos] or "none"] = counts.get(zone_of[pos] or "none", 0) + 1
|
||||
want, radius = tier_zone.get(vein, (None, 0))
|
||||
if zone_of[pos] == want:
|
||||
continue
|
||||
reach = 2 * radius
|
||||
near = any(veins.get(q) == vein and zone_of.get(q) == want
|
||||
for q in ((pos[0] + dx, pos[1] + dy, pos[2] + dz) for dx in range(-reach, reach + 1)
|
||||
for dy in range(-reach, reach + 1) for dz in range(-reach, reach + 1)))
|
||||
if not near:
|
||||
stray.append((vein, pos, zone_of[pos]))
|
||||
problems = [f"{n} {name} outside any regenerating vein" for name, n in sorted(untracked.items())]
|
||||
if not sample:
|
||||
problems += [f"ore tier {tier} has no vein in the world" for tier in plan["ores"] if tier not in per_tier]
|
||||
by_vein = {}
|
||||
for vein, pos, zone in stray:
|
||||
by_vein.setdefault((vein, zone), []).append(pos)
|
||||
problems += [f"{len(at)} blocks of vein {vein} in zone {zone}, not {tier_zone.get(vein, ('(no tier)',))[0]} "
|
||||
f"(e.g. {at[0]})" for (vein, zone), at in sorted(by_vein.items(), key=str)]
|
||||
return per_tier, problems
|
||||
|
||||
|
||||
def border_of(level):
|
||||
return level.get("BorderCenterX", 0.0), level.get("BorderCenterZ", 0.0), level.get("BorderSize", 5.9999968e7)
|
||||
|
||||
@@ -348,10 +427,12 @@ def main():
|
||||
biomes = json.loads((ROOT / "world/biomes.json").read_text())
|
||||
listed = set(biomes["allowed"]) | set(biomes["water"])
|
||||
unlisted = {b: n for b, n in data["sections_per_biome"].items() if b not in listed}
|
||||
vein_counts, vein_problems = check_veins(args.world, data["ore_at"], args.sample)
|
||||
report = {
|
||||
"chunks_scanned": data["chunks"],
|
||||
"ore_blocks": {k: v for k, v in data["ores"].items() if ORE.search(k)},
|
||||
"mob_source_blocks": {k: v for k, v in data["ores"].items() if not ORE.search(k)},
|
||||
"ore_blocks": {k: v for k, v in data["ores"].items() if ORE.search(k) or k in VEIN_BLOCKS},
|
||||
"vein_blocks_per_tier_and_zone": vein_counts,
|
||||
"mob_source_blocks": {k: v for k, v in data["ores"].items() if MOB_BLOCK.search(k) or "shrieker" in k},
|
||||
"unlisted_biomes": unlisted,
|
||||
"gamerules_not_as_recipe": wrong_rules,
|
||||
"vanilla_mobs_in_world": vanilla_mobs,
|
||||
@@ -368,7 +449,9 @@ def main():
|
||||
args.json.write_text(json.dumps(report, indent=2) + "\n")
|
||||
print(json.dumps({k: v for k, v in report.items() if k != "land_biomes_percent"}, indent=2))
|
||||
|
||||
failures = ([f"{n} {name}" for name, n in data["ores"].items()] + border["problems"]
|
||||
failures = ([f"{n} {name}" for name, n in data["ores"].items() if MOB_BLOCK.search(name) or "shrieker" in name]
|
||||
+ vein_problems
|
||||
+ border["problems"]
|
||||
+ [f"unlisted biome {b} in {n} chunk sections" for b, n in unlisted.items()]
|
||||
+ [f"gamerule {r} is {v}, recipe says {recipe['gamerules'][r]}" for r, v in wrong_rules.items()]
|
||||
+ [f"{n} vanilla {mob} in the world" for mob, n in vanilla_mobs.items()])
|
||||
@@ -378,7 +461,8 @@ def main():
|
||||
sys.exit(1)
|
||||
if report["missing_listed_biomes"] and not args.sample:
|
||||
print(f"note: listed biomes not on this island: {', '.join(report['missing_listed_biomes'])}")
|
||||
print(f"\nWORLD CHECK PASSED: {data['chunks']} chunks, no ore, no vanilla mobs or mob sources, only listed biomes"
|
||||
print(f"\nWORLD CHECK PASSED: {data['chunks']} chunks, ore only in regenerating veins in their zones, "
|
||||
f"no vanilla mobs or mob sources, only listed biomes"
|
||||
+ ("" if args.sample else ", border fits the coast"))
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"""The zone grid as the companion mod sees it (datapacks/core/data/cobblesurvival/zones/island.json).
|
||||
|
||||
zone_at() answers like zone/Zones.kt: a cave biome is its cave zone; otherwise the grid cell's zone.
|
||||
"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
ISLAND = ROOT / "datapacks/core/data/cobblesurvival/zones/island.json"
|
||||
|
||||
|
||||
class Grid:
|
||||
def __init__(self, data):
|
||||
self.radius, self.step, self.size = data["radius"], data["step"], data["size"]
|
||||
self.ids = [z["id"] for z in data["zones"]]
|
||||
self.caves = {c["biome"]: c["id"] for c in data["caves"]}
|
||||
self.cells = [value for value, count in data["grid_rle"] for _ in range(count)]
|
||||
if len(self.cells) != self.size * self.size:
|
||||
raise ValueError(f"zone grid has {len(self.cells)} cells, expected {self.size ** 2}")
|
||||
|
||||
def zone_at(self, x, z, biome=None):
|
||||
if biome in self.caves:
|
||||
return self.caves[biome]
|
||||
col = (x + self.radius + self.step // 2) // self.step
|
||||
row = (z + self.radius + self.step // 2) // self.step
|
||||
if not (0 <= col < self.size and 0 <= row < self.size):
|
||||
return None
|
||||
index = self.cells[row * self.size + col]
|
||||
return self.ids[index] if 0 <= index < len(self.ids) else None
|
||||
|
||||
|
||||
def load(path=ISLAND):
|
||||
return Grid(json.loads(Path(path).read_text()))
|
||||
Executable
+171
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Write Regenerating Ore Veins' config from the zone plan, so every ore tier only has veins in its zone.
|
||||
|
||||
world/zones.json → "ores" lists the tiers. This tool writes pack/config/regenerating_ore_veins/:
|
||||
|
||||
* areas.json: each surface zone that has a tier, as boxes from the zone grid (build/zone-layout.json,
|
||||
24-block cells; rows of cells merged into rectangles), named <zone>_<n>, reaching from bedrock to
|
||||
the build limit. The mod's boxes are centre plus half-size, so a box covers its cells except their
|
||||
last row and column of blocks: no box reaches into a neighbouring zone.
|
||||
* veins.json: one vein per tier, or two when the tier has "stone" and "deepslate" blocks (an upper vein
|
||||
from y 0 up, "<tier>_deep" below). A surface tier's veins are limited to its zone's boxes and kept out
|
||||
of the cave biomes, which are zones of their own; a cave tier's veins to its cave biome. The mod checks
|
||||
the area for every block and the biome at a vein's centre. One vein only for the self-tests comes
|
||||
last ("selftest", a single coal block); its area lies far outside the world border, so it never
|
||||
generates, and the tests place it by command.
|
||||
* global.json: regenerators can't be broken by players (allow_breaking false), so nobody can remove a
|
||||
vein for good; explosions don't destroy veins.
|
||||
* vein_locator.json: the Vein Locator from a compass and a copper ingot (mid-game) instead of netherite.
|
||||
|
||||
Re-run after changing the zone plan or the grid (tools/zone_layout.py) and commit the result.
|
||||
|
||||
Usage: tools/zone_ores.py
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
OUT = ROOT / "pack/config/regenerating_ore_veins"
|
||||
LAYOUT = ROOT / "build/zone-layout.json"
|
||||
BOTTOM, TOP = -64, 320
|
||||
SELFTEST_AREA = "selftest_nowhere"
|
||||
|
||||
|
||||
def rectangles(mask, size):
|
||||
"""Cover the True cells of a size×size row-major mask with rectangles (col0, row0, col1, row1)."""
|
||||
open_runs, done = {}, []
|
||||
for row in range(size + 1):
|
||||
runs = set()
|
||||
col = 0
|
||||
while row < size and col < size:
|
||||
if mask[row * size + col]:
|
||||
start = col
|
||||
while col < size and mask[row * size + col]:
|
||||
col += 1
|
||||
runs.add((start, col - 1))
|
||||
col += 1
|
||||
for run in list(open_runs):
|
||||
if run not in runs:
|
||||
done.append((run[0], open_runs.pop(run), run[1], row - 1))
|
||||
for run in runs:
|
||||
open_runs.setdefault(run, row)
|
||||
return sorted(done, key=lambda r: (r[1], r[0]))
|
||||
|
||||
|
||||
def zone_areas(layout, zone_id):
|
||||
size, step, radius = layout["size"], layout["step"], layout["radius"]
|
||||
index = layout["zones"].index(zone_id)
|
||||
mask = [value == index for value in layout["grid"]]
|
||||
areas = []
|
||||
for n, (c0, r0, c1, r1) in enumerate(rectangles(mask, size)):
|
||||
# Cell i covers -radius - step/2 + i*step ... +step-1 (as in zone/Zones.kt).
|
||||
x0, z0 = c0 * step - radius - step // 2, r0 * step - radius - step // 2
|
||||
w, h = (c1 - c0 + 1) * step, (r1 - r0 + 1) * step
|
||||
areas.append({"dimension": "minecraft:overworld", "name": f"{zone_id}_{n}", "type": "box",
|
||||
"x": x0 + w // 2 - 1, "y": (BOTTOM + TOP) // 2, "z": z0 + h // 2 - 1,
|
||||
"dimx": w // 2 - 1, "dimy": (TOP - BOTTOM) // 2, "dimz": h // 2 - 1})
|
||||
return areas
|
||||
|
||||
|
||||
def vein(vein_id, blocks, spec, defaults, y_range, where):
|
||||
seconds = spec["regen_minutes"] * 60
|
||||
jitter = round(seconds * spec.get("jitter", defaults["jitter"]))
|
||||
return {
|
||||
"id": vein_id,
|
||||
"blocks": [b for b, _ in blocks],
|
||||
"weights": [w for _, w in blocks],
|
||||
"dimension_whitelist": ["minecraft:overworld"],
|
||||
**where,
|
||||
"shape": "sphere",
|
||||
"min_radius": spec["radius"][0],
|
||||
"max_radius": spec["radius"][1],
|
||||
"attempts": spec.get("attempts", defaults["attempts"]),
|
||||
"min_y": y_range[0],
|
||||
"max_y": y_range[1],
|
||||
"chunk_minimum_generation_separation": spec["spacing"],
|
||||
"fill_factor": spec.get("fill", defaults["fill"]),
|
||||
"regeneration_interval_seconds": seconds,
|
||||
"regeneration_interval_jitter": {"range_min": -jitter, "range_max": jitter},
|
||||
}
|
||||
|
||||
|
||||
def selftest_vein():
|
||||
return {"id": "selftest", "blocks": ["minecraft:coal_ore"], "dimension_whitelist": ["minecraft:overworld"],
|
||||
"area_whitelist": [SELFTEST_AREA], "shape": "sphere", "min_radius": 0, "max_radius": 0,
|
||||
"attempts": 1, "min_y": 0, "max_y": 0, "chunk_minimum_generation_separation": 64,
|
||||
"fill_factor": 1.0, "regeneration_interval_seconds": 3600,
|
||||
"regeneration_interval_jitter": {"range_min": 0, "range_max": 0}}
|
||||
|
||||
|
||||
def main():
|
||||
if not LAYOUT.exists():
|
||||
sys.exit(f"{LAYOUT.relative_to(ROOT)} is missing: run tools/zone_layout.py first.")
|
||||
layout = json.loads(LAYOUT.read_text())
|
||||
plan = json.loads((ROOT / "world/zones.json").read_text())
|
||||
defaults = plan["ore_defaults"]
|
||||
caves = {c["id"]: c["biome"] for c in plan["caves"]}
|
||||
surface = {z["id"] for z in plan["zones"]}
|
||||
|
||||
areas, area_names, veins = [], {}, []
|
||||
for tier, spec in plan["ores"].items():
|
||||
zone = spec["zone"]
|
||||
if zone in caves:
|
||||
where = {"biome_whitelist": [caves[zone]]}
|
||||
elif zone in surface:
|
||||
if zone not in area_names:
|
||||
boxes = zone_areas(layout, zone)
|
||||
if not boxes:
|
||||
sys.exit(f"zone {zone} (ore tier {tier}) has no cells in the grid")
|
||||
areas += boxes
|
||||
area_names[zone] = [a["name"] for a in boxes]
|
||||
where = {"area_whitelist": area_names[zone], "biome_blacklist": sorted(caves.values())}
|
||||
else:
|
||||
sys.exit(f"ore tier {tier}: unknown zone {zone}")
|
||||
low, high = spec["y"]
|
||||
if "blocks" in spec:
|
||||
veins.append(vein(tier, spec["blocks"], spec, defaults, (low, high), where))
|
||||
continue
|
||||
if spec.get("stone") and high >= 0:
|
||||
veins.append(vein(tier, [(spec["stone"], 1)], spec, defaults, (max(low, 0), high), where))
|
||||
if spec.get("deepslate") and low < 0:
|
||||
deep_id = f"{tier}_deep" if spec.get("stone") and high >= 0 else tier
|
||||
veins.append(vein(deep_id, [(spec["deepslate"], 1)], spec, defaults, (low, min(high, -1)), where))
|
||||
|
||||
areas.append({"dimension": "minecraft:overworld", "name": SELFTEST_AREA, "type": "box",
|
||||
"x": 29_000_000, "y": 0, "z": 29_000_000, "dimx": 0, "dimy": 0, "dimz": 0})
|
||||
veins.append(selftest_vein())
|
||||
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
(OUT / "areas.json").write_text(json.dumps(areas, indent=1) + "\n")
|
||||
(OUT / "veins.json").write_text(json.dumps(veins, indent=1) + "\n")
|
||||
(OUT / "global.json").write_text(json.dumps({
|
||||
"allow_breaking": False,
|
||||
"break_hardness": 5.0,
|
||||
"default_regeneration_seconds": 3600,
|
||||
"default_jitter_interval": {"range_min": 0, "range_max": 0},
|
||||
"regeneration_smoke_particles": True,
|
||||
"destroyed_by_explosives": False,
|
||||
"default_fill_factor": defaults["fill"],
|
||||
}, indent=2) + "\n")
|
||||
(OUT / "vein_locator.json").write_text(json.dumps({
|
||||
"allow_crafting": True,
|
||||
"allow_use": True,
|
||||
"crafting_recipe": ["minecraft:compass", "minecraft:copper_ingot"],
|
||||
"craft_any_order": True,
|
||||
"chance_to_break": 0.75,
|
||||
"use_durability": False,
|
||||
"durability": 2,
|
||||
"stack_size": 64,
|
||||
}, indent=2) + "\n")
|
||||
per_zone = {}
|
||||
for tier, spec in plan["ores"].items():
|
||||
per_zone.setdefault(spec["zone"], []).append(tier)
|
||||
print(f"wrote {len(veins)} veins ({len(plan['ores'])} tiers) and {len(areas)} areas to {OUT.relative_to(ROOT)}")
|
||||
for zone, tiers in per_zone.items():
|
||||
boxes = len(area_names.get(zone, []))
|
||||
print(f" {zone}: {', '.join(tiers)}" + (f" ({boxes} boxes)" if boxes else " (cave biome)"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user