diff --git a/Common/build.gradle b/Common/build.gradle index 9ca261bd..e8c983f2 100644 --- a/Common/build.gradle +++ b/Common/build.gradle @@ -23,6 +23,9 @@ dependencies { modCompileOnly libs.paucal.common modCompileOnly libs.hexcasting.fabric modCompileOnly libs.patchouli.xplat + + modApi libs.lsp4j + modApi libs.lsp4j.debug } publishing { diff --git a/Common/src/main/java/ca/objectobject/hexdebug/HexDebug.java b/Common/src/main/java/ca/objectobject/hexdebug/HexDebug.java deleted file mode 100644 index 93462817..00000000 --- a/Common/src/main/java/ca/objectobject/hexdebug/HexDebug.java +++ /dev/null @@ -1,30 +0,0 @@ -package ca.objectobject.hexdebug; - -import net.minecraft.resources.ResourceLocation; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -/** - * This is effectively the loading entrypoint for most of your code, at least - * if you are using Architectury as intended. - */ -public class HexDebug { - public static final String MOD_ID = "hexdebug"; - public static final Logger LOGGER = LogManager.getLogger(MOD_ID); - - - public static void init() { - LOGGER.info("HexDebug says hello!"); - - HexDebugAbstractions.initPlatformSpecific(); - - LOGGER.info(HexDebugAbstractions.getConfigDirectory().toAbsolutePath().normalize().toString()); - } - - /** - * Shortcut for identifiers specific to this mod. - */ - public static ResourceLocation id(String string) { - return new ResourceLocation(MOD_ID, string); - } -} diff --git a/Common/src/main/java/ca/objectobject/hexdebug/HexDebugAbstractions.java b/Common/src/main/java/ca/objectobject/hexdebug/HexDebugAbstractions.java index 6e4495b3..e8afdc90 100644 --- a/Common/src/main/java/ca/objectobject/hexdebug/HexDebugAbstractions.java +++ b/Common/src/main/java/ca/objectobject/hexdebug/HexDebugAbstractions.java @@ -3,8 +3,6 @@ import dev.architectury.injectables.annotations.ExpectPlatform; import dev.architectury.platform.Platform; -import java.nio.file.Path; - public class HexDebugAbstractions { /** * This explanation is mostly from Architectury's template project. @@ -18,22 +16,17 @@ public class HexDebugAbstractions { *

* Example: *

- * Expect: ca.objectobject.hexdebug.HexDebugAbstractions#getConfigDirectory() + * Expect: ca.objectobject.hexdebug.HexDebugAbstractions#get() *

- * Actual Fabric: ca.objectobject.hexdebug.fabric.HexDebugAbstractionsImpl#getConfigDirectory() + * Actual Fabric: ca.objectobject.hexdebug.fabric.HexDebugAbstractionsImpl#get() *

- * Actual Forge: ca.objectobject.hexdebug.forge.HexDebugAbstractionsImpl#getConfigDirectory() + * Actual Forge: ca.objectobject.hexdebug.forge.HexDebugAbstractionsImpl#get() *

* You should also get the IntelliJ plugin to help with @ExpectPlatform. */ @ExpectPlatform - public static Path getConfigDirectory() { + public static IHexDebugAbstractions get() { // Just throw an error, the content should get replaced at runtime. throw new AssertionError(); } - - @ExpectPlatform - public static void initPlatformSpecific() { - throw new AssertionError(); - } } diff --git a/Common/src/main/java/ca/objectobject/hexdebug/IHexDebugAbstractions.java b/Common/src/main/java/ca/objectobject/hexdebug/IHexDebugAbstractions.java new file mode 100644 index 00000000..ce1187ef --- /dev/null +++ b/Common/src/main/java/ca/objectobject/hexdebug/IHexDebugAbstractions.java @@ -0,0 +1,16 @@ +package ca.objectobject.hexdebug; + +import net.minecraft.server.MinecraftServer; + +import java.nio.file.Path; +import java.util.function.Consumer; + +public interface IHexDebugAbstractions { + Path getConfigDirectory(); + + void initPlatformSpecific(); + + void onServerStarted(Consumer callback); + + void onServerStopping(Consumer callback); +} diff --git a/Common/src/main/java/ca/objectobject/hexdebug/registry/HexDebugItemRegistry.java b/Common/src/main/java/ca/objectobject/hexdebug/registry/HexDebugItemRegistry.java new file mode 100644 index 00000000..b8a11d17 --- /dev/null +++ b/Common/src/main/java/ca/objectobject/hexdebug/registry/HexDebugItemRegistry.java @@ -0,0 +1,22 @@ +package ca.objectobject.hexdebug.registry; + +import ca.objectobject.hexdebug.HexDebug; +import ca.objectobject.hexdebug.common.items.ItemDebugger; +import dev.architectury.registry.registries.DeferredRegister; +import dev.architectury.registry.registries.RegistrySupplier; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.item.Item; + +public class HexDebugItemRegistry { + // Register items through this + public static final DeferredRegister ITEMS = DeferredRegister.create(HexDebug.MODID, Registries.ITEM); + + public static void init() { + ITEMS.register(); + } + + // During the loading phase, refrain from accessing suppliers' items (e.g. EXAMPLE_ITEM.get()), they will not be available + public static final RegistrySupplier DUMMY_ITEM = ITEMS.register("debugger", () -> new ItemDebugger(new ItemDebugger.Properties())); + + +} \ No newline at end of file diff --git a/Common/src/main/kotlin/ca/objectobject/hexdebug/HexDebug.kt b/Common/src/main/kotlin/ca/objectobject/hexdebug/HexDebug.kt new file mode 100644 index 00000000..874a46e1 --- /dev/null +++ b/Common/src/main/kotlin/ca/objectobject/hexdebug/HexDebug.kt @@ -0,0 +1,32 @@ +package ca.objectobject.hexdebug + +import ca.objectobject.hexdebug.registry.HexDebugItemRegistry +import ca.objectobject.hexdebug.server.HexDebugServerManager +import net.minecraft.resources.ResourceLocation +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.Logger + +object HexDebug { + const val MODID = "hexdebug" + + @JvmField + val LOGGER: Logger = LogManager.getLogger(MODID) + + @JvmStatic + fun init() { + LOGGER.info("HexDebug is here!") + HexDebugItemRegistry.init() + HexDebugAbstractions.get().apply { + initPlatformSpecific() + onServerStarted { + HexDebugServerManager.start() + } + onServerStopping { + HexDebugServerManager.stop() + } + } + } + + @JvmStatic + fun id(path: String) = ResourceLocation(MODID, path) +} diff --git a/Common/src/main/kotlin/ca/objectobject/hexdebug/common/items/ItemDebugger.kt b/Common/src/main/kotlin/ca/objectobject/hexdebug/common/items/ItemDebugger.kt new file mode 100644 index 00000000..4c5e5991 --- /dev/null +++ b/Common/src/main/kotlin/ca/objectobject/hexdebug/common/items/ItemDebugger.kt @@ -0,0 +1,111 @@ +package ca.objectobject.hexdebug.common.items + +import at.petrak.hexcasting.api.casting.ParticleSpray +import at.petrak.hexcasting.api.casting.eval.vm.CastingVM +import at.petrak.hexcasting.api.casting.iota.Iota +import at.petrak.hexcasting.api.casting.iota.ListIota +import at.petrak.hexcasting.api.casting.iota.PatternIota +import at.petrak.hexcasting.api.item.IotaHolderItem +import at.petrak.hexcasting.api.mod.HexConfig +import at.petrak.hexcasting.api.utils.getCompound +import at.petrak.hexcasting.common.items.magic.ItemPackagedHex +import at.petrak.hexcasting.common.msgs.MsgNewSpiralPatternsS2C +import at.petrak.hexcasting.xplat.IXplatAbstractions +import ca.objectobject.hexdebug.debugger.DebugCastArgs +import ca.objectobject.hexdebug.debugger.DebugItemCastEnv +import ca.objectobject.hexdebug.server.HexDebugServerManager +import ca.objectobject.hexdebug.server.HexDebugServerState +import net.minecraft.network.chat.Component +import net.minecraft.server.level.ServerLevel +import net.minecraft.server.level.ServerPlayer +import net.minecraft.stats.Stat +import net.minecraft.stats.Stats +import net.minecraft.world.InteractionHand +import net.minecraft.world.InteractionResultHolder +import net.minecraft.world.entity.player.Player +import net.minecraft.world.item.ItemStack +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 + +class ItemDebugger(properties: Properties) : ItemPackagedHex(properties), IotaHolderItem { + override fun canDrawMediaFromInventory(stack: ItemStack?) = true + + override fun breakAfterDepletion() = false + + override fun cooldown() = HexConfig.common().artifactCooldown() + + override fun readIotaTag(stack: ItemStack?) = stack?.getCompound(TAG_PROGRAM) + + override fun canWrite(stack: ItemStack?, iota: Iota?) = iota is ListIota + + override fun writeDatum(stack: ItemStack?, iota: Iota?) = writeHex(stack, (iota as ListIota).list.toList(), null, 0) + + override fun use(world: Level, player: Player, usedHand: InteractionHand): InteractionResultHolder { + val stack = player.getItemInHand(usedHand) + + if (world.isClientSide) { + return InteractionResultHolder.success(stack) + } + + val serverPlayer = player as ServerPlayer + val serverLevel = world as ServerLevel + + val instrs = if (hasHex(stack)) { + getHex(stack, serverLevel) ?: return InteractionResultHolder.fail(stack) + } else { + val datumHolder = IXplatAbstractions.INSTANCE.findDataHolder(player.getItemInHand(usedHand.otherHand)) + when (val iota = datumHolder?.readIota(serverLevel)) { + is ListIota -> iota.list.toList() + else -> null + } + } ?: return InteractionResultHolder.fail(stack) + + val ctx = DebugItemCastEnv(serverPlayer, usedHand) + val vm = CastingVM.empty(ctx) + val castArgs = DebugCastArgs(vm, instrs, serverLevel) { + if (it is PatternIota) { + val packet = MsgNewSpiralPatternsS2C(serverPlayer.uuid, listOf(it.pattern), 140) + IXplatAbstractions.INSTANCE.sendPacketToPlayer(serverPlayer, packet) + IXplatAbstractions.INSTANCE.sendPacketTracking(serverPlayer, packet) + } + } + + val debugServer = HexDebugServerManager.server + if (debugServer == null) { + HexDebugServerManager.queuedCast = castArgs + player.sendSystemMessage(Component.translatable("text.hexdebug.no_client"), false) + } else when (debugServer.state) { + HexDebugServerState.NOT_READY, HexDebugServerState.READY -> if (!debugServer.startDebugging(castArgs)) { + return InteractionResultHolder.fail(stack) + } + HexDebugServerState.DEBUGGING -> debugServer.next(null) + else -> return InteractionResultHolder.fail(stack) + } + + ParticleSpray(player.position(), Vec3(0.0, 1.5, 0.0), 0.4, Math.PI / 3, 30) + .sprayParticles(serverPlayer.serverLevel(), ctx.pigment) + + val broken = breakAfterDepletion() && getMedia(stack) == 0L + val stat: Stat<*> = if (broken) { + Stats.ITEM_BROKEN[this] + } else { + Stats.ITEM_USED[this] + } + player.awardStat(stat) + + serverPlayer.cooldowns.addCooldown(this, this.cooldown()) + + if (broken) { + stack.shrink(1) + player.broadcastBreakEvent(usedHand) + return InteractionResultHolder.consume(stack) + } else { + return InteractionResultHolder.success(stack) + } + } +} + +val InteractionHand.otherHand get() = when (this) { + InteractionHand.MAIN_HAND -> InteractionHand.OFF_HAND + InteractionHand.OFF_HAND -> InteractionHand.MAIN_HAND +} diff --git a/Common/src/main/kotlin/ca/objectobject/hexdebug/debugger/DebugCastArgs.kt b/Common/src/main/kotlin/ca/objectobject/hexdebug/debugger/DebugCastArgs.kt new file mode 100644 index 00000000..df046775 --- /dev/null +++ b/Common/src/main/kotlin/ca/objectobject/hexdebug/debugger/DebugCastArgs.kt @@ -0,0 +1,12 @@ +package ca.objectobject.hexdebug.debugger + +import at.petrak.hexcasting.api.casting.eval.vm.CastingVM +import at.petrak.hexcasting.api.casting.iota.Iota +import net.minecraft.server.level.ServerLevel + +data class DebugCastArgs( + val vm: CastingVM, + val iotas: List, + val world: ServerLevel, + val onExecute: ((Iota) -> Unit)? = null +) \ No newline at end of file diff --git a/Common/src/main/kotlin/ca/objectobject/hexdebug/debugger/DebugCastEnv.kt b/Common/src/main/kotlin/ca/objectobject/hexdebug/debugger/DebugCastEnv.kt new file mode 100644 index 00000000..c211ff17 --- /dev/null +++ b/Common/src/main/kotlin/ca/objectobject/hexdebug/debugger/DebugCastEnv.kt @@ -0,0 +1,14 @@ +package ca.objectobject.hexdebug.debugger + +import at.petrak.hexcasting.api.casting.eval.env.PackagedItemCastEnv +import ca.objectobject.hexdebug.server.HexDebugServerManager +import net.minecraft.network.chat.Component +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.InteractionHand + +class DebugItemCastEnv(caster: ServerPlayer, castingHand: InteractionHand) : PackagedItemCastEnv(caster, castingHand) { + override fun printMessage(message: Component) { + super.printMessage(message) + HexDebugServerManager.server?.print(message.string + "\n") + } +} diff --git a/Common/src/main/kotlin/ca/objectobject/hexdebug/debugger/HexDebugger.kt b/Common/src/main/kotlin/ca/objectobject/hexdebug/debugger/HexDebugger.kt new file mode 100644 index 00000000..01b7a985 --- /dev/null +++ b/Common/src/main/kotlin/ca/objectobject/hexdebug/debugger/HexDebugger.kt @@ -0,0 +1,334 @@ +package ca.objectobject.hexdebug.debugger + +import at.petrak.hexcasting.api.HexAPI +import at.petrak.hexcasting.api.casting.PatternShapeMatch +import at.petrak.hexcasting.api.casting.SpellList +import at.petrak.hexcasting.api.casting.eval.SpecialPatterns +import at.petrak.hexcasting.api.casting.eval.sideeffects.OperatorSideEffect +import at.petrak.hexcasting.api.casting.eval.vm.* +import at.petrak.hexcasting.api.casting.iota.* +import at.petrak.hexcasting.api.casting.mishaps.Mishap +import at.petrak.hexcasting.api.casting.mishaps.MishapInternalException +import at.petrak.hexcasting.common.casting.PatternRegistryManifest +import ca.objectobject.hexdebug.server.LaunchArgs +import org.eclipse.lsp4j.debug.* + +class HexDebugger( + private val initArgs: InitializeRequestArguments, + private val launchArgs: LaunchArgs, + cast: DebugCastArgs, +) { + private val vm = cast.vm + private val world = cast.world + private val onExecute = cast.onExecute + + var continuation = SpellContinuation.Done.pushFrame( + FrameEvaluate(SpellList.LList(0, cast.iotas), false) + ) + + var continuations: List = getContinuations(continuation) + + // current continuation is last + private fun getContinuations(current: SpellContinuation) = + generateSequence(current as? SpellContinuation.NotDone) { + when (val next = it.next) { + is SpellContinuation.Done -> null + is SpellContinuation.NotDone -> next + } + }.toList().asReversed() + + var currentLineNumber = 0 + + private val breakpoints = mutableMapOf>() // source id -> line number + private val allocatedVariables = mutableListOf>() + private var sources: List? = null + + fun getStackFrames(): Sequence = continuations.mapIndexed { i, it -> + StackFrame().apply { + id = i + 1 + name = "Frame $id (${it.frame.name})" + source = getSource(id, it.frame) + if (id == continuations.count()) { + line = serverToClientLineNumber(currentLineNumber) + } + } + }.asReversed().asSequence() + + fun getScopes(frameId: Int): List { + val scopes = mutableListOf( + Scope().apply { + name = "State" + variablesReference = vm.image.run { + val variables = mutableListOf( + toVariable("Stack", stack.asReversed()), + toVariable("Ravenmind", getRavenmind()), + toVariable("OpsConsumed", opsConsumed.toString()), + toVariable("EscapeNext", escapeNext.toString()), + ) + if (parenCount > 0) { + variables += toVariable("Intro/Retro", parenthesized.map { it.iota }) + } + allocateVariables(variables.asSequence()) + } + } + ) + + val frame = getContinuationFrame(frameId) + when (frame) { + is FrameEvaluate -> sequenceOf( + toVariable("Code", frame.list), + toVariable("IsMetacasting", frame.isMetacasting.toString()), + ) + + is FrameForEach -> { + sequenceOf( + toVariable("Code", frame.code), + toVariable("Data", frame.data), + frame.baseStack?.let { toVariable("BaseStack", it) }, + toVariable("Result", frame.acc), + ).filterNotNull() + } + + else -> null + }?.also { + scopes += Scope().apply { + name = frame.name + variablesReference = allocateVariables(it) + } + } + + return scopes + } + + fun getVariables(variablesReference: Int): Sequence { + return allocatedVariables.getOrElse(variablesReference - 1) { sequenceOf() } + } + + private fun getRavenmind() = if (vm.image.userData.contains(HexAPI.RAVENMIND_USERDATA)) { + IotaType.deserialize(vm.image.userData.getCompound(HexAPI.RAVENMIND_USERDATA), vm.env.world) + } else { + NullIota() + } + + private fun toVariables(iotas: Iterable) = toVariables(iotas.asSequence()) + + private fun toVariables(iotas: Sequence) = iotas.mapIndexed(::toVariable) + + private fun toVariable(index: Number, iota: Iota) = toVariable("$index", iota) + + private fun toVariable(name: String, iota: Iota): Variable = Variable().apply { + this.name = name + type = iota::class.simpleName + value = when (iota) { + is ListIota -> { + variablesReference = allocateVariables(toVariables(iota.list)) + indexedVariables = iota.list.size() + "(${iota.list.count()}) ${iotaToString(iota, false)}" + } + else -> iotaToString(iota, false) + } + } + + private fun toVariable(name: String, iotas: Iterable): Variable = Variable().apply { + this.name = name + value = "" + variablesReference = allocateVariables(iotas) + } + + private fun toVariable(name: String, value: String): Variable = Variable().also { + it.name = name + it.value = value + } + + private fun allocateVariables(iotas: Iterable) = allocateVariables(toVariables(iotas)) + + private fun allocateVariables(vararg variables: Variable) = allocateVariables(sequenceOf(*variables)) + + private fun allocateVariables(values: Sequence): Int { + allocatedVariables.add(values) + return allocatedVariables.lastIndex + 1 + } + + fun getSources() = sources ?: continuations.mapIndexedNotNull { i, it -> + getSource(i + 1, it.frame) + }.also { + this.sources = it + } + + fun getSource(frameId: Int, frame: ContinuationFrame) = if (hasSource(frame)) { + Source().apply { + name = "frame${frameId}_${frame.name}.hexpattern" + sourceReference = frameId + } + } else { + null + } + + fun hasSource(frameId: Int) = hasSource(getContinuationFrame(frameId)) + + fun hasSource(frame: ContinuationFrame) = when (frame) { + is FrameEvaluate, is FrameForEach -> true + else -> false + } + + fun getSourceContents(sourceReference: Int) = when (val frame = getContinuationFrame(sourceReference)) { + is FrameEvaluate -> getSourceContents(frame.list) + is FrameForEach -> getSourceContents(frame.code) + is FrameFinishEval -> null + else -> null + } + + private fun getSourceContents(list: SpellList) = getSourceContents(list.toList()) + + private fun getSourceContents(iotas: List): String { + return iotas.joinToString("\n") { iotaToString(it, true) } + } + + private fun getContinuationFrame(frameId: Int) = continuations.elementAt(frameId - 1).frame + + fun setBreakpoints(sourceReference: Int?, sourceBreakpoints: Array) = sourceBreakpoints.map { + Breakpoint().apply { + isVerified = true + line = it.line + column = it.column + } + }.also { + breakpoints.clear() + breakpoints[sourceReference ?: continuations.count()] = it.map { + breakpoint -> clientToServerLineNumber(breakpoint.line) + }.toSet() + } + + private fun clientToServerLineNumber(line: Int) = if (initArgs.linesStartAt1) { + line - 1 + } else { + line + } + + private fun serverToClientLineNumber(line: Int) = if (initArgs.linesStartAt1) { + line + 1 + } else { + line + } + + fun executeUntilStopped(stopType: StopType? = null): String? { + val callStackSize = continuations.count() + val lineNumber = currentLineNumber + while (executeOnce() != null) { + if (isAtBreakpoint) return "breakpoint" + val newCallStackSize = continuations.count() + when (stopType) { + StopType.STEP_OVER -> if (newCallStackSize == callStackSize) { + currentLineNumber = lineNumber + 1 + return "step" + } else if (newCallStackSize < callStackSize) return "step" + StopType.STEP_OUT -> if (newCallStackSize < callStackSize) return "step" + else -> {} + } + } + return null + } + + val isAtBreakpoint get() = breakpoints[continuations.count()]?.contains(currentLineNumber) == true + + // Copy of CastingVM.queueExecuteAndWrapIotas to allow stepping by one pattern at a time. + fun executeOnce(): String? { + allocatedVariables.clear() + currentLineNumber += 1 + + val callStackSize = continuations.count() + + val info = CastingVM.TempControllerInfo(earlyExit = false) + var currentContinuation = continuation + while (currentContinuation is SpellContinuation.NotDone) { + // Take the top of the continuation stack... + val next = currentContinuation.frame + // ...and execute it. + // TODO there used to be error checking code here; I'm pretty sure any and all mishaps should already + // get caught and folded into CastResult by evaluate. + val image2 = next.evaluate(currentContinuation.next, world, vm) + // Then write all pertinent data back to the harness for the next iteration. + if (image2.newData != null) { + vm.image = image2.newData!! + } + vm.env.postExecution(image2) + + currentContinuation = image2.continuation + val notDoneContinuation = currentContinuation as? SpellContinuation.NotDone + + try { + vm.performSideEffects(info, image2.sideEffects) + } catch (e: Exception) { + e.printStackTrace() + vm.performSideEffects( + info, + listOf(OperatorSideEffect.DoMishap(MishapInternalException(e), Mishap.Context(null, null))) + ) + } + + // if we detect a nested evaluation, reset the line number and invalidate all sources + // TODO: ask Alwinfy or someone if there's a better way to do this???? + val evaluatedFrame = next as? FrameEvaluate + val currentFrame = notDoneContinuation?.frame as? FrameEvaluate + if ( + evaluatedFrame != null + && currentFrame != null + && evaluatedFrame.list.cdr.toList() != currentFrame.list.toList() + || currentFrame !is FrameEvaluate + ) { + currentLineNumber = 0 + sources = null + } + + if (info.earlyExit) return null + + if (notDoneContinuation?.frame is FrameEvaluate) { + onExecute?.invoke(image2.cast) + break + } + } + + continuation = currentContinuation + continuations = getContinuations(continuation) + + return if (continuation is SpellContinuation.NotDone) { "step" } else { null } + } + + private fun iotaToString(iota: Iota, isSource: Boolean): String = when (iota) { + // i feel like hex should have a thing for this... + is PatternIota -> HexAPI.instance().run { + when (val lookup = PatternRegistryManifest.matchPattern(iota.pattern, vm.env, false)) { + is PatternShapeMatch.Normal -> getActionI18n(lookup.key, false) + is PatternShapeMatch.PerWorld -> getActionI18n(lookup.key, true) + is PatternShapeMatch.Special -> lookup.handler.name + is PatternShapeMatch.Nothing -> when (iota.pattern) { + SpecialPatterns.INTROSPECTION -> getRawHookI18n(HexAPI.modLoc("open_paren")) + SpecialPatterns.RETROSPECTION -> getRawHookI18n(HexAPI.modLoc("close_paren")) + SpecialPatterns.INTROSPECTION -> getRawHookI18n(HexAPI.modLoc("escape")) + SpecialPatterns.INTROSPECTION -> getRawHookI18n(HexAPI.modLoc("undo")) + else -> iota.display() + } + }.string + } + + else -> { + val result = when (iota) { + is ListIota -> "[" + iota.list.joinToString { iotaToString(it, isSource) } + "]" + is GarbageIota -> "Garbage" + else -> iota.display().string + } + if (isSource) { + "<$result>" + } else { + result + } + } + } +} + +val ContinuationFrame.name get() = this::class.simpleName ?: "Unknown" + +enum class StopType { + STEP_OVER, + STEP_OUT, +} diff --git a/Common/src/main/kotlin/ca/objectobject/hexdebug/server/HexDebugServer.kt b/Common/src/main/kotlin/ca/objectobject/hexdebug/server/HexDebugServer.kt new file mode 100644 index 00000000..03ff03a4 --- /dev/null +++ b/Common/src/main/kotlin/ca/objectobject/hexdebug/server/HexDebugServer.kt @@ -0,0 +1,276 @@ +package ca.objectobject.hexdebug.server + +import ca.objectobject.hexdebug.HexDebug +import ca.objectobject.hexdebug.debugger.DebugCastArgs +import ca.objectobject.hexdebug.debugger.HexDebugger +import ca.objectobject.hexdebug.debugger.StopType +import net.minecraft.network.chat.Component +import net.minecraft.server.level.ServerPlayer +import org.eclipse.lsp4j.debug.* +import org.eclipse.lsp4j.debug.launch.DSPLauncher +import org.eclipse.lsp4j.debug.services.IDebugProtocolClient +import org.eclipse.lsp4j.debug.services.IDebugProtocolServer +import java.io.InputStream +import java.io.OutputStream +import java.net.Socket +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Future + +/* +TODO: +- auto-evaluate all non-eval frames +- line number is super wrong +- program source not refreshing?? + */ + +class HexDebugServer( + input: InputStream, + output: OutputStream, + private var queuedCast: DebugCastArgs? = null, +) : IDebugProtocolServer { + constructor(clientSocket: Socket, queuedCast: DebugCastArgs? = null) : this( + clientSocket.inputStream, + clientSocket.outputStream, + queuedCast, + ) + + private val launcher = DSPLauncher.createServerLauncher(this, input, output) + private var future: Future? = null + + private val remoteProxy: IDebugProtocolClient get() = launcher.remoteProxy + + public var state: HexDebugServerState = HexDebugServerState.NOT_READY + + private lateinit var initArgs: InitializeRequestArguments + private lateinit var launchArgs: LaunchArgs + private lateinit var debugger: HexDebugger + + fun start(): Future { + return launcher.startListening().also { future = it } + } + + fun stop(notifyClient: Boolean = true) { + state = HexDebugServerState.CLOSED + HexDebug.LOGGER.info("Stopping debug server") + + future?.cancel(true) + + if (notifyClient) { + remoteProxy.exited(ExitedEventArguments().also { it.exitCode = 0 }) + remoteProxy.terminated(TerminatedEventArguments()) + } + } + + fun print(value: String, category: String = OutputEventArgumentsCategory.STDOUT) { + remoteProxy.output(OutputEventArguments().also { + it.category = category + it.output = value + }) + } + + // lifecycle requests + + override fun initialize(args: InitializeRequestArguments): CompletableFuture { + logRequest("initialize", args) + + initArgs = args + + return Capabilities().apply { + supportsConfigurationDoneRequest = true + }.toFuture() + } + + override fun attach(args: MutableMap): CompletableFuture { + logRequest("attach", args) + + launchArgs = LaunchArgs(args) + state = HexDebugServerState.READY + + queuedCast?.also(::startDebugging) + queuedCast = null + + return futureOf() + } + + // not a request + fun startDebugging(cast: DebugCastArgs) = when (state) { + HexDebugServerState.NOT_READY -> { + queuedCast = cast + (cast.vm.env.castingEntity as? ServerPlayer)?.sendSystemMessage( + Component.translatable("text.hexdebug.no_client"), false + ) + true + } + HexDebugServerState.READY -> { + state = HexDebugServerState.DEBUGGING + debugger = HexDebugger(initArgs, launchArgs, cast) + remoteProxy.initialized() + (cast.vm.env.castingEntity as? ServerPlayer)?.sendSystemMessage( + Component.translatable("text.hexdebug.connected"), false + ) + true + } + else -> { + HexDebug.LOGGER.info("Debugger is already started, cancelling") + false + } + } + + override fun setBreakpoints(args: SetBreakpointsArguments): CompletableFuture { + logRequest("setBreakpoints", args) + return SetBreakpointsResponse().apply { + breakpoints = debugger.setBreakpoints(args.source.sourceReference, args.breakpoints).toTypedArray() + }.toFuture() + } + + override fun setExceptionBreakpoints(args: SetExceptionBreakpointsArguments): CompletableFuture { + logRequest("setExceptionBreakpoints", args) + + // tell the client we didn't enable any of their breakpoints + val count = args.filters.size + (args.filterOptions?.size ?: 0) + (args.exceptionOptions?.size ?: 0) + val breakpoints = Array(count) { Breakpoint().apply { isVerified = false } } + + return SetExceptionBreakpointsResponse().apply { + this.breakpoints = breakpoints + }.toFuture() + } + + override fun configurationDone(args: ConfigurationDoneArguments?): CompletableFuture { + if (launchArgs.stopOnEntry) { + sendStoppedEvent("entry") + } else if (debugger.isAtBreakpoint) { + sendStoppedEvent("breakpoint") + } else { + handleDebuggerStep(debugger.executeUntilStopped()) + } + return futureOf() + } + + override fun next(args: NextArguments?): CompletableFuture { + logRequest("next", args) + handleDebuggerStep(debugger.executeUntilStopped(StopType.STEP_OVER)) + return futureOf() + } + + override fun continue_(args: ContinueArguments): CompletableFuture { + logRequest("continue", args) + handleDebuggerStep(debugger.executeUntilStopped()) + return futureOf() + } + + override fun stepIn(args: StepInArguments?): CompletableFuture { + logRequest("stepIn", args) + handleDebuggerStep(debugger.executeOnce()) + return futureOf() + } + + override fun stepOut(args: StepOutArguments?): CompletableFuture { + logRequest("stepOut", args) + handleDebuggerStep(debugger.executeUntilStopped(StopType.STEP_OUT)) + return futureOf() + } + + override fun pause(args: PauseArguments): CompletableFuture { + logRequest("pause", args) + return futureOf() + } + + override fun disconnect(args: DisconnectArguments): CompletableFuture { + logRequest("disconnect", args) + stop(notifyClient = false) + return futureOf() + } + + // runtime data + + override fun threads(): CompletableFuture { + // always return the same dummy thread - we don't support multithreading + logRequest("threads") + return ThreadsResponse().apply { + threads = arrayOf(Thread().apply { + id = 0 + name = "Main Thread" + }) + }.toFuture() + } + + override fun scopes(args: ScopesArguments): CompletableFuture { + logRequest("scopes", args) + return ScopesResponse().apply { + scopes = debugger.getScopes(args.frameId).toTypedArray() + }.toFuture() + } + + override fun variables(args: VariablesArguments): CompletableFuture { + logRequest("variables", args) + return VariablesResponse().apply { + variables = debugger.getVariables(args.variablesReference).paginate(args.start, args.count) + }.toFuture() + } + + override fun stackTrace(args: StackTraceArguments): CompletableFuture { + logRequest("stackTrace", args) + return StackTraceResponse().apply { + stackFrames = debugger.getStackFrames().paginate(args.startFrame, args.levels) + }.toFuture() + } + + override fun source(args: SourceArguments): CompletableFuture { + val source = debugger.getSourceContents(args.source.sourceReference) + return if (source != null) { + SourceResponse().apply { + content = debugger.getSourceContents(args.source.sourceReference) + }.toFuture() + } else { + futureOf() + } + } + + // helpers + + private fun handleDebuggerStep(reason: String?) { + if (reason != null) { + sendStoppedEvent(reason) + if (debugger.currentLineNumber == 0) { + // we stepped into a nested eval; refresh all the sources + for (source in debugger.getSources()) { + remoteProxy.loadedSource(LoadedSourceEventArguments().also { + it.reason = LoadedSourceEventArgumentsReason.NEW + it.source = source + }) + } + } + } else { + HexDebug.LOGGER.info("Program exited, stopping debug server") + stop() + } + } + + private fun sendStoppedEvent(reason: String) { + remoteProxy.stopped(StoppedEventArguments().also { + it.threadId = 0 + it.reason = reason + }) + } + + private fun logRequest(name: String, args: Any? = null) { + HexDebug.LOGGER.debug("Got request {} with args {}", name, args) + } +} + +fun T.toFuture(): CompletableFuture = CompletableFuture.completedFuture(this) + +fun futureOf(value: T): CompletableFuture = CompletableFuture.completedFuture(value) + +fun futureOf(): CompletableFuture = CompletableFuture.completedFuture(null) + +inline fun Sequence.paginate(start: Int?, count: Int?): Array { + var result = this + if (start != null && start > 0) { + result = result.drop(start) + } + if (count != null && count > 0) { + result = result.take(count) + } + return result.toList().toTypedArray() +} diff --git a/Common/src/main/kotlin/ca/objectobject/hexdebug/server/HexDebugServerManager.kt b/Common/src/main/kotlin/ca/objectobject/hexdebug/server/HexDebugServerManager.kt new file mode 100644 index 00000000..dcd133e1 --- /dev/null +++ b/Common/src/main/kotlin/ca/objectobject/hexdebug/server/HexDebugServerManager.kt @@ -0,0 +1,57 @@ +package ca.objectobject.hexdebug.server + +import ca.objectobject.hexdebug.HexDebug +import ca.objectobject.hexdebug.debugger.DebugCastArgs +import java.net.ServerSocket +import java.net.SocketException +import java.util.concurrent.CancellationException +import kotlin.concurrent.thread + +object HexDebugServerManager { + var server: HexDebugServer? = null + + var queuedCast: DebugCastArgs? = null + + private var activeThread: Thread? = null + private var stop: Boolean = false + + private val port get() = 4444 // TODO: config + + fun start() { + if (activeThread != null) { + HexDebug.LOGGER.warn("Tried to start server manager while already running") + return + } + + activeThread = thread(name="HexDebugServer_$port") { + val serverSocket = ServerSocket(port) + + while (!stop) { + HexDebug.LOGGER.info("Listening on port ${serverSocket.localPort}...") + val clientSocket = serverSocket.accept() + HexDebug.LOGGER.info("Client connected!") + + try { + server = HexDebugServer(clientSocket, queuedCast) + queuedCast = null + server?.start()?.get() // blocking + clientSocket.close() + } catch (_: SocketException) {} catch (_: CancellationException) {} + finally { + server = null + } + } + + serverSocket.close() + } + } + + fun stop() { + stop = true + HexDebug.LOGGER.info("Stopping server manager") + server?.stop() + activeThread?.join() + server = null + stop = false + } +} \ No newline at end of file diff --git a/Common/src/main/kotlin/ca/objectobject/hexdebug/server/HexDebugServerState.kt b/Common/src/main/kotlin/ca/objectobject/hexdebug/server/HexDebugServerState.kt new file mode 100644 index 00000000..3791e39c --- /dev/null +++ b/Common/src/main/kotlin/ca/objectobject/hexdebug/server/HexDebugServerState.kt @@ -0,0 +1,8 @@ +package ca.objectobject.hexdebug.server + +enum class HexDebugServerState { + NOT_READY, + READY, + DEBUGGING, + CLOSED, +} \ No newline at end of file diff --git a/Common/src/main/kotlin/ca/objectobject/hexdebug/server/LaunchArgs.kt b/Common/src/main/kotlin/ca/objectobject/hexdebug/server/LaunchArgs.kt new file mode 100644 index 00000000..f6c37ef9 --- /dev/null +++ b/Common/src/main/kotlin/ca/objectobject/hexdebug/server/LaunchArgs.kt @@ -0,0 +1,5 @@ +package ca.objectobject.hexdebug.server + +data class LaunchArgs(val data: Map) { + val stopOnEntry = data.getOrDefault("stopOnEntry", false) as Boolean +} diff --git a/Common/src/main/resources/assets/hexdebug/lang/en_us.json b/Common/src/main/resources/assets/hexdebug/lang/en_us.json index a752f9f1..244380bd 100644 --- a/Common/src/main/resources/assets/hexdebug/lang/en_us.json +++ b/Common/src/main/resources/assets/hexdebug/lang/en_us.json @@ -9,5 +9,7 @@ "hexdebug.page.dummy_spells.congrats": "Accepts a player entity, tells them they are doing a good job and makes them look up.", "hexdebug.page.dummy_actions.signum": "Accepts a $(l:casting/101)number/$, returns -1 if it is negative, 0 if 0, 1 if positive.", "text.hexdebug.congrats": "Good job, %1$s!", - "text.hexdebug.congrats.player": "a Player" + "text.hexdebug.congrats.player": "a Player", + "text.hexdebug.no_client": "Waiting for debug client to connect...", + "text.hexdebug.connected": "Debug client connected!" } \ No newline at end of file diff --git a/Fabric/build.gradle b/Fabric/build.gradle index 566b35e9..32aea80f 100644 --- a/Fabric/build.gradle +++ b/Fabric/build.gradle @@ -31,6 +31,9 @@ repositories { dependencies { modCompileOnly libs.findbugs.jsr305 + modImplementation libs.lsp4j + modImplementation libs.lsp4j.debug + // Loaders and base APIs modImplementation libs.fabric.loader modApi libs.fabric.api diff --git a/Fabric/src/main/java/ca/objectobject/hexdebug/fabric/HexDebugAbstractionsImpl.java b/Fabric/src/main/java/ca/objectobject/hexdebug/fabric/HexDebugAbstractionsImpl.java index e4fe9a36..afca1ce7 100644 --- a/Fabric/src/main/java/ca/objectobject/hexdebug/fabric/HexDebugAbstractionsImpl.java +++ b/Fabric/src/main/java/ca/objectobject/hexdebug/fabric/HexDebugAbstractionsImpl.java @@ -1,19 +1,35 @@ package ca.objectobject.hexdebug.fabric; +import ca.objectobject.hexdebug.IHexDebugAbstractions; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.loader.api.FabricLoader; -import ca.objectobject.hexdebug.HexDebugAbstractions; +import net.minecraft.server.MinecraftServer; import java.nio.file.Path; +import java.util.function.Consumer; -public class HexDebugAbstractionsImpl { - /** - * This is the actual implementation of {@link HexDebugAbstractions#getConfigDirectory()}. - */ - public static Path getConfigDirectory() { +public class HexDebugAbstractionsImpl implements IHexDebugAbstractions { + public static IHexDebugAbstractions get() { + return new HexDebugAbstractionsImpl(); + } + + @Override + public Path getConfigDirectory() { return FabricLoader.getInstance().getConfigDir(); } - - public static void initPlatformSpecific() { + + @Override + public void initPlatformSpecific() { HexDebugConfigFabric.init(); } + + @Override + public void onServerStarted(Consumer callback) { + ServerLifecycleEvents.SERVER_STARTED.register(callback::accept); + } + + @Override + public void onServerStopping(Consumer callback) { + ServerLifecycleEvents.SERVER_STOPPING.register(callback::accept); + } } diff --git a/Fabric/src/main/java/ca/objectobject/hexdebug/fabric/HexDebugConfigFabric.java b/Fabric/src/main/java/ca/objectobject/hexdebug/fabric/HexDebugConfigFabric.java index 785a756e..1f91b94a 100644 --- a/Fabric/src/main/java/ca/objectobject/hexdebug/fabric/HexDebugConfigFabric.java +++ b/Fabric/src/main/java/ca/objectobject/hexdebug/fabric/HexDebugConfigFabric.java @@ -13,7 +13,7 @@ import net.fabricmc.api.EnvType; @SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal"}) -@Config(name = HexDebug.MOD_ID) +@Config(name = HexDebug.MODID) public class HexDebugConfigFabric extends PartitioningSerializer.GlobalData { @ConfigEntry.Category("common") @ConfigEntry.Gui.TransitiveObject diff --git a/Fabric/src/main/java/ca/objectobject/hexdebug/fabric/HexDebugFabric.java b/Fabric/src/main/java/ca/objectobject/hexdebug/fabric/HexDebugFabric.java index f58221b0..f02cf4e3 100644 --- a/Fabric/src/main/java/ca/objectobject/hexdebug/fabric/HexDebugFabric.java +++ b/Fabric/src/main/java/ca/objectobject/hexdebug/fabric/HexDebugFabric.java @@ -1,7 +1,7 @@ package ca.objectobject.hexdebug.fabric; -import net.fabricmc.api.ModInitializer; import ca.objectobject.hexdebug.HexDebug; +import net.fabricmc.api.ModInitializer; /** * This is your loading entrypoint on fabric(-likes), in case you need to initialize diff --git a/Forge/build.gradle b/Forge/build.gradle index 2a8f475e..71587444 100644 --- a/Forge/build.gradle +++ b/Forge/build.gradle @@ -33,6 +33,9 @@ configurations { dependencies { modCompileOnly libs.findbugs.jsr305 + modImplementation libs.lsp4j + modImplementation libs.lsp4j.debug + forge libs.forge modApi libs.architectury.forge diff --git a/Forge/src/main/java/ca/objectobject/hexdebug/forge/HexDebugAbstractionsImpl.java b/Forge/src/main/java/ca/objectobject/hexdebug/forge/HexDebugAbstractionsImpl.java index 9db3ec29..da9448ab 100644 --- a/Forge/src/main/java/ca/objectobject/hexdebug/forge/HexDebugAbstractionsImpl.java +++ b/Forge/src/main/java/ca/objectobject/hexdebug/forge/HexDebugAbstractionsImpl.java @@ -1,19 +1,42 @@ package ca.objectobject.hexdebug.forge; -import ca.objectobject.hexdebug.HexDebugAbstractions; +import ca.objectobject.hexdebug.IHexDebugAbstractions; +import net.minecraft.server.MinecraftServer; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.server.ServerStartedEvent; +import net.minecraftforge.event.server.ServerStoppingEvent; import net.minecraftforge.fml.loading.FMLPaths; import java.nio.file.Path; +import java.util.function.Consumer; -public class HexDebugAbstractionsImpl { - /** - * This is the actual implementation of {@link HexDebugAbstractions#getConfigDirectory()}. - */ - public static Path getConfigDirectory() { +public class HexDebugAbstractionsImpl implements IHexDebugAbstractions { + public static IHexDebugAbstractions get() { + return new HexDebugAbstractionsImpl(); + } + + @Override + public Path getConfigDirectory() { return FMLPaths.CONFIGDIR.get(); } - - public static void initPlatformSpecific() { + + @Override + public void initPlatformSpecific() { HexDebugConfigForge.init(); } + + @Override + public void onServerStarted(Consumer callback) { + // TODO: is this how you do this??? + MinecraftForge.EVENT_BUS.addListener((Consumer) event -> { + callback.accept(event.getServer()); + }); + } + + @Override + public void onServerStopping(Consumer callback) { + MinecraftForge.EVENT_BUS.addListener((Consumer) event -> { + callback.accept(event.getServer()); + }); + } } diff --git a/Forge/src/main/java/ca/objectobject/hexdebug/forge/HexDebugForge.java b/Forge/src/main/java/ca/objectobject/hexdebug/forge/HexDebugForge.java index 4597e990..f37ec1fe 100644 --- a/Forge/src/main/java/ca/objectobject/hexdebug/forge/HexDebugForge.java +++ b/Forge/src/main/java/ca/objectobject/hexdebug/forge/HexDebugForge.java @@ -10,12 +10,12 @@ * This is your loading entrypoint on forge, in case you need to initialize * something platform-specific. */ -@Mod(HexDebug.MOD_ID) +@Mod(HexDebug.MODID) public class HexDebugForge { public HexDebugForge() { // Submit our event bus to let architectury register our content on the right time IEventBus bus = FMLJavaModLoadingContext.get().getModEventBus(); - EventBuses.registerModEventBus(HexDebug.MOD_ID, bus); + EventBuses.registerModEventBus(HexDebug.MODID, bus); bus.addListener(HexDebugClientForge::init); HexDebug.init(); } diff --git a/README.md b/README.md index 62d201d0..987119b9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ # HexDebug Hex Casting addon that runs a local debug server using DAP. + +## TODOs + +* Step over/out don't work in certain cases. +* Switching to a different frame and back makes the line number wrong since frames don't keep track of evaluated patterns. +* The server sometimes refuses to die when closing the game??? diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fe0c75e8..8989e375 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -39,6 +39,8 @@ asm = "9.4" dotenv = "2.0.0" # for template.env api keys modPublish = "0.3.0" +lsp4j = "0.22.0" + [bundles] asm = ["asm", "asm-analysis", "asm-commons", "asm-tree", "asm-util"] @@ -50,7 +52,7 @@ kotlin-forge = { module="thedarkcolour:kotlinforforge", version.ref="kotlin-for minecraft = { module="com.mojang:minecraft", version.ref="minecraft" } architectury = { module="dev.architectury:architectury", version.ref="architectury" } -architectury-forge = { module="dev.architectury:architectury-fabric", version.ref="architectury" } +architectury-forge = { module="dev.architectury:architectury-forge", version.ref="architectury" } architectury-fabric = { module="dev.architectury:architectury-fabric", version.ref="architectury" } fabric-loader = { module="net.fabricmc:fabric-loader", version.ref="fabric-loader" } @@ -86,6 +88,9 @@ asm-commons = { module="org.ow2.asm:asm-commons", version.ref="asm" } asm-tree = { module="org.ow2.asm:asm-tree", version.ref="asm" } asm-util = { module="org.ow2.asm:asm-util", version.ref="asm" } +lsp4j = { module="org.eclipse.lsp4j:org.eclipse.lsp4j", version.ref="lsp4j" } +lsp4j-debug = { module="org.eclipse.lsp4j:org.eclipse.lsp4j.debug", version.ref="lsp4j" } + [plugins] kotlin = { id="org.jetbrains.kotlin.jvm", version.ref="kotlin" }