Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Added a config toggle (default false) for the effect that makes iota-holding items display their entire NBT data when Advanced Tooltips is enabled ([#1021](https://github.com/FallingColors/HexMod/pull/1021)) @Robotgiggle
- Added a pattern display overlay for pattern-holding items (ie scrolls or slates) while holding shift in the inventory ([#879](https://github.com/FallingColors/HexMod/pull/879)) @SamsTheNerd
- Added connected textures for Akashic Ligatures when using Continuity or Optifine ([#885](https://github.com/FallingColors/HexMod/pull/885)) @kineticneticat
- Added Interjection, Meditation, and Recollection for advanced pattern-list creation ([#1103](https://github.com/FallingColors/HexMod/pull/1103)) @Robotgiggle

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ interface Action {
*
* The userdata tag is copied for you, so you don't need to worry about mutation messing up things
* behind the scenes.
*
* Note that `image.parenCount` will always be 0 here - if it's greater, [operateInParens] is used instead.
*/
@Throws(Mishap::class)
fun operate(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import net.minecraft.server.level.ServerLevel
/**
* A list of patterns to be evaluated in sequence.
* @property list the *remaining* list of patterns to be evaluated
* @property isMetacasting only for sound effects, if this is being cast from a hermes / iris
* @property isMetacasting if this is being cast from a hermes / iris / thoth
Comment thread
Robotgiggle marked this conversation as resolved.
Outdated
*/
data class FrameEvaluate(val list: SpellList, val isMetacasting: Boolean) : ContinuationFrame {
// Discard this frame and keep discarding frames.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import at.petrak.hexcasting.api.casting.eval.*;
import at.petrak.hexcasting.api.casting.eval.sideeffects.OperatorSideEffect;
import at.petrak.hexcasting.api.casting.eval.vm.CastingVM;
import at.petrak.hexcasting.api.casting.eval.vm.FrameEvaluate;
import at.petrak.hexcasting.api.casting.eval.vm.SpellContinuation;
import at.petrak.hexcasting.api.casting.math.HexPattern;
import at.petrak.hexcasting.api.casting.mishaps.Mishap;
Expand Down Expand Up @@ -167,10 +168,19 @@ public boolean executable() {
result.getSound());

} catch (Mishap mishap) {
// if a mishap happens mid-parens at the top level (ie when staffcasting), keep any in-progress parens
boolean wipeParens = false;
// if a mishap happens mid-parens while metacasting, wipe the parens - if we didn't do this, any open paren
// levels would remain open even though the metacast itself has mishapped and thus been aborted
Comment thread
Robotgiggle marked this conversation as resolved.
Outdated
if (continuation instanceof SpellContinuation.NotDone cnd && cnd.getFrame() instanceof FrameEvaluate frameEval) {
if (frameEval.isMetacasting()) {
wipeParens = true;
}
}
Comment thread
Robotgiggle marked this conversation as resolved.
Outdated
return new CastResult(
this,
continuation,
null,
wipeParens ? vm.getImage().withResetEscape() : null,
List.of(new OperatorSideEffect.DoMishap(mishap, new Mishap.Context(this.getPattern(), castedName.get()))),
mishap.resolutionType(vm.getEnv()),
HexEvalSounds.MISHAP);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,8 @@ class GuiSpellcasting constructor(

// TODO this is the kinda hacky bit
if (info.resolutionType == ResolvedPatternType.UNDONE) {
// find the last escaped pattern (or the opening paren if there's nothing else) and set it to UNDONE
this.patterns.reversed().drop(1).firstOrNull {
it.type == ResolvedPatternType.ESCAPED ||
(it.type == ResolvedPatternType.EVALUATED && it.pattern.angles == HexActions.OPEN_PAREN.prototype.angles)
}?.let { it.type = ResolvedPatternType.UNDONE }
// find the last undo-able pattern and set its coloring to UNDONE
this.patterns.reversed().drop(1).firstOrNull { canBeUndone(it) }?.let { it.type = ResolvedPatternType.UNDONE }
// use the normal EVALUATED coloring for the Evanition that was just drawn
this.patterns.getOrNull(index)?.let { it.type = ResolvedPatternType.EVALUATED }
} else this.patterns.getOrNull(index)?.let {
Expand All @@ -88,6 +85,21 @@ class GuiSpellcasting constructor(
this.calculateIotaDisplays()
}

fun canBeUndone(resPat: ResolvedPattern): Boolean {
Comment thread
Robotgiggle marked this conversation as resolved.
Outdated
// standard escaped patterns can always be undone
if (resPat.type == ResolvedPatternType.ESCAPED) return true
// everything else cannot be undone, with three exceptions:
// - unescaped Introspection and Meditation can be undone if there's nothing else left to undo
// - Interjection can always be undone (undoing its inserted iota) despite using the EVALUATED coloring
if (resPat.type == ResolvedPatternType.EVALUATED) {
Comment thread
Robotgiggle marked this conversation as resolved.
Outdated
if (resPat.pattern.angles == HexActions.OPEN_PAREN.prototype.angles ||
resPat.pattern.angles == HexActions.OPEN_N_PARENS.prototype.angles ||
resPat.pattern.angles == HexActions.READ_INTO_PARENS.prototype.angles)
return true
}
return false
}

fun calculateIotaDisplays() {
val mc = Minecraft.getInstance()
val width = (this.width * LHS_IOTAS_ALLOCATION).toInt()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package at.petrak.hexcasting.common.casting.actions.escaping

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.ParenthesizedOperationResult
import at.petrak.hexcasting.api.casting.eval.ResolvedPatternType
import at.petrak.hexcasting.api.casting.eval.vm.CastingImage
import at.petrak.hexcasting.api.casting.eval.vm.SpellContinuation
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.mishaps.MishapNeedsParens
import at.petrak.hexcasting.common.lib.hex.HexEvalSounds

object OpCloseAllParens : Action {
override fun operate(env: CastingEnvironment, image: CastingImage, continuation: SpellContinuation): OperationResult {
throw MishapNeedsParens()
}

override fun operateInParens(env: CastingEnvironment, image: CastingImage, continuation: SpellContinuation, thisIota: Iota): ParenthesizedOperationResult {
val newStack = image.stack.toMutableList()
newStack.add(DoubleIota(image.parenCount.toDouble()))
newStack.add(ListIota(image.parenthesized.toList().map { it.iota }))
val image2 = image.copy(
stack = newStack,
parenCount = 0,
parenthesized = listOf()
)
return ParenthesizedOperationResult(image2, listOf(), continuation, HexEvalSounds.NORMAL_EXECUTE, ResolvedPatternType.EVALUATED)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package at.petrak.hexcasting.common.casting.actions.escaping

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.getPositiveInt
import at.petrak.hexcasting.api.casting.mishaps.MishapNotEnoughArgs
import at.petrak.hexcasting.common.lib.hex.HexEvalSounds

object OpOpenNParens : Action {
override fun operate(env: CastingEnvironment, image: CastingImage, continuation: SpellContinuation): OperationResult {
val newStack = image.stack.toMutableList()
val layers = when (val topIota = newStack.removeLastOrNull()) {
null -> throw MishapNotEnoughArgs(1, 0)
else -> listOf(topIota).getPositiveInt(0)
}
Comment thread
Robotgiggle marked this conversation as resolved.
Outdated
val image2 = image.copy(
stack = newStack,
parenCount = layers
)
return OperationResult(image2, listOf(), continuation, HexEvalSounds.NORMAL_EXECUTE)
}

// Since there's no nice way to determine how many new layers it should open when drawn inside parens, we don't
// override operateInParens() at all. This pattern is just treated as any other pattern when parenthesized.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package at.petrak.hexcasting.common.casting.actions.escaping

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.ParenthesizedOperationResult
import at.petrak.hexcasting.api.casting.eval.ResolvedPatternType
import at.petrak.hexcasting.api.casting.eval.vm.CastingImage
import at.petrak.hexcasting.api.casting.eval.vm.SpellContinuation
import at.petrak.hexcasting.api.casting.iota.Iota
import at.petrak.hexcasting.api.casting.mishaps.MishapBadOffhandItem
import at.petrak.hexcasting.api.casting.mishaps.MishapNeedsParens
import at.petrak.hexcasting.common.lib.hex.HexEvalSounds
import at.petrak.hexcasting.xplat.IXplatAbstractions

object OpReadIntoParens : Action {
override fun operate(env: CastingEnvironment, image: CastingImage, continuation: SpellContinuation): OperationResult {
throw MishapNeedsParens()
}

override fun operateInParens(env: CastingEnvironment, image: CastingImage, continuation: SpellContinuation, thisIota: Iota): ParenthesizedOperationResult {
val (handStack) = env.getHeldItemToOperateOn {
val dataHolder = IXplatAbstractions.INSTANCE.findDataHolder(it)
dataHolder != null && (dataHolder.readIota(env.world) != null || dataHolder.emptyIota() != null)
}
// If there are no data holders that are readable, find a data holder that isn't readable
// so that the error message is more helpful.
?: env.getHeldItemToOperateOn {
val dataHolder = IXplatAbstractions.INSTANCE.findDataHolder(it)
dataHolder != null
} ?: throw MishapBadOffhandItem.of(null, "iota.read")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this indentation is pretty weird

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is straight out of OpRead. That said, it is indeed quite weird, so I'll change it.


val datumHolder = IXplatAbstractions.INSTANCE.findDataHolder(handStack)
?: throw MishapBadOffhandItem.of(handStack, "iota.read")

val datum = datumHolder.readIota(env.world)
?: datumHolder.emptyIota()
?: throw MishapBadOffhandItem.of(handStack, "iota.read")

return ParenthesizedOperationResult(
image.withNewParenthesized(datum),
listOf(), continuation,
HexEvalSounds.NORMAL_EXECUTE,
ResolvedPatternType.EVALUATED
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ object OpUndo : Action {
var newParenCount = image.parenCount
if (last == null) {
// if there was nothing in the parenthesized list, undo the initial open paren

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you update the comment to mention why we specifically need to set it to 0, not decrement it? Also, do we need more special handing for undoing Mediation/Recollection/Interjection? I almost wonder if we should update the new escaped-action API to add an undo handler method on actions so they don't need to be hardcoded here (and so addons can handle it too).

@Robotgiggle Robotgiggle Jun 12, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No additional handling is necessary as far as I'm aware:

  • Opening with Meditation is handled by setting layers to 0 if the list was empty, and nested Meditation does nothing so it doesn't need special handling
  • Recollection can't even be in an in-progress list (and thus be undo-able) unless it was considered, so no special handling needed there either
  • Undoing an Interjection just removes whatever iota it inserted, and there's a line in GuiSpellcasting that allows the Interjection pattern to turn red like any other undone pattern

@Robotgiggle Robotgiggle Jun 12, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, there was a bit of a weird interaction if you Interjected an open or close paren and then undid it, but I was able to fix that by just making Interjection set the escaped boolean on the ParenthesizedIota the same way Consideration does.

newParenCount--
newParenCount = 0
} else if (last.iota is PatternIota && !last.escaped) {
// adjust paren count if undoing a non-escaped open or close paren
when (last.iota.pattern.angles) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,12 @@ public class HexActions {
new ActionRegistryEntry(HexPattern.fromAngles("qqq", HexDir.WEST), OpOpenParen.INSTANCE));
public static final ActionRegistryEntry CLOSE_PAREN = make("close_paren",
new ActionRegistryEntry(HexPattern.fromAngles("eee", HexDir.EAST), OpCloseParen.INSTANCE));
public static final ActionRegistryEntry OPEN_N_PARENS = make("open_n_parens",
new ActionRegistryEntry(HexPattern.fromAngles("eqqqe", HexDir.SOUTH_WEST), OpOpenNParens.INSTANCE));
public static final ActionRegistryEntry CLOSE_ALL_PARENS = make("close_all_parens",
new ActionRegistryEntry(HexPattern.fromAngles("qeeeq", HexDir.SOUTH_EAST), OpCloseAllParens.INSTANCE));
public static final ActionRegistryEntry READ_INTO_PARENS = make("read_into_parens",
new ActionRegistryEntry(HexPattern.fromAngles("aqqqqqwded", HexDir.EAST), OpReadIntoParens.INSTANCE));
public static final ActionRegistryEntry UNDO = make("undo",
new ActionRegistryEntry(HexPattern.fromAngles("eeedw", HexDir.EAST), OpUndo.INSTANCE));

Expand Down
Loading