Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
eb2e683
Extract narrative features into an addon
HollowHorizon Oct 21, 2026
982276b
Move narrative features into an addon
HollowHorizon Oct 21, 2026
2d51392
Stabilize shared script identity and loading
HollowHorizon Oct 21, 2026
0f69546
Build addons for IDE application runs
HollowHorizon Oct 21, 2026
4e67a9f
Initialize scripting on addon load
HollowHorizon Oct 21, 2026
f5292d8
Add resumable narrative story stages
HollowHorizon Oct 22, 2026
e916694
Expand resumable stages and lambda support
HollowHorizon Oct 22, 2026
5b3dcd4
Add reversible story actions and rewind support
HollowHorizon Oct 22, 2026
8163a2d
Share JVM scripting classes with runtime
HollowHorizon Oct 22, 2026
2362bcf
Fix Kotlin addon test classpath metadata
HollowHorizon Oct 22, 2026
aa68452
Harden resumable stages and story runtime
HollowHorizon Oct 22, 2026
5ee3148
Support shared vars in stage lambdas
HollowHorizon Oct 22, 2026
a6aa481
Load scripts with namespace classloaders
HollowHorizon Oct 22, 2026
8049eba
Add story markers to the IDE gutter
HollowHorizon Oct 22, 2026
4fc9cfa
Rework story checkpoints and rewind semantics
HollowHorizon Oct 22, 2026
5e6be8f
Test stage safety with non-saveable locals
HollowHorizon Oct 22, 2026
11054d6
Fix NBT tag decoding and script variable keys
HollowHorizon Oct 23, 2026
78da736
Improve story persistence and gutter navigation
HollowHorizon Oct 23, 2026
d07b547
Handle external story-action cancellations
HollowHorizon Oct 23, 2026
c762f86
Resume action calls across story restarts
HollowHorizon Oct 23, 2026
6e9cbaf
Keep NPC movement chunks loaded
HollowHorizon Oct 23, 2026
7b11fcc
Add trackball rotation and timeline selection #117
HollowHorizon Oct 24, 2026
7fbdd42
Sync external text-file edits in the IDE #124
HollowHorizon Oct 24, 2026
eedfc99
Improve IDE editing and console controls
HollowHorizon Oct 25, 2026
c945a6a
Fix completion for companion extensions #125
HollowHorizon Oct 25, 2026
f814fb3
Keep UI audio playing while paused
HollowHorizon Oct 25, 2026
655699a
Add configurable NPC spawn helper
HollowHorizon Oct 25, 2026
a0861f7
Add text selection drag-and-drop support
HollowHorizon Oct 25, 2026
7cd6cbe
Refactor code editor options and syntax-aware editing
HollowHorizon Oct 25, 2026
c3af439
Improve rename field selection #125
HollowHorizon Oct 26, 2026
8b19dbf
Add resumable story scenes
HollowHorizon Oct 26, 2026
5ac829a
Use registered actions as story commands
HollowHorizon Oct 26, 2026
f6a60a6
Persist NPC move options and simplify navigation
HollowHorizon Oct 26, 2026
192697e
Add scene checkpoints and dialogue timeouts
HollowHorizon Oct 26, 2026
46f49a1
Add resumable scene tracks and story calls
HollowHorizon Oct 26, 2026
9febfac
Refactor dialogue runtime into scene player
HollowHorizon Oct 27, 2026
7c94163
Fix unused concurrent operations
HollowHorizon Oct 27, 2026
f2b47d6
Add card-based story editor preview
HollowHorizon Oct 27, 2026
ca0f3ba
Keep story rails fixed while dragging rows
HollowHorizon Oct 27, 2026
d7d1764
Update UiEditableTextField.kt
HollowHorizon Oct 27, 2026
ba3cbd9
Expand narrative editor and scene runtime
HollowHorizon Oct 27, 2026
2761d06
Refactor narrative dialogue into speech and polls
HollowHorizon Oct 27, 2026
4491f77
Delete tests.yml
HollowHorizon Oct 27, 2026
e2e7f14
Support nested command calls in stories
HollowHorizon Oct 28, 2026
3eb5cdb
Expand narrative editor and story launching
HollowHorizon Oct 28, 2026
9306d86
Add screen-aware UI overlays and dialogue HUD
HollowHorizon Oct 29, 2026
28dd013
Replace narrative DSL with story graphs
HollowHorizon Oct 31, 2026
79f743f
Expand narrative graphs and shared graph editor
HollowHorizon Oct 31, 2026
4ebfa33
Rework story graph editing and compilation
HollowHorizon Nov 1, 2026
e506f64
Expand typed story graph editing and analysis
HollowHorizon Nov 1, 2026
771489d
Update StoryInspector.kt
HollowHorizon Nov 1, 2026
484defd
Cull graph nodes and preserve UI draw order
HollowHorizon Nov 1, 2026
8713a3e
Improve story graph type analysis
HollowHorizon Nov 1, 2026
672c275
Optimize UI layer and path rendering
HollowHorizon Nov 1, 2026
89ebb9d
Fix graph rendering and style cache invalidation
HollowHorizon Nov 1, 2026
3b87c5f
Optimize HollowUI rendering and hit testing
HollowHorizon Nov 1, 2026
db7fb22
Optimize UI layout and graph recomposition
HollowHorizon Nov 1, 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
  •  
  •  
  •  
145 changes: 0 additions & 145 deletions .github/workflows/tests.yml

This file was deleted.

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