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" }