From 1c7fd2cffd1558d4816b3f2ca2886be07e565bc3 Mon Sep 17 00:00:00 2001 From: poolcritter Date: Thu, 9 Jul 2026 19:36:58 -0400 Subject: [PATCH 1/2] tests: add mock IXplatAbstractions Change-Id: I6f4fdb764e727fa211db96140ba2b6f26a6a6964 --- .../xplat/DummyXplatAbstractions.kt | 115 ++++++++++++++++++ .../hexcasting/xplat/IXplatAbstractions.java | 2 + ...petrak.hexcasting.xplat.IXplatAbstractions | 1 + 3 files changed, 118 insertions(+) create mode 100644 Common/src/main/java/at/petrak/hexcasting/xplat/DummyXplatAbstractions.kt create mode 100644 Common/src/test/resources/META-INF/services/at.petrak.hexcasting.xplat.IXplatAbstractions diff --git a/Common/src/main/java/at/petrak/hexcasting/xplat/DummyXplatAbstractions.kt b/Common/src/main/java/at/petrak/hexcasting/xplat/DummyXplatAbstractions.kt new file mode 100644 index 0000000000..ea5c3d7b0f --- /dev/null +++ b/Common/src/main/java/at/petrak/hexcasting/xplat/DummyXplatAbstractions.kt @@ -0,0 +1,115 @@ +package at.petrak.hexcasting.xplat + +import at.petrak.hexcasting.api.addldata.ADHexHolder +import at.petrak.hexcasting.api.addldata.ADIotaHolder +import at.petrak.hexcasting.api.addldata.ADMediaHolder +import at.petrak.hexcasting.api.addldata.ADVariantItem +import at.petrak.hexcasting.api.casting.ActionRegistryEntry +import at.petrak.hexcasting.api.casting.arithmetic.Arithmetic +import at.petrak.hexcasting.api.casting.castables.SpecialHandler +import at.petrak.hexcasting.api.casting.eval.ResolvedPattern +import at.petrak.hexcasting.api.casting.eval.sideeffects.EvalSound +import at.petrak.hexcasting.api.casting.eval.vm.CastingImage +import at.petrak.hexcasting.api.casting.eval.vm.CastingVM +import at.petrak.hexcasting.api.casting.eval.vm.ContinuationFrame +import at.petrak.hexcasting.api.casting.iota.IotaType +import at.petrak.hexcasting.api.pigment.ColorProvider +import at.petrak.hexcasting.api.pigment.FrozenPigment +import at.petrak.hexcasting.api.player.AltioraAbility +import at.petrak.hexcasting.api.player.FlightAbility +import at.petrak.hexcasting.api.player.Sentinel +import at.petrak.hexcasting.common.lib.HexRegistries +import at.petrak.hexcasting.common.lib.hex.HexIotaTypes +import at.petrak.hexcasting.common.msgs.IMessage +import at.petrak.hexcasting.interop.pehkui.PehkuiInterop +import com.google.common.base.Suppliers +import com.mojang.serialization.Lifecycle +import net.minecraft.core.BlockPos +import net.minecraft.core.MappedRegistry +import net.minecraft.core.Registry +import net.minecraft.network.protocol.Packet +import net.minecraft.network.protocol.game.ClientGamePacketListener +import net.minecraft.server.Bootstrap +import net.minecraft.server.level.ServerLevel +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.InteractionHand +import net.minecraft.world.entity.Entity +import net.minecraft.world.entity.EquipmentSlot +import net.minecraft.world.entity.Mob +import net.minecraft.world.entity.player.Player +import net.minecraft.world.item.Item +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Tier +import net.minecraft.world.item.crafting.Ingredient +import net.minecraft.world.level.Level +import net.minecraft.world.level.block.Block +import net.minecraft.world.level.block.entity.BlockEntity +import net.minecraft.world.level.block.entity.BlockEntityType +import net.minecraft.world.level.block.state.BlockState +import net.minecraft.world.level.material.Fluid +import net.minecraft.world.level.storage.loot.predicates.LootItemCondition +import net.minecraft.world.phys.Vec3 +import java.util.function.BiFunction +import java.util.function.Supplier + +/** A dummy instance of [IXplatAbstractions] to be used in tests. All methods of this implementation throw unconditionally. + * @throws IllegalStateException always, no matter what + */ +internal class DummyXplatAbstractions: IXplatAbstractions { + companion object { + /** must call .get() at least once before constructing iotas */ + val forbiddenMagics: Supplier = Suppliers.memoize { + try { Bootstrap.bootStrap() } catch (_: Throwable) {} + HexIotaTypes.registerTypes { t, id -> Registry.register(HexIotaTypes.REGISTRY, id, t) } + } + } + override fun platform(): Platform? = error("Found use of DummyXplatAbstractions.") + override fun isModPresent(id: String): Boolean = error("Found use of DummyXplatAbstractions.") + override fun isPhysicalClient(): Boolean = error("Found use of DummyXplatAbstractions.") + override fun initPlatformSpecific() = error("Found use of DummyXplatAbstractions.") + override fun sendPacketToPlayer(target: ServerPlayer, packet: IMessage) = error("Found use of DummyXplatAbstractions.") + override fun sendPacketNear(pos: Vec3, radius: Double, dimension: ServerLevel, packet: IMessage) = error("Found use of DummyXplatAbstractions.") + override fun sendPacketTracking(entity: Entity, packet: IMessage) = error("Found use of DummyXplatAbstractions.") + override fun toVanillaClientboundPacket(message: IMessage): Packet = error("Found use of DummyXplatAbstractions.") + override fun setBrainsweepAddlData(mob: Mob) = error("Found use of DummyXplatAbstractions.") + override fun isBrainswept(mob: Mob): Boolean = error("Found use of DummyXplatAbstractions.") + override fun setPigment(target: Player, colorizer: FrozenPigment?): FrozenPigment? = error("Found use of DummyXplatAbstractions.") + override fun setSentinel(target: Player, sentinel: Sentinel?) = error("Found use of DummyXplatAbstractions.") + override fun setFlight(target: ServerPlayer, flight: FlightAbility?) = error("Found use of DummyXplatAbstractions.") + override fun setAltiora(target: Player, altiora: AltioraAbility?) = error("Found use of DummyXplatAbstractions.") + override fun setStaffcastImage(target: ServerPlayer, image: CastingImage?) = error("Found use of DummyXplatAbstractions.") + override fun setPatterns(target: ServerPlayer, patterns: List) = error("Found use of DummyXplatAbstractions.") + override fun getFlight(player: ServerPlayer): FlightAbility? = error("Found use of DummyXplatAbstractions.") + override fun getAltiora(player: Player): AltioraAbility? = error("Found use of DummyXplatAbstractions.") + override fun getPigment(player: Player): FrozenPigment = error("Found use of DummyXplatAbstractions.") + override fun getSentinel(player: Player): Sentinel? = error("Found use of DummyXplatAbstractions.") + override fun getStaffcastVM(player: ServerPlayer, hand: InteractionHand): CastingVM = error("Found use of DummyXplatAbstractions.") + override fun getPatternsSavedInUi(player: ServerPlayer): List = error("Found use of DummyXplatAbstractions.") + override fun clearCastingData(player: ServerPlayer) = error("Found use of DummyXplatAbstractions.") + override fun findMediaHolder(stack: ItemStack): ADMediaHolder? = error("Found use of DummyXplatAbstractions.") + override fun findMediaHolder(player: ServerPlayer): ADMediaHolder? = error("Found use of DummyXplatAbstractions.") + override fun findDataHolder(stack: ItemStack): ADIotaHolder? = error("Found use of DummyXplatAbstractions.") + override fun findDataHolder(entity: Entity): ADIotaHolder? = error("Found use of DummyXplatAbstractions.") + override fun findHexHolder(stack: ItemStack): ADHexHolder? = error("Found use of DummyXplatAbstractions.") + override fun findVariantHolder(stack: ItemStack): ADVariantItem? = error("Found use of DummyXplatAbstractions.") + override fun isPigment(stack: ItemStack) = error("Found use of DummyXplatAbstractions.") + override fun getColorProvider(pigment: FrozenPigment): ColorProvider? = error("Found use of DummyXplatAbstractions.") + override fun addEquipSlotFabric(slot: EquipmentSlot): Item.Properties? = error("Found use of DummyXplatAbstractions.") + override fun createBlockEntityType(func: BiFunction, vararg blocks: Block?): BlockEntityType? = error("Found use of DummyXplatAbstractions.") + override fun tryPlaceFluid(level: Level, hand: InteractionHand, pos: BlockPos, fluid: Fluid): Boolean = error("Found use of DummyXplatAbstractions.") + override fun drainAllFluid(level: Level, pos: BlockPos): Boolean = error("Found use of DummyXplatAbstractions.") + override fun isCorrectTierForDrops(tier: Tier, bs: BlockState): Boolean = error("Found use of DummyXplatAbstractions.") + override fun getUnsealedIngredient(stack: ItemStack): Ingredient? = error("Found use of DummyXplatAbstractions.") + override fun tags(): IXplatTags? = error("Found use of DummyXplatAbstractions.") + override fun isShearsCondition(): LootItemCondition.Builder? = error("Found use of DummyXplatAbstractions.") + override fun getModName(namespace: String): String? = error("Found use of DummyXplatAbstractions.") + override fun getActionRegistry(): Registry? = error("Found use of DummyXplatAbstractions.") + override fun getSpecialHandlerRegistry(): Registry?>? = error("Found use of DummyXplatAbstractions.") + override fun getIotaTypeRegistry(): Registry> = MappedRegistry(HexRegistries.IOTA_TYPE, Lifecycle.stable()) + override fun getArithmeticRegistry(): Registry? = error("Found use of DummyXplatAbstractions.") + override fun getContinuationTypeRegistry(): Registry?>? = error("Found use of DummyXplatAbstractions.") + override fun getEvalSoundRegistry(): Registry? = error("Found use of DummyXplatAbstractions.") + override fun isBreakingAllowed(world: ServerLevel, pos: BlockPos, state: BlockState, player: Player?): Boolean = error("Found use of DummyXplatAbstractions.") + override fun isPlacingAllowed(world: ServerLevel, pos: BlockPos, blockStack: ItemStack, player: Player?): Boolean = error("Found use of DummyXplatAbstractions.") + override fun getPehkuiApi(): PehkuiInterop.ApiAbstraction? = error("Found use of DummyXplatAbstractions.") +} diff --git a/Common/src/main/java/at/petrak/hexcasting/xplat/IXplatAbstractions.java b/Common/src/main/java/at/petrak/hexcasting/xplat/IXplatAbstractions.java index 86dbc24586..17e863cbda 100644 --- a/Common/src/main/java/at/petrak/hexcasting/xplat/IXplatAbstractions.java +++ b/Common/src/main/java/at/petrak/hexcasting/xplat/IXplatAbstractions.java @@ -47,6 +47,7 @@ import net.minecraft.world.phys.Vec3; import org.jetbrains.annotations.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; import java.util.List; import java.util.ServiceLoader; import java.util.UUID; @@ -56,6 +57,7 @@ /** * more like IHexplatAbstracts lmaooooooo */ +@ParametersAreNonnullByDefault public interface IXplatAbstractions { Platform platform(); diff --git a/Common/src/test/resources/META-INF/services/at.petrak.hexcasting.xplat.IXplatAbstractions b/Common/src/test/resources/META-INF/services/at.petrak.hexcasting.xplat.IXplatAbstractions new file mode 100644 index 0000000000..4bbafc14cd --- /dev/null +++ b/Common/src/test/resources/META-INF/services/at.petrak.hexcasting.xplat.IXplatAbstractions @@ -0,0 +1 @@ +at.petrak.hexcasting.xplat.DummyXplatAbstractions \ No newline at end of file From eded368b9ded9c40b00ab418adc88111241b2c56 Mon Sep 17 00:00:00 2001 From: poolcritter Date: Wed, 8 Jul 2026 01:23:13 -0400 Subject: [PATCH 2/2] feat(api/iota): endofunctors adds `visit` and `visitChildren` to iotas letting you apply a UnaryOperator to an entire tree of them. this is like hexal's IMappableIota except I don't know if that does self pre or post (or if it does self at all). awa. I haven't tested this in prod; hex doesn't have anything that would use this yet. @IridescentVoid I believe you have a patch that may want to use this? Cc: @IridescentVoid Change-Id: Ic464f4702ba9b0379c93e31723e2a5fe6a6a6964 Issue: closes #1083 --- .../hexcasting/api/casting/iota/Iota.java | 19 +++ .../hexcasting/api/casting/iota/ListIota.java | 14 +++ .../src/test/java/iota_endofunctors_tests.kt | 112 ++++++++++++++++++ 3 files changed, 145 insertions(+) create mode 100644 Common/src/test/java/iota_endofunctors_tests.kt diff --git a/Common/src/main/java/at/petrak/hexcasting/api/casting/iota/Iota.java b/Common/src/main/java/at/petrak/hexcasting/api/casting/iota/Iota.java index c884b052ce..66c3b5395e 100644 --- a/Common/src/main/java/at/petrak/hexcasting/api/casting/iota/Iota.java +++ b/Common/src/main/java/at/petrak/hexcasting/api/casting/iota/Iota.java @@ -18,6 +18,7 @@ import org.jetbrains.annotations.Nullable; import java.util.List; +import java.util.function.UnaryOperator; public abstract class Iota { @NotNull @@ -102,6 +103,24 @@ public boolean executable() { return null; } + /** + * Applies the given operator to all iotas in this iota's tree (recursive subIotas), including this iota. + * @param visitor Applied to this iota then every element of the resulting iota's tree. + * @return The iota after all subiotas have been visited (and potentially replaced). + */ + public Iota visit(UnaryOperator visitor) { + return visitor.apply(this).visitChildren(visitor); + } + + /** + * Applies the given operator to all iotas in this iota's tree, **excluding** this iota. This must be called on an + * iota just before it is returned from {@link Iota#visit}. You should try to preserve your iota's identity if + * the operator doesn't modify anything. + */ + protected Iota visitChildren(UnaryOperator visitor) { + return this; + } + /** * This method is called to determine whether the iota is above the max serialisation depth/serialisation count limits. * This is an alternative to deriving subIotas for if your Iota is a datastructure of variable size over something that diff --git a/Common/src/main/java/at/petrak/hexcasting/api/casting/iota/ListIota.java b/Common/src/main/java/at/petrak/hexcasting/api/casting/iota/ListIota.java index 588c72bca3..8bab23bd58 100644 --- a/Common/src/main/java/at/petrak/hexcasting/api/casting/iota/ListIota.java +++ b/Common/src/main/java/at/petrak/hexcasting/api/casting/iota/ListIota.java @@ -15,7 +15,9 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.function.UnaryOperator; import static java.lang.Math.max; @@ -97,6 +99,18 @@ public boolean toleratesOther(Iota that) { return this.getList(); } + @Override + protected Iota visitChildren(UnaryOperator visitor) { + var out = new ArrayList(); + boolean conserve = true; + for (Iota orig : getList()) { + var replacement = orig.visit(visitor); + conserve &= replacement == orig; + out.add(replacement); + } + return conserve ? this : new ListIota(out); + } + @Override public int size() { return size; diff --git a/Common/src/test/java/iota_endofunctors_tests.kt b/Common/src/test/java/iota_endofunctors_tests.kt new file mode 100644 index 0000000000..23aecfcce5 --- /dev/null +++ b/Common/src/test/java/iota_endofunctors_tests.kt @@ -0,0 +1,112 @@ +import at.petrak.hexcasting.api.casting.iota.DoubleIota +import at.petrak.hexcasting.api.casting.iota.Iota +import at.petrak.hexcasting.api.casting.iota.ListIota +import at.petrak.hexcasting.api.casting.iota.NullIota +import at.petrak.hexcasting.api.casting.iota.PatternIota +import at.petrak.hexcasting.api.casting.math.HexDir +import at.petrak.hexcasting.api.casting.math.HexPattern +import at.petrak.hexcasting.xplat.DummyXplatAbstractions +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.opentest4j.AssertionFailedError +import java.util.function.UnaryOperator + +internal class IotaEndofunctorsTests { + fun exampleWalker(iota: Iota) = + when (iota) { + is DoubleIota -> DoubleIota(iota.double + 1) + is ListIota -> if (iota.list.any { it is DoubleIota && it.double == 7.0 }) { NullIota() } else { iota } + else -> iota + } + + internal class IotaComparator(val inner: Iota) { + override fun equals(other: Any?): Boolean = + when (other) { + is IotaComparator -> Iota.tolerates(inner, other.inner) + is Iota -> Iota.tolerates(inner, other) + else -> false + } + override fun hashCode(): Int = inner.hashCode() + override fun toString(): String = inner.display().string + } + + companion object { + @BeforeAll + @JvmStatic + fun ensureBlackMagicExecuted() { + DummyXplatAbstractions.forbiddenMagics.get() + } + } + + internal fun assertTolerates(left: Iota, right: Iota, message: String? = "iotas were not equal") { + Assertions.assertEquals(IotaComparator(left), IotaComparator(right), message) + } + + internal fun assertNotTolerates(left: Iota, right: Iota, message: String? = "iotas were equal") { + Assertions.assertNotEquals(IotaComparator(left), IotaComparator(right), message) + } + + @Test + fun `my tests are working properly`() { + Assertions.assertAll( + { assertTolerates(DoubleIota(42.0), DoubleIota(42.0)) }, + { assertNotTolerates(DoubleIota(42.0), DoubleIota(47.0)) }, + ) + } + + @Test + fun `a sole double iota should be incremented`() { + assertTolerates(DoubleIota(43.0), DoubleIota(42.0).visit(::exampleWalker)) + } + + @Test + fun `a list of untouched iotas should be conserved`() { + val iota = ListIota(listOf( + PatternIota(HexPattern.fromAnglesUnchecked("aqaaw", HexDir.SOUTH_EAST)), + PatternIota(HexPattern.fromAnglesUnchecked("aqaawa", HexDir.SOUTH_EAST)), + PatternIota(HexPattern.fromAnglesUnchecked("aqaawaw", HexDir.SOUTH_EAST)), + )) + Assertions.assertSame(iota, iota.visit(::exampleWalker)) + } + + @Test + fun `iotas should be visited recursively`() { + val orig = ListIota(listOf( + DoubleIota(1.0), + ListIota(listOf( + DoubleIota(2.0), + ListIota(listOf( + DoubleIota(3.0), + )) + )) + )) + val expected = ListIota(listOf( + DoubleIota(2.0), + ListIota(listOf( + DoubleIota(3.0), + ListIota(listOf( + DoubleIota(4.0), + )) + )) + )) + assertTolerates(expected, orig.visit(::exampleWalker)) + } + + @Test + fun `lists should be visited before their elements`() { + val orig = ListIota(listOf( + ListIota(listOf(DoubleIota(5.0))), + ListIota(listOf(DoubleIota(6.0))), + ListIota(listOf(DoubleIota(7.0))), + ListIota(listOf(DoubleIota(9.0))), + )) + val expected = ListIota(listOf( + ListIota(listOf(DoubleIota(6.0))), + ListIota(listOf(DoubleIota(7.0))), + NullIota(), + ListIota(listOf(DoubleIota(10.0))), + )) + assertTolerates(expected, orig.visit(::exampleWalker)) + } +}