Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1dabf0a
Extract narrative features into an addon
HollowHorizon Oct 21, 2026
b276d59
Move narrative features into an addon
HollowHorizon Oct 21, 2026
4adaf76
Stabilize shared script identity and loading
HollowHorizon Oct 21, 2026
eff81db
Build addons for IDE application runs
HollowHorizon Oct 21, 2026
1ba8277
Initialize scripting on addon load
HollowHorizon Oct 21, 2026
d2f553d
Add resumable narrative story stages
HollowHorizon Oct 22, 2026
8827577
Expand resumable stages and lambda support
HollowHorizon Oct 22, 2026
c87eac8
Add reversible story actions and rewind support
HollowHorizon Oct 22, 2026
1341baa
Share JVM scripting classes with runtime
HollowHorizon Oct 22, 2026
4ff02cc
Fix Kotlin addon test classpath metadata
HollowHorizon Oct 22, 2026
156b633
Harden resumable stages and story runtime
HollowHorizon Oct 22, 2026
86ef95f
Support shared vars in stage lambdas
HollowHorizon Oct 22, 2026
fcc4e6f
Load scripts with namespace classloaders
HollowHorizon Oct 22, 2026
207ff6c
Add story markers to the IDE gutter
HollowHorizon Oct 22, 2026
5958bb7
Rework story checkpoints and rewind semantics
HollowHorizon Oct 22, 2026
d3c73f6
Test stage safety with non-saveable locals
HollowHorizon Oct 22, 2026
33b5e4a
Fix NBT tag decoding and script variable keys
HollowHorizon Oct 23, 2026
2a8deb4
Improve story persistence and gutter navigation
HollowHorizon Oct 23, 2026
125c806
Handle external story-action cancellations
HollowHorizon Oct 23, 2026
a8af653
Resume action calls across story restarts
HollowHorizon Oct 23, 2026
44814b6
Keep NPC movement chunks loaded
HollowHorizon Oct 23, 2026
d503534
Add trackball rotation and timeline selection #117
HollowHorizon Oct 24, 2026
7514619
Update BlockBenchModelTests.kt
HollowHorizon Oct 24, 2026
05ac86e
Sync external text-file edits in the IDE #124
HollowHorizon Oct 24, 2026
b0217c6
addons manager ui panel
bat1set Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions addons/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Copy the jar to either of these directories:
- `mods/` - the usual choice for distributed addons and modpacks;
- `hollowengine/addons/`- useful during development or when you want to keep addons separate from mods.

In a development run of this repository nothing has to be copied: `runClient` and `runServer` build every addon first and pass `build/addon-jars` to the game with `-Dhollowengine.devAddons`. That directory is looked at before the two others, so a copy of the same addon left in `mods/` never wins, and it is watched like them: run `./gradlew buildAddons` while the game is open and the rebuilt addons are reloaded in place. Addons with bootstrap libraries still need a restart.

The runtime scans and watches both directories. If the same addon id is present in both at startup, the newest version wins; when the versions are equal, the copy in `hollowengine/addons/` has priority. Do not keep duplicate copies unless you are deliberately testing version selection.

Runtime diagnostics and lifecycle controls are available to operators:
Expand Down
12 changes: 11 additions & 1 deletion addons/compiler/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ val ijPlatform = "261.25134.147"
val ijJava = "261.25134.137"

