Add ability to generate structure.json files

This commit is contained in:
Andrew Miner
2025-10-17 18:35:22 -06:00
parent 9ed659e856
commit 464b68fcbe
24 changed files with 203 additions and 118 deletions
@@ -1,6 +1,6 @@
from collections.abc import Iterable from collections.abc import Iterable
from mcpacker.format.datapack.biomemodifier import BiomeModifier from mcpacker.format.datapack.biomemodifier import BiomeModifier
from mcpacker.format.datapack.biomemodifier.generationstep import GenerationStep from mcpacker.format.datapack.generationstep import GenerationStep
from mcpacker.json import JsonBlob from mcpacker.json import JsonBlob
@@ -1,6 +1,6 @@
from collections.abc import Iterable from collections.abc import Iterable
from mcpacker.format.datapack.biomemodifier import BiomeModifier from mcpacker.format.datapack.biomemodifier import BiomeModifier
from mcpacker.format.datapack.biomemodifier.generationstep import GenerationStep from mcpacker.format.datapack.generationstep import GenerationStep
from mcpacker.json import JsonBlob from mcpacker.json import JsonBlob
@@ -1,5 +1,6 @@
from collections.abc import Iterable from collections.abc import Iterable
from mcpacker.format.datapack.biomemodifier import BiomeModifier from mcpacker.format.datapack.biomemodifier import BiomeModifier
from mcpacker.model.core.resourceid import ResourceId
from mcpacker.json import JsonBlob from mcpacker.json import JsonBlob
@@ -7,7 +8,7 @@ from mcpacker.json import JsonBlob
class RemoveSpawnBiomeModifier(BiomeModifier): class RemoveSpawnBiomeModifier(BiomeModifier):
def __init__(self, biomes:Iterable[str]|str, entityTypes:Iterable[str]): def __init__(self, biomes:Iterable[str]|str, entityTypes:Iterable[ResourceId]):
if isinstance(biomes, str): if isinstance(biomes, str):
biomes = [biomes] biomes = [biomes]
@@ -18,5 +19,5 @@ class RemoveSpawnBiomeModifier(BiomeModifier):
return { return {
"type": "neoforge:remove_spawns", "type": "neoforge:remove_spawns",
"biomes": list(self.biomes), "biomes": list(self.biomes),
"entity_types": list(self.entityTypes), "entity_types": [str(r) for r in self.entityTypes],
} }
+9 -3
View File
@@ -7,11 +7,17 @@ class HeightMapType:
see: https://minecraft.wiki/w/Heightmap see: https://minecraft.wiki/w/Heightmap
""" """
def __init__(self, gameId:str): def __init__(self, name:str):
self.gameId = gameId self.name = name
def __repr__(self) -> str:
return f"HeightMapType(name={self.name})"
def __str__(self) -> str:
return self.name
def asData(self) -> str: def asData(self) -> str:
return self.gameId return self.name
# Constants ######################################################################################## # Constants ########################################################################################
+33
View File
@@ -0,0 +1,33 @@
from collections.abc import Iterable
from mcpacker.model.core.resourceid import ResourceId
from mcpacker.json import JsonBlob
import mcpacker.format.datapack.heightmaptype as HM
import mcpacker.format.datapack.generationstep as GS
# Class ############################################################################################
class Structure:
def __init__(
self,
gameId:ResourceId,
biomes:Iterable[ResourceId],
):
self.gameId = gameId
self.biomes = list(biomes)
def asJsonBlob(self) -> JsonBlob:
return {
"biomes": [str(b) for b in self.biomes],
"max_distance_from_center": 10,
"project_start_to_heightmap": str(HM.OCEAN_FLOOR),
"size": 1,
"spawn_overrides": {},
"start_height": { "absolute": 0 },
"start_pool": f"{self.gameId.mod}:deposit/{self.gameId.name}_marker",
"step": str(GS.SURFACE_STRUCTURES),
"type": "minecraft:jigsaw",
"use_expansion_hack": False,
}
+1 -1
View File
@@ -9,7 +9,7 @@ class BiomeReport(ReportComposer):
def doCompose(self): def doCompose(self):
for biome in self.pack.world.biomes: for biome in self.pack.world.biomes:
self.line(f"Biome: {biome.gameId} ({biome.city})") self.line(f"Biome: {biome.gameId} ({biome.name})")
self.line() self.line()
self.indent() self.indent()
+7 -7
View File
@@ -48,17 +48,17 @@ def createReport(pack:ModPack):
def test_report(report): def test_report(report):
assert str(report).strip() == textwrap.dedent(""" assert str(report).strip() == textwrap.dedent("""
Biome: minecraft:plains (kansascity)
Traits: field, sedimentary, temperate, damp, loamy, inland
Deposits:
<no deposits>
Biome: minecraft:savanna (dallas) Biome: minecraft:savanna (dallas)
Traits: field, sedimentary, subtropical, dry, sandy, inland Traits: field, sedimentary, subtropical, dry, sandy, inland
Deposits:
<no deposits>
Biome: minecraft:plains (kansascity)
Traits: field, sedimentary, temperate, damp, loamy, inland
Deposits: Deposits:
<no deposits> <no deposits>
""").strip() """).strip()
+1 -1
View File
@@ -10,7 +10,7 @@ class MobSpawnReport(ReportComposer):
def doCompose(self): def doCompose(self):
for spawn in self.pack.world.mobSpawns: for spawn in self.pack.world.mobSpawns:
self.line(f"Mob: {spawn.gameId}") self.line(f"Mob: {spawn.name}")
self.line() self.line()
self.indent() self.indent()
+17 -22
View File
@@ -9,12 +9,12 @@ from typing import TypeVar
# Type Support ##################################################################################### # Type Support #####################################################################################
class HasGameId(Protocol): class HasName(Protocol):
@property @property
def gameId(self) -> str: ... def name(self) -> str: ...
CatalogItem = TypeVar("CatalogItem", bound=HasGameId) CatalogItem = TypeVar("CatalogItem", bound=HasName)
MapTarget = TypeVar("MapTarget") MapTarget = TypeVar("MapTarget")
@@ -29,39 +29,34 @@ class Catalog(Generic[CatalogItem]):
for item in (items or []): for item in (items or []):
self.add(item) self.add(item)
def __contains__(self, gameId:str) -> bool: def __contains__(self, name:str) -> bool:
return gameId in self._items return name in self._items
def __delitem__(self, gameId:str): def __delitem__(self, name:str):
del self._items[gameId] del self._items[name]
def __eq__(self, other:Any) -> bool: def __getitem__(self, name:str) -> CatalogItem:
if type(self) != type(other): return False return self._items[name]
if self._items != other._items: return False
return True
def __getitem__(self, gameId:str) -> CatalogItem:
return self._items[gameId]
def __iter__(self) -> Iterator[CatalogItem]: def __iter__(self) -> Iterator[CatalogItem]:
for gameId in sorted([i.gameId for i in self._items.values()]): for name in sorted([i.name for i in self._items.values()]):
yield self._items[gameId] yield self._items[name]
def __len__(self) -> int: def __len__(self) -> int:
return len(self._items) return len(self._items)
def __repr__(self) -> str: def __repr__(self) -> str:
return "".join(str(p) for p in [ return "".join(str(p) for p in [
self.__class__.__name__, "[", type(self).__name__, "(",
", ".join(str(i) for i in self), "items=", repr(self._items),
"]" ")"
]) ])
def add(self, item:CatalogItem) -> "Catalog[CatalogItem]": def add(self, item:CatalogItem) -> "Catalog[CatalogItem]":
if item.gameId in self._items: if item.name in self._items:
raise Exception(f"Catalog already contains {item.gameId}") raise Exception(f"Catalog already contains {item.name}")
self._items[item.gameId] = item self._items[item.name] = item
return self return self
def filter(self, isAllowed:Callable[[CatalogItem], bool]) -> Iterator[CatalogItem]: def filter(self, isAllowed:Callable[[CatalogItem], bool]) -> Iterator[CatalogItem]:
+13 -15
View File
@@ -7,14 +7,14 @@ from typing import Any
class SampleItem: class SampleItem:
def __init__(self, gameId:str): def __init__(self, name:str):
self.gameId = gameId self.name = name
def __eq__(self, other:Any): def __eq__(self, other:Any):
return self.gameId == other.gameId return self.name == other.name
def __repr__(self) -> str: def __repr__(self) -> str:
return self.gameId return self.name
# Fixtures ######################################################################################### # Fixtures #########################################################################################
@@ -44,11 +44,11 @@ def buildAlmostEmptyCatalog(empty):
@fixture(name="filtered") @fixture(name="filtered")
def buildFilteredList(full): def buildFilteredList(full):
yield list(full.filter(lambda x: x.gameId.endswith("a"))) yield list(full.filter(lambda x: x.name.endswith("a")))
@fixture(name="mapped") @fixture(name="mapped")
def buildMappedList(full): def buildMappedList(full):
yield list(full.map(lambda x: x.gameId)) yield list(full.map(lambda x: x.name))
# Tests ############################################################################################ # Tests ############################################################################################
@@ -63,18 +63,14 @@ def test_contains(empty, full):
def test_del(almostFull): def test_del(almostFull):
assert "bravo" not in almostFull assert "bravo" not in almostFull
def test_eq():
assert Catalog([SampleItem("alpha")]) == Catalog([SampleItem("alpha")])
assert Catalog([SampleItem("alpha")]) != Catalog([SampleItem("bravo")])
def test_filter(filtered): def test_filter(filtered):
assert [a.gameId for a in filtered] == ["alpha", "delta"] assert [a.name for a in filtered] == ["alpha", "delta"]
def test_getitem(full): def test_getitem(full):
assert full["bravo"].gameId == "bravo" assert full["bravo"].name == "bravo"
def test_iter(full): def test_iter(full):
assert [s.gameId for s in full] == ["alpha", "bravo", "charlie", "delta"] assert [s.name for s in full] == ["alpha", "bravo", "charlie", "delta"]
def test_len(empty, full): def test_len(empty, full):
assert len(empty) == 0 assert len(empty) == 0
@@ -84,5 +80,7 @@ def test_map(mapped):
assert mapped == ["alpha", "bravo", "charlie", "delta"] assert mapped == ["alpha", "bravo", "charlie", "delta"]
def test_repr(empty, full): def test_repr(empty, full):
assert repr(empty) == "Catalog[]" assert repr(empty) == "Catalog(items={})"
assert repr(full) == "Catalog[alpha, bravo, charlie, delta]" assert repr(full) == (
"Catalog(items={'alpha': alpha, 'bravo': bravo, 'charlie': charlie, 'delta': delta})"
)
+9 -8
View File
@@ -5,19 +5,20 @@ from mcpacker.model.core.ecology.heat import Heat
from mcpacker.model.core.ecology.humidity import Humidity from mcpacker.model.core.ecology.humidity import Humidity
from mcpacker.model.core.ecology.soil import Soil from mcpacker.model.core.ecology.soil import Soil
from mcpacker.model.core.ecology.water import Water from mcpacker.model.core.ecology.water import Water
from mcpacker.model.core.resourceid import ResourceId
#################################################################################################### ####################################################################################################
class Biome(object): class Biome:
""" """
A region of the world defined by a unique combination of geology, topography, and ecology. A region of the world defined by a unique combination of geology, topography, and ecology.
""" """
def __init__( def __init__(
self, self,
city:str, name:str,
gameId:str, gameId:ResourceId|str,
flora:Flora, flora:Flora,
geology:Geology, geology:Geology,
heat:Heat, heat:Heat,
@@ -25,8 +26,8 @@ class Biome(object):
soil:Soil, soil:Soil,
water:Water, water:Water,
): ):
self.city = city self.name = name
self.gameId = gameId self.gameId = ResourceId.parse(gameId)
self.flora = flora self.flora = flora
self.geology = geology self.geology = geology
@@ -38,14 +39,14 @@ class Biome(object):
def __eq__(self, other) -> bool: def __eq__(self, other) -> bool:
if type(self) != type(other): return False if type(self) != type(other): return False
if self.gameId != other.gameId: return False if self.gameId != other.gameId: return False
if self.city != other.city: return False if self.name != other.name: return False
return True return True
def __hash__(self) -> int: def __hash__(self) -> int:
return hash((self.gameId, self.city)) return hash((self.gameId, self.name))
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.gameId} ({self.city})" return f"{self.gameId} ({self.name})"
def __repr__(self) -> str: def __repr__(self) -> str:
return "".join([str(p) for p in [ return "".join([str(p) for p in [
+1 -1
View File
@@ -21,5 +21,5 @@ def createDesert():
# Tests ############################################################################################ # Tests ############################################################################################
def test_createBiome(desert): def test_createBiome(desert):
assert desert.city == "phoenix" assert desert.name == "phoenix"
assert desert.soil == S.SANDY assert desert.soil == S.SANDY
@@ -22,7 +22,7 @@ def createCatalog():
Biome("dallas", "minecraft:savanna", Biome("dallas", "minecraft:savanna",
F.FIELD, G.SEDIMENTARY, E.SUBTROPICAL, U.DRY, S.SANDY, W.INLAND F.FIELD, G.SEDIMENTARY, E.SUBTROPICAL, U.DRY, S.SANDY, W.INLAND
), ),
Biome("kansascity", "minecraft:plains", Biome("kansasname", "minecraft:plains",
F.FIELD, G.SEDIMENTARY, E.TEMPERATE, U.DAMP, S.LOAMY, W.INLAND F.FIELD, G.SEDIMENTARY, E.TEMPERATE, U.DAMP, S.LOAMY, W.INLAND
), ),
Biome("mobile", "minecraft:swamp", Biome("mobile", "minecraft:swamp",
@@ -43,4 +43,4 @@ def createCatalog():
# Tests ############################################################################################ # Tests ############################################################################################
def test_findJungles(catalog, jungles): def test_findJungles(catalog, jungles):
assert [b.city for b in catalog.filter(lambda b: jungles.accepts(b))] == ["singapore"] assert [b.name for b in catalog.filter(lambda b: jungles.accepts(b))] == ["singapore"]
+8 -3
View File
@@ -1,5 +1,6 @@
from collections.abc import Iterable from collections.abc import Iterable
from mcpacker.model.core.fauna.active import Active from mcpacker.model.core.fauna.active import Active
from mcpacker.model.core.resourceid import ResourceId
import mcpacker.model.core.fauna.active as AC import mcpacker.model.core.fauna.active as AC
@@ -11,11 +12,11 @@ class Mob:
A creature which may appear in the game world. A creature which may appear in the game world.
""" """
def __init__(self, gameId:str, active:Active|Iterable[Active]=AC.DIURNAL): def __init__(self, gameId:ResourceId|str, active:Active|Iterable[Active]=AC.DIURNAL):
if isinstance(active, Active): if isinstance(active, Active):
active = [active] active = [active]
self.gameId = gameId self.gameId = ResourceId.parse(gameId)
self.active = active self.active = active
def __eq__(self, other) -> bool: def __eq__(self, other) -> bool:
@@ -27,7 +28,11 @@ class Mob:
return hash(self.gameId) return hash(self.gameId)
def __str__(self) -> str: def __str__(self) -> str:
return self.gameId return str(self.gameId)
def __repr__(self) -> str: def __repr__(self) -> str:
return f"Mob<{self.gameId}>{{active:{self.active}}}" return f"Mob<{self.gameId}>{{active:{self.active}}}"
@property
def name(self) -> str:
return str(self.gameId)
+2 -2
View File
@@ -16,5 +16,5 @@ class MobSpawn(Spawn):
self.mob = mob self.mob = mob
@property @property
def gameId(self) -> str: def name(self) -> str:
return self.mob.gameId return self.mob.name
+9 -2
View File
@@ -5,5 +5,12 @@ from mcpacker.model.core.resourceid import ResourceId
class Item: class Item:
def __init__(self, gameId:str): def __init__(self, gameId:ResourceId|str):
self.gameId = gameId self.gameId = ResourceId.parse(gameId)
def __repr__(self) -> str:
return f"Item(gameId={repr(self.gameId)})"
@property
def name(self) -> str:
return str(self.gameId)
+2 -21
View File
@@ -1,30 +1,11 @@
from collections.abc import Iterable from collections.abc import Iterable
from mcpacker.model.catalog import Catalog from mcpacker.model.catalog import Catalog
from mcpacker.model.core.material.item import Item from mcpacker.model.core.material.item import Item
from mcpacker.model.core.resourceid import ResourceId
from typing import Iterator from typing import Iterator
# Class ############################################################################################ # Class ############################################################################################
class ItemCatalog(Catalog[Item]): class ItemCatalog(Catalog[Item]):
pass
def __init__(self, items:Iterable[Item]|None=None):
self._items = {}
for item in (items or []):
self.add(item)
def add(self, item:Item) -> "ItemCatalog":
if item.gameId in self._items:
raise Exception(f"Catalog already contains an entry for {item.gameId}")
self._items[item.gameId] = item
return self
def all(self) -> Iterator[Item]:
for item in self._items.values():
yield item
def get(self, gameId:str) -> Item|None:
return self._items.get(gameId, None)
+40 -11
View File
@@ -1,4 +1,10 @@
from collections.abc import Callable from collections.abc import Callable
from typing import Any
# Typing Support ###################################################################################
type NameTransformFunc = Callable[[str],str]
# Class ############################################################################################ # Class ############################################################################################
@@ -6,7 +12,7 @@ from collections.abc import Callable
class ResourceId: class ResourceId:
@staticmethod @staticmethod
def alterName(text:str, doTransform:Callable[[str],str]) -> str: def alterName(text:str, doTransform:NameTransformFunc) -> str:
resourceId = ResourceId.parse(text) resourceId = ResourceId.parse(text)
resourceId.name = doTransform(resourceId.name) resourceId.name = doTransform(resourceId.name)
return str(resourceId) return str(resourceId)
@@ -43,17 +49,40 @@ class ResourceId:
self.mod = mod self.mod = mod
self.name = name self.name = name
def __str__(self) -> str: def __eq__(self, other:Any) -> bool:
if not self.isTag: if type(self) != type(other): return False
return f"{self.mod}:{self.name}" if self.isTag != other.isTag: return False
if self.mod != other.mod: return False
if self.name != other.name: return False
return True
def __hash__(self) -> int:
return hash((self.isTag, self.mod, self.name))
def __lt__(self, other:Any) -> bool:
if type(self) != type(other): raise TypeError()
if self.isTag != other.isTag:
return self.isTag
if self.mod != other.mod:
return self.mod < other.mod
if self.name != other.name:
return self.name < other.name
return False
def __str__(self) -> str:
if self.isTag:
return f"#{self.mod}:{self.name}" return f"#{self.mod}:{self.name}"
return f"{self.mod}:{self.name}"
def __repr__(self) -> str: def __repr__(self) -> str:
return "".join([str(p) for p in [ return (
"ResourceId{", "ResourceId(" +
"isTag: ", ("True" if self.isTag else "False"), ", ", f"isTag={repr(self.isTag)}, " +
"mod: ", self.mod, ", ", f"mod={repr(self.mod)}, " +
"name: ", self.name, f"name={repr(self.name)}" +
"}" ")"
]]) )
+1 -1
View File
@@ -16,7 +16,7 @@ def addBlocks(pack:ModPack):
blocks.add(Block( blocks.add(Block(
gameId = "kubejs:peat_block", gameId = "kubejs:peat_block",
loot = LootTable(Loot.of(items.get("kubejs:peat_chunk"), 2, 9)), loot = LootTable(Loot.of(items["kubejs:peat_chunk"], 2, 9)),
name = "Peat Block", name = "Peat Block",
soundType = SO.DIRT, soundType = SO.DIRT,
tags = ["minecraft:mineable/shovel"], tags = ["minecraft:mineable/shovel"],
+1 -1
View File
@@ -5,4 +5,4 @@ from mcpacker.model.modpack import ModPack
# Constants ######################################################################################## # Constants ########################################################################################
def addItems(pack:ModPack): def addItems(pack:ModPack):
pack.world.items.add(Item("kubejs:peak_chunk")) pack.world.items.add(Item("kubejs:peat_chunk"))
+4 -2
View File
@@ -4,12 +4,13 @@ from mcpacker.format.report.mineralreport import MineralReport
from mcpacker.format.report.mobspawnreport import MobSpawnReport from mcpacker.format.report.mobspawnreport import MobSpawnReport
from mcpacker.model.modpack import ModPack from mcpacker.model.modpack import ModPack
from mcpacker.write.compositewriter import CompositeWriter from mcpacker.write.compositewriter import CompositeWriter
from mcpacker.write.datapack.depositwriter import DepositWriter
from mcpacker.write.datapack.disablefeaturewriter import DisableFeatureWriter from mcpacker.write.datapack.disablefeaturewriter import DisableFeatureWriter
from mcpacker.write.datapack.disablespawnwriter import DisableSpawnWriter from mcpacker.write.datapack.disablespawnwriter import DisableSpawnWriter
from mcpacker.write.datapack.metawriter import DataPackMetaWriter from mcpacker.write.datapack.metawriter import DataPackMetaWriter
from mcpacker.write.datapack.writer import DataPackWriter from mcpacker.write.datapack.writer import DataPackWriter
from mcpacker.write.incontrol.spawnerwriter import SpawnerWriter from mcpacker.write.incontrol.spawnerwriter import SpawnerWriter
from mcpacker.write.largeoredeposits.depositwriter import DepositWriter from mcpacker.write.largeoredeposits.depositwriter import DepositWriter as LodDepositWriter
from mcpacker.write.report.writer import ReportWriter from mcpacker.write.report.writer import ReportWriter
from mcpacker.write.resourcepack.metawriter import ResourcePackMetaWriter from mcpacker.write.resourcepack.metawriter import ResourcePackMetaWriter
from mcpacker.write.resourcepack.writer import ResourcePackWriter from mcpacker.write.resourcepack.writer import ResourcePackWriter
@@ -69,9 +70,10 @@ class Runner:
CompositeWriter(p, o, [ CompositeWriter(p, o, [
StaticWriter(p, o), StaticWriter(p, o),
SpawnerWriter(p, o), SpawnerWriter(p, o),
DepositWriter(p, o), LodDepositWriter(p, o),
DataPackWriter(p, o, [ DataPackWriter(p, o, [
DataPackMetaWriter(p, o, 48), DataPackMetaWriter(p, o, 48),
DepositWriter(p, o),
DisableFeatureWriter(p, o), DisableFeatureWriter(p, o),
DisableSpawnWriter(p, o), DisableSpawnWriter(p, o),
]), ]),
+27
View File
@@ -0,0 +1,27 @@
from mcpacker.format.datapack.structure import Structure
from mcpacker.model.core.geology.deposit import Deposit
from mcpacker.model.core.resourceid import ResourceId
from mcpacker.write.writer import Writer
import mcpacker.json as json
# Class ############################################################################################
class DepositWriter(Writer):
def doWrite(self):
self._writeDepositMarkerStructures()
# Private Methods ##########################################################
def _writeDepositMarkerStructures(self):
biomes = self.pack.world.biomes
for deposit in self.pack.world.deposits:
biomeNames = [b.gameId for b in biomes.filter(lambda b: deposit.acceptsBiome(b))]
resourceId = ResourceId.parse(f"{self.pack.name}:{deposit.name}")
structure = Structure(resourceId, biomeNames)
path = self.locator.structures() / f"{deposit.name}_marker.json"
self.resetOutputFile(path)
path.write_text(json.dumps(structure.asJsonBlob(), indent=json.INDENT))
+2 -2
View File
@@ -31,7 +31,7 @@ class SpawnerWriter(Writer):
def _computeBiomeNames(self, biomeFilter:BiomeFilter) -> JsonBlob: def _computeBiomeNames(self, biomeFilter:BiomeFilter) -> JsonBlob:
return list( return list(
b.gameId for b in self.pack.world.biomes.filter( str(b.gameId) for b in self.pack.world.biomes.filter(
lambda b: biomeFilter.accepts(b) lambda b: biomeFilter.accepts(b)
) )
) )
@@ -93,7 +93,7 @@ class SpawnerWriter(Writer):
def _makeRule(self, spawn:MobSpawn, habitat:Habitat, season:Season, active:Active,) -> JsonBlob: def _makeRule(self, spawn:MobSpawn, habitat:Habitat, season:Season, active:Active,) -> JsonBlob:
return { return {
"mob": spawn.mob.gameId, "mob": str(spawn.mob.gameId),
"amount": { "amount": {
"minimum": habitat.group.smallest, "minimum": habitat.group.smallest,
"maximum": habitat.group.largest, "maximum": habitat.group.largest,