diff --git a/mcpacker/format/datapack/biomemodifier/addfeature.py b/mcpacker/format/datapack/biomemodifier/addfeature.py index 3af87d1..30d7e42 100644 --- a/mcpacker/format/datapack/biomemodifier/addfeature.py +++ b/mcpacker/format/datapack/biomemodifier/addfeature.py @@ -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 diff --git a/mcpacker/format/datapack/biomemodifier/removefeature.py b/mcpacker/format/datapack/biomemodifier/removefeature.py index 7727262..732417e 100644 --- a/mcpacker/format/datapack/biomemodifier/removefeature.py +++ b/mcpacker/format/datapack/biomemodifier/removefeature.py @@ -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 diff --git a/mcpacker/format/datapack/biomemodifier/removespawn.py b/mcpacker/format/datapack/biomemodifier/removespawn.py index 971906a..23af5d4 100644 --- a/mcpacker/format/datapack/biomemodifier/removespawn.py +++ b/mcpacker/format/datapack/biomemodifier/removespawn.py @@ -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], } diff --git a/mcpacker/format/datapack/biomemodifier/generationstep.py b/mcpacker/format/datapack/generationstep.py similarity index 100% rename from mcpacker/format/datapack/biomemodifier/generationstep.py rename to mcpacker/format/datapack/generationstep.py diff --git a/mcpacker/format/datapack/heightmaptype.py b/mcpacker/format/datapack/heightmaptype.py index 8738b85..ba14ae9 100644 --- a/mcpacker/format/datapack/heightmaptype.py +++ b/mcpacker/format/datapack/heightmaptype.py @@ -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 ######################################################################################## diff --git a/mcpacker/format/datapack/structure.py b/mcpacker/format/datapack/structure.py new file mode 100644 index 0000000..1def808 --- /dev/null +++ b/mcpacker/format/datapack/structure.py @@ -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, + } diff --git a/mcpacker/format/report/biomereport.py b/mcpacker/format/report/biomereport.py index 34602e9..d428a3d 100644 --- a/mcpacker/format/report/biomereport.py +++ b/mcpacker/format/report/biomereport.py @@ -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() diff --git a/mcpacker/format/report/biomereport_test.py b/mcpacker/format/report/biomereport_test.py index c266270..191d4fe 100644 --- a/mcpacker/format/report/biomereport_test.py +++ b/mcpacker/format/report/biomereport_test.py @@ -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: - - Biome: minecraft:savanna (dallas) Traits: field, sedimentary, subtropical, dry, sandy, inland + Deposits: + + + Biome: minecraft:plains (kansascity) + + Traits: field, sedimentary, temperate, damp, loamy, inland + Deposits: """).strip() diff --git a/mcpacker/format/report/mobspawnreport.py b/mcpacker/format/report/mobspawnreport.py index 2c1c26d..96a26f8 100644 --- a/mcpacker/format/report/mobspawnreport.py +++ b/mcpacker/format/report/mobspawnreport.py @@ -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() diff --git a/mcpacker/model/catalog.py b/mcpacker/model/catalog.py index 311cda2..da35962 100644 --- a/mcpacker/model/catalog.py +++ b/mcpacker/model/catalog.py @@ -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]: diff --git a/mcpacker/model/catalog_test.py b/mcpacker/model/catalog_test.py index be3d900..ee001fa 100644 --- a/mcpacker/model/catalog_test.py +++ b/mcpacker/model/catalog_test.py @@ -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})" + ) diff --git a/mcpacker/model/core/ecology/biome.py b/mcpacker/model/core/ecology/biome.py index 867d254..44bb048 100644 --- a/mcpacker/model/core/ecology/biome.py +++ b/mcpacker/model/core/ecology/biome.py @@ -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 [ diff --git a/mcpacker/model/core/ecology/biome_test.py b/mcpacker/model/core/ecology/biome_test.py index 1c492f2..227172a 100644 --- a/mcpacker/model/core/ecology/biome_test.py +++ b/mcpacker/model/core/ecology/biome_test.py @@ -21,5 +21,5 @@ def createDesert(): # Tests ############################################################################################ def test_createBiome(desert): - assert desert.city == "phoenix" + assert desert.name == "phoenix" assert desert.soil == S.SANDY diff --git a/mcpacker/model/core/ecology/biomecatalog_test.py b/mcpacker/model/core/ecology/biomecatalog_test.py index 89d61c2..b7291e3 100644 --- a/mcpacker/model/core/ecology/biomecatalog_test.py +++ b/mcpacker/model/core/ecology/biomecatalog_test.py @@ -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"] diff --git a/mcpacker/model/core/fauna/mob.py b/mcpacker/model/core/fauna/mob.py index 79f11af..0dafbd2 100644 --- a/mcpacker/model/core/fauna/mob.py +++ b/mcpacker/model/core/fauna/mob.py @@ -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) diff --git a/mcpacker/model/core/fauna/mobspawn.py b/mcpacker/model/core/fauna/mobspawn.py index b7418af..d849e67 100644 --- a/mcpacker/model/core/fauna/mobspawn.py +++ b/mcpacker/model/core/fauna/mobspawn.py @@ -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 diff --git a/mcpacker/model/core/material/item.py b/mcpacker/model/core/material/item.py index 0f0b06d..6f13179 100644 --- a/mcpacker/model/core/material/item.py +++ b/mcpacker/model/core/material/item.py @@ -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) diff --git a/mcpacker/model/core/material/itemcatalog.py b/mcpacker/model/core/material/itemcatalog.py index 9288b1d..47a0e7d 100644 --- a/mcpacker/model/core/material/itemcatalog.py +++ b/mcpacker/model/core/material/itemcatalog.py @@ -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 diff --git a/mcpacker/model/core/resourceid.py b/mcpacker/model/core/resourceid.py index ced286a..bb9799b 100644 --- a/mcpacker/model/core/resourceid.py +++ b/mcpacker/model/core/resourceid.py @@ -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)}" + + ")" + ) diff --git a/mcpacker/pack/mysteriousisland/addblocks.py b/mcpacker/pack/mysteriousisland/addblocks.py index 97c6995..97fd63d 100644 --- a/mcpacker/pack/mysteriousisland/addblocks.py +++ b/mcpacker/pack/mysteriousisland/addblocks.py @@ -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"], diff --git a/mcpacker/pack/mysteriousisland/additems.py b/mcpacker/pack/mysteriousisland/additems.py index 388b76b..caf074e 100644 --- a/mcpacker/pack/mysteriousisland/additems.py +++ b/mcpacker/pack/mysteriousisland/additems.py @@ -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")) diff --git a/mcpacker/ui/runner.py b/mcpacker/ui/runner.py index 165189f..cbb2a40 100644 --- a/mcpacker/ui/runner.py +++ b/mcpacker/ui/runner.py @@ -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), ]), diff --git a/mcpacker/write/datapack/depositwriter.py b/mcpacker/write/datapack/depositwriter.py new file mode 100644 index 0000000..92f4ada --- /dev/null +++ b/mcpacker/write/datapack/depositwriter.py @@ -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)) diff --git a/mcpacker/write/incontrol/spawnerwriter.py b/mcpacker/write/incontrol/spawnerwriter.py index 1f8804b..db3b495 100644 --- a/mcpacker/write/incontrol/spawnerwriter.py +++ b/mcpacker/write/incontrol/spawnerwriter.py @@ -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,