val runtimeProjectPath = ":runtime"
val minecraftTestClasspath = project(runtimeProjectPath).configurations.named("compileClasspath").map { classpath ->
classpath.filter { file -> listOf("org.jetbrains", "androidx", "io.insert-koin").none { file.path.contains(it) } }
}
val narrativeTestJar = provider { project(":addons:narrative").tasks.named<Jar>("jar").get().archiveFile.get() }
val lightTreeClasses = project.projectDir.resolve("lightTree")
val patchedLightTreeJar = tasks.register<ShadowJar>("patchedLightTreeJar") {
archiveBaseName.set("hollowengine-light-tree")
Expand Down Expand Up @@ -169,14 +173,19 @@ dependencies {
runtimeOnly(files(patchedLightTreeJar.flatMap { it.archiveFile }))

testImplementation(kotlin("test"))
testImplementation("io.insert-koin:koin-core:${rootProject.property("koinVersion")}")
testImplementation("androidx.compose.runtime:runtime:$composeRuntimeVersion")
testImplementation(files(narrativeTestJar) { builtBy(":addons:narrative:jar") })
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0")
testImplementation(files(minecraftTestClasspath))
testRuntimeOnly(files(patchedLightTreeJar.flatMap { it.archiveFile }))
testRuntimeOnly(files(compilerPluginRuntimeJar.flatMap { it.archiveFile }))
testRuntimeOnly("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion")
testRuntimeOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
testRuntimeOnly("org.jetbrains.kotlinx:kotlinx-serialization-core:$serializationVersion")
testRuntimeOnly("org.jetbrains.kotlinx:kotlinx-serialization-json:$serializationVersion")
testRuntimeOnly("org.apache.logging.log4j:log4j-api:2.23.1")
testRuntimeOnly("org.apache.logging.log4j:log4j-core:2.23.1")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

Expand Down Expand Up @@ -305,7 +314,8 @@ tasks.withType<Test>().configureEach {
})
inputs.files(analysisClasspath).withPropertyName("ideAnalysisClasspath")
doFirst {
systemProperty("hollowengine.ide.testClasspath", analysisClasspath.get().asPath)
val classpath = analysisClasspath.get().files + narrativeTestJar.get().asFile
systemProperty("hollowengine.ide.testClasspath", classpath.joinToString(File.pathSeparator))
}
useJUnitPlatform()
maxHeapSize = "2g"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import kotlinx.coroutines.*
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import ru.hollowhorizon.hollowengine.common.addons.HollowAddonContext
import ru.hollowhorizon.hollowengine.common.addons.HollowAddonEntrypoint
import ru.hollowhorizon.hollowengine.common.addons.HollowAddonsChangedEvent
import ru.hollowhorizon.hollowengine.common.addons.publish
import ru.hollowhorizon.hollowengine.common.compiler.ScriptingCompilerImpl
import ru.hollowhorizon.hollowengine.common.compiler.configuration.HollowScriptConfiguration
import ru.hollowhorizon.hollowengine.common.compiler.configuration.clientSideImplicitReceivers
import ru.hollowhorizon.hollowengine.common.events.SubscribeEvent
import ru.hollowhorizon.hollowengine.common.ide.session.AnalysisEnvironment
import ru.hollowhorizon.hollowengine.common.ide.session.EmptyLogger
import ru.hollowhorizon.hollowengine.common.scripting.DefaultScriptDefinitions
Expand All @@ -33,10 +35,16 @@ import kotlin.script.experimental.jvm.JvmDependency
import kotlin.script.experimental.jvm.JvmGetScriptingClass
import kotlin.script.experimental.jvm.defaultJvmScriptingHostConfiguration

class ScriptingEnvironmentInitializerImpl : ScriptingEnvironmentInitializer, HollowAddonEntrypoint {
class ScriptingEnvironmentInitializerImpl(
private val prepareClasspath: (File) -> Pair<Mappings, List<File>> = CommonEnvironment::setup,
private val definitions: () -> List<ScriptClassProvider> = DefaultScriptDefinitions::providers,
) : ScriptingEnvironmentInitializer, HollowAddonEntrypoint {
@Volatile
private var loaded: LoadedAddon? = null

@Volatile
private var built: BuiltEnvironment? = null

override suspend fun load(context: HollowAddonContext, scope: CoroutineScope) {
loaded = LoadedAddon(context.addonFile, scope)
setUp(context.addonFile, scope)
Expand All @@ -45,6 +53,7 @@ class ScriptingEnvironmentInitializerImpl : ScriptingEnvironmentInitializer, Hol

override suspend fun unload(context: HollowAddonContext) {
loaded = null
built = null
ScriptingEnvironment.clear()
}

Expand All @@ -56,20 +65,52 @@ class ScriptingEnvironmentInitializerImpl : ScriptingEnvironmentInitializer, Hol
setUp(addon.file, addon.scope)
}

/**
* Addons loaded after this one may add script types and imports. Once they are all in, the
* environment is recreated with them on the same classpath, so nothing is remapped again.
*/
@SubscribeEvent
@Synchronized
fun onAddonsChanged(event: HollowAddonsChangedEvent) {
val addon = loaded ?: return
val current = built ?: return
val scriptTypes = definitions()
if (scriptTypes == current.scriptTypes) return
logI("Reloading script definitions after addon imports or script types changed")
ScriptingEnvironment.clear()
start(addon, current.mappings, current.classpath, scriptTypes)
}

private fun setUp(addonFile: File, scope: CoroutineScope) {
val (mappings, classpath) = CommonEnvironment.setup(addonFile)
val (mappings, classpath) = prepareClasspath(addonFile)
start(LoadedAddon(addonFile, scope), mappings, classpath, definitions())
}

private fun start(
addon: LoadedAddon,
mappings: Mappings,
classpath: List<File>,
scriptTypes: List<ScriptClassProvider>,
) {
val environment = initialize(
javaHome = File(System.getProperty("java.home")),
classpath = classpath,
hostClasspath = classpath + addonFile,
scriptTypes = DefaultScriptDefinitions.providers(),
hostClasspath = classpath + addon.file,
scriptTypes = scriptTypes,
mappings = mappings,
)
environment.warmUpAnalysis(scope)
built = BuiltEnvironment(mappings, classpath, scriptTypes)
environment.warmUpAnalysis(addon.scope)
}

private class LoadedAddon(val file: File, val scope: CoroutineScope)

private class BuiltEnvironment(
val mappings: Mappings,
val classpath: List<File>,
val scriptTypes: List<ScriptClassProvider>,
)

override fun initialize(
javaHome: File,
classpath: List<File>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,19 @@ import kotlin.script.experimental.api.ScriptCompilationConfiguration
import kotlin.script.experimental.api.ScriptEvaluationConfiguration
import kotlin.script.experimental.api.implicitReceivers
import kotlin.script.experimental.api.with
import kotlin.script.experimental.api.valueOr
import kotlin.script.experimental.api.CompiledScript as KotlinCompiledScript

data class CompiledScriptImpl(
override val name: String,
val script: kotlin.script.experimental.api.CompiledScript,
val script: KotlinCompiledScript,
val evalConfiguration: ScriptEvaluationConfiguration,
) : CompiledScript {
private val scriptClassDelegate = lazy {
runScriptingBlocking {
(script.getClass(evalConfiguration) as ResultWithDiagnostics.Success).value
script.getClass(evalConfiguration).valueOr { failure ->
throw ScriptEvaluationException(name, failure.reports.map { it.convert() })
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package ru.hollowhorizon.hollowengine.common.compiler.configuration

import ru.hollowhorizon.hollowengine.common.scripting.compiling.isSharedScript
import ru.hollowhorizon.hollowengine.common.scripting.compiling.sharedScriptImportError
import org.jetbrains.kotlin.scripting.resolve.resolvedImportScripts
import kotlin.script.experimental.api.ScriptCompilationConfiguration
import kotlin.script.experimental.api.ScriptDiagnostic
import kotlin.script.experimental.api.SourceCode

internal fun sharedImportDiagnostics(
sources: List<SourceCode>,
configuration: (SourceCode) -> ScriptCompilationConfiguration,
): List<ScriptDiagnostic> = buildList {
sources.forEach { script ->
val configured = configuration(script)
if (configured[ScriptCompilationConfiguration.isSharedScript] != true) return@forEach
configured[ScriptCompilationConfiguration.resolvedImportScripts].orEmpty().filter {
configuration(it)[ScriptCompilationConfiguration.isSharedScript] != true
}.forEach { dependency ->
add(ScriptDiagnostic(
ScriptDiagnostic.unspecifiedError,
sharedScriptImportError(script.locationId ?: script.name.orEmpty(), dependency.locationId ?: dependency.name.orEmpty()),
ScriptDiagnostic.Severity.ERROR,
script.locationId,
))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.name.NameUtils
import org.jetbrains.kotlin.platform.jvm.JvmPlatforms
import org.jetbrains.kotlin.scripting.compiler.plugin.configureFirSession
import org.jetbrains.kotlin.scripting.compiler.plugin.definitions.getOrStoreRefinedCompilationConfiguration
import org.jetbrains.kotlin.scripting.compiler.plugin.definitions.getRefinedOrBaseCompilationConfiguration
import org.jetbrains.kotlin.scripting.compiler.plugin.definitions.scriptRefinedCompilationConfigurationsCache
import org.jetbrains.kotlin.scripting.compiler.plugin.dependencies.collectScriptsCompilationDependenciesRecursively
Expand All @@ -53,6 +52,8 @@ import org.jetbrains.kotlin.scripting.compiler.plugin.impl.makeCompiledScript
import org.jetbrains.kotlin.scripting.compiler.plugin.impl.refineAllForK2
import org.jetbrains.kotlin.scripting.compiler.plugin.impl.selectJvmTarget
import org.jetbrains.kotlin.scripting.compiler.plugin.impl.updateWithCompilerOptions
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import ru.hollowhorizon.hollowengine.common.compiler.configuration.sharedImportDiagnostics
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import kotlin.script.experimental.api.CompiledScript
import kotlin.script.experimental.api.ResultWithDiagnostics
Expand Down Expand Up @@ -83,6 +84,7 @@ import kotlin.script.experimental.jvm.util.toClassPathOrEmpty
internal class AnnotationFirScriptJvmCompiler(
state: K2ScriptingCompilerEnvironment,
private val convertToFir: SourceCode.(FirSession, BaseDiagnosticsCollector) -> FirFile,
private val definitions: List<ScriptDefinition>,
) {
private val state = state as? K2ScriptingCompilerEnvironmentInternal
?: error("Expected the internal K2 scripting compiler environment, got ${state::class}")
Expand Down Expand Up @@ -160,12 +162,21 @@ internal class AnnotationFirScriptJvmCompiler(
val allSourceFiles = mutableListOf(script)
val (classpath, importedSources, sourceDependencies) =
collectScriptsCompilationDependenciesRecursively(allSourceFiles) { importedScript ->
state.hostConfiguration.getOrStoreRefinedCompilationConfiguration(importedScript) { source, baseConfig ->
baseConfig.refineAll(source)
if (importedScript == script) scriptRefinedCompilationConfiguration.asSuccess()
else {
val definition = definitions.filter { importedScript.name?.endsWith(it.fileExtension) == true }
.maxByOrNull { it.fileExtension.length }
val refined = (definition?.compilationConfiguration ?: state.baseScriptCompilationConfiguration)
.refineAll(importedScript)
state.hostConfiguration[ScriptingHostConfiguration.scriptRefinedCompilationConfigurationsCache]
?.storeRefinedCompilationConfiguration(importedScript, refined)
refined
}
}.valueOr { return it }
allSourceFiles.addAll(importedSources)
val compilationSources = importedSources + script
val sharedErrors = sharedImportDiagnostics(compilationSources, ::getRefinedConfiguration)
if (sharedErrors.isNotEmpty()) return failure(reporting, reporting.diagnosticsCollector, *sharedErrors.toTypedArray())

val ignoredOptionsReportingState = state.compilerContext.ignoredOptionsReportingState
val updatedCompilerOptions = allSourceFiles.flatMapTo(mutableListOf()) {
Expand Down
Loading
Loading