Skip to content
Open
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: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ dependencies {

testImplementation(libs.junit)
testImplementation(libs.opentest4j)
testImplementation(libs.mockk)

// IntelliJ Platform Gradle Plugin Dependencies Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-dependencies-extension.html
intellijPlatform {
Expand All @@ -75,6 +76,7 @@ dependencies {

// Configure IntelliJ Platform Gradle Plugin - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-extension.html
intellijPlatform {
autoReload = true
pluginConfiguration {
name = providers.gradleProperty("pluginName")
version = providers.gradleProperty("pluginVersion")
Expand Down
3 changes: 1 addition & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ pluginVersion = 1.4.2

# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild = 242
pluginUntilBuild = 253.*

# IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension
platformVersion = 2025.3.1.1
platformVersion = 2026.1

# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
# Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP
Expand Down
8 changes: 5 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
[versions]
# libraries
junit = "4.13.2"
junit = "6.0.3"
opentest4j = "1.3.0"
mockk = "1.14.9"

# plugins
changelog = "2.5.0"
intelliJPlatform = "2.10.5"
intelliJPlatform = "2.11.0"
kotlin = "2.2.21"
kover = "0.9.3"
qodana = "2025.2.2"

[libraries]
junit = { group = "junit", name = "junit", version.ref = "junit" }
junit = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit" }
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk"}
opentest4j = { group = "org.opentest4j", name = "opentest4j", version.ref = "opentest4j" }

[plugins]
Expand Down
Empty file modified gradlew
100644 → 100755
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.hytaledocs.intellij.hotReload

interface FileChangeClassifier {
fun classify(
absolutePath: String,
projectBasePath: String,
isDeleted: Boolean,
): FileChangeType?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.hytaledocs.intellij.hotReload

sealed class FileChangeType {

data class SyncWrite(
val absoluteSourcePath: String,
val absoluteTargetPath: String,
) : FileChangeType()

data class SyncDelete(
val absoluteTargetPath: String,
) : FileChangeType()

data object SourceCodeChanged : FileChangeType()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.hytaledocs.intellij.hotReload

import java.io.File

interface FileSynchronizer {
fun sync(source: File, target: File)
fun delete(target: File)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.hytaledocs.intellij.hotReload

import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.newvfs.BulkFileListener
import com.intellij.openapi.vfs.newvfs.events.VFileDeleteEvent
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
import com.intellij.util.concurrency.AppExecutorUtil
import java.io.File
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference

class HotReloadListener(
private val project: Project,
private val classifier: FileChangeClassifier,
private val synchronizer: FileSynchronizer,
private val recentlySyncedPath: MutableSet<String>,
private val updateJarPlugin: () -> Boolean
) : BulkFileListener {

private val scheduler = AppExecutorUtil.getAppScheduledExecutorService()
private val updateJob = AtomicReference<java.util.concurrent.ScheduledFuture<*>?>(null)

override fun after(events: List<VFileEvent>) {
val basePath = project.basePath ?: return
var sourceCodeChanges = false

for (event in events) {
val path = event.path
val canonicalPath = File(path).canonicalPath

if (recentlySyncedPath.remove(canonicalPath)) continue

val isDelete = event is VFileDeleteEvent

when (val change = classifier.classify(path, basePath, isDelete)) {
is FileChangeType.SyncWrite -> synchronizer.sync(
File(change.absoluteSourcePath),
File(change.absoluteTargetPath)
)

is FileChangeType.SyncDelete -> synchronizer.delete(File(change.absoluteTargetPath))

FileChangeType.SourceCodeChanged -> sourceCodeChanges = true

null -> Unit
}
}

if (sourceCodeChanges) {
scheduleUpdate()
sourceCodeChanges = false
}
}

private fun scheduleUpdate() {
val oldJob = updateJob.getAndSet(
scheduler.schedule({
updateJarPlugin()
updateJob.set(null)
}, 500, TimeUnit.MILLISECONDS)
)
oldJob?.cancel(false)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.hytaledocs.intellij.hotReload

import java.io.File

class HytaleFileChangeClassifier : FileChangeClassifier {
override fun classify(
absolutePath: String,
projectBasePath: String,
isDeleted: Boolean
): FileChangeType? {
val path = absolutePath.replace(File.separatorChar, '/')
val base = projectBasePath.replace(File.separatorChar, '/')

if (!path.startsWith(base)) return null

return when {
path.containsSegment(RESOURCES_SEGMENT) ->
syncOrDelete(
path = path,
isDelete = isDeleted,
targetPath = { relative -> "$base/$SERVER_MODS_SEGMENT/$relative" },
segmentAfter = RESOURCES_SEGMENT,
)

path.containsSegment(SERVER_MODS_SEGMENT) ->
syncOrDelete(
path = path,
isDelete = isDeleted,
targetPath = { relative -> "$base/$RESOURCES_SEGMENT/$relative" },
segmentAfter = SERVER_MODS_SEGMENT,
)


SOURCE_SEGMENTS.any { path.containsSegment(it) } ->
FileChangeType.SourceCodeChanged

else -> null
}

}


private fun syncOrDelete(
path: String,
isDelete: Boolean,
targetPath: (relative: String) -> String,
segmentAfter: String,
): FileChangeType {
val relative = path.substringAfter("$segmentAfter/")
val target = targetPath(relative)
return if (isDelete) {
FileChangeType.SyncDelete(absoluteTargetPath = target)
} else {
FileChangeType.SyncWrite(absoluteSourcePath = path, absoluteTargetPath = target)
}
}

private fun String.containsSegment(segment: String): Boolean =
contains("/$segment/") || contains("/$segment")

private companion object {
const val RESOURCES_SEGMENT = "src/main/resources"
const val SERVER_MODS_SEGMENT =
"server/mods/developmentPlugin" //TODO create better ways for configuration of the mod info (needs to change to a dynamic name) //TODO add auto install of the resources for first run
val SOURCE_SEGMENTS = listOf("src/main/java", "src/main/kotlin")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.hytaledocs.intellij.hotReload

import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.vfs.LocalFileSystem
import java.io.File

class IntellijFileSynchronizer(
private val recentlySyncedPath: MutableSet<String>,
) : FileSynchronizer {

private val logger: Logger = Logger.getInstance(IntellijFileSynchronizer::class.java)

override fun sync(source: File, target: File) {
if (!source.exists()) {
logger.warn("Skipping sync: source does not exist at ${source.path}")
return
}

if (target.exists() && source.readBytes().contentEquals(target.readBytes())) return

try {
target.parentFile.mkdirs()

recentlySyncedPath.add(target.canonicalPath)
source.copyTo(target, overwrite = true)
LocalFileSystem.getInstance().refreshAndFindFileByIoFile(target)

logger.info("Successfully synced ${source.path} -> ${target.path}")
} catch (e: Exception) {
recentlySyncedPath.remove(target.canonicalPath)
logger.error("Failed to sync ${source.path}", e)
}
}

override fun delete(target: File) {
if (!target.exists()) return

try {
recentlySyncedPath.add(target.canonicalPath)
target.delete()
LocalFileSystem.getInstance().refresh(true)

logger.info("Successfully deleted ${target.path}")
} catch (e: Exception) {
recentlySyncedPath.remove(target.canonicalPath)
logger.error("Failed to delete ${target.path}", e)
}
}
}
64 changes: 64 additions & 0 deletions src/main/kotlin/com/hytaledocs/intellij/run/HytaleBuildService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.hytaledocs.intellij.run

import java.nio.file.Path

class HytaleBuildService(
private val pathResolver: HytalePathResolver,
private val config: HytaleServerRunConfiguration,
private val console: HytaleConsole
) {
fun executeBuild(projectBasePath: String): Boolean {
val gradleWrapper = pathResolver.findGradleWrapper(projectBasePath)
val mavenWrapper = pathResolver.findMavenWrapper(projectBasePath)
val globalGradle = pathResolver.findGlobalGradle()
val globalMaven = pathResolver.findGlobalMaven()

val (command, workDir) = when {
gradleWrapper != null -> {
console.printInfo("Using Gradle wrapper: ${config.buildTask}")
listOf(gradleWrapper, config.buildTask, "--no-daemon") to projectBasePath
}

mavenWrapper != null -> {
console.printInfo("Using Maven wrapper: ${config.buildTask}")
listOf(mavenWrapper, config.buildTask) to projectBasePath
}

globalGradle != null && pathResolver.hasGradleBuildFile(projectBasePath) -> {
console.printInfo("Using global Gradle: ${config.buildTask}")
listOf(globalGradle, config.buildTask, "--no-daemon") to projectBasePath
}

globalMaven != null && pathResolver.hasMavenBuildFile(projectBasePath) -> {
console.printInfo("Using global Maven: ${config.buildTask}")
listOf(globalMaven, config.buildTask) to projectBasePath
}

else -> {
console.printError("No Gradle or Maven found (wrapper or global)")
console.printInfo("Tip: Add gradlew.bat/gradlew to your project or install Gradle globally")
return false
}
}

return try {
val process = ProcessBuilder(command)
.directory(Path.of(workDir).toFile())
.redirectErrorStream(true)
.start()

// Read build output
process.inputStream.bufferedReader().useLines { lines ->
lines.forEach { line ->
console.println(line)
}
}

val exitCode = process.waitFor()
exitCode == 0
} catch (e: Exception) {
console.printError("Build error: ${e.message}")
false
}
}
}
8 changes: 8 additions & 0 deletions src/main/kotlin/com/hytaledocs/intellij/run/HytaleConsole.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.hytaledocs.intellij.run

interface HytaleConsole {
fun println(text: String)
fun printInfo(text: String)
fun printSuccess(text: String)
fun printError(text: String)
}
Loading