Consolidate writer logic to a single package

This commit is contained in:
Andrew Miner
2025-10-09 18:15:27 -06:00
parent cc72fa1fbb
commit 320e9fb240
44 changed files with 758 additions and 230 deletions
-12
View File
@@ -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()
-7
View File
@@ -1,7 +0,0 @@
import mcpacker.emit.datapack.writer
# Tests ############################################################################################
def test_syntax():
pass
@@ -1,4 +0,0 @@
class SpawnerRuleEmitter:
def __init__(self):
pass
View File
-8
View File
@@ -1,8 +0,0 @@
import mcpacker.emit.markdown.mobreport
# Tests ############################################################################################
def test_syntax():
pass
View File
+38 -3
View File
@@ -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:
removeNoneValues(blob[key])
blob[key] = 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
+51
View File
@@ -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)
+1 -5
View File
@@ -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
@@ -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"]
+3 -6
View File
@@ -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
+1 -1
View File
@@ -36,4 +36,4 @@ def test_str(squid):
assert str(squid) == "minecraft:squid"
def test_repr(cow):
assert repr(cow) == "Mob<minecraft:cow>{active:(Active<day>{start:0, end:12000},)}"
assert repr(cow) == "Mob<minecraft:cow>{active:[Active<day>{start:0, end:12000}]}"
-38
View File
@@ -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(),
})
@@ -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,
})
-41
View File
@@ -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(),
}
+7
View File
@@ -0,0 +1,7 @@
# Constants ########################################################################################
TICK = 1
SEC = 20 * TICK
MIN = 60 * SEC
HOUR = 60 * MIN
DAY = 24 * HOUR
@@ -1,4 +1,4 @@
import mcpacker.emit.modpack
import mcpacker.model.time
# Tests ############################################################################################
+21 -11
View File
@@ -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.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()
+94 -3
View File
@@ -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()
@@ -1,4 +1,4 @@
import mcpacker.emit.datapack.locator
import mcpacker.write.datapack.locator
# Tests ############################################################################################
+7
View File
@@ -0,0 +1,7 @@
from pathlib import Path
# Constants ########################################################################################
INCONTROL_CONFIG_DIR = Path("config") / "incontrol"
+108
View File
@@ -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),
}
@@ -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"]
@@ -1,15 +1,16 @@
from mcpacker.emit.markdown.report import Report
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()
@@ -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()
+71
View File
@@ -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
@@ -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()
@@ -1,15 +1,16 @@
from mcpacker.emit.markdown.report import Report
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()
@@ -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()
@@ -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)
@@ -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()
@@ -1,4 +1,4 @@
from mcpacker.emit.markdown.report import Report
from mcpacker.write.markdown.report import Report
from pytest import fixture
@@ -1,4 +1,4 @@
from mcpacker.emit.markdown.report import Report
from mcpacker.write.markdown.report import Report
import os
import shutil
@@ -1,4 +1,4 @@
import mcpacker.emit.markdown.reportwriter
import mcpacker.write.markdown.reportwriter
# Tests ############################################################################################
@@ -1,4 +1,4 @@
import mcpacker.emit.resourcepack
import mcpacker.write.resourcepack
# Tests ############################################################################################
+54
View File
@@ -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()