from .lib.utils import pascal_case, split_prefixed from .lib.java import java_float, java_static_invoke def resolve_block(block: dict) -> dict: block = dict(block) block["concreteClassName"] = block["customClassName"] or pascal_case(block["path"]) block["propertyCalls"], block["propertyImports"] = build_property_calls(block["properties"]) if block["textures"] is not None: block["resolvedTextures"] = resolve_textures(block["textures"]) if block["blockEntity"] is not None: block_entity = dict(block["blockEntity"]) block_entity["concreteClassName"] = block_entity["customClassName"] or pascal_case(block["path"]) + "Entity" block["blockEntity"] = block_entity return block def resolve_textures(textures: dict) -> dict: """Resolves the all/top/bottom/side/ convenience fields into the six explicit faces (using the vanilla "cube" parent model's own variable names: up/down instead of top/bottom) plus particle, applying face-specific > side > all precedence.""" all_tex = textures["all"] side = textures["side"] or all_tex faces = { "up": textures["top"] or all_tex, "down": textures["bottom"] or all_tex, "north": textures["north"] or side, "south": textures["south"] or side, "east": textures["east"] or side, "west": textures["west"] or side, } faces["particle"] = faces["up"] or next(iter(faces.values())) return faces # Each entry describes how one `properties.*` field becomes a chained # BlockBehaviour.Properties builder call. Extra fully-qualified imports are # collected separately (Java method references can use FQNs inline, so only # constants/enums referenced by simple name need an import). def build_property_calls(properties: dict) -> tuple[list[str], list[str]]: p = properties calls: list[str] = [] imports: set[str] = set() calls.append(f".destroyTime({java_float(p['hardness'])})") if p["resistance"] is not None: calls.append(f".explosionResistance({java_float(p['resistance'])})") if p["requiresTool"]: calls.append(".requiresCorrectToolForDrops()") if p["noCollision"]: calls.append(".noCollission()") if p["noOcclusion"]: calls.append(".noOcclusion()") if p["dynamicShape"]: calls.append(".dynamicShape()") if p["forceSolidOn"]: calls.append(".forceSolidOn()") if p["pushReaction"] != "NORMAL": calls.append(f".pushReaction(PushReaction.{p['pushReaction']})") imports.add("net.minecraft.world.level.material.PushReaction") if p["offsetType"] != "NONE": calls.append(f".offsetType(BlockBehaviour.OffsetType.{p['offsetType']})") if p["replaceable"]: calls.append(".replaceable()") if p["friction"] != 0.6: calls.append(f".friction({java_float(p['friction'])})") if p["speedFactor"] != 1.0: calls.append(f".speedFactor({java_float(p['speedFactor'])})") if p["jumpFactor"] != 1.0: calls.append(f".jumpFactor({java_float(p['jumpFactor'])})") if p["mapColor"] is not None: kind, value = split_prefixed(p["mapColor"], "properties.mapColor") if kind == "dye": calls.append(f".mapColor(DyeColor.{value})") imports.add("net.minecraft.world.item.DyeColor") elif kind == "map": calls.append(f".mapColor(MapColor.{value})") imports.add("net.minecraft.world.level.material.MapColor") elif kind == "closure": calls.append(f".mapColor({value})") else: raise ValueError(f"properties.mapColor: unknown prefix '{kind}:', expected 'dye:', 'map:', or 'closure:'") if p["lightLevel"] is not None: light_level = p["lightLevel"] if isinstance(light_level, int): calls.append(f".lightLevel(state -> {light_level})") else: kind, value = split_prefixed(light_level, "properties.lightLevel") if kind != "closure": raise ValueError(f"properties.lightLevel: string values must be prefixed with 'closure:', got: {light_level!r}") calls.append(f".lightLevel({value})") if p["noTerrainParticles"]: calls.append(".noTerrainParticles()") if p["isViewBlocking"] is not None: _, value = split_prefixed(p["isViewBlocking"], "properties.isViewBlocking") calls.append(f".isViewBlocking({value})") if p["isSuffocating"] is not None: _, value = split_prefixed(p["isSuffocating"], "properties.isSuffocating") calls.append(f".isSuffocating({value})") calls.append(f".sound(SoundType.{p['sounds']})") if p["instruments"] is not None: calls.append(f".instrument(NoteBlockInstrument.{p['instruments']})") imports.add("net.minecraft.world.level.block.state.properties.NoteBlockInstrument") if p["noLootTable"]: calls.append(".noLootTable()") if p["lootFrom"] is not None: kind, value = split_prefixed(p["lootFrom"], "properties.lootFrom") if kind != "closure": raise ValueError(f"properties.lootFrom: expected 'closure:pkg.Class::method', got: {p['lootFrom']!r}") calls.append(f".dropsLike({java_static_invoke(value, 'properties.lootFrom')})") if p["randomTicks"]: calls.append(".randomTicks()") if p["ignitesByLava"]: calls.append(".ignitedByLava()") if p["liquid"]: calls.append(".liquid()") if p["isValidSpawn"] is not None: _, value = split_prefixed(p["isValidSpawn"], "properties.isValidSpawn") calls.append(f".isValidSpawn({value})") if p["air"]: calls.append(".air()") if p["requiredFeatures"]: parts = [] for feature in p["requiredFeatures"]: if feature.startswith("closure:"): _, value = split_prefixed(feature, "properties.requiredFeatures") parts.append(value) else: namespace, _, name = feature.partition(":") if namespace != "minecraft": raise ValueError( f"properties.requiredFeatures: non-vanilla feature '{feature}' needs 'closure:pkg.Class::FIELD' " "pointing at a static FeatureFlag field -- snowy doesn't yet know how to look up custom mods' flags by id" ) parts.append(f"FeatureFlags.{name.upper()}") imports.add("net.minecraft.world.flag.FeatureFlags") calls.append(f".requiredFeatures({', '.join(parts)})") return calls, sorted(imports)