Compare commits

...
Author SHA1 Message Date
MeAlam1 58cdf2e756 Added Debug Logging to hopefully find the issue 2026-08-06 18:26:43 +02:00
Hiroku d4b6e36aab Merge branch 'gamemasters' into 'main'
Interaction Fix

See merge request cable-mc/cobblemon!2339
2026-08-03 08:49:25 +00:00
dhi_holo 0fb721702d Interaction Fix
Solves interaction problem reported https://discord.com/channels/725110639554592879/1481053968716009493/1533385403170820148

Habitat Block acts like a block without interactions if right clicked by a player in survival, or without operator permissions of level 2 or higher (same as command blocks, known as Gamemaster)
With those permissions, it works as usual

Also removed Habitat Block from the Cobblemon Blocks tab, leaving it solely in the Operator Items Tab, like Command Blocks.
2026-08-02 21:08:13 +10:00
Polymeta 0e5053b787 Merge branch 'config-screen-scrolling-labels' into 'main'
Labels on the config screen now scroll instead of overflowing

See merge request cable-mc/cobblemon!2338
2026-08-01 22:09:46 +00:00
lionandPolymeta f3867e0b1d Labels on the config screen now scroll instead of overflowing 2026-08-01 22:09:46 +00:00
Hiroku a4fcd1ae7b Merge branch 'transparent-occlusion' into 'main'
Fix translucent draw order

See merge request cable-mc/cobblemon!2337
2026-08-01 01:56:58 +00:00
JordanandHiroku ed9aff2448 Fix translucent draw order 2026-08-01 01:56:57 +00:00
Nick 8497a2a083 Merge branch 'trade-dupe-fix' into 'main'
Trade dupe fix

See merge request cable-mc/cobblemon!2336
2026-07-31 15:22:15 -07:00
Deltric b389a95b5e Trade dupe fix 2026-07-31 17:03:33 -05:00
Polymeta c310027ce1 Merge branch 'Habitat-Bug-Fixes' into 'main'
Fixin some things

See merge request cable-mc/cobblemon!2335
2026-07-31 20:34:24 +00:00
Slippy 5ddb01df36 More Bug Fixes 2026-07-31 14:25:33 -05:00
Slippy 5a6f49aad5 found one more thing 2026-07-31 02:54:16 -05:00
Slippy 793efcfa3d Fixin some things 2026-07-31 02:49:24 -05:00
81 changed files with 255 additions and 60 deletions
+1
View File
@@ -396,6 +396,7 @@
- Fixed `/giveallpokemon` not respecting the min and max parameters
- Fixed the choose starter toast not disappearing when capturing a Pokémon instead of opening the starter selection screen
- Fixed an issue where certain Sinister Tea seasonings were only applying as instant effects and would not apply over time
- Long config option labels now scroll instead of overflowing
### Developer
- Added `PokeSnackSpawnerFactory` which allows influence registration on PokeSnack encounters
@@ -20,6 +20,8 @@ import net.minecraft.world.level.levelgen.structure.pools.alias.PoolAliasLookup;
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
@@ -31,6 +33,9 @@ import java.util.Optional;
@Mixin(JigsawPlacement.class)
public class JigsawPlacementMixin {
@Unique
private static final Logger cobblemon$LOGGER = LoggerFactory.getLogger("Cobblemon/HeightRangeVeto");
@Inject(
method = "addPieces(Lnet/minecraft/world/level/levelgen/structure/Structure$GenerationContext;Lnet/minecraft/core/Holder;Ljava/util/Optional;ILnet/minecraft/core/BlockPos;ZLjava/util/Optional;ILnet/minecraft/world/level/levelgen/structure/pools/alias/PoolAliasLookup;Lnet/minecraft/world/level/levelgen/structure/pools/DimensionPadding;Lnet/minecraft/world/level/levelgen/structure/templatesystem/LiquidSettings;)Ljava/util/Optional;",
at = @At("RETURN"),
@@ -54,18 +59,30 @@ public class JigsawPlacementMixin {
if (result.isEmpty()) {
return;
}
BlockPos spawnPos = result.get().position();
boolean canPlace = cobblemon$canAnyStartElementPlace(context, startPool, spawnPos);
String poolId = startPool.unwrapKey().map(key -> key.location().toString()).orElse("unknown");
boolean canPlace = cobblemon$canStructurePlace(context, startPool, spawnPos);
cobblemon$LOGGER.info(
"[HeightRangeVeto] addPieces evaluated requestedPos={} spawnPos={} startPool={} -> canPlace={}",
pos, spawnPos, poolId, canPlace
);
if (canPlace) {
return;
}
cobblemon$LOGGER.info(
"[HeightRangeVeto] Vetoing structure start at {} (startPool={})",
spawnPos, poolId
);
cir.setReturnValue(Optional.empty());
}
@Unique
private static boolean cobblemon$canAnyStartElementPlace(
private static boolean cobblemon$canStructurePlace(
Structure.GenerationContext generationContext,
Holder<StructureTemplatePool> startPool,
BlockPos startPos
@@ -74,7 +91,7 @@ public class JigsawPlacementMixin {
Optional<HeightRangeStructureProcessor> heightRangeProcessor = cobblemon$getHeightRangeProcessor(element);
if (heightRangeProcessor.isEmpty()) {
return true;
continue;
}
HeightRangeStructureProcessor processor = heightRangeProcessor.get();
@@ -85,12 +102,17 @@ public class JigsawPlacementMixin {
startPos
);
if (valid) {
return true;
cobblemon$LOGGER.info(
"[HeightRangeVeto] template={} pos={} valid={}",
element, startPos, valid
);
if (!valid) {
return false;
}
}
return false;
return true;
}
@Unique
@@ -25,6 +25,7 @@ import net.minecraft.world.level.block.SoundType
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.entity.BlockEntityTicker
import net.minecraft.world.level.block.entity.BlockEntityType
import net.minecraft.world.level.block.entity.CommandBlockEntity
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.BooleanProperty
@@ -80,11 +81,13 @@ class HabitatBlock(properties: Properties) : Block(properties), EntityBlock {
}
override fun useWithoutItem(state: BlockState, level: Level, pos: BlockPos, player: Player, hitResult: BlockHitResult): InteractionResult {
if (!level.isClientSide) {
val be = level.getBlockEntity(pos) as? HabitatBlockEntity ?: return InteractionResult.PASS
return be.onUse(player, hitResult)
val blockEntity = level.getBlockEntity(pos)
val be = level.getBlockEntity(pos) as? HabitatBlockEntity ?: return InteractionResult.PASS
return if (blockEntity is HabitatBlockEntity && player.canUseGameMasterBlocks()) {
be.onUse(player, hitResult)
} else {
InteractionResult.PASS
}
return InteractionResult.SUCCESS
}
override fun getDrops(state: BlockState, params: LootParams.Builder): MutableList<ItemStack> {
@@ -41,6 +41,7 @@ class CobblemonConfigScreen(
const val WIDGET_HEIGHT = SLOT_HEIGHT - PADDING
const val LABEL_Y_OFFSET = 4
const val WIDGET_Y_OFFSET = 0
const val LABEL_MAX_WIDTH = SLOT_WIDTH - RESET_SLOT_WIDTH - WIDGET_WIDTH - PADDING * 2
}
val clonedConfig = Cobblemon.config.clone()
@@ -8,17 +8,18 @@
package com.cobblemon.mod.common.client.gui.config.variable
import com.cobblemon.mod.common.client.gui.config.CobblemonConfigScreen.Companion.LABEL_Y_OFFSET
import com.cobblemon.mod.common.client.gui.config.CobblemonConfigScreen.Companion.LABEL_MAX_WIDTH
import com.cobblemon.mod.common.client.gui.config.CobblemonConfigScreen.Companion.RESET_SLOT_WIDTH
import com.cobblemon.mod.common.client.gui.config.CobblemonConfigScreen.Companion.SLOT_WIDTH
import com.cobblemon.mod.common.client.gui.config.CobblemonConfigScreen.Companion.WIDGET_HEIGHT
import com.cobblemon.mod.common.client.gui.config.CobblemonConfigScreen.Companion.WIDGET_Y_OFFSET
import com.cobblemon.mod.common.client.gui.config.CobblemonConfigVariableList
import com.cobblemon.mod.common.client.gui.config.CobblemonConfigVariableList.CobblemonConfigVariableListEntry
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.drawScrollingText
import com.cobblemon.mod.common.config.CobblemonConfig
import com.cobblemon.mod.common.config.CobblemonConfigField
import com.cobblemon.mod.common.util.lang
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.Button
import net.minecraft.client.gui.components.Tooltip
@@ -91,12 +92,15 @@ abstract class CobblemonConfigVariable(
val x = getX(left)
val y = getY(top)
drawScaledText(
context = context,
text = labelLang,
x = getX(left),
y = getY(top) + LABEL_Y_OFFSET,
shadow = true
drawScrollingText(
context,
Minecraft.getInstance().font,
labelLang,
x,
y + WIDGET_Y_OFFSET,
x + LABEL_MAX_WIDTH,
y + WIDGET_Y_OFFSET + WIDGET_HEIGHT,
-1
)
resetButton.x = x + SLOT_WIDTH - RESET_SLOT_WIDTH
@@ -18,17 +18,23 @@ import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.vertex.PoseStack
import com.mojang.blaze3d.vertex.VertexConsumer
import com.mojang.math.Axis
import net.minecraft.Util
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.Font
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.renderer.MultiBufferSource
import net.minecraft.client.renderer.RenderType
import net.minecraft.client.renderer.texture.OverlayTexture
import net.minecraft.client.renderer.texture.TextureAtlas
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.MutableComponent
import net.minecraft.resources.ResourceLocation
import net.minecraft.util.FormattedCharSequence
import net.minecraft.util.Mth
import net.minecraft.world.item.ItemDisplayContext
import net.minecraft.world.item.ItemStack
import kotlin.math.cos
import kotlin.math.sin
fun renderScaledGuiItemIcon(itemStack: ItemStack, x: Double, y: Double, scale: Double = 1.0, zTranslation: Float = 100.0F, matrixStack: PoseStack? = null) {
val itemRenderer = Minecraft.getInstance().itemRenderer
@@ -97,6 +103,55 @@ fun getDepletableRedGreen(
return r.toFloat() to g.toFloat()
}
fun drawScrollingText(
context: GuiGraphics,
font: Font,
text: Component,
minX: Int,
minY: Int,
maxX: Int,
maxY: Int,
colour: Int,
minScrollVelocity: Double? = 10.0
) {
val textWidth = font.width(text)
val fittedMaxX = if (textWidth <= maxX - minX) minX + textWidth else maxX
renderScrollingString(context, font, text, minX, minY, fittedMaxX, maxY, colour, minScrollVelocity)
}
/** Copied from [net.minecraft.client.gui.components.AbstractWidget.renderScrollingString] **/
fun renderScrollingString(
context: GuiGraphics,
font: Font,
text: Component,
minX: Int,
minY: Int,
maxX: Int,
maxY: Int,
colour: Int,
minScrollVelocity: Double? = 10.0
) {
val textWidth = font.width(text)
val centerY = (minY + maxY - 9) / 2 + 1
val boxWidth = maxX - minX
if (textWidth <= boxWidth) {
val centerX = Mth.clamp((minX + maxX) / 2, minX + textWidth / 2, maxX - textWidth / 2)
context.drawCenteredString(font, text, centerX, centerY, colour)
return
}
val overflow = textWidth - boxWidth
var period = (overflow * 0.5).coerceAtLeast(3.0)
if (minScrollVelocity != null) {
period = period.coerceAtMost(2 * overflow / minScrollVelocity)
}
val time = Util.getMillis() / 1000.0
val easedProgress = sin((Math.PI / 2) * cos(2 * Math.PI * time / period)) / 2.0 + 0.5
val offset = Mth.lerp(easedProgress, 0.0, overflow.toDouble())
context.enableScissor(minX, minY, maxX, maxY)
context.drawString(font, text, minX - offset.toInt(), centerY, colour)
context.disableScissor()
}
fun drawScaledText(
context: GuiGraphics,
@@ -34,7 +34,6 @@ import com.cobblemon.mod.common.client.render.models.blockbench.quirk.ModelQuirk
import com.cobblemon.mod.common.client.render.models.blockbench.quirk.SimpleQuirk
import com.cobblemon.mod.common.client.render.models.blockbench.repository.RenderContext
import com.cobblemon.mod.common.client.render.models.blockbench.wavefunction.WaveFunction
import com.cobblemon.mod.common.client.util.exists
import com.cobblemon.mod.common.entity.PosableEntity
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.generic.GenericBedrockEntity
@@ -256,7 +255,7 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
}
fun setLayerContext(buffer: MultiBufferSource, state: PosableState, layers: Iterable<ModelLayer>) {
currentLayers = layers
currentLayers = layers.sortedBy { it.translucent }
bufferProvider = buffer
currentState = state
}
@@ -316,7 +316,6 @@ object CobblemonItemGroups {
entries.accept(CobblemonItems.PASTURE)
entries.accept(CobblemonItems.TM_MACHINE)
entries.accept(CobblemonItems.DISC_SHELF)
entries.accept(CobblemonItems.HABITAT_BLOCK)
entries.accept(CobblemonItems.GILDED_CHEST)
entries.accept(CobblemonItems.YELLOW_GILDED_CHEST)
@@ -115,8 +115,26 @@ object TradeManager : RequestManager<TradeManager.TradeRequest>() {
}
CobblemonEvents.TRADE_EVENT_PRE.postThen(TradeEvent.Pre(player1, pokemon2, player2, pokemon1)) {
party1.remove(pokemon1)
party2.remove(pokemon2)
val party1 = player1.party
val party2 = player2.party
val player1Removed = party1.remove(pokemon1)
val player2Removed = party2.remove(pokemon2)
if (!player1Removed || !player2Removed) {
if (!player1Removed) {
Cobblemon.LOGGER.warn("Failed to remove traded Pokémon ${pokemon1.species.resourceIdentifier} from ${player1.name.string}(${player1.uuid}). Aborting trade with ${player2.name.string}(${player2.uuid}), potential exploit abused.")
}
if (!player2Removed) {
Cobblemon.LOGGER.warn("Failed to remove traded Pokémon ${pokemon2.species.resourceIdentifier} from ${player2.name.string}(${player2.uuid}). Aborting trade with ${player1.name.string}(${player1.uuid}), potential exploit abused.")
}
// Revert any changes if one of the removals failed
if (player1Removed) party1.add(pokemon1)
if (player2Removed) party2.add(pokemon2)
return@postThen
}
// If player1 is the OT, keep track of the friendship
pokemon1.cacheFriendship(player1.uuid)
@@ -20,6 +20,7 @@ import net.minecraft.world.level.levelgen.RandomState
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate
import org.slf4j.LoggerFactory
class HeightRangeStructureProcessor(
val minOffset: Int,
@@ -43,7 +44,14 @@ class HeightRangeStructureProcessor(
val referenceY = reference.getY(chunkGenerator, heightAccessor, randomState, pos)
val minY = referenceY + minOffset
val maxY = referenceY + maxOffset
return pos.y in minY..maxY
val valid = pos.y in minY..maxY
LOGGER.info(
"[HeightRangeVeto] isValidPlacement pos={} reference={} referenceY={} minY={} maxY={} pos.y={} -> valid={}",
pos, reference, referenceY, minY, maxY, pos.y, valid
)
return valid
}
override fun processBlock(
@@ -105,6 +113,8 @@ class HeightRangeStructureProcessor(
}
companion object {
private val LOGGER = LoggerFactory.getLogger("Cobblemon/HeightRangeStructureProcessor")
val CODEC: MapCodec<HeightRangeStructureProcessor> = RecordCodecBuilder.mapCodec { instance ->
instance.group(
Codec.INT.fieldOf("min_offset").forGetter { it.minOffset },
@@ -28,30 +28,21 @@
"output_state": {
"Name": "minecraft:air"
}
},
{
"input_predicate": {
"block": "cobblemon:habitat_block",
"predicate_type": "minecraft:random_block_match",
"probability": 1.0
},
"location_predicate": {
"predicate_type": "minecraft:always_true"
},
"output_state": {
"Name": "minecraft:packed_ice"
}
}
]
},
{
"delegate": {
"processor_type": "minecraft:rule",
"rules": [
{
"input_predicate": {
"block": "cobblemon:habitat_block",
"predicate_type": "minecraft:random_block_match",
"probability": 1.0
},
"location_predicate": {
"predicate_type": "minecraft:always_true"
},
"output_state": {
"Name": "minecraft:packed_ice"
}
}
]
},
"limit": 9,
"processor_type": "minecraft:capped"
}
]
}
@@ -0,0 +1,57 @@
{
"processors": [
{
"processor_type": "minecraft:rule",
"rules": [
{
"input_predicate": {
"block": "minecraft:ice",
"predicate_type": "minecraft:random_block_match",
"probability": 0.5
},
"location_predicate": {
"predicate_type": "minecraft:always_true"
},
"output_state": {
"Name": "minecraft:packed_ice"
}
},
{
"input_predicate": {
"block": "minecraft:gold_block",
"predicate_type": "minecraft:random_block_match",
"probability": 1.0
},
"location_predicate": {
"predicate_type": "minecraft:always_true"
},
"output_state": {
"Name": "minecraft:air"
}
}
]
},
{
"delegate": {
"processor_type": "minecraft:rule",
"rules": [
{
"input_predicate": {
"block": "cobblemon:habitat_block",
"predicate_type": "minecraft:random_block_match",
"probability": 1.0
},
"location_predicate": {
"predicate_type": "minecraft:always_true"
},
"output_state": {
"Name": "minecraft:packed_ice"
}
}
]
},
"limit": 9,
"processor_type": "minecraft:capped"
}
]
}
@@ -36,6 +36,28 @@
}
}
]
},
{
"delegate": {
"processor_type": "minecraft:rule",
"rules": [
{
"input_predicate": {
"block": "cobblemon:habitat_block",
"predicate_type": "minecraft:random_block_match",
"probability": 1.0
},
"location_predicate": {
"predicate_type": "minecraft:always_true"
},
"output_state": {
"Name": "minecraft:grass_block"
}
}
]
},
"limit": 9,
"processor_type": "minecraft:capped"
}
]
}
@@ -208,7 +208,7 @@
"input_predicate": {
"block": "cobblemon:habitat_block",
"predicate_type": "minecraft:random_block_match",
"probability": 0.4
"probability": 1.0
},
"location_predicate": {
"predicate_type": "minecraft:always_true"
@@ -43,7 +43,7 @@
}
]
},
"limit": 9,
"limit": 4,
"processor_type": "minecraft:capped"
}
]
@@ -29,6 +29,19 @@
"Name": "minecraft:cave_air"
}
},
{
"input_predicate": {
"block": "minecraft:redstone_block",
"predicate_type": "minecraft:random_block_match",
"probability": 1.0
},
"location_predicate": {
"predicate_type": "minecraft:always_true"
},
"output_state": {
"Name": "minecraft:emerald_block"
}
},
{
"input_predicate": {
"block": "cobblemon:relic_coin_sack",
@@ -8,7 +8,7 @@
"location": "cobblemon:habitats/drifting_iceberg1",
"element_type": "minecraft:single_pool_element",
"override_liquid_settings": "apply_waterlogging",
"processors": "cobblemon:habitats/drifting_icebergs",
"processors": "cobblemon:habitats/drifting_icebergs_start",
"projection": "rigid"
}
},
@@ -18,7 +18,7 @@
"location": "cobblemon:habitats/drifting_iceberg2",
"element_type": "minecraft:single_pool_element",
"override_liquid_settings": "apply_waterlogging",
"processors": "cobblemon:habitats/drifting_icebergs",
"processors": "cobblemon:habitats/drifting_icebergs_start",
"projection": "rigid"
}
},
@@ -28,7 +28,7 @@
"location": "cobblemon:habitats/drifting_iceberg3",
"element_type": "minecraft:single_pool_element",
"override_liquid_settings": "apply_waterlogging",
"processors": "cobblemon:habitats/drifting_icebergs",
"processors": "cobblemon:habitats/drifting_icebergs_start",
"projection": "rigid"
}
},
@@ -38,7 +38,7 @@
"location": "cobblemon:habitats/drifting_iceberg4",
"element_type": "minecraft:single_pool_element",
"override_liquid_settings": "apply_waterlogging",
"processors": "cobblemon:habitats/drifting_icebergs",
"processors": "cobblemon:habitats/drifting_icebergs_start",
"projection": "rigid"
}
},
@@ -48,7 +48,7 @@
"location": "cobblemon:habitats/drifting_iceberg5",
"element_type": "minecraft:single_pool_element",
"override_liquid_settings": "apply_waterlogging",
"processors": "cobblemon:habitats/drifting_icebergs",
"processors": "cobblemon:habitats/drifting_icebergs_start",
"projection": "rigid"
}
},
@@ -58,7 +58,7 @@
"location": "cobblemon:habitats/drifting_iceberg6",
"element_type": "minecraft:single_pool_element",
"override_liquid_settings": "apply_waterlogging",
"processors": "cobblemon:habitats/drifting_icebergs",
"processors": "cobblemon:habitats/drifting_icebergs_start",
"projection": "rigid"
}
},
@@ -68,7 +68,7 @@
"location": "cobblemon:habitats/drifting_iceberg7",
"element_type": "minecraft:single_pool_element",
"override_liquid_settings": "apply_waterlogging",
"processors": "cobblemon:habitats/drifting_icebergs",
"processors": "cobblemon:habitats/drifting_icebergs_start",
"projection": "rigid"
}
},
@@ -78,7 +78,7 @@
"location": "cobblemon:habitats/drifting_iceberg8",
"element_type": "minecraft:single_pool_element",
"override_liquid_settings": "apply_waterlogging",
"processors": "cobblemon:habitats/drifting_icebergs",
"processors": "cobblemon:habitats/drifting_icebergs_start",
"projection": "rigid"
}
},
@@ -88,7 +88,7 @@
"location": "cobblemon:habitats/drifting_iceberg9",
"element_type": "minecraft:single_pool_element",
"override_liquid_settings": "apply_waterlogging",
"processors": "cobblemon:habitats/drifting_icebergs",
"processors": "cobblemon:habitats/drifting_icebergs_start",
"projection": "rigid"
}
},
@@ -98,7 +98,7 @@
"location": "cobblemon:habitats/drifting_iceberg10",
"element_type": "minecraft:single_pool_element",
"override_liquid_settings": "apply_waterlogging",
"processors": "cobblemon:habitats/drifting_icebergs",
"processors": "cobblemon:habitats/drifting_icebergs_start",
"projection": "rigid"
}
},
@@ -108,7 +108,7 @@
"location": "cobblemon:habitats/drifting_iceberg11",
"element_type": "minecraft:single_pool_element",
"override_liquid_settings": "apply_waterlogging",
"processors": "cobblemon:habitats/drifting_icebergs",
"processors": "cobblemon:habitats/drifting_icebergs_start",
"projection": "rigid"
}
},
@@ -118,7 +118,7 @@
"location": "cobblemon:habitats/drifting_iceberg12",
"element_type": "minecraft:single_pool_element",
"override_liquid_settings": "apply_waterlogging",
"processors": "cobblemon:habitats/drifting_icebergs",
"processors": "cobblemon:habitats/drifting_icebergs_start",
"projection": "rigid"
}
}