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