Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ jobs:
fabric-api: 0.85.0
# NOTE: these must be quoted and formatted exactly like this, since they'll be used as a bash array
dependencies: >-
'https://cdn.modrinth.com/data/Ha28R6CL/versions/ADg3gvlr/fabric-language-kotlin-1.9.5%2Bkotlin.1.8.22.jar'
'https://cdn.modrinth.com/data/Ha28R6CL/versions/vlhvI5Li/fabric-language-kotlin-1.10.18%2Bkotlin.1.9.22.jar'
'https://cdn.modrinth.com/data/K01OU20C/versions/qW85eawp/cardinal-components-api-5.2.2.jar'
'https://cdn.modrinth.com/data/nU0bVIaL/versions/PKvFvHeb/Patchouli-1.20.1-80-FABRIC.jar'
'https://cdn.modrinth.com/data/9s6osm5g/versions/s7VTKfLA/cloth-config-11.1.106-fabric.jar'
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [UNRELEASED]

### Added

- Added support for debugging cyclic wisps with [HexDebug](https://modrinth.com/mod/hexdebug)!

### Changed

- Increased minimum dependency versions:
- Fabric Loader: `0.16`
- Fabric Language Kotlin: `1.10.18+kotlin.1.9.22`

## `0.3.1` - 2025-10-30

### Changed
Expand Down
2 changes: 2 additions & 0 deletions Common/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ dependencies {
compileOnly "ram.talia.moreiotas:moreiotas-common-$minecraftVersion:$moreIotasVersion"
compileOnly "vazkii.patchouli:Patchouli-xplat:$minecraftVersion-$patchouliVersion-SNAPSHOT"
compileOnly "software.bernie.geckolib:geckolib-forge-$minecraftVersion:$geckolibVersion"
compileOnly "gay.object.hexdebug:hexdebug-core-common-mojmap:$hexdebugVersion+$minecraftVersion"
compileOnly "gay.object.hexdebug:hexdebug-common-mojmap:$hexdebugVersion+$minecraftVersion"

testImplementation "at.petra-k.paucal:paucal-common-$minecraftVersion:$paucalVersion"
testImplementation "at.petra-k.hexcasting:hexcasting-common-$minecraftVersion:$hexcastingVersion"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import at.petrak.hexcasting.api.casting.iota.Iota
import at.petrak.hexcasting.api.casting.iota.IotaType.isTooLargeToSerialize
import at.petrak.hexcasting.api.utils.asCompound
import at.petrak.hexcasting.api.utils.putCompound
import gay.`object`.hexdebug.core.api.HexDebugCoreAPI
import gay.`object`.hexdebug.core.api.exceptions.DebugException
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.ListTag
import net.minecraft.server.MinecraftServer
Expand All @@ -16,6 +18,7 @@ import ram.talia.hexal.api.HexalAPI
import ram.talia.hexal.api.casting.eval.env.WispCastEnv
import ram.talia.hexal.api.nbt.SerialisedIotaList
import ram.talia.hexal.common.entities.BaseCastingWisp
import ram.talia.hexal.common.entities.TickingWisp
import java.util.*

class WispCastingManager(private val casterUUID: UUID, private var cachedServer: MinecraftServer?) {
Expand Down Expand Up @@ -130,22 +133,28 @@ class WispCastingManager(private val casterUUID: UUID, private var cachedServer:
userData = userData
)

val harness = CastingVM(image, ctx)
val hex = cast.hex.getIotas(ctx.world)

val info = harness.queueExecuteAndWrapIotas(cast.hex.getIotas(ctx.world), wisp.level() as ServerLevel)
// if we're debugging this wisp, delegate to the debugger
if (wisp is TickingWisp && wisp.isDebugging) {
wisp.getDebugEnv()?.let { debugEnv ->
debugEnv.isPaused = true
try {
HexDebugCoreAPI.INSTANCE.startDebuggingIotas(debugEnv, ctx, hex, image)
} catch (e: DebugException) {
HexalAPI.LOGGER.warn("Failed to start debugging wisp hex", e)
}
}
return WispCastResult(wisp, succeeded = true, image = image, cancelled = true)
}

// TODO: Make this a mishap
// Clear stack if it gets too large
var endStack = harness.image.stack
if (isTooLargeToSerialize(endStack)) {
endStack = mutableListOf()
}
val harness = CastingVM(image, ctx)

val endRavenmind = harness.image.userData.getCompound(HexAPI.RAVENMIND_USERDATA)
val info = harness.queueExecuteAndWrapIotas(hex, wisp.level() as ServerLevel)

// the wisp will have things it wants to do once the cast is successful, so a callback on it is called to let it know that happened, and what the end state of the
// stack and ravenmind is. This is returned and added to a list that [executeCasts] will loop over to hopefully prevent concurrent modification problems.
return WispCastResult(wisp, info.resolutionType.success, endStack, endRavenmind)
return WispCastResult(wisp, info.resolutionType.success, harness.image)
}

fun readFromNbt(tag: CompoundTag?, level: ServerLevel) {
Expand Down Expand Up @@ -248,6 +257,21 @@ class WispCastingManager(private val casterUUID: UUID, private var cachedServer:
* the result passed back to the Wisp after its cast is successfully executed.
*/
data class WispCastResult(val wisp: BaseCastingWisp, val succeeded: Boolean, val endStack: List<Iota>, val endRavenmind: CompoundTag, val cancelled: Boolean = false) {
constructor(
wisp: BaseCastingWisp,
succeeded: Boolean,
image: CastingImage,
cancelled: Boolean = false,
) : this(
wisp = wisp,
succeeded = succeeded,
// TODO: Make this a mishap
// Clear stack if it gets too large
endStack = if (isTooLargeToSerialize(image.stack)) mutableListOf() else image.stack,
endRavenmind = image.userData.getCompound(HexAPI.RAVENMIND_USERDATA),
cancelled = cancelled,
)

fun callback() { wisp.castCallback(this) }
}

Expand All @@ -266,4 +290,4 @@ class WispCastingManager(private val casterUUID: UUID, private var cachedServer:
specialHandlers.add { _, cast -> cast.wisp?.seon == true }
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,26 @@
package ram.talia.hexal.common.casting.actions.spells.wisp

import at.petrak.hexcasting.api.HexAPI
import at.petrak.hexcasting.api.misc.MediaConstants
import at.petrak.hexcasting.api.casting.*
import at.petrak.hexcasting.api.casting.castables.SpellAction
import at.petrak.hexcasting.api.casting.eval.CastingEnvironment
import at.petrak.hexcasting.api.casting.iota.Iota
import at.petrak.hexcasting.api.casting.iota.IotaType
import at.petrak.hexcasting.api.casting.iota.NullIota
import at.petrak.hexcasting.api.misc.MediaConstants
import gay.`object`.hexdebug.core.api.HexDebugCoreAPI
import gay.`object`.hexdebug.core.api.exceptions.DebugException
import net.minecraft.nbt.CompoundTag
import net.minecraft.world.entity.player.Player
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.phys.Vec3
import ram.talia.hexal.api.HexalAPI
import ram.talia.hexal.api.addBounded
import ram.talia.hexal.api.casting.eval.env.WispCastEnv
import ram.talia.hexal.api.config.HexalConfig
import ram.talia.hexal.api.casting.mishaps.MishapExcessiveReproduction
import ram.talia.hexal.api.config.HexalConfig
import ram.talia.hexal.common.entities.ProjectileWisp
import ram.talia.hexal.common.entities.TickingWisp
import ram.talia.hexal.interop.hexdebug.WispDebugEnv
import kotlin.math.max

class OpSummonWisp(val ticking: Boolean) : SpellAction {
Expand Down Expand Up @@ -72,15 +76,30 @@ class OpSummonWisp(val ticking: Boolean) : SpellAction {
if (env is WispCastEnv)
env.wisp.summonedChildThisCast = true

val player = env.castingEntity as? ServerPlayer

val pigment = env.pigment
val wisp = when (ticking) {
true -> TickingWisp(env.world, pos, env.castingEntity as? Player, media)
false -> ProjectileWisp(env.world, pos, vel, env.castingEntity as? Player, media)
true -> TickingWisp(env.world, pos, player, media)
false -> ProjectileWisp(env.world, pos, vel, player, media)
}
wisp.setPigment(pigment)
wisp.setHex(hex.toMutableList())
wisp.setRavenmind(ravenmind)
env.world.addFreshEntity(wisp)

// if the current cast is being debugged, try to spawn the wisp in debug mode too
if (HexDebugCoreAPI.INSTANCE.getDebugEnv(env) != null && player != null && wisp is TickingWisp) {
val debugEnv = WispDebugEnv(player, wisp.uuid, ravenmind)
try {
HexDebugCoreAPI.INSTANCE.createDebugThread(debugEnv, null)
} catch (e: DebugException) {
// if there are no threads available, just spawn the wisp in normal mode
HexalAPI.LOGGER.debug("Not starting wisp in debug mode", e)
return
}
wisp.setDebugEnv(debugEnv)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ abstract class BaseCastingWisp(entityType: EntityType<out BaseCastingWisp>, worl
/**
* Returns true if there are no triggers limiting when the wisp can cast, false otherwise
*/
fun canScheduleCast(): Boolean {
open fun canScheduleCast(): Boolean {
// HexalAPI.LOGGER.info("active trigger is $activeTrigger, shouldRemove: ${activeTrigger?.shouldRemoveTrigger(this)}, shouldTrigger: ${activeTrigger?.shouldTrigger(this)}")
if (activeTrigger?.shouldRemoveTrigger(this) == true)
activeTrigger = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import at.petrak.hexcasting.api.casting.iota.EntityIota
import at.petrak.hexcasting.api.casting.iota.Iota
import at.petrak.hexcasting.api.utils.hasByte
import at.petrak.hexcasting.api.utils.hasFloat
import gay.`object`.hexdebug.core.api.HexDebugCoreAPI
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.ListTag
import net.minecraft.network.chat.Component
import net.minecraft.network.syncher.EntityDataAccessor
import net.minecraft.network.syncher.EntityDataSerializers
import net.minecraft.network.syncher.SynchedEntityData
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.EntityType
import net.minecraft.world.entity.player.Player
import net.minecraft.world.level.Level
Expand All @@ -23,7 +25,9 @@ import ram.talia.hexal.api.nbt.SerialisedIotaList
import ram.talia.hexal.api.plus
import ram.talia.hexal.api.times
import ram.talia.hexal.common.lib.HexalEntities
import ram.talia.hexal.interop.hexdebug.WispDebugEnv
import java.lang.Double.min
import java.util.UUID

class TickingWisp : BaseCastingWisp {
override val shouldComplainNotEnoughMedia = false
Expand Down Expand Up @@ -73,6 +77,24 @@ class TickingWisp : BaseCastingWisp {
val maximumMoveMultiplier: Float
get() = entityData.get(MAXIMUM_MOVE_MULTIPLIER)

private var debugSessionId: UUID? = null

val isDebugging: Boolean
get() = debugSessionId != null

private val isDebuggingAndPaused: Boolean
get() = isDebugging && getDebugEnv()?.isPaused != false

fun getDebugEnv(): WispDebugEnv? {
val debugSessionId = debugSessionId ?: return null
val caster = caster as? ServerPlayer ?: return null
return HexDebugCoreAPI.INSTANCE.getDebugEnv(caster, debugSessionId) as? WispDebugEnv
}

fun setDebugEnv(debugEnv: WispDebugEnv) {
debugSessionId = debugEnv.sessionId
}

constructor(entityType: EntityType<out BaseCastingWisp>, world: Level) : super(entityType, world)
constructor(
entityType: EntityType<out TickingWisp>,
Expand Down Expand Up @@ -121,6 +143,12 @@ class TickingWisp : BaseCastingWisp {
}
}

// if the debug session has ended, destroy the wisp
// TODO: this can result in a zombie session when the player dies (https://github.com/object-Object/HexDebug/issues/64)
if (isDebugging && caster != null && getDebugEnv() == null) {
discard()
}

super.tick()
}

Expand All @@ -143,7 +171,7 @@ class TickingWisp : BaseCastingWisp {
}

override fun move() {
if (reachedTargetPos()) // also checks if within close enough distance of target.
if (reachedTargetPos() || isDebuggingAndPaused) // also checks if within close enough distance of target.
return

val currentTarget = getTargetMovePosRaw()
Expand All @@ -163,10 +191,16 @@ class TickingWisp : BaseCastingWisp {
// Seon wisps have the same max range as the caster.
override fun maxSqrCastingDistance() = if (seon) { PlayerBasedCastEnv.AMBIT_RADIUS * PlayerBasedCastEnv.AMBIT_RADIUS } else { CASTING_RADIUS * CASTING_RADIUS }

override fun canScheduleCast(): Boolean {
return super.canScheduleCast() && !isDebuggingAndPaused
}

override fun castCallback(result: WispCastingManager.WispCastResult) {
// HexalAPI.LOGGER.info("ticking wisp $uuid had a cast successfully completed!")
setStack(result.endStack)
setRavenmind(result.endRavenmind)
if (!result.cancelled) {
setStack(result.endStack)
setRavenmind(result.endRavenmind)
}

super.castCallback(result)
}
Expand All @@ -184,25 +218,40 @@ class TickingWisp : BaseCastingWisp {
entityData.set(TARGET_MOVE_POS_Z, pos.z.toFloat())
}

fun clearTargetMovePos() {
entityData.set(HAS_TARGET_MOVE_POS, false)
}

fun reachedTargetPos(): Boolean {
return if (!entityData.get(HAS_TARGET_MOVE_POS)) {
true
} else if ((getTargetMovePosRaw() - position()).lengthSqr() < 0.01) {
setPos(getTargetMovePosRaw())
entityData.set(HAS_TARGET_MOVE_POS, false)
clearTargetMovePos()
true
} else {
false
}
}

override fun remove(reason: RemovalReason) {
if (reason.shouldDestroy()) {
getDebugEnv()?.let { HexDebugCoreAPI.INSTANCE.removeDebugThread(it) }
}
super.remove(reason)
}

override fun readAdditionalSaveData(compound: CompoundTag) {
super.readAdditionalSaveData(compound)

when (val stackTag = compound.get(TAG_STACK)) {
null -> serStack.set(mutableListOf())
else -> serStack.set(stackTag as ListTag)
}
debugSessionId = when (compound.hasUUID(TAG_DEBUG_SESSION_ID)) {
true -> compound.getUUID(TAG_DEBUG_SESSION_ID)
false -> null
}
entityData.set(HAS_TARGET_MOVE_POS, when(compound.hasByte(TAG_HAS_TARGET_MOVE_POS)) {
true -> compound.getBoolean(TAG_HAS_TARGET_MOVE_POS)
false -> false
Expand Down Expand Up @@ -233,6 +282,7 @@ class TickingWisp : BaseCastingWisp {
super.addAdditionalSaveData(compound)

compound.put(TAG_STACK, serStack.getTag())
debugSessionId?.let { compound.putUUID(TAG_DEBUG_SESSION_ID, it) }
compound.putBoolean(TAG_HAS_TARGET_MOVE_POS, entityData.get(HAS_TARGET_MOVE_POS))
compound.putFloat(TAG_TARGET_MOVE_POS_X, entityData.get(TARGET_MOVE_POS_X))
compound.putFloat(TAG_TARGET_MOVE_POS_Y, entityData.get(TARGET_MOVE_POS_Y))
Expand Down Expand Up @@ -261,6 +311,7 @@ class TickingWisp : BaseCastingWisp {
val MAXIMUM_MOVE_MULTIPLIER: EntityDataAccessor<Float> = SynchedEntityData.defineId(TickingWisp::class.java, EntityDataSerializers.FLOAT)

const val TAG_STACK = "stack"
const val TAG_DEBUG_SESSION_ID = "debug_session_id"
const val TAG_HAS_TARGET_MOVE_POS = "has_target_move_pos"
const val TAG_TARGET_MOVE_POS_X = "target_move_pos_x"
const val TAG_TARGET_MOVE_POS_Y = "target_move_pos_y"
Expand All @@ -274,4 +325,4 @@ class TickingWisp : BaseCastingWisp {
const val BASE_MAX_SPEED_PER_TICK = 6.0 / 20
const val SCALE = 0.2
}
}
}
Loading
Loading