From deb5b89f8bdfea3b9e0b05fae2a8070142ef17ec Mon Sep 17 00:00:00 2001 From: Alex Bates Date: Thu, 12 Feb 2026 03:42:26 +0000 Subject: [PATCH] refactor asset artifact collection to use upfront declaration Previously, the build system collected artifacts by walking the filesystem after all assets were built. Now assets declare their artifacts upfront via getArtifacts(), and the build system collects them during the build process through channels. Changes: - Replace BuildResult sealed class with getArtifacts()/build() pattern - Remove collectArtifacts() filesystem scanning - Change from BuildArtifact wrapper objects to plain Path objects - Add validation that declared artifacts actually get created - Update AssetsArchiveCompressor to work with paths instead of types --- src/main/java/assets/Asset.kt | 19 +++- .../assets/archive/AssetsArchiveCompressor.kt | 15 ++-- src/main/java/assets/ui/MapAsset.kt | 39 +++----- src/main/java/project/Build.kt | 89 ++++++++----------- src/main/java/project/build/BuildResult.kt | 36 -------- 5 files changed, 73 insertions(+), 125 deletions(-) delete mode 100644 src/main/java/project/build/BuildResult.kt diff --git a/src/main/java/assets/Asset.kt b/src/main/java/assets/Asset.kt index 4ca17a1..b002581 100644 --- a/src/main/java/assets/Asset.kt +++ b/src/main/java/assets/Asset.kt @@ -3,7 +3,6 @@ package assets import app.Environment import org.apache.commons.io.FileUtils import project.build.BuildCtx -import project.build.BuildResult import java.awt.Image import java.awt.RenderingHints import java.awt.datatransfer.DataFlavor @@ -128,8 +127,22 @@ open class Asset internal constructor( */ open suspend fun writeHeader(headerPath: Path): Boolean = false - /** Build this asset. Override in subclasses that require compilation. */ - open suspend fun build(ctx: BuildCtx): BuildResult = BuildResult.NoOp + /** + * Returns the list of artifact paths this asset will produce when built. + * Called before building to know what to expect. + * + * Returns empty list by default for assets that don't produce build artifacts. + */ + open fun getArtifacts(ctx: BuildCtx): List = emptyList() + + /** + * Build this asset. Override in subclasses that require compilation. + * Artifacts are declared via getArtifacts(), not returned from build(). + */ + open suspend fun build(ctx: BuildCtx) { + // No default implementation + // Asset subclasses will implement this when they support building + } /** Whether to paint a checkerboard behind the thumbnail for transparency. */ open fun thumbnailHasCheckerboard(): Boolean = true diff --git a/src/main/java/assets/archive/AssetsArchiveCompressor.kt b/src/main/java/assets/archive/AssetsArchiveCompressor.kt index 099d019..ccba081 100644 --- a/src/main/java/assets/archive/AssetsArchiveCompressor.kt +++ b/src/main/java/assets/archive/AssetsArchiveCompressor.kt @@ -1,8 +1,10 @@ package assets.archive import game.yay0.Yay0Helper -import project.build.ArtifactType import util.Logger +import java.nio.file.Path +import kotlin.io.path.extension +import kotlin.io.path.name /** Handles Yay0 compression for AssetsArchive entries. */ object AssetsArchiveCompressor { @@ -25,11 +27,10 @@ object AssetsArchiveCompressor { } /** - * Determines whether an artifact type should be compressed. + * Determines whether an artifact should be compressed based on its file extension. + * TODO: engine should support any asset being compressed */ - fun shouldCompress(type: ArtifactType): Boolean = type in setOf( - ArtifactType.BINARY, - ArtifactType.SHAPE, - ArtifactType.COLLISION - ) + fun shouldCompress(path: Path): Boolean { + return path.name.endsWith("_shape") || path.name.endsWith("_hit") + } } diff --git a/src/main/java/assets/ui/MapAsset.kt b/src/main/java/assets/ui/MapAsset.kt index 7ea2809..55da0f1 100644 --- a/src/main/java/assets/ui/MapAsset.kt +++ b/src/main/java/assets/ui/MapAsset.kt @@ -9,10 +9,7 @@ import game.map.Map import game.map.compiler.CollisionCompiler import game.map.compiler.GeometryCompiler import game.map.editor.MapEditor -import project.build.ArtifactType -import project.build.BuildArtifact import project.build.BuildCtx -import project.build.BuildResult import util.Logger import util.Priority import java.awt.Image @@ -58,28 +55,20 @@ class MapAsset(assetsDir: AssetsDir, relativePath: Path) : Asset(assetsDir, rela override fun getAssetDescription(): String = desc - override suspend fun build(ctx: BuildCtx): BuildResult { - return try { - // Load the map from XML - val map = Map.loadMap(xmlFile) - - // Compile geometry - val shape = ctx.artifact(this, "_shape") - GeometryCompiler(map, shape) - - // Compile collision - val hit = ctx.artifact(this, "_hit") - CollisionCompiler(map, hit) - - BuildResult.Success( - artifacts = listOf( - BuildArtifact(shape, ArtifactType.SHAPE), - BuildArtifact(hit, ArtifactType.COLLISION) - ) - ) - } catch (e: Exception) { - BuildResult.Failed(e) - } + override fun getArtifacts(ctx: BuildCtx): List { + val shape = ctx.artifact(this, "_shape") + val hit = ctx.artifact(this, "_hit") + return listOf(shape, hit) + } + + override suspend fun build(ctx: BuildCtx) { + val map = Map.loadMap(xmlFile) + + val shape = ctx.artifact(this, "_shape") + GeometryCompiler(map, shape) + + val hit = ctx.artifact(this, "_hit") + CollisionCompiler(map, hit) } override fun delete(): Boolean { diff --git a/src/main/java/project/Build.kt b/src/main/java/project/Build.kt index 4d7ce7f..a2cd105 100644 --- a/src/main/java/project/Build.kt +++ b/src/main/java/project/Build.kt @@ -47,11 +47,10 @@ class Build(private val project: Project) { ): Boolean { return withContext(Dispatchers.IO) { // Phase 1: Build all assets - val success = buildAllAssets(progressCallback) + val (success, artifacts) = buildAllAssets(progressCallback) if (!success || !generateArchive) return@withContext success // Phase 2: Generate AssetsArchive - val artifacts = collectArtifacts() val archivePath = generateAssetsArchive(artifacts) // Phase 3: Package Diorama (optional) @@ -118,9 +117,9 @@ class Build(private val project: Project) { /** * Builds all assets in parallel, with header generation first. - * @return true if the build succeeded, false if there were errors + * @return Pair of (success, artifacts) where success is true if the build succeeded */ - private suspend fun buildAllAssets(progressCallback: ProgressCallback?): Boolean { + private suspend fun buildAllAssets(progressCallback: ProgressCallback?): Pair> { val buildDir = project.directory.toPath() / ".starrod" / "build" buildDir.createDirectories() @@ -147,7 +146,7 @@ class Build(private val project: Project) { if (assetsToRebuild.isEmpty()) { progressCallback?.onBuildComplete(0, 0) - return true + return true to emptyList() } progressCallback?.onBuildStarted(assetsToRebuild.size) @@ -178,7 +177,7 @@ class Build(private val project: Project) { ) // Build all assets in parallel - val (successCount, errorCount, _) = buildAssetsInParallel( + val (successCount, errorCount, buildArtifacts) = buildAssetsInParallel( assetsToRebuild, ctx, buildState, @@ -194,10 +193,10 @@ class Build(private val project: Project) { if (errorCount > 0) { Logger.logError("Build completed with $errorCount error(s)") - return false + return false to emptyList() } else { Logger.log("Build completed successfully: $successCount assets built") - return true + return true to buildArtifacts } } @@ -212,10 +211,10 @@ class Build(private val project: Project) { progressCallback: ProgressCallback?, currentSuccessCount: Int, totalAssets: Int - ): Triple> { + ): Triple> { val dispatcher = Dispatchers.IO.limitedParallelism(32) val errorChannel = Channel>(Channel.UNLIMITED) - val artifactChannel = Channel(Channel.UNLIMITED) + val artifactChannel = Channel(Channel.UNLIMITED) var successCount = currentSuccessCount @@ -223,23 +222,31 @@ class Build(private val project: Project) { val jobs = assets.map { asset -> CoroutineScope(dispatcher + SupervisorJob()).launch { try { - when (val result = asset.build(ctx)) { - is BuildResult.NoOp -> { - // Asset doesn't need building, but mark as visited - buildState.markBuilt(asset) - } - is BuildResult.Success -> { + // Get declared artifacts (empty list means asset doesn't produce artifacts) + val declaredArtifacts = asset.getArtifacts(ctx) + + if (declaredArtifacts.isEmpty()) { + // Asset doesn't produce build artifacts - skip it + buildState.markBuilt(asset) + } else { + // Build the asset (no return value) + asset.build(ctx) + + // Validate all declared artifacts exist + val missingArtifacts = declaredArtifacts.filter { !it.exists() } + if (missingArtifacts.isNotEmpty()) { + val error = Exception("Build succeeded but artifacts not created: ${missingArtifacts.map { it.fileName }}") + errorChannel.send(asset to error) + progressCallback?.onAssetFailed(asset, error) + } else { + // Success - mark built and collect artifacts buildState.markBuilt(asset) - result.artifacts.forEach { artifactChannel.send(it) } + declaredArtifacts.forEach { artifactChannel.send(it) } synchronized(this@Build) { successCount++ progressCallback?.onAssetBuilt(asset, successCount, totalAssets) } } - is BuildResult.Failed -> { - errorChannel.send(asset to result.error) - progressCallback?.onAssetFailed(asset, result.error) - } } } catch (e: Exception) { if (e is CancellationException) throw e @@ -262,7 +269,7 @@ class Build(private val project: Project) { // Collect artifacts artifactChannel.close() - val artifacts = mutableListOf() + val artifacts = mutableListOf() for (artifact in artifactChannel) { artifacts.add(artifact) } @@ -290,48 +297,22 @@ class Build(private val project: Project) { } } - /** - * Collects all built artifacts from the build directory. - */ - private fun collectArtifacts(): List { - val buildDir = project.directory.toPath() / ".starrod" / "build" - if (!buildDir.exists() || !buildDir.isDirectory()) - return emptyList() - - val artifacts = mutableListOf() - - // Collect binary, shape, and collision artifacts - buildDir.walk() - .filter { it.isRegularFile() } - .filter { !it.startsWith(buildDir / "headers") } // Exclude headers - .filter { it.extension in setOf("bin", "shape", "collision") } - .forEach { file -> - val type = when (file.extension) { - "shape" -> ArtifactType.SHAPE - "collision" -> ArtifactType.COLLISION - else -> ArtifactType.BINARY - } - artifacts.add(BuildArtifact(file, type)) - } - - return artifacts - } - /** * Generates an AssetsArchive binary from built artifacts. * @return Path to the generated assets.bin */ - private suspend fun generateAssetsArchive(artifacts: List): Path { + private suspend fun generateAssetsArchive(artifacts: List): Path { return withContext(Dispatchers.IO) { Logger.log("Generating AssetsArchive from ${artifacts.size} artifacts...") + val buildDir = project.directory.toPath() / ".starrod" / "build" val builder = AssetsArchiveBuilder(project.manifest.name) // Add each artifact to the archive - for (artifact in artifacts) { - val name = artifact.path.fileName.toString() - val data = artifact.path.readBytes() - val compress = AssetsArchiveCompressor.shouldCompress(artifact.type) + for (artifactPath in artifacts) { + val name = buildDir.relativize(artifactPath).toString() + val data = artifactPath.readBytes() + val compress = AssetsArchiveCompressor.shouldCompress(artifactPath) builder.addEntry(name, data, compress) } diff --git a/src/main/java/project/build/BuildResult.kt b/src/main/java/project/build/BuildResult.kt deleted file mode 100644 index 569f499..0000000 --- a/src/main/java/project/build/BuildResult.kt +++ /dev/null @@ -1,36 +0,0 @@ -package project.build - -import java.nio.file.Path - -/** - * Result of building an asset. - */ -sealed class BuildResult { - /** Asset doesn't need building. */ - object NoOp : BuildResult() - - /** Asset built successfully. */ - data class Success( - val artifacts: List = emptyList() - ) : BuildResult() - - /** Asset build failed. */ - data class Failed(val error: Exception) : BuildResult() -} - -/** - * A file produced by building an asset. - */ -data class BuildArtifact( - val path: Path, - val type: ArtifactType -) - -enum class ArtifactType { - HEADER, - OBJECT, - BINARY, - SHAPE, - COLLISION, - OTHER -} -- 2.51.2