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 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
@@ -1,6 +1,6 @@
from collections.abc import Iterable
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
@@ -1,5 +1,6 @@
from collections.abc import Iterable
from mcpacker.format.datapack.biomemodifier import BiomeModifier
from mcpacker.model.core.resourceid import ResourceId
from mcpacker.json import JsonBlob
@@ -7,7 +8,7 @@ from mcpacker.json import JsonBlob
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):
biomes = [biomes]
@@ -18,5 +19,5 @@ class RemoveSpawnBiomeModifier(BiomeModifier):
return {
"type": "neoforge:remove_spawns",
"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
"""
def __init__(self, gameId:str):
self.gameId = gameId
def __init__(self, name:str):
self.name = name
def __repr__(self) -> str:
return f"HeightMapType(name={self.name})"
def __str__(self) -> str:
return self.name
def asData(self) -> str:
return self.gameId
return self.name
# 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):
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.indent()
+7 -7
View File
@@ -48,17 +48,17 @@ def createReport(pack:ModPack):
def test_report(report):
assert str(report).strip() == textwrap.dedent("""
Biome: minecraft:plains (kansascity)
Traits: field, sedimentary, temperate, damp, loamy, inland
Deposits:
<no deposits>
Biome: minecraft:savanna (dallas)
Traits: field, sedimentary, subtropical, dry, sandy, inland
Deposits:
<no deposits>
Biome: minecraft:plains (kansascity)
Traits: field, sedimentary, temperate, damp, loamy, inland
Deposits:
<no deposits>
""").strip()
+1 -1
View File
@@ -10,7 +10,7 @@ class MobSpawnReport(ReportComposer):
def doCompose(self):
for spawn in self.pack.world.mobSpawns:
self.line(f"Mob: {spawn.gameId}")
self.line(f"Mob: {spawn.name}")
self.line()
self.indent()
+23 -28
View File
@@ -1,20 +1,20 @@
from collections.abc import Iterable
from typing import Any
from typing import Callable
from typing import Generic
from typing import Iterator
from typing import Protocol
from typing import TypeVar
from typing import Any
from typing import Callable
from typing import Generic
from typing import Iterator
from typing import Protocol
from typing import TypeVar
# Type Support #####################################################################################
class HasGameId(Protocol):
class HasName(Protocol):
@property
def gameId(self) -> str: ...
def name(self) -> str: ...
CatalogItem = TypeVar("CatalogItem", bound=HasGameId)
CatalogItem = TypeVar("CatalogItem", bound=HasName)
MapTarget = TypeVar("MapTarget")
@@ -29,39 +29,34 @@ class Catalog(Generic[CatalogItem]):
for item in (items or []):
self.add(item)
def __contains__(self, gameId:str) -> bool:
return gameId in self._items
def __contains__(self, name:str) -> bool:
return name in self._items
def __delitem__(self, gameId:str):
del self._items[gameId]
def __delitem__(self, name:str):
del self._items[name]
def __eq__(self, other:Any) -> bool:
if type(self) != type(other): return False
if self._items != other._items: return False
return True
def __getitem__(self, gameId:str) -> CatalogItem:
return self._items[gameId]
def __getitem__(self, name:str) -> CatalogItem:
return self._items[name]
def __iter__(self) -> Iterator[CatalogItem]:
for gameId in sorted([i.gameId for i in self._items.values()]):
yield self._items[gameId]
for name in sorted([i.name for i in self._items.values()]):
yield self._items[name]
def __len__(self) -> int:
return len(self._items)
def __repr__(self) -> str:
return "".join(str(p) for p in [
self.__class__.__name__, "[",
", ".join(str(i) for i in self),
"]"
type(self).__name__, "(",
"items=", repr(self._items),
")"
])
def add(self, item:CatalogItem) -> "Catalog[CatalogItem]":
if item.gameId in self._items:
raise Exception(f"Catalog already contains {item.gameId}")
if item.name in self._items:
raise Exception(f"Catalog already contains {item.name}")
self._items[item.gameId] = item
self._items[item.name] = item
return self
def filter(self, isAllowed:Callable[[CatalogItem], bool]) -> Iterator[CatalogItem]:
+13 -15
View File
@@ -7,14 +7,14 @@ from typing import Any
class SampleItem:
def __init__(self, gameId:str):
self.gameId = gameId
def __init__(self, name:str):
self.name = name
def __eq__(self, other:Any):
return self.gameId == other.gameId
return self.name == other.name
def __repr__(self) -> str:
return self.gameId
return self.name
# Fixtures #########################################################################################
@@ -44,11 +44,11 @@ def buildAlmostEmptyCatalog(empty):
@fixture(name="filtered")
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")
def buildMappedList(full):
yield list(full.map(lambda x: x.gameId))
yield list(full.map(lambda x: x.name))
# Tests ############################################################################################
@@ -63,18 +63,14 @@ def test_contains(empty, full):
def test_del(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):
assert [a.gameId for a in filtered] == ["alpha", "delta"]
assert [a.name for a in filtered] == ["alpha", "delta"]
def test_getitem(full):
assert full["bravo"].gameId == "bravo"
assert full["bravo"].name == "bravo"
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):
assert len(empty) == 0
@@ -84,5 +80,7 @@ def test_map(mapped):
assert mapped == ["alpha", "bravo", "charlie", "delta"]
def test_repr(empty, full):
assert repr(empty) == "Catalog[]"
assert repr(full) == "Catalog[alpha, bravo, charlie, delta]"
assert repr(empty) == "Catalog(items={})"
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.soil import Soil
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.
"""
def __init__(
self,
city:str,
gameId:str,
name:str,
gameId:ResourceId|str,
flora:Flora,
geology:Geology,
heat:Heat,
@@ -25,8 +26,8 @@ class Biome(object):
soil:Soil,
water:Water,
):
self.city = city
self.gameId = gameId
self.name = name
self.gameId = ResourceId.parse(gameId)
self.flora = flora
self.geology = geology
@@ -38,14 +39,14 @@ class Biome(object):
def __eq__(self, other) -> bool:
if type(self) != type(other): 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
def __hash__(self) -> int:
return hash((self.gameId, self.city))
return hash((self.gameId, self.name))
def __str__(self) -> str:
return f"{self.gameId} ({self.city})"
return f"{self.gameId} ({self.name})"
def __repr__(self) -> str:
return "".join([str(p) for p in [
+1 -1
View File
@@ -21,5 +21,5 @@ def createDesert():
# Tests ############################################################################################
def test_createBiome(desert):
assert desert.city == "phoenix"
assert desert.name == "phoenix"
assert desert.soil == S.SANDY
@@ -22,7 +22,7 @@ def createCatalog():
Biome("dallas", "minecraft:savanna",
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
),
Biome("mobile", "minecraft:swamp",
@@ -43,4 +43,4 @@ def createCatalog():
# Tests ############################################################################################
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 mcpacker.model.core.fauna.active import Active
from mcpacker.model.core.resourceid import ResourceId
import mcpacker.model.core.fauna.active as AC
@@ -11,11 +12,11 @@ class Mob:
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):
active = [active]
self.gameId = gameId
self.gameId = ResourceId.parse(gameId)
self.active = active
def __eq__(self, other) -> bool:
@@ -27,7 +28,11 @@ class Mob:
return hash(self.gameId)
def __str__(self) -> str:
return self.gameId
return str(self.gameId)
def __repr__(self) -> str:
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
@property
def gameId(self) -> str:
return self.mob.gameId
def name(self) -> str:
return self.mob.name
+9 -2
View File
@@ -5,5 +5,12 @@ from mcpacker.model.core.resourceid import ResourceId
class Item:
def __init__(self, gameId:str):
self.gameId = gameId
def __init__(self, gameId:ResourceId|str):
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)
+5 -24
View File
@@ -1,30 +1,11 @@
from collections.abc import Iterable
from mcpacker.model.catalog import Catalog
from collections.abc import Iterable
from mcpacker.model.catalog import Catalog
from mcpacker.model.core.material.item import Item
from typing import Iterator
from mcpacker.model.core.resourceid import ResourceId
from typing import Iterator
# Class ############################################################################################
class ItemCatalog(Catalog[Item]):
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)
pass
+40 -11
View File
@@ -1,4 +1,10 @@
from collections.abc import Callable
from typing import Any
# Typing Support ###################################################################################
type NameTransformFunc = Callable[[str],str]
# Class ############################################################################################
@@ -6,7 +12,7 @@ from collections.abc import Callable
class ResourceId:
@staticmethod
def alterName(text:str, doTransform:Callable[[str],str]) -> str:
def alterName(text:str, doTransform:NameTransformFunc) -> str:
resourceId = ResourceId.parse(text)
resourceId.name = doTransform(resourceId.name)
return str(resourceId)
@@ -43,17 +49,40 @@ class ResourceId:
self.mod = mod
self.name = name
def __eq__(self, other:Any) -> bool:
if type(self) != type(other): return False
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 not self.isTag:
return f"{self.mod}:{self.name}"
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:
return "".join([str(p) for p in [
"ResourceId{",
"isTag: ", ("True" if self.isTag else "False"), ", ",
"mod: ", self.mod, ", ",
"name: ", self.name,
"}"
]])
return (
"ResourceId(" +
f"isTag={repr(self.isTag)}, " +
f"mod={repr(self.mod)}, " +
f"name={repr(self.name)}" +
")"
)
+1 -1
View File
@@ -16,7 +16,7 @@ def addBlocks(pack:ModPack):
blocks.add(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",
soundType = SO.DIRT,
tags = ["minecraft:mineable/shovel"],
+1 -1
View File
@@ -5,4 +5,4 @@ from mcpacker.model.modpack import ModPack
# Constants ########################################################################################
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.model.modpack import ModPack
from mcpacker.write.compositewriter import CompositeWriter
from mcpacker.write.datapack.depositwriter import DepositWriter
from mcpacker.write.datapack.disablefeaturewriter import DisableFeatureWriter
from mcpacker.write.datapack.disablespawnwriter import DisableSpawnWriter
from mcpacker.write.datapack.metawriter import DataPackMetaWriter
from mcpacker.write.datapack.writer import DataPackWriter
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.resourcepack.metawriter import ResourcePackMetaWriter
from mcpacker.write.resourcepack.writer import ResourcePackWriter
@@ -69,9 +70,10 @@ class Runner:
CompositeWriter(p, o, [
StaticWriter(p, o),
SpawnerWriter(p, o),
DepositWriter(p, o),
LodDepositWriter(p, o),
DataPackWriter(p, o, [
DataPackMetaWriter(p, o, 48),
DepositWriter(p, o),
DisableFeatureWriter(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:
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)
)
)
@@ -93,7 +93,7 @@ class SpawnerWriter(Writer):
def _makeRule(self, spawn:MobSpawn, habitat:Habitat, season:Season, active:Active,) -> JsonBlob:
return {
"mob": spawn.mob.gameId,
"mob": str(spawn.mob.gameId),
"amount": {
"minimum": habitat.group.smallest,
"maximum": habitat.group.largest,