From 92acab91b84290b9f9d7c0976840c9a7557ff839 Mon Sep 17 00:00:00 2001 From: IridescentVoid <265486018+IridescentVoid@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:04:35 -0700 Subject: [PATCH 1/4] Implement patterns from overevaluate --- .../api/casting/eval/vm/ContinuationFrame.kt | 7 ++++ .../api/casting/eval/vm/FrameForEach.kt | 3 ++ .../common/casting/actions/eval/OpContinue.kt | 26 ++++++++++++++ .../common/casting/actions/eval/OpNop.kt | 13 +++++++ .../casting/actions/eval/OpTrulyHalt.kt | 13 +++++++ .../casting/actions/lists/OpSnapList.kt | 21 ++++++++++++ .../casting/actions/stack/OpDuplicateMany.kt | 23 +++++++++++++ .../hexcasting/common/lib/hex/HexActions.java | 22 ++++++++---- .../hexcasting/lang/en_us.flatten.json5 | 21 ++++++++++-- .../en_us/entries/patterns/lists.json | 8 +++++ .../en_us/entries/patterns/meta.json | 18 ++++++++++ .../en_us/entries/patterns/stackmanip.json | 34 ++++++++++++++++--- 12 files changed, 196 insertions(+), 13 deletions(-) create mode 100644 Common/src/main/java/at/petrak/hexcasting/common/casting/actions/eval/OpContinue.kt create mode 100644 Common/src/main/java/at/petrak/hexcasting/common/casting/actions/eval/OpNop.kt create mode 100644 Common/src/main/java/at/petrak/hexcasting/common/casting/actions/eval/OpTrulyHalt.kt create mode 100644 Common/src/main/java/at/petrak/hexcasting/common/casting/actions/lists/OpSnapList.kt create mode 100644 Common/src/main/java/at/petrak/hexcasting/common/casting/actions/stack/OpDuplicateMany.kt diff --git a/Common/src/main/java/at/petrak/hexcasting/api/casting/eval/vm/ContinuationFrame.kt b/Common/src/main/java/at/petrak/hexcasting/api/casting/eval/vm/ContinuationFrame.kt index 476cd22a56..c96a487d38 100644 --- a/Common/src/main/java/at/petrak/hexcasting/api/casting/eval/vm/ContinuationFrame.kt +++ b/Common/src/main/java/at/petrak/hexcasting/api/casting/eval/vm/ContinuationFrame.kt @@ -43,6 +43,13 @@ interface ContinuationFrame { */ fun breakDownwards(stack: List): Pair> + /** + * The OpContinue instruction wants us to "jump to" the end of the current ForEach or loop iteration. + * It leaves the frame intact on the stack to be executed next. + * @return whether the break should stop here + */ + fun breakSideways(): Boolean = false + /** * Return the number of iotas contained inside this frame, used for determining whether it is valid to serialise. */ diff --git a/Common/src/main/java/at/petrak/hexcasting/api/casting/eval/vm/FrameForEach.kt b/Common/src/main/java/at/petrak/hexcasting/api/casting/eval/vm/FrameForEach.kt index 6133b2b627..0fdf8d705e 100644 --- a/Common/src/main/java/at/petrak/hexcasting/api/casting/eval/vm/FrameForEach.kt +++ b/Common/src/main/java/at/petrak/hexcasting/api/casting/eval/vm/FrameForEach.kt @@ -40,6 +40,9 @@ data class FrameForEach( return true to newStack } + /** When casting OpContinue, we should stop popping frames here. */ + override fun breakSideways(): Boolean = true + /** Step the Thoth computation, enqueueing one code evaluation. */ override fun evaluate( continuation: SpellContinuation, diff --git a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/eval/OpContinue.kt b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/eval/OpContinue.kt new file mode 100644 index 0000000000..e7997eac0e --- /dev/null +++ b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/eval/OpContinue.kt @@ -0,0 +1,26 @@ +package at.petrak.hexcasting.common.casting.actions.eval + +import at.petrak.hexcasting.api.casting.castables.Action +import at.petrak.hexcasting.api.casting.eval.CastingEnvironment +import at.petrak.hexcasting.api.casting.eval.OperationResult +import at.petrak.hexcasting.api.casting.eval.vm.CastingImage +import at.petrak.hexcasting.api.casting.eval.vm.SpellContinuation +import at.petrak.hexcasting.common.lib.hex.HexEvalSounds + +object OpContinue : Action { + override fun operate(env: CastingEnvironment, image: CastingImage, continuation: SpellContinuation): OperationResult { + var newCont = continuation + while (newCont is SpellContinuation.NotDone && !newCont.frame.breakSideways()) + newCont = newCont.next + + var newStack = image.stack.toList() + // we didn't hit any loops, so we have fully exited the hex + if (newCont !is SpellContinuation.NotDone) { + // clear the stack so staffcasting exits + newStack = listOf() + } + + val image2 = image.withUsedOp().copy(stack = newStack) + return OperationResult(image2, listOf(), newCont, HexEvalSounds.SPELL) + } +} diff --git a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/eval/OpNop.kt b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/eval/OpNop.kt new file mode 100644 index 0000000000..22e2907219 --- /dev/null +++ b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/eval/OpNop.kt @@ -0,0 +1,13 @@ +package at.petrak.hexcasting.common.casting.actions.eval + +import at.petrak.hexcasting.api.casting.castables.Action +import at.petrak.hexcasting.api.casting.eval.CastingEnvironment +import at.petrak.hexcasting.api.casting.eval.OperationResult +import at.petrak.hexcasting.api.casting.eval.vm.CastingImage +import at.petrak.hexcasting.api.casting.eval.vm.SpellContinuation +import at.petrak.hexcasting.common.lib.hex.HexEvalSounds + +object OpNop : Action { + override fun operate(env: CastingEnvironment, image: CastingImage, continuation: SpellContinuation): OperationResult = + OperationResult(image, listOf(), continuation, HexEvalSounds.NOTHING) +} diff --git a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/eval/OpTrulyHalt.kt b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/eval/OpTrulyHalt.kt new file mode 100644 index 0000000000..574f7e4917 --- /dev/null +++ b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/eval/OpTrulyHalt.kt @@ -0,0 +1,13 @@ +package at.petrak.hexcasting.common.casting.actions.eval + +import at.petrak.hexcasting.api.casting.castables.Action +import at.petrak.hexcasting.api.casting.eval.CastingEnvironment +import at.petrak.hexcasting.api.casting.eval.OperationResult +import at.petrak.hexcasting.api.casting.eval.vm.CastingImage +import at.petrak.hexcasting.api.casting.eval.vm.SpellContinuation +import at.petrak.hexcasting.common.lib.hex.HexEvalSounds + +object OpTrulyHalt : Action { + override fun operate(env: CastingEnvironment, image: CastingImage, continuation: SpellContinuation): OperationResult = + OperationResult(image.withUsedOp(), listOf(), SpellContinuation.Done, HexEvalSounds.SPELL) +} diff --git a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/lists/OpSnapList.kt b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/lists/OpSnapList.kt new file mode 100644 index 0000000000..bf1cb458f6 --- /dev/null +++ b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/lists/OpSnapList.kt @@ -0,0 +1,21 @@ +package at.petrak.hexcasting.common.casting.actions.lists + +import at.petrak.hexcasting.api.casting.castables.ConstMediaAction +import at.petrak.hexcasting.api.casting.eval.CastingEnvironment +import at.petrak.hexcasting.api.casting.getList +import at.petrak.hexcasting.api.casting.getPositiveIntUnder +import at.petrak.hexcasting.api.casting.iota.Iota +import at.petrak.hexcasting.api.casting.iota.ListIota + +object OpSnapList : ConstMediaAction { + override val argc = 2 + override fun execute(args: List, env: CastingEnvironment): List { + val list = args.getList(0, argc).toList() + val breakPosition = args.getPositiveIntUnder(1, list.size) + return listOf( + ListIota(list.subList(0, breakPosition)), + list[breakPosition], + ListIota(list.subList(breakPosition + 1, list.size)) + ) + } +} diff --git a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/stack/OpDuplicateMany.kt b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/stack/OpDuplicateMany.kt new file mode 100644 index 0000000000..4bcbbfb0df --- /dev/null +++ b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/stack/OpDuplicateMany.kt @@ -0,0 +1,23 @@ +package at.petrak.hexcasting.common.casting.actions.stack + +import at.petrak.hexcasting.api.casting.castables.Action +import at.petrak.hexcasting.api.casting.eval.CastingEnvironment +import at.petrak.hexcasting.api.casting.eval.OperationResult +import at.petrak.hexcasting.api.casting.eval.vm.CastingImage +import at.petrak.hexcasting.api.casting.eval.vm.SpellContinuation +import at.petrak.hexcasting.api.casting.getPositiveIntUnderInclusive +import at.petrak.hexcasting.api.casting.mishaps.MishapNotEnoughArgs +import at.petrak.hexcasting.common.lib.hex.HexEvalSounds + +object OpDuplicateMany : Action { + override fun operate(env: CastingEnvironment, image: CastingImage, continuation: SpellContinuation): OperationResult { + val stack = image.stack.toMutableList() + if (stack.isEmpty()) + throw MishapNotEnoughArgs(1, 0) + val count = stack.takeLast(1).getPositiveIntUnderInclusive(0, stack.size - 1) + stack.removeAt(stack.lastIndex) + for (iota in stack.takeLast(count)) + stack.add(iota) + return OperationResult(image.withUsedOp().copy(stack = stack), listOf(), continuation, HexEvalSounds.NORMAL_EXECUTE) + } +} diff --git a/Common/src/main/java/at/petrak/hexcasting/common/lib/hex/HexActions.java b/Common/src/main/java/at/petrak/hexcasting/common/lib/hex/HexActions.java index eb049a8e78..32f6d32365 100644 --- a/Common/src/main/java/at/petrak/hexcasting/common/lib/hex/HexActions.java +++ b/Common/src/main/java/at/petrak/hexcasting/common/lib/hex/HexActions.java @@ -18,10 +18,7 @@ import at.petrak.hexcasting.common.casting.actions.circles.OpImpetusPos; import at.petrak.hexcasting.common.casting.actions.escaping.*; import at.petrak.hexcasting.common.casting.actions.eval.*; -import at.petrak.hexcasting.common.casting.actions.lists.OpEmptyList; -import at.petrak.hexcasting.common.casting.actions.lists.OpLastNToList; -import at.petrak.hexcasting.common.casting.actions.lists.OpSingleton; -import at.petrak.hexcasting.common.casting.actions.lists.OpSplat; +import at.petrak.hexcasting.common.casting.actions.lists.*; import at.petrak.hexcasting.common.casting.actions.local.OpPeekLocal; import at.petrak.hexcasting.common.casting.actions.local.OpPushLocal; import at.petrak.hexcasting.common.casting.actions.math.OpCoerceToAxial; @@ -115,8 +112,11 @@ public class HexActions { public static final ActionRegistryEntry ROTATE = make("rotate", new ActionRegistryEntry(HexPattern.fromAngles("aaeaa", HexDir.EAST), new OpTwiddling(3, new int[]{1, 2, 0}))); public static final ActionRegistryEntry ROTATE_REVERSE = make("rotate_reverse", - new ActionRegistryEntry(HexPattern.fromAngles("ddqdd", - HexDir.NORTH_EAST), new OpTwiddling(3, new int[]{2, 0, 1}))); + new ActionRegistryEntry(HexPattern.fromAngles("ddqdd", HexDir.NORTH_EAST), new OpTwiddling(3, new int[]{2, 0, 1}))); + public static final ActionRegistryEntry SWAP_ONE_THREE = make("swap_one_three", + new ActionRegistryEntry(HexPattern.fromAngles("ddwqaq", HexDir.NORTH_EAST), new OpTwiddling(3, new int[]{2, 1, 0}))); + public static final ActionRegistryEntry SWAP_TWO_THREE = make("swap_two_three", + new ActionRegistryEntry(HexPattern.fromAngles("aawede", HexDir.EAST), new OpTwiddling(3, new int[]{1, 0, 2}))); public static final ActionRegistryEntry DUPLICATE = make("duplicate", new ActionRegistryEntry(HexPattern.fromAngles("aadaa", HexDir.EAST), new OpTwiddling(1, new int[]{0, 0}))); public static final ActionRegistryEntry OVER = make("over", @@ -126,6 +126,8 @@ public class HexActions { public static final ActionRegistryEntry TWO_DUP = make("2dup", new ActionRegistryEntry(HexPattern.fromAngles("aadadaaw", HexDir.EAST), new OpTwiddling(2, new int[]{0, 1, 0, 1}))); + public static final ActionRegistryEntry DUPLICATE_MANY = make("duplicate_many", + new ActionRegistryEntry(HexPattern.fromAngles("waadadaa", HexDir.EAST), OpDuplicateMany.INSTANCE)); public static final ActionRegistryEntry STACK_LEN = make("stack_len", new ActionRegistryEntry(HexPattern.fromAngles("qwaeawqaeaqa", HexDir.NORTH_WEST), OpStackSize.INSTANCE)); @@ -405,6 +407,12 @@ public class HexActions { new ActionRegistryEntry(HexPattern.fromAngles("qwaqde", HexDir.NORTH_WEST), OpEvalBreakable.INSTANCE)); public static final ActionRegistryEntry HALT = make("halt", new ActionRegistryEntry(HexPattern.fromAngles("aqdee", HexDir.SOUTH_WEST), OpHalt.INSTANCE)); + public static final ActionRegistryEntry TRULY_HALT = make("truly_halt", + new ActionRegistryEntry(HexPattern.fromAngles("aadee", HexDir.SOUTH_WEST), OpTrulyHalt.INSTANCE)); + public static final ActionRegistryEntry CONTINUE = make("continue", + new ActionRegistryEntry(HexPattern.fromAngles("aqdea", HexDir.SOUTH_WEST), OpContinue.INSTANCE)); + public static final ActionRegistryEntry NOP = make("nop", + new ActionRegistryEntry(HexPattern.fromAngles("eedqa", HexDir.WEST), OpNop.INSTANCE)); public static final ActionRegistryEntry READ = make("read", new ActionRegistryEntry(HexPattern.fromAngles("aqqqqq", HexDir.EAST), OpRead.INSTANCE)); @@ -577,6 +585,8 @@ public class HexActions { new OperationAction(HexPattern.fromAngles("ddewedd", HexDir.SOUTH_EAST))); public static final ActionRegistryEntry DECONSTRUCT = make("deconstruct", new OperationAction(HexPattern.fromAngles("aaqwqaa", HexDir.SOUTH_WEST))); + public static final ActionRegistryEntry SNAP_LIST = make("snap_list", + new ActionRegistryEntry(HexPattern.fromAngles("eawdq", HexDir.EAST), OpSnapList.INSTANCE)); // Xplat interops static { diff --git a/Common/src/main/resources/assets/hexcasting/lang/en_us.flatten.json5 b/Common/src/main/resources/assets/hexcasting/lang/en_us.flatten.json5 index 528f826464..f62f342e7f 100644 --- a/Common/src/main/resources/assets/hexcasting/lang/en_us.flatten.json5 +++ b/Common/src/main/resources/assets/hexcasting/lang/en_us.flatten.json5 @@ -762,6 +762,7 @@ replace: "Surgeon's Exaltation", construct: "Speaker's Distillation", deconstruct: "Speaker's Decomposition", + snap_list: "Extirpating Gambit", get_entity: "Entity Purification", "get_entity/": { @@ -789,10 +790,13 @@ swap: "Jester's Gambit", rotate: "Rotation Gambit", rotate_reverse: "Rotation Gambit II", + swap_one_three: "Reflecting Gambit", + swap_two_three: "Bubbling Gambit", duplicate: "Gemini Decomposition", over: "Prospector's Gambit", tuck: "Undertaker's Gambit", "2dup": "Dioscuri Gambit", + duplicate_many: "Dioscuri Gambit II", duplicate_n: "Gemini Gambit", stack_len: "Flock's Reflection", fisherman: "Fisherman's Gambit", @@ -923,7 +927,10 @@ "eval/cc": "Iris' Gambit", for_each: "Thoth's Gambit", halt: "Charon's Gambit", + truly_halt: "Janus' Gambit", + continue: "Atalanta's Gambit", thanatos: "Thanatos' Reflection", + nop: "Tutu's Gambit", "interop/": { "gravity/": { @@ -1877,10 +1884,13 @@ swap: "Swaps the top two iotas of the stack.", rotate: "Yanks the iota third from the top of the stack to the top. [0, 1, 2] becomes [1, 2, 0].", rotate_reverse: "Yanks the top iota to the third position. [0, 1, 2] becomes [2, 0, 1].", + swap_one_three: "Swaps the top and third-from-the-top iota. [0, 1, 2] becomes [2, 1 0].", + swap_two_three: "Swaps the second-from-the-top and third-from-the-top iota. [0, 1, 2] becomes [1, 0, 2].", duplicate: "Duplicates the top iota of the stack.", over: "Copy the second-to-last iota of the stack to the top. [0, 1] becomes [0, 1, 0].", tuck: "Copy the top iota of the stack, then put it under the second iota. [0, 1] becomes [1, 0, 1].", "2dup": "Copy the top two iotas of the stack. [0, 1] becomes [0, 1, 0, 1].", + duplicate_many: "Copy the top n iotas of the stack while preserving order.", stack_len: "Pushes the size of the stack as a number to the top of the stack. (For example, a stack of [0, 1] will become [0, 1, 2].)", duplicate_n: "Removes the number at the top of the stack, then copies the top iota of the stack that number of times. (A count of 2 results in two of the iota on the stack, not three.)", fisherman: "Grabs the element in the stack indexed by the number and brings it to the top. If the number is negative, instead moves the top element of the stack down that many elements.", @@ -1958,6 +1968,7 @@ splat: "Remove the list at the top of the stack, then push its contents to the stack.", construct: "Remove the top iota, then add it as the first element to the list at the top of the stack.", deconstruct: "Remove the first iota from the list at the top of the stack, then push that iota to the stack.", + snap_list: "Remove the number at the top of the stack, then split the list at the top of the stack at the given index into a list of what came before, the iota, and what comes after.", }, patterns_as_iotas: { @@ -2006,11 +2017,17 @@ "halt.1": "This pattern forcibly halts a _Hex. This is mostly useless on its own, as I could simply just stop writing patterns, or put down my staff.", "halt.2": "But when combined with $(l:patterns/meta#hexcasting:eval)$(action)Hermes'/$ or $(l:patterns/meta#hexcasting:for_each)$(action)Thoth's Gambits/$, it becomes $(italics)far/$ more interesting. Those patterns serve to 'contain' that halting, and rather than ending the entire _Hex, those gambits end instead. This can be used to cause $(l:patterns/meta#hexcasting:for_each)$(action)Thoth's Gambit/$ not to operate on every iota it's given. An escape from the madness, as it were.", - + + "truly_halt.1": "This pattern forcibly halts a _Hex, ignoring any $(l:patterns/meta#hexcasting:eval)$(action)Hermes'/$ or $(l:patterns/meta#hexcasting:for_each)$(action)Thoth's Gambits/$ that might otherwise contain $(l:patterns/meta#hexcasting:halt)$(action)Charon's Gambit/$.", + + "continue.1": "This pattern halts the current iteration of $(l:patterns/meta#hexcasting:for_each)$(action)Thoth's Gambit/$, or if cast outside of one, the entire hex.", + "eval/cc.1": "Cast a pattern or list of patterns from the stack exactly like $(l:patterns/meta#hexcasting:eval)$(action)Hermes' Gambit/$, except that a unique \"Jump\" iota is pushed to the stack beforehand. ", "eval/cc.2": "When the \"Jump\"-iota is executed, it'll skip the rest of the patterns and jump directly to the end of the pattern list.$(p)While this may seem redundant given $(l:patterns/meta#hexcasting:halt)$(action)Charon's Gambit/$ exists, this allows you to exit $(italic)nested/$ $(l:patterns/meta#hexcasting:eval)$(action)Hermes'/$ invocations in a controlled way, where Charon only allows you to exit one.$(p)The \"Jump\" iota will apparently stay on the stack even after execution is finished... better not think about the implications of that.", - "thanatos.1": "Adds the number of patterns a _Hex is still capable of evaluating to the stack. This is reduced by one for each pattern cast by the _Hex." + "thanatos.1": "Adds the number of patterns a _Hex is still capable of evaluating to the stack. This is reduced by one for each pattern cast by the _Hex.", + + "nop.1": "Does nothing. Executing it does not consume an operation, consume media, produce particles, or have any other impact on the world.", }, circle_patterns: { diff --git a/Common/src/main/resources/assets/hexcasting/patchouli_books/thehexbook/en_us/entries/patterns/lists.json b/Common/src/main/resources/assets/hexcasting/patchouli_books/thehexbook/en_us/entries/patterns/lists.json index 8dfe25a29d..96748370b9 100644 --- a/Common/src/main/resources/assets/hexcasting/patchouli_books/thehexbook/en_us/entries/patterns/lists.json +++ b/Common/src/main/resources/assets/hexcasting/patchouli_books/thehexbook/en_us/entries/patterns/lists.json @@ -135,6 +135,14 @@ "input": "list", "output": "list, any", "text": "hexcasting.page.lists.deconstruct" + }, + { + "type": "hexcasting:pattern", + "op_id": "hexcasting:snap_list", + "anchor": "hexcasting:snap_list", + "input": "list, number", + "output": "list, any, list", + "text": "hexcasting.page.lists.snap_list" } ] } diff --git a/Common/src/main/resources/assets/hexcasting/patchouli_books/thehexbook/en_us/entries/patterns/meta.json b/Common/src/main/resources/assets/hexcasting/patchouli_books/thehexbook/en_us/entries/patterns/meta.json index 63e7f68c4a..e93ec0d8c1 100644 --- a/Common/src/main/resources/assets/hexcasting/patchouli_books/thehexbook/en_us/entries/patterns/meta.json +++ b/Common/src/main/resources/assets/hexcasting/patchouli_books/thehexbook/en_us/entries/patterns/meta.json @@ -52,6 +52,24 @@ "type": "patchouli:text", "text": "hexcasting.page.meta.halt.2" }, + { + "type": "hexcasting:pattern", + "op_id": "hexcasting:truly_halt", + "anchor": "hexcasting:truly_halt", + "text": "hexcasting.page.meta.truly_halt.1" + }, + { + "type": "hexcasting:pattern", + "op_id": "hexcasting:continue", + "anchor": "hexcasting:continue", + "text": "hexcasting.page.meta.continue.1" + }, + { + "type": "hexcasting:pattern", + "op_id": "hexcasting:nop", + "anchor": "hexcasting:nop", + "text": "hexcasting.page.meta.nop.1" + }, { "type": "hexcasting:pattern", "op_id": "hexcasting:thanatos", diff --git a/Common/src/main/resources/assets/hexcasting/patchouli_books/thehexbook/en_us/entries/patterns/stackmanip.json b/Common/src/main/resources/assets/hexcasting/patchouli_books/thehexbook/en_us/entries/patterns/stackmanip.json index dcae03f1ce..8bd94af6f2 100644 --- a/Common/src/main/resources/assets/hexcasting/patchouli_books/thehexbook/en_us/entries/patterns/stackmanip.json +++ b/Common/src/main/resources/assets/hexcasting/patchouli_books/thehexbook/en_us/entries/patterns/stackmanip.json @@ -46,6 +46,22 @@ "output": "any, any, any", "text": "hexcasting.page.stackmanip.rotate_reverse" }, + { + "type": "hexcasting:pattern", + "op_id": "hexcasting:swap_one_three", + "anchor": "hexcasting:swap_one_three", + "input": "any, any, any", + "output": "any, any, any", + "text": "hexcasting.page.stackmanip.swap_one_three" + }, + { + "type": "hexcasting:pattern", + "op_id": "hexcasting:swap_two_three", + "anchor": "hexcasting:swap_two_three", + "input": "any, any, any", + "output": "any, any, any", + "text": "hexcasting.page.stackmanip.swap_two_three" + }, { "type": "hexcasting:pattern", "op_id": "hexcasting:duplicate", @@ -88,11 +104,11 @@ }, { "type": "hexcasting:pattern", - "op_id": "hexcasting:stack_len", - "anchor": "hexcasting:stack_len", - "input": "", - "output": "number", - "text": "hexcasting.page.stackmanip.stack_len" + "op_id": "hexcasting:duplicate_many", + "anchor": "hexcasting:duplicate_many", + "input": "many, number", + "output": "many", + "text": "hexcasting.page.stackmanip.duplicate_many" }, { "type": "hexcasting:pattern", @@ -153,6 +169,14 @@ "text": "hexcasting.page.stackmanip.swizzle.2", "link_text": "hexcasting.page.stackmanip.swizzle.link", "url": "https://github.com/gamma-delta/HexMod/wiki/Table-of-Lehmer-Codes-for-Swindler's-Gambit" + }, + { + "type": "hexcasting:pattern", + "op_id": "hexcasting:stack_len", + "anchor": "hexcasting:stack_len", + "input": "", + "output": "number", + "text": "hexcasting.page.stackmanip.stack_len" } ] } From b9351514d3ac09c7a6bbf805abd3cdcad26e1ab7 Mon Sep 17 00:00:00 2001 From: IridescentVoid <265486018+IridescentVoid@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:10:54 -0700 Subject: [PATCH 2/4] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d401546f73..c65e8c77a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Added - Updated to Minecraft 1.21.1 ([#985](https://github.com/FallingColors/HexMod/pull/985)) @SuperKnux @slava110 +- Added Extirpating Gambit, Reflecting Gambit, Bubbling Gambit, Dioscuri Gambit II, Tutu's Gambit, Janus' Gambit, and Atalanta's Gambit ([#1177](https://github.com/FallingColors/HexMod/pull/1177)) @IridescentVoid ### Fixed From 9c1a790508d25427ba32a3e08947b125c1ddbc40 Mon Sep 17 00:00:00 2001 From: IridescentVoid <265486018+IridescentVoid@users.noreply.github.com> Date: Mon, 22 Jun 2026 17:44:00 -0700 Subject: [PATCH 3/4] Mishap when Atalanta's Gambit is cast outside of Thoth's Gambit --- .../casting/mishaps/MishapNeedsLoopContext.kt | 18 ++++++++++++++++++ .../common/casting/actions/eval/OpContinue.kt | 10 ++++------ .../assets/hexcasting/lang/en_us.flatten.json5 | 6 +++++- .../en_us/entries/casting/mishaps.json | 5 +++++ 4 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 Common/src/main/java/at/petrak/hexcasting/api/casting/mishaps/MishapNeedsLoopContext.kt diff --git a/Common/src/main/java/at/petrak/hexcasting/api/casting/mishaps/MishapNeedsLoopContext.kt b/Common/src/main/java/at/petrak/hexcasting/api/casting/mishaps/MishapNeedsLoopContext.kt new file mode 100644 index 0000000000..130560f73e --- /dev/null +++ b/Common/src/main/java/at/petrak/hexcasting/api/casting/mishaps/MishapNeedsLoopContext.kt @@ -0,0 +1,18 @@ +package at.petrak.hexcasting.api.casting.mishaps + +import at.petrak.hexcasting.api.casting.eval.CastingEnvironment +import at.petrak.hexcasting.api.casting.iota.Iota +import at.petrak.hexcasting.api.pigment.FrozenPigment +import net.minecraft.network.chat.Component +import net.minecraft.world.item.DyeColor + +class MishapNeedsLoopContext : Mishap() { + // TODO: need to add to notebook? + override fun accentColor(ctx: CastingEnvironment, errorCtx: Context): FrozenPigment = + dyeColor(DyeColor.BLUE) + + override fun errorMessage(ctx: CastingEnvironment, errorCtx: Context): Component = + error("needs_loop_context") + + override fun execute(env: CastingEnvironment, errorCtx: Context, stack: MutableList) {} +} diff --git a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/eval/OpContinue.kt b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/eval/OpContinue.kt index e7997eac0e..0cdae94c2c 100644 --- a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/eval/OpContinue.kt +++ b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/eval/OpContinue.kt @@ -5,6 +5,7 @@ import at.petrak.hexcasting.api.casting.eval.CastingEnvironment import at.petrak.hexcasting.api.casting.eval.OperationResult import at.petrak.hexcasting.api.casting.eval.vm.CastingImage import at.petrak.hexcasting.api.casting.eval.vm.SpellContinuation +import at.petrak.hexcasting.api.casting.mishaps.MishapNeedsLoopContext import at.petrak.hexcasting.common.lib.hex.HexEvalSounds object OpContinue : Action { @@ -13,14 +14,11 @@ object OpContinue : Action { while (newCont is SpellContinuation.NotDone && !newCont.frame.breakSideways()) newCont = newCont.next - var newStack = image.stack.toList() - // we didn't hit any loops, so we have fully exited the hex if (newCont !is SpellContinuation.NotDone) { - // clear the stack so staffcasting exits - newStack = listOf() + // failed to find a loop frame + throw MishapNeedsLoopContext() } - val image2 = image.withUsedOp().copy(stack = newStack) - return OperationResult(image2, listOf(), newCont, HexEvalSounds.SPELL) + return OperationResult(image.withUsedOp(), listOf(), newCont, HexEvalSounds.SPELL) } } diff --git a/Common/src/main/resources/assets/hexcasting/lang/en_us.flatten.json5 b/Common/src/main/resources/assets/hexcasting/lang/en_us.flatten.json5 index f62f342e7f..ed4667cd92 100644 --- a/Common/src/main/resources/assets/hexcasting/lang/en_us.flatten.json5 +++ b/Common/src/main/resources/assets/hexcasting/lang/en_us.flatten.json5 @@ -1060,6 +1060,7 @@ no_spell_circle: "requires a spell circle", others_name: "Tried to invade the privacy of %s's soul", "others_name.self": "Tried to divulge my Name too recklessly", + needs_loop_context: "needs to be cast within a skippable meta-evaluation pattern", divide_by_zero: { divide: "Attempted to divide %s by %s", @@ -1516,6 +1517,9 @@ "needs_parens.title": "Absent Introspection", needs_parens: "I attempted to draw $(l:patterns/patterns_as_iotas#hexcasting:close_paren)$(action)Retrospection/$ or $(l:patterns/patterns_as_iotas#hexcasting:undo)$(action)Evanition/$ without first drawing $(l:patterns/patterns_as_iotas#hexcasting:open_paren)$(action)Introspection/$.$(br2)Causes orange sparks, and pushes the pattern I tried to draw to the stack as an iota.", + "needs_loop_context.title": "Absent Metaevaluation", + needs_loop_context: "I attempted to draw $(l:patterns/meta#hexcasting:continue)$(action)Atalanta's Gambit/$ while not using $(l:patterns/meta#hexcasting:for_each)$(action)Thoth's Gambit/$.$(br2)Causes dark blue sparks.", + "too_many_patterns.title": "Lost in Thought", too_many_patterns: "I attempted to evaluate too many patterns in one _Hex. Often, this happens because I've accidentally created an infinite loop.$(br2)Causes dark blue sparks, and chokes all the air out of me.", @@ -2020,7 +2024,7 @@ "truly_halt.1": "This pattern forcibly halts a _Hex, ignoring any $(l:patterns/meta#hexcasting:eval)$(action)Hermes'/$ or $(l:patterns/meta#hexcasting:for_each)$(action)Thoth's Gambits/$ that might otherwise contain $(l:patterns/meta#hexcasting:halt)$(action)Charon's Gambit/$.", - "continue.1": "This pattern halts the current iteration of $(l:patterns/meta#hexcasting:for_each)$(action)Thoth's Gambit/$, or if cast outside of one, the entire hex.", + "continue.1": "This pattern halts the current iteration of $(l:patterns/meta#hexcasting:for_each)$(action)Thoth's Gambit/$, immediately causing the next iteration, if any, to start. Mishaps if cast outside of a special context like $(l:patterns/meta#hexcasting:for_each)$(action)Thoth's Gambit/$.", "eval/cc.1": "Cast a pattern or list of patterns from the stack exactly like $(l:patterns/meta#hexcasting:eval)$(action)Hermes' Gambit/$, except that a unique \"Jump\" iota is pushed to the stack beforehand. ", "eval/cc.2": "When the \"Jump\"-iota is executed, it'll skip the rest of the patterns and jump directly to the end of the pattern list.$(p)While this may seem redundant given $(l:patterns/meta#hexcasting:halt)$(action)Charon's Gambit/$ exists, this allows you to exit $(italic)nested/$ $(l:patterns/meta#hexcasting:eval)$(action)Hermes'/$ invocations in a controlled way, where Charon only allows you to exit one.$(p)The \"Jump\" iota will apparently stay on the stack even after execution is finished... better not think about the implications of that.", diff --git a/Common/src/main/resources/assets/hexcasting/patchouli_books/thehexbook/en_us/entries/casting/mishaps.json b/Common/src/main/resources/assets/hexcasting/patchouli_books/thehexbook/en_us/entries/casting/mishaps.json index 45fdc0aa70..4a68f90e68 100644 --- a/Common/src/main/resources/assets/hexcasting/patchouli_books/thehexbook/en_us/entries/casting/mishaps.json +++ b/Common/src/main/resources/assets/hexcasting/patchouli_books/thehexbook/en_us/entries/casting/mishaps.json @@ -73,6 +73,11 @@ "title": "hexcasting.page.mishaps.needs_parens.title", "text": "hexcasting.page.mishaps.needs_parens" }, + { + "type": "patchouli:text", + "title": "hexcasting.page.mishaps.needs_loop_context.title", + "text": "hexcasting.page.mishaps.needs_loop_context" + }, { "type": "patchouli:text", "title": "hexcasting.page.mishaps.too_many_patterns.title", From 3a982f173e7b3672d06783213fcec21132c27e33 Mon Sep 17 00:00:00 2001 From: IridescentVoid <265486018+IridescentVoid@users.noreply.github.com> Date: Mon, 22 Jun 2026 17:45:52 -0700 Subject: [PATCH 4/4] Update changelog to mention the ContinuationFrame.breakSideways --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c65e8c77a1..0c223cdfbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Updated to Minecraft 1.21.1 ([#985](https://github.com/FallingColors/HexMod/pull/985)) @SuperKnux @slava110 - Added Extirpating Gambit, Reflecting Gambit, Bubbling Gambit, Dioscuri Gambit II, Tutu's Gambit, Janus' Gambit, and Atalanta's Gambit ([#1177](https://github.com/FallingColors/HexMod/pull/1177)) @IridescentVoid +- API: Added `breakSideways` method to `ContinuationFrame` to control behavior when used with Atalanta's Gambit ([#1177](https://github.com/FallingColors/HexMod/pull/1177)) @IridescentVoid ### Fixed