diff --git a/mcpacker/emit/datapack/writer.py b/mcpacker/emit/datapack/writer.py deleted file mode 100644 index 058383a..0000000 --- a/mcpacker/emit/datapack/writer.py +++ /dev/null @@ -1,12 +0,0 @@ -from mcpacker.model.core.world import World - - -# Class ############################################################################################ - -class Writer: - - def __init__(self, world:World): - self.world = world - - def write(self): - raise NotImplementedError() diff --git a/mcpacker/emit/datapack/writer_test.py b/mcpacker/emit/datapack/writer_test.py deleted file mode 100644 index af3bcc3..0000000 --- a/mcpacker/emit/datapack/writer_test.py +++ /dev/null @@ -1,7 +0,0 @@ -import mcpacker.emit.datapack.writer - - -# Tests ############################################################################################ - -def test_syntax(): - pass diff --git a/mcpacker/emit/incontrol/spawnerruleemitter.py b/mcpacker/emit/incontrol/spawnerruleemitter.py deleted file mode 100644 index a9aede9..0000000 --- a/mcpacker/emit/incontrol/spawnerruleemitter.py +++ /dev/null @@ -1,4 +0,0 @@ -class SpawnerRuleEmitter: - - def __init__(self): - pass diff --git a/mcpacker/emit/markdown/__init__.py b/mcpacker/emit/markdown/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/mcpacker/emit/markdown/mobreport_test.py b/mcpacker/emit/markdown/mobreport_test.py deleted file mode 100644 index 3b12627..0000000 --- a/mcpacker/emit/markdown/mobreport_test.py +++ /dev/null @@ -1,8 +0,0 @@ -import mcpacker.emit.markdown.mobreport - - -# Tests ############################################################################################ - -def test_syntax(): - pass - diff --git a/mcpacker/emit/modpack/__init__.py b/mcpacker/emit/modpack/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/mcpacker/emit/resourcepack/__init__.py b/mcpacker/emit/resourcepack/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/mcpacker/json.py b/mcpacker/json.py index ebe9672..db6afbc 100644 --- a/mcpacker/json.py +++ b/mcpacker/json.py @@ -5,6 +5,11 @@ from json import loads from typing import TypeAlias +# Constants ######################################################################################## + +INDENT = " " + + # Type Support ##################################################################################### JsonBlob:TypeAlias = dict[str, "JsonBlob"] | list["JsonBlob"] | str | int | float | bool | None @@ -13,12 +18,42 @@ JsonBlob:TypeAlias = dict[str, "JsonBlob"] | list["JsonBlob"] | str | int | floa # Functions ######################################################################################## def removeNoneValues(blob:JsonBlob) -> JsonBlob: - if not isinstance(blob, dict): return blob + if isinstance(blob, dict): + for key in list(blob.keys()): + if blob[key] == None: + del blob[key] + else: + blob[key] = removeNoneValues(blob[key]) - for key in list(blob.keys()): - if blob[key] == None: - del blob[key] - else: - removeNoneValues(blob[key]) + if isinstance(blob, list): + result:list[JsonBlob] = [] + for value in blob: + if value != None: + result.append(removeNoneValues(value)) + + return result + + return blob + +def removeEmptyObjects(blob:JsonBlob) -> JsonBlob: + if isinstance(blob, dict): + for key in list(blob.keys()): + value = blob[key] + if isinstance(value, dict): + if len(value) == 0: + del blob[key] + else: + blob[key] = removeEmptyObjects(value) + + return blob + + if isinstance(blob, list): + result:list[JsonBlob] = [] + for value in blob: + if isinstance(value, dict): + if len(value) > 0: + result.append(removeEmptyObjects(value)) + + return result return blob diff --git a/mcpacker/json_test.py b/mcpacker/json_test.py new file mode 100644 index 0000000..1ac199c --- /dev/null +++ b/mcpacker/json_test.py @@ -0,0 +1,51 @@ +from mcpacker.json import JsonBlob +from pytest import fixture +from typing import cast + +import mcpacker.json as json + + +# Fixtures ######################################################################################### + +@fixture(name="blob") +def createBlob(): + yield { + "alpha": "a", + "bravo": 12, + "charlie": True, + "delta": [ + "foxtrot", + 13, + False, + [ "golf" ], + { "hotel": "india" }, + None, + ], + "juliette": { + "kilo": "lima", + "mike": {}, + "november": None + }, + "oscar": None, + "papa": [], + } + +@fixture(name="noNone") +def removeNoneValues(blob:JsonBlob): + yield json.removeNoneValues(blob) + +@fixture(name="noEmpty") +def remmoveEmptyObjects(blob:JsonBlob): + yield json.removeEmptyObjects(blob) + + +# Tests ############################################################################################ + +def test_removeNoneValues(noNone:JsonBlob): + assert len(cast(dict[str,list[str]], noNone)["delta"]) == 5 + assert "november" not in cast(dict[str,dict[str,JsonBlob]], noNone)["juliette"] + assert "oscar" not in cast(dict[str,JsonBlob], noNone) + +def test_removeEmptyObjects(noEmpty:JsonBlob): + assert "mike" not in cast(dict[str,dict[str,JsonBlob]], noEmpty)["juliette"] + assert "papa" in cast(dict[str,JsonBlob], noEmpty) diff --git a/mcpacker/model/core/ecology/biomecatalog.py b/mcpacker/model/core/ecology/biomecatalog.py index cabfd5a..61a1c11 100644 --- a/mcpacker/model/core/ecology/biomecatalog.py +++ b/mcpacker/model/core/ecology/biomecatalog.py @@ -7,8 +7,4 @@ from typing import Iterator # Classes ########################################################################################## class BiomeCatalog(Catalog[Biome]): - - def matching(self, biomeFilter:BiomeFilter) -> Iterator[Biome]: - for biome in self: - if biomeFilter.accepts(biome): - yield biome + pass diff --git a/mcpacker/model/core/ecology/biomecatalog_test.py b/mcpacker/model/core/ecology/biomecatalog_test.py index 8d9742c..89d61c2 100644 --- a/mcpacker/model/core/ecology/biomecatalog_test.py +++ b/mcpacker/model/core/ecology/biomecatalog_test.py @@ -43,4 +43,4 @@ def createCatalog(): # Tests ############################################################################################ def test_findJungles(catalog, jungles): - assert [b.city for b in catalog.matching(jungles)] == ["singapore"] + assert [b.city for b in catalog.filter(lambda b: jungles.accepts(b))] == ["singapore"] diff --git a/mcpacker/model/core/fauna/mob.py b/mcpacker/model/core/fauna/mob.py index 5f625ac..79f11af 100644 --- a/mcpacker/model/core/fauna/mob.py +++ b/mcpacker/model/core/fauna/mob.py @@ -12,15 +12,12 @@ class Mob: """ def __init__(self, gameId:str, active:Active|Iterable[Active]=AC.DIURNAL): + if isinstance(active, Active): + active = [active] + self.gameId = gameId self.active = active - if not self.active: - self.active = AC.DIURNAL - - if isinstance(self.active, Active): - self.active = (self.active,) - def __eq__(self, other) -> bool: if type(self) != type(other): return False if self.gameId != other.gameId: return False diff --git a/mcpacker/model/core/fauna/mob_test.py b/mcpacker/model/core/fauna/mob_test.py index 1619f3c..6f773c1 100644 --- a/mcpacker/model/core/fauna/mob_test.py +++ b/mcpacker/model/core/fauna/mob_test.py @@ -36,4 +36,4 @@ def test_str(squid): assert str(squid) == "minecraft:squid" def test_repr(cow): - assert repr(cow) == "Mob{active:(Active{start:0, end:12000},)}" + assert repr(cow) == "Mob{active:[Active{start:0, end:12000}]}" diff --git a/mcpacker/model/incontrol/__init__.py b/mcpacker/model/incontrol/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/mcpacker/model/incontrol/condition.py b/mcpacker/model/incontrol/condition.py deleted file mode 100644 index b723d2d..0000000 --- a/mcpacker/model/incontrol/condition.py +++ /dev/null @@ -1,38 +0,0 @@ -from mcpacker.json import JsonBlob -from mcpacker.model.incontrol.extracondition import ExtraCondition -from mcpacker.model.core.dimension import Dimension - -import mcpacker.json as json -import mcpacker.model.core.altitude as A -import mcpacker.model.core.dimension as D - - -# Class ############################################################################################ - -class Condition: - - def __init__( - self, - dimension:Dimension=D.OVERWORLD, - heightMin:int|None=None, - heightMax:int|None=None, - thisMax:int|None=None, - inWater:bool|None=None, - andCondition:ExtraCondition|None=None, - ): - self.dimension = dimension - self.heightMin = heightMin - self.heightMax = heightMax - self.thisMax = thisMax - self.inWater = inWater - self.andCondition = andCondition - - def asJsonBlob(self) -> JsonBlob: - return json.removeNoneValues({ - "dimension": self.dimension.name, - "heightmin": self.heightMin, - "heightmax": self.heightMax, - "maxthis": self.thisMax, - "inwater": self.inWater, - "and": None if not self.andCondition else self.andCondition.asJsonBlob(), - }) diff --git a/mcpacker/model/incontrol/extracondition.py b/mcpacker/model/incontrol/extracondition.py deleted file mode 100644 index 6a69b9c..0000000 --- a/mcpacker/model/incontrol/extracondition.py +++ /dev/null @@ -1,44 +0,0 @@ -from collections.abc import Iterable -from mcpacker.json import JsonBlob - -import mcpacker.json as json - - -# Class ############################################################################################ - -class ExtraCondition: - - def __init__( - self, - autumn:bool|None=None, - biome:Iterable[str]|None=None, - cave:bool|None=None, - seeSky:bool|None=None, - spring:bool|None=None, - summer:bool|None=None, - timeMax:int|None=None, - timeMin:int|None=None, - winter:bool|None=None, - ): - self.autumn = autumn - self.biome = biome - self.cave = cave - self.seeSky = seeSky - self.spring = spring - self.summer = summer - self.timeMax = timeMax - self.timeMin = timeMin - self.winter = winter - - def asJsonBlob(self) -> JsonBlob: - return json.removeNoneValues({ - "autumn": self.autumn, - "biome": None if not self.biome else "[" + ", ".join(self.biome) + "]", - "cave": self.cave, - "seesky": self.seeSky, - "spring": self.spring, - "summer": self.summer, - "maxtime": self.timeMax, - "mintime": self.timeMin, - "winter": self.winter, - }) diff --git a/mcpacker/model/incontrol/spawnerrule.py b/mcpacker/model/incontrol/spawnerrule.py deleted file mode 100644 index aa95a1c..0000000 --- a/mcpacker/model/incontrol/spawnerrule.py +++ /dev/null @@ -1,41 +0,0 @@ -from mcpacker.json import JsonBlob -from mcpacker.model.core.fauna.mobspawn import MobSpawn -from mcpacker.model.incontrol.condition import Condition - - -# Class ############################################################################################ - -class SpawnerRule: - - def __init__( - self, - mobGameId:str, - condition:Condition, - amountMin:int=1, - amountMax:int=1, - rate:float=1.0, - attempts:int=1, - weight:int=1, - ): - self.mobGameId = mobGameId - self.amountMin = amountMin - self.amountMax = amountMax - self.rate = rate - self.attempts = attempts - self.weight = weight - self.condition = condition - - def asJsonBlob(self) -> JsonBlob: - return { - "mob": [self.mobGameId], - "amount": { - "minimum": self.amountMin, - "maximum": self.amountMax, - "groupdistance": 4, - }, - "persecond": self.rate, - "attempts": self.attempts, - "weights": [self.weight], - "conditions": self.condition.asJsonBlob(), - } - diff --git a/mcpacker/model/time.py b/mcpacker/model/time.py new file mode 100644 index 0000000..8e5f856 --- /dev/null +++ b/mcpacker/model/time.py @@ -0,0 +1,7 @@ +# Constants ######################################################################################## + +TICK = 1 +SEC = 20 * TICK +MIN = 60 * SEC +HOUR = 60 * MIN +DAY = 24 * HOUR diff --git a/mcpacker/emit/modpack/__init___test.py b/mcpacker/model/time_test.py similarity index 81% rename from mcpacker/emit/modpack/__init___test.py rename to mcpacker/model/time_test.py index 16c2b27..051744e 100644 --- a/mcpacker/emit/modpack/__init___test.py +++ b/mcpacker/model/time_test.py @@ -1,4 +1,4 @@ -import mcpacker.emit.modpack +import mcpacker.model.time # Tests ############################################################################################ diff --git a/mcpacker/ui/runner.py b/mcpacker/ui/runner.py index 009edf2..c1b49c7 100644 --- a/mcpacker/ui/runner.py +++ b/mcpacker/ui/runner.py @@ -1,8 +1,11 @@ -from mcpacker.emit.markdown.reportwriter import ReportWriter -from mcpacker.emit.markdown.biomereport import BiomeReport -from mcpacker.emit.markdown.mineralreport import MineralReport -from mcpacker.emit.markdown.mobreport import MobReport -from mcpacker.model.modpack import ModPack +from mcpacker.model.modpack import ModPack +from mcpacker.write.incontrol.spawnerwriter import SpawnerWriter +from mcpacker.write.markdown.biomereport import BiomeReport +from mcpacker.write.markdown.mineralreport import MineralReport +from mcpacker.write.markdown.mobspawnreport import MobSpawnReport +from mcpacker.write.markdown.reportwriter import ReportWriter +from mcpacker.write.writer import CompositeWriter +from pathlib import Path import inspect import os @@ -11,15 +14,16 @@ import sys # Constants ######################################################################################## -OUTPUT_PATH = "output" +OUTPUT_PATH = Path("output") # Class ############################################################################################ class Runner: - def __init__(self, pack:ModPack|None=None): + def __init__(self, pack:ModPack|None=None, outputDir:Path=OUTPUT_PATH): self.pack = pack or ModPack("untitled") + self.outputDir = outputDir def abort(self, message:str, status:int=-1): print(message) @@ -40,9 +44,15 @@ class Runner: # Commands ################################################################# def _command_writeReports(self): - writer = ReportWriter([ - BiomeReport(self.pack), - MineralReport(self.pack), - MobReport(self.pack), + writer = CompositeWriter(self.pack, self.outputDir, [ + BiomeReport, + MineralReport, + MobSpawnReport, ]) - writer.write(os.path.join(OUTPUT_PATH, self.pack.name, "markdown")) + writer.write() + + def _command_writeModPack(self): + writer = CompositeWriter(self.pack, self.outputDir, [ + SpawnerWriter, + ]) + writer.write() diff --git a/mcpacker/ui/runner_test.py b/mcpacker/ui/runner_test.py index e29eb96..0e643a0 100644 --- a/mcpacker/ui/runner_test.py +++ b/mcpacker/ui/runner_test.py @@ -1,8 +1,99 @@ -import mcpacker.ui.runner +from mcpacker.model.core.ecology.biome import Biome +from mcpacker.model.core.ecology.biomefilter import BiomeFilter as BF +from mcpacker.model.core.fauna.mob import Mob +from mcpacker.model.core.fauna.mobspawn import MobSpawn +from mcpacker.model.core.geology.mineral import Mineral +from mcpacker.model.core.geology.replacement import Replacement +from mcpacker.model.core.habitat import Habitat +from mcpacker.model.modpack import ModPack +from mcpacker.ui.runner import Runner +from mcpacker.write.incontrol.spawnerwriter import SpawnerWriter +from pathlib import Path +from pytest import fixture +import mcpacker.json as json +import mcpacker.model.core.altitude as AL +import mcpacker.model.core.ecology.flora as FL +import mcpacker.model.core.ecology.geology as GE +import mcpacker.model.core.ecology.heat as HE +import mcpacker.model.core.ecology.humidity as HU +import mcpacker.model.core.ecology.soil as SO +import mcpacker.model.core.ecology.water as WA +import mcpacker.model.core.fauna.active as AC +import mcpacker.model.core.fauna.group as GR +import mcpacker.model.core.fauna.location as LO +import mcpacker.model.core.scarcity as SC +import mcpacker.model.core.season as SE +import textwrap + + +# Fixtures ######################################################################################### + +@fixture(name="addBiomes") +def defineAddBiomes(): + def addBiomes(pack:ModPack): + pack.world.biomes.add(Biome("singapore", "minecraft:jungle", + FL.CANOPY, GE.SEDIMENTARY, HE.TROPICAL, HU.WET, SO.ACIDIC, WA.INLAND + )) + + yield addBiomes + +@fixture(name="addMobs") +def defineAddMobs(): + def addMobs(pack:ModPack): + pack.world.mobs.add(Mob("minecraft:chicken", AC.DIURNAL)) + + yield addMobs + +@fixture(name="addMobSpawns") +def defineAddMobSpawnss(): + def addMobSpawns(pack:ModPack): + mobs = pack.world.mobs + spawns = pack.world.mobSpawns + + pack.world.mobSpawns.add( + MobSpawn(mobs["minecraft:chicken"], + Habitat( + altitude = AL.span(AL.LOWLANDS, AL.UPLANDS), + biomeFilter = BF([HE.TROPICAL, HU.WET, FL.within(FL.CANOPY, FL.CLEARING)]), + seasons = SE.SUMMER, + group = GR.TROUP, + scarcity = SC.COMMON, + ).derive( + seasons = SE.exclude(SE.SUMMER), + scarcity = SC.UNCOMMON + ) + ) + ) + + yield addMobSpawns + +@fixture(name="pack") +def createPack(addBiomes, addMobs, addMobSpawns): + pack = ModPack("testModPack") + pack.augment(addBiomes) + pack.augment(addMobs) + pack.augment(addMobSpawns) + yield pack + +@fixture(name="modPackRunner") +def createModPackRunner(tmp_path:Path): + runner = Runner(ModPack("testModPack"), tmp_path) + runner._command_writeModPack() + yield runner + +@fixture(name="reportRunner") +def createReportRunner(tmp_path:Path): + runner = Runner(ModPack("testModPack"), tmp_path) + runner._command_writeReports() + yield runner # Tests ############################################################################################ -def test_syntax(): - pass +def test_writeModPack(tmp_path:Path, modPackRunner:Runner): + assert (tmp_path/"testModPack"/"config"/"incontrol"/"spawner.json").exists() +def test_writeReports(tmp_path:Path, reportRunner:Runner): + assert (tmp_path/"testModPack"/"reports"/"biomes.md").exists() + assert (tmp_path/"testModPack"/"reports"/"minerals.md").exists() + assert (tmp_path/"testModPack"/"reports"/"mobspawns.md").exists() diff --git a/mcpacker/convert/__init__.py b/mcpacker/write/__init__.py similarity index 100% rename from mcpacker/convert/__init__.py rename to mcpacker/write/__init__.py diff --git a/mcpacker/emit/__init__.py b/mcpacker/write/datapack/__init__.py similarity index 100% rename from mcpacker/emit/__init__.py rename to mcpacker/write/datapack/__init__.py diff --git a/mcpacker/emit/datapack/locator.py b/mcpacker/write/datapack/locator.py similarity index 100% rename from mcpacker/emit/datapack/locator.py rename to mcpacker/write/datapack/locator.py diff --git a/mcpacker/emit/datapack/locator_test.py b/mcpacker/write/datapack/locator_test.py similarity index 77% rename from mcpacker/emit/datapack/locator_test.py rename to mcpacker/write/datapack/locator_test.py index d9cf9aa..aca0389 100644 --- a/mcpacker/emit/datapack/locator_test.py +++ b/mcpacker/write/datapack/locator_test.py @@ -1,4 +1,4 @@ -import mcpacker.emit.datapack.locator +import mcpacker.write.datapack.locator # Tests ############################################################################################ diff --git a/mcpacker/write/incontrol/__init__.py b/mcpacker/write/incontrol/__init__.py new file mode 100644 index 0000000..8e420be --- /dev/null +++ b/mcpacker/write/incontrol/__init__.py @@ -0,0 +1,7 @@ +from pathlib import Path + + +# Constants ######################################################################################## + +INCONTROL_CONFIG_DIR = Path("config") / "incontrol" + diff --git a/mcpacker/write/incontrol/spawnerwriter.py b/mcpacker/write/incontrol/spawnerwriter.py new file mode 100644 index 0000000..2619a7b --- /dev/null +++ b/mcpacker/write/incontrol/spawnerwriter.py @@ -0,0 +1,108 @@ +from mcpacker.json import JsonBlob +from mcpacker.model.core.ecology.biomefilter import BiomeFilter +from mcpacker.model.core.fauna.active import Active +from mcpacker.model.core.fauna.mobspawn import MobSpawn +from mcpacker.model.core.habitat import Habitat +from mcpacker.model.core.scarcity import Scarcity +from mcpacker.model.core.season import Season +from mcpacker.model.modpack import ModPack +from mcpacker.write.incontrol import INCONTROL_CONFIG_DIR +from mcpacker.write.writer import Writer +from pathlib import Path + +import mcpacker.json as json +import mcpacker.model.core.dimension as DI +import mcpacker.model.core.fauna.location as LO +import mcpacker.model.core.scarcity as SC +import mcpacker.model.time as TI + + +# Class ############################################################################################ + +class SpawnerWriter(Writer): + + def __init__(self, pack:ModPack, outputDir:Path): + super().__init__(pack, outputDir) + + def write(self): + path = self.outputDir / self.pack.name / INCONTROL_CONFIG_DIR / "spawner.json" + path.parent.mkdir(parents=True) + path.write_text(json.dumps(self._makeAllRules(), indent=json.INDENT)) + + # Private Functions ######################################################## + + def _computeBiomeNames(self, biomeFilter:BiomeFilter) -> JsonBlob: + return list( + b.gameId for b in self.pack.world.biomes.filter( + lambda b: biomeFilter.accepts(b) + ) + ) + + def _computeSpawnRate(self, scarcity:Scarcity) -> float: + base = { + SC.ABSENT: 24 * TI.HOUR, + SC.RARE: 10 * TI.MIN, + SC.UNUSUAL: 5 * TI.MIN, + SC.SPARSE: 1 * TI.MIN, + SC.UNCOMMON: 15 * TI.SEC, + SC.COMMON: 5 * TI.SEC, + SC.CARPET: 1 * TI.SEC, + }[scarcity] + + return 1.0 / (base / 20) + + def _computeWeight(self, scarcity:Scarcity) -> int: + return { + SC.ABSENT: 0, + SC.RARE: 1, + SC.UNUSUAL: 2, + SC.SPARSE: 4, + SC.UNCOMMON: 8, + SC.COMMON: 16, + SC.CARPET: 32, + }[scarcity] + + def _makeAndCondition(self, habitat:Habitat, season:Season, active:Active) -> JsonBlob: + return { + "biome": self._computeBiomeNames(habitat.biomeFilter), + season.name: True, + "seesky": True if LO.OUTSIDE == habitat.location else None, + "cave": True if LO.CAVE == habitat.location else None, + "mintime": active.start, + "maxtime": active.end, + } + + def _makeAllRules(self) -> JsonBlob: + result:list[JsonBlob] = [] + + for spawn in self.pack.world.mobSpawns: + for habitat in spawn.habitats: + for season in habitat.seasons: + for active in spawn.mob.active: + result.append(self._makeRule(spawn, habitat, season, active)) + + return json.removeEmptyObjects(json.removeNoneValues(result)) + + def _makeCondition(self, habitat:Habitat, season:Season, active:Active) -> JsonBlob: + return { + "dimension": DI.OVERWORLD.name, + "minheight": habitat.altitude.bottom, + "maxheight": habitat.altitude.top, + "maxthis": habitat.group.largest * 2, + "inwater": LO.WATER == habitat.location, + "and": self._makeAndCondition(habitat, season, active), + } + + def _makeRule(self, spawn:MobSpawn, habitat:Habitat, season:Season, active:Active,) -> JsonBlob: + return { + "mob": spawn.mob.gameId, + "amount": { + "minimum": habitat.group.smallest, + "maximum": habitat.group.largest, + "groupdistance": 4, + }, + "persecond": self._computeSpawnRate(habitat.scarcity), + "attempts": habitat.group.largest * 4, + "weights": [ self._computeWeight(habitat.scarcity) ], + "conditions": self._makeCondition(habitat, season, active), + } diff --git a/mcpacker/write/incontrol/spawnerwriter_test.py b/mcpacker/write/incontrol/spawnerwriter_test.py new file mode 100644 index 0000000..aedd5d4 --- /dev/null +++ b/mcpacker/write/incontrol/spawnerwriter_test.py @@ -0,0 +1,105 @@ +from mcpacker.model.core.ecology.biome import Biome +from mcpacker.model.core.ecology.biomefilter import BiomeFilter as BF +from mcpacker.model.core.fauna.mob import Mob +from mcpacker.model.core.fauna.mobspawn import MobSpawn +from mcpacker.model.core.geology.mineral import Mineral +from mcpacker.model.core.geology.replacement import Replacement +from mcpacker.model.core.habitat import Habitat +from mcpacker.model.modpack import ModPack +from mcpacker.write.incontrol.spawnerwriter import SpawnerWriter +from pathlib import Path +from pytest import fixture + +import mcpacker.json as json +import mcpacker.model.core.altitude as AL +import mcpacker.model.core.ecology.flora as FL +import mcpacker.model.core.ecology.geology as GE +import mcpacker.model.core.ecology.heat as HE +import mcpacker.model.core.ecology.humidity as HU +import mcpacker.model.core.ecology.soil as SO +import mcpacker.model.core.ecology.water as WA +import mcpacker.model.core.fauna.active as AC +import mcpacker.model.core.fauna.group as GR +import mcpacker.model.core.fauna.location as LO +import mcpacker.model.core.scarcity as SC +import mcpacker.model.core.season as SE +import textwrap + + +# Fixtures ######################################################################################### + +@fixture(name="addBiomes") +def defineAddBiomes(): + def addBiomes(pack:ModPack): + pack.world.biomes.add(Biome("singapore", "minecraft:jungle", + FL.CANOPY, GE.SEDIMENTARY, HE.TROPICAL, HU.WET, SO.ACIDIC, WA.INLAND + )) + + yield addBiomes + +@fixture(name="addMobs") +def defineAddMobs(): + def addMobs(pack:ModPack): + pack.world.mobs.add(Mob("minecraft:chicken", AC.DIURNAL)) + + yield addMobs + +@fixture(name="addMobSpawns") +def defineAddMobSpawnss(): + def addMobSpawns(pack:ModPack): + mobs = pack.world.mobs + spawns = pack.world.mobSpawns + + pack.world.mobSpawns.add( + MobSpawn(mobs["minecraft:chicken"], + Habitat( + altitude = AL.span(AL.LOWLANDS, AL.UPLANDS), + biomeFilter = BF([HE.TROPICAL, HU.WET, FL.within(FL.CANOPY, FL.CLEARING)]), + seasons = SE.SUMMER, + group = GR.TROUP, + scarcity = SC.COMMON, + ).derive( + seasons = SE.exclude(SE.SUMMER), + scarcity = SC.UNCOMMON + ) + ) + ) + + yield addMobSpawns + +@fixture(name="pack") +def createPack(addBiomes, addMobs, addMobSpawns): + pack = ModPack("testModPack") + pack.augment(addBiomes) + pack.augment(addMobs) + pack.augment(addMobSpawns) + yield pack + +@fixture(name="writer") +def createWriter(pack, tmp_path): + writer = SpawnerWriter(pack, tmp_path) + writer.write() + yield writer + + +# Tests ############################################################################################ + +def test_write(writer:SpawnerWriter, tmp_path:Path): + path = tmp_path / "testModPack" / "config" / "incontrol" / "spawner.json" + text = path.read_text() + data = json.loads(text) + + assert len(data) == 4 + + rule = data[0] + assert rule["mob"] == "minecraft:chicken" + assert rule["amount"]["maximum"] == 6 + assert abs(rule["persecond"] - 0.2) < 0.001 + assert rule["conditions"]["and"]["biome"] == ["minecraft:jungle"] + assert rule["conditions"]["and"]["summer"] + + rule = data[1] + assert abs(rule["persecond"] - 0.066) < 0.001 + assert rule["conditions"]["and"]["spring"] + + diff --git a/mcpacker/emit/datapack/__init__.py b/mcpacker/write/markdown/__init__.py similarity index 100% rename from mcpacker/emit/datapack/__init__.py rename to mcpacker/write/markdown/__init__.py diff --git a/mcpacker/emit/markdown/biomereport.py b/mcpacker/write/markdown/biomereport.py similarity index 53% rename from mcpacker/emit/markdown/biomereport.py rename to mcpacker/write/markdown/biomereport.py index 8494afc..db33972 100644 --- a/mcpacker/emit/markdown/biomereport.py +++ b/mcpacker/write/markdown/biomereport.py @@ -1,15 +1,16 @@ -from mcpacker.emit.markdown.report import Report -from mcpacker.model.modpack import ModPack +from mcpacker.write.markdown.markdownwriter import MarkdownWriter +from mcpacker.model.modpack import ModPack +from pathlib import Path # Class ############################################################################################ -class BiomeReport(Report): +class BiomeReport(MarkdownWriter): - def __init__(self, pack:ModPack): - super().__init__("biomes.md", pack) + def __init__(self, pack:ModPack, outputDir:Path): + super().__init__("biomes.md", pack, outputDir) - def build(self): + def compose(self): for biome in self.pack.world.biomes: self.line(f"# Biome: {biome.gameId} ({biome.city})") self.line() diff --git a/mcpacker/emit/markdown/biomereport_test.py b/mcpacker/write/markdown/biomereport_test.py similarity index 82% rename from mcpacker/emit/markdown/biomereport_test.py rename to mcpacker/write/markdown/biomereport_test.py index 80a7bb1..3de921f 100644 --- a/mcpacker/emit/markdown/biomereport_test.py +++ b/mcpacker/write/markdown/biomereport_test.py @@ -1,4 +1,4 @@ -from mcpacker.emit.markdown.biomereport import BiomeReport +from mcpacker.write.markdown.biomereport import BiomeReport from mcpacker.model.core.ecology.biome import Biome from mcpacker.model.modpack import ModPack from pytest import fixture @@ -33,20 +33,21 @@ def defineAddBiomes(): @fixture(name="pack") def createPack(addBiomes): - pack = ModPack("test") + pack = ModPack("testModPack") pack.augment(addBiomes) yield pack @fixture(name="report") -def createReport(pack): - report = BiomeReport(pack) - report.build() +def createReport(pack, tmp_path): + report = BiomeReport(pack, tmp_path) + report.write() yield report # Tests ############################################################################################ -def test_report(report): - assert str(report) == textwrap.dedent(""" +def test_report(tmp_path, report): + path = tmp_path / "testModPack" / "reports" / "biomes.md" + assert path.read_text() == textwrap.dedent(""" # Biome: minecraft:plains (kansascity) * Flora: field @@ -64,4 +65,5 @@ def test_report(report): * Humidity: dry * Soil: sandy * Water: inland - """).strip() + + """).lstrip() diff --git a/mcpacker/write/markdown/markdownwriter.py b/mcpacker/write/markdown/markdownwriter.py new file mode 100644 index 0000000..acf33e7 --- /dev/null +++ b/mcpacker/write/markdown/markdownwriter.py @@ -0,0 +1,71 @@ +from mcpacker.write.writer import Writer +from mcpacker.model.modpack import ModPack +from pathlib import Path +from typing import TextIO + +import os +import shutil + + +# Constants ######################################################################################## + +INDENT = " " +REPORT_DIR_PATH = "reports" + + +# Class ############################################################################################ + +class MarkdownWriter(Writer): + + def __init__(self, name:str, pack:ModPack, outputDir:Path): + super().__init__(pack, outputDir) + + self._file:TextIO|None = None + self._name = name + self._indent = 0 + self._lineBuffer:list[str] = [] + + def write(self): + path = self.outputDir / self.pack.name / REPORT_DIR_PATH / self._name + + path.parent.mkdir(exist_ok=True, parents=True) + with path.open("w") as file: + try: + self._file = file + self.compose() + finally: + self._file = None + + def compose(self) -> str: + raise NotImplementedError() + + # Helper Methods ########################################################### + + def indent(self) -> "MarkdownWriter": + self._indent += 1 + self._lineBuffer.insert(0, INDENT) + return self + + def line(self, text:str="") -> "MarkdownWriter": + assert self._file != None + self.text(text) + + self._file.write("".join(self._lineBuffer) + "\n") + self._lineBuffer = [] + while len(self._lineBuffer) < self._indent: + self._lineBuffer.append(INDENT) + + return self + + def outdent(self) -> "MarkdownWriter": + self._indent = max(0, self._indent - 1) + + if self._lineBuffer: + if self._lineBuffer[0] == INDENT: + self._lineBuffer.pop() + + return self + + def text(self, text:str) -> "MarkdownWriter": + self._lineBuffer.append(str(text)) + return self diff --git a/mcpacker/write/markdown/markdownwriter_test.py b/mcpacker/write/markdown/markdownwriter_test.py new file mode 100644 index 0000000..d0ba28c --- /dev/null +++ b/mcpacker/write/markdown/markdownwriter_test.py @@ -0,0 +1,43 @@ +from mcpacker.model.modpack import ModPack +from mcpacker.write.markdown.markdownwriter import MarkdownWriter +from pathlib import Path +from pytest import fixture + +import textwrap + + +# Helpers ########################################################################################## + +class SampleWriter(MarkdownWriter): + + def compose(self): + (self.text("alpha") + .line("bravo") + .indent() + .line("charlie") + .indent() + .line("delta") + .outdent() + .line("echo") + ) + + +# Fixtures ######################################################################################### + +@fixture(name="writer") +def createWriter(tmp_path:Path): + writer = SampleWriter("testReport.md", ModPack("testModPack"), tmp_path) + writer.write() + yield writer + + +# Tests ############################################################################################ + +def test_write(tmp_path, writer): + path = tmp_path / "testModPack" / "reports" / "testReport.md" + assert path.read_text() == textwrap.dedent(""" + alphabravo + charlie + delta + echo + """).lstrip() diff --git a/mcpacker/emit/markdown/mineralreport.py b/mcpacker/write/markdown/mineralreport.py similarity index 54% rename from mcpacker/emit/markdown/mineralreport.py rename to mcpacker/write/markdown/mineralreport.py index bbb7386..e32eb1f 100644 --- a/mcpacker/emit/markdown/mineralreport.py +++ b/mcpacker/write/markdown/mineralreport.py @@ -1,15 +1,16 @@ -from mcpacker.emit.markdown.report import Report -from mcpacker.model.modpack import ModPack +from mcpacker.write.markdown.markdownwriter import MarkdownWriter +from mcpacker.model.modpack import ModPack +from pathlib import Path # Class ############################################################################################ -class MineralReport(Report): +class MineralReport(MarkdownWriter): - def __init__(self, pack:ModPack): - super().__init__("minerals.md", pack) + def __init__(self, pack:ModPack, outputDir:Path): + super().__init__("minerals.md", pack, outputDir) - def build(self): + def compose(self): for mineral in self.pack.world.minerals: self.line(f"# Mineral: {mineral.name}") self.line() diff --git a/mcpacker/emit/markdown/mineralreport_test.py b/mcpacker/write/markdown/mineralreport_test.py similarity index 80% rename from mcpacker/emit/markdown/mineralreport_test.py rename to mcpacker/write/markdown/mineralreport_test.py index ad1bdfa..920d392 100644 --- a/mcpacker/emit/markdown/mineralreport_test.py +++ b/mcpacker/write/markdown/mineralreport_test.py @@ -1,4 +1,4 @@ -from mcpacker.emit.markdown.mineralreport import MineralReport +from mcpacker.write.markdown.mineralreport import MineralReport from mcpacker.model.core.geology.mineral import Mineral from mcpacker.model.core.geology.replacement import Replacement from mcpacker.model.modpack import ModPack @@ -26,21 +26,23 @@ def defineAddMinerals(): @fixture(name="pack") def createPack(addMinerals): - pack = ModPack("test") + pack = ModPack("testModPack") pack.augment(addMinerals) yield pack @fixture(name="report") -def createReport(pack): - report = MineralReport(pack) - report.build() +def createReport(pack, tmp_path): + report = MineralReport(pack, tmp_path) + report.write() yield report # Tests ############################################################################################ -def test_report(report): - assert str(report) == textwrap.dedent(""" +def test_report(tmp_path, report): + path = tmp_path / "testModPack" / "reports" / "minerals.md" + + assert path.read_text() == textwrap.dedent(""" # Mineral: copper * #minecraft:stone_replaceables => minecraft:copper_ore @@ -50,4 +52,5 @@ def test_report(report): * #minecraft:stone_replaceables => minecraft:iron_ore * #minecraft:deepslate_replaceables => minecraft:deepslate_iron_ore - """).strip() + + """).lstrip() diff --git a/mcpacker/emit/markdown/mobreport.py b/mcpacker/write/markdown/mobspawnreport.py similarity index 77% rename from mcpacker/emit/markdown/mobreport.py rename to mcpacker/write/markdown/mobspawnreport.py index bf56b46..206ef29 100644 --- a/mcpacker/emit/markdown/mobreport.py +++ b/mcpacker/write/markdown/mobspawnreport.py @@ -1,22 +1,23 @@ -from mcpacker.emit.markdown.report import Report from mcpacker.model.core.ecology.biometrait import BiomeTrait from mcpacker.model.modpack import ModPack +from mcpacker.write.markdown.markdownwriter import MarkdownWriter +from pathlib import Path # Class ############################################################################################ -class MobReport(Report): +class MobSpawnReport(MarkdownWriter): - def __init__(self, pack:ModPack): - super().__init__("mobs.md", pack) + def __init__(self, pack:ModPack, outputDir:Path): + super().__init__("mobspawns.md", pack, outputDir) - def build(self): - for placement in self.pack.world.mobs: - self.line(f"# Mob: {placement.gameId}") + def compose(self): + for spawn in self.pack.world.mobSpawns: + self.line(f"# Mob: {spawn.gameId}") self.line() self.indent() - for index, habitat in enumerate(placement.habitats, start=1): + for index, habitat in enumerate(spawn.habitats, start=1): self.text("* habitat ").line(index) self.indent() self.text("* altitude: ").line(habitat.altitude) diff --git a/mcpacker/write/markdown/mobspawnreport_test.py b/mcpacker/write/markdown/mobspawnreport_test.py new file mode 100644 index 0000000..bfd0468 --- /dev/null +++ b/mcpacker/write/markdown/mobspawnreport_test.py @@ -0,0 +1,99 @@ +from mcpacker.model.core.ecology.biomefilter import BiomeFilter as BF +from mcpacker.model.core.fauna.mob import Mob +from mcpacker.model.core.fauna.mobspawn import MobSpawn +from mcpacker.model.core.geology.mineral import Mineral +from mcpacker.model.core.geology.replacement import Replacement +from mcpacker.model.core.habitat import Habitat +from mcpacker.model.modpack import ModPack +from mcpacker.write.markdown.mobspawnreport import MobSpawnReport +from pytest import fixture + +import mcpacker.model.core.altitude as AL +import mcpacker.model.core.ecology.flora as FL +import mcpacker.model.core.ecology.geology as GE +import mcpacker.model.core.ecology.heat as HE +import mcpacker.model.core.ecology.humidity as HU +import mcpacker.model.core.ecology.soil as SO +import mcpacker.model.core.ecology.water as WA +import mcpacker.model.core.fauna.active as AC +import mcpacker.model.core.fauna.group as GR +import mcpacker.model.core.fauna.location as LO +import mcpacker.model.core.scarcity as SC +import mcpacker.model.core.season as SE +import textwrap + + +# Fixtures ######################################################################################### + +@fixture(name="addMobs") +def defineAddMobs(): + def addMobs(pack:ModPack): + pack.world.mobs.add(Mob("minecraft:chicken", AC.DIURNAL)) + + yield addMobs + +@fixture(name="addMobSpawns") +def defineAddMobSpawnss(): + def addMobSpawns(pack:ModPack): + mobs = pack.world.mobs + spawns = pack.world.mobSpawns + + pack.world.mobSpawns.add( + MobSpawn(mobs["minecraft:chicken"], + Habitat( + altitude = AL.span(AL.LOWLANDS, AL.UPLANDS), + biomeFilter = BF([HE.TROPICAL, HU.WET, FL.within(FL.CANOPY, FL.CLEARING)]), + seasons = SE.SUMMER, + group = GR.TROUP, + scarcity = SC.COMMON, + ).derive( + seasons = SE.exclude(SE.SUMMER), + scarcity = SC.UNCOMMON + ) + ) + ) + + yield addMobSpawns + +@fixture(name="pack") +def createPack(addMobs, addMobSpawns): + pack = ModPack("testModPack") + pack.augment(addMobs) + pack.augment(addMobSpawns) + yield pack + +@fixture(name="report") +def createReport(pack, tmp_path): + report = MobSpawnReport(pack, tmp_path) + report.write() + yield report + +# Tests ############################################################################################ + +def test_write(report, tmp_path): + path = tmp_path / "testModPack" / "reports" / "mobspawns.md" + assert path.read_text() == textwrap.dedent(""" + # Mob: minecraft:chicken + + * habitat 1 + * altitude: lowlands-uplands + * biomeFilter: + * Heat: tropical + * Humidity: wet + * Flora: any of: canopy, forest, clearing + * seasons: summer + * group: troup + * location: outside + * scarcity: common + * habitat 2 + * altitude: lowlands-uplands + * biomeFilter: + * Heat: tropical + * Humidity: wet + * Flora: any of: canopy, forest, clearing + * seasons: spring, autumn, winter + * group: troup + * location: outside + * scarcity: uncommon + + """).lstrip() diff --git a/mcpacker/emit/markdown/report.py b/mcpacker/write/markdown/report.py similarity index 100% rename from mcpacker/emit/markdown/report.py rename to mcpacker/write/markdown/report.py diff --git a/mcpacker/emit/markdown/report_test.py b/mcpacker/write/markdown/report_test.py similarity index 93% rename from mcpacker/emit/markdown/report_test.py rename to mcpacker/write/markdown/report_test.py index 214c8fb..e98f298 100644 --- a/mcpacker/emit/markdown/report_test.py +++ b/mcpacker/write/markdown/report_test.py @@ -1,4 +1,4 @@ -from mcpacker.emit.markdown.report import Report +from mcpacker.write.markdown.report import Report from pytest import fixture diff --git a/mcpacker/emit/markdown/reportwriter.py b/mcpacker/write/markdown/reportwriter.py similarity index 92% rename from mcpacker/emit/markdown/reportwriter.py rename to mcpacker/write/markdown/reportwriter.py index 205ce39..25974ae 100644 --- a/mcpacker/emit/markdown/reportwriter.py +++ b/mcpacker/write/markdown/reportwriter.py @@ -1,4 +1,4 @@ -from mcpacker.emit.markdown.report import Report +from mcpacker.write.markdown.report import Report import os import shutil diff --git a/mcpacker/emit/markdown/reportwriter_test.py b/mcpacker/write/markdown/reportwriter_test.py similarity index 75% rename from mcpacker/emit/markdown/reportwriter_test.py rename to mcpacker/write/markdown/reportwriter_test.py index 8ce6e94..b00aa9d 100644 --- a/mcpacker/emit/markdown/reportwriter_test.py +++ b/mcpacker/write/markdown/reportwriter_test.py @@ -1,4 +1,4 @@ -import mcpacker.emit.markdown.reportwriter +import mcpacker.write.markdown.reportwriter # Tests ############################################################################################ diff --git a/mcpacker/emit/incontrol/__init__.py b/mcpacker/write/resourcepack/__init__.py similarity index 100% rename from mcpacker/emit/incontrol/__init__.py rename to mcpacker/write/resourcepack/__init__.py diff --git a/mcpacker/emit/resourcepack/__init___test.py b/mcpacker/write/resourcepack/__init___test.py similarity index 79% rename from mcpacker/emit/resourcepack/__init___test.py rename to mcpacker/write/resourcepack/__init___test.py index d2c89b3..1513f8d 100644 --- a/mcpacker/emit/resourcepack/__init___test.py +++ b/mcpacker/write/resourcepack/__init___test.py @@ -1,4 +1,4 @@ -import mcpacker.emit.resourcepack +import mcpacker.write.resourcepack # Tests ############################################################################################ diff --git a/mcpacker/write/writer.py b/mcpacker/write/writer.py new file mode 100644 index 0000000..da9c375 --- /dev/null +++ b/mcpacker/write/writer.py @@ -0,0 +1,54 @@ +from collections.abc import Iterable +from mcpacker.model.modpack import ModPack +from pathlib import Path +from typing import TypeVar + +import os +import shutil + + +# Type Support ##################################################################################### + +WriterSubclass = TypeVar("WriterSubclass", bound="Writer") + + +# Class ############################################################################################ + +class Writer: + + def __init__(self, pack:ModPack, outputDir:Path): + self.pack = pack + self.outputDir = outputDir + + def resetOutputDir(self): + if self.outputDir.exists(): + shutil.rmtree(self.outputDir) + + self.outputDir.mkdir(parents=True) + + def write(self): + raise NotImplementedError() + + +# Class ############################################################################################ + +class CompositeWriter(Writer): + + def __init__( + self, + pack:ModPack, + outputDir:Path, + writers:Iterable[type[WriterSubclass]]|None=None + ): + super().__init__(pack, outputDir) + + self._writers:list[Writer] = [] + for writerClass in (writers or []): + self.add(writerClass) + + def add(self, writerClass:type[WriterSubclass]): + self._writers.append(writerClass(self.pack, self.outputDir)) + + def write(self): + for writer in self._writers: + writer.write()