this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+70 -122
+16 -3
src/main/java/assets/Asset.kt
··· 3 3 import app.Environment 4 4 import org.apache.commons.io.FileUtils 5 5 import project.build.BuildCtx 6 - import project.build.BuildResult 7 6 import java.awt.Image 8 7 import java.awt.RenderingHints 9 8 import java.awt.datatransfer.DataFlavor ··· 128 127 */ 129 128 open suspend fun writeHeader(headerPath: Path): Boolean = false 130 129 131 - /** Build this asset. Override in subclasses that require compilation. */ 132 - open suspend fun build(ctx: BuildCtx): BuildResult = BuildResult.NoOp 130 + /** 131 + * Returns the list of artifact paths this asset will produce when built. 132 + * Called before building to know what to expect. 133 + * 134 + * Returns empty list by default for assets that don't produce build artifacts. 135 + */ 136 + open fun getArtifacts(ctx: BuildCtx): List<Path> = emptyList() 137 + 138 + /** 139 + * Build this asset. Override in subclasses that require compilation. 140 + * Artifacts are declared via getArtifacts(), not returned from build(). 141 + */ 142 + open suspend fun build(ctx: BuildCtx) { 143 + // No default implementation 144 + // Asset subclasses will implement this when they support building 145 + } 133 146 134 147 /** Whether to paint a checkerboard behind the thumbnail for transparency. */ 135 148 open fun thumbnailHasCheckerboard(): Boolean = true
+8 -7
src/main/java/assets/archive/AssetsArchiveCompressor.kt
··· 1 1 package assets.archive 2 2 3 3 import game.yay0.Yay0Helper 4 - import project.build.ArtifactType 5 4 import util.Logger 5 + import java.nio.file.Path 6 + import kotlin.io.path.extension 7 + import kotlin.io.path.name 6 8 7 9 /** Handles Yay0 compression for AssetsArchive entries. */ 8 10 object AssetsArchiveCompressor { ··· 25 27 } 26 28 27 29 /** 28 - * Determines whether an artifact type should be compressed. 30 + * Determines whether an artifact should be compressed based on its file extension. 31 + * TODO: engine should support any asset being compressed 29 32 */ 30 - fun shouldCompress(type: ArtifactType): Boolean = type in setOf( 31 - ArtifactType.BINARY, 32 - ArtifactType.SHAPE, 33 - ArtifactType.COLLISION 34 - ) 33 + fun shouldCompress(path: Path): Boolean { 34 + return path.name.endsWith("_shape") || path.name.endsWith("_hit") 35 + } 35 36 }
+11 -22
src/main/java/assets/ui/MapAsset.kt
··· 9 9 import game.map.compiler.CollisionCompiler 10 10 import game.map.compiler.GeometryCompiler 11 11 import game.map.editor.MapEditor 12 - import project.build.ArtifactType 13 - import project.build.BuildArtifact 14 12 import project.build.BuildCtx 15 - import project.build.BuildResult 16 13 import util.Logger 17 14 import util.Priority 18 15 import java.awt.Image ··· 58 55 59 56 override fun getAssetDescription(): String = desc 60 57 61 - override suspend fun build(ctx: BuildCtx): BuildResult { 62 - return try { 63 - // Load the map from XML 64 - val map = Map.loadMap(xmlFile) 58 + override fun getArtifacts(ctx: BuildCtx): List<Path> { 59 + val shape = ctx.artifact(this, "_shape") 60 + val hit = ctx.artifact(this, "_hit") 61 + return listOf(shape, hit) 62 + } 65 63 66 - // Compile geometry 67 - val shape = ctx.artifact(this, "_shape") 68 - GeometryCompiler(map, shape) 64 + override suspend fun build(ctx: BuildCtx) { 65 + val map = Map.loadMap(xmlFile) 69 66 70 - // Compile collision 71 - val hit = ctx.artifact(this, "_hit") 72 - CollisionCompiler(map, hit) 67 + val shape = ctx.artifact(this, "_shape") 68 + GeometryCompiler(map, shape) 73 69 74 - BuildResult.Success( 75 - artifacts = listOf( 76 - BuildArtifact(shape, ArtifactType.SHAPE), 77 - BuildArtifact(hit, ArtifactType.COLLISION) 78 - ) 79 - ) 80 - } catch (e: Exception) { 81 - BuildResult.Failed(e) 82 - } 70 + val hit = ctx.artifact(this, "_hit") 71 + CollisionCompiler(map, hit) 83 72 } 84 73 85 74 override fun delete(): Boolean {
+35 -54
src/main/java/project/Build.kt
··· 47 47 ): Boolean { 48 48 return withContext(Dispatchers.IO) { 49 49 // Phase 1: Build all assets 50 - val success = buildAllAssets(progressCallback) 50 + val (success, artifacts) = buildAllAssets(progressCallback) 51 51 if (!success || !generateArchive) return@withContext success 52 52 53 53 // Phase 2: Generate AssetsArchive 54 - val artifacts = collectArtifacts() 55 54 val archivePath = generateAssetsArchive(artifacts) 56 55 57 56 // Phase 3: Package Diorama (optional) ··· 118 117 119 118 /** 120 119 * Builds all assets in parallel, with header generation first. 121 - * @return true if the build succeeded, false if there were errors 120 + * @return Pair of (success, artifacts) where success is true if the build succeeded 122 121 */ 123 - private suspend fun buildAllAssets(progressCallback: ProgressCallback?): Boolean { 122 + private suspend fun buildAllAssets(progressCallback: ProgressCallback?): Pair<Boolean, List<Path>> { 124 123 val buildDir = project.directory.toPath() / ".starrod" / "build" 125 124 buildDir.createDirectories() 126 125 ··· 147 146 148 147 if (assetsToRebuild.isEmpty()) { 149 148 progressCallback?.onBuildComplete(0, 0) 150 - return true 149 + return true to emptyList() 151 150 } 152 151 153 152 progressCallback?.onBuildStarted(assetsToRebuild.size) ··· 178 177 ) 179 178 180 179 // Build all assets in parallel 181 - val (successCount, errorCount, _) = buildAssetsInParallel( 180 + val (successCount, errorCount, buildArtifacts) = buildAssetsInParallel( 182 181 assetsToRebuild, 183 182 ctx, 184 183 buildState, ··· 194 193 195 194 if (errorCount > 0) { 196 195 Logger.logError("Build completed with $errorCount error(s)") 197 - return false 196 + return false to emptyList() 198 197 } else { 199 198 Logger.log("Build completed successfully: $successCount assets built") 200 - return true 199 + return true to buildArtifacts 201 200 } 202 201 } 203 202 ··· 212 211 progressCallback: ProgressCallback?, 213 212 currentSuccessCount: Int, 214 213 totalAssets: Int 215 - ): Triple<Int, Int, List<BuildArtifact>> { 214 + ): Triple<Int, Int, List<Path>> { 216 215 val dispatcher = Dispatchers.IO.limitedParallelism(32) 217 216 val errorChannel = Channel<Pair<Asset, Exception>>(Channel.UNLIMITED) 218 - val artifactChannel = Channel<BuildArtifact>(Channel.UNLIMITED) 217 + val artifactChannel = Channel<Path>(Channel.UNLIMITED) 219 218 220 219 var successCount = currentSuccessCount 221 220 ··· 223 222 val jobs = assets.map { asset -> 224 223 CoroutineScope(dispatcher + SupervisorJob()).launch { 225 224 try { 226 - when (val result = asset.build(ctx)) { 227 - is BuildResult.NoOp -> { 228 - // Asset doesn't need building, but mark as visited 229 - buildState.markBuilt(asset) 230 - } 231 - is BuildResult.Success -> { 225 + // Get declared artifacts (empty list means asset doesn't produce artifacts) 226 + val declaredArtifacts = asset.getArtifacts(ctx) 227 + 228 + if (declaredArtifacts.isEmpty()) { 229 + // Asset doesn't produce build artifacts - skip it 230 + buildState.markBuilt(asset) 231 + } else { 232 + // Build the asset (no return value) 233 + asset.build(ctx) 234 + 235 + // Validate all declared artifacts exist 236 + val missingArtifacts = declaredArtifacts.filter { !it.exists() } 237 + if (missingArtifacts.isNotEmpty()) { 238 + val error = Exception("Build succeeded but artifacts not created: ${missingArtifacts.map { it.fileName }}") 239 + errorChannel.send(asset to error) 240 + progressCallback?.onAssetFailed(asset, error) 241 + } else { 242 + // Success - mark built and collect artifacts 232 243 buildState.markBuilt(asset) 233 - result.artifacts.forEach { artifactChannel.send(it) } 244 + declaredArtifacts.forEach { artifactChannel.send(it) } 234 245 synchronized(this@Build) { 235 246 successCount++ 236 247 progressCallback?.onAssetBuilt(asset, successCount, totalAssets) 237 248 } 238 249 } 239 - is BuildResult.Failed -> { 240 - errorChannel.send(asset to result.error) 241 - progressCallback?.onAssetFailed(asset, result.error) 242 - } 243 250 } 244 251 } catch (e: Exception) { 245 252 if (e is CancellationException) throw e ··· 262 269 263 270 // Collect artifacts 264 271 artifactChannel.close() 265 - val artifacts = mutableListOf<BuildArtifact>() 272 + val artifacts = mutableListOf<Path>() 266 273 for (artifact in artifactChannel) { 267 274 artifacts.add(artifact) 268 275 } ··· 291 298 } 292 299 293 300 /** 294 - * Collects all built artifacts from the build directory. 295 - */ 296 - private fun collectArtifacts(): List<BuildArtifact> { 297 - val buildDir = project.directory.toPath() / ".starrod" / "build" 298 - if (!buildDir.exists() || !buildDir.isDirectory()) 299 - return emptyList() 300 - 301 - val artifacts = mutableListOf<BuildArtifact>() 302 - 303 - // Collect binary, shape, and collision artifacts 304 - buildDir.walk() 305 - .filter { it.isRegularFile() } 306 - .filter { !it.startsWith(buildDir / "headers") } // Exclude headers 307 - .filter { it.extension in setOf("bin", "shape", "collision") } 308 - .forEach { file -> 309 - val type = when (file.extension) { 310 - "shape" -> ArtifactType.SHAPE 311 - "collision" -> ArtifactType.COLLISION 312 - else -> ArtifactType.BINARY 313 - } 314 - artifacts.add(BuildArtifact(file, type)) 315 - } 316 - 317 - return artifacts 318 - } 319 - 320 - /** 321 301 * Generates an AssetsArchive binary from built artifacts. 322 302 * @return Path to the generated assets.bin 323 303 */ 324 - private suspend fun generateAssetsArchive(artifacts: List<BuildArtifact>): Path { 304 + private suspend fun generateAssetsArchive(artifacts: List<Path>): Path { 325 305 return withContext(Dispatchers.IO) { 326 306 Logger.log("Generating AssetsArchive from ${artifacts.size} artifacts...") 327 307 308 + val buildDir = project.directory.toPath() / ".starrod" / "build" 328 309 val builder = AssetsArchiveBuilder(project.manifest.name) 329 310 330 311 // Add each artifact to the archive 331 - for (artifact in artifacts) { 332 - val name = artifact.path.fileName.toString() 333 - val data = artifact.path.readBytes() 334 - val compress = AssetsArchiveCompressor.shouldCompress(artifact.type) 312 + for (artifactPath in artifacts) { 313 + val name = buildDir.relativize(artifactPath).toString() 314 + val data = artifactPath.readBytes() 315 + val compress = AssetsArchiveCompressor.shouldCompress(artifactPath) 335 316 336 317 builder.addEntry(name, data, compress) 337 318 }
-36
src/main/java/project/build/BuildResult.kt
··· 1 - package project.build 2 - 3 - import java.nio.file.Path 4 - 5 - /** 6 - * Result of building an asset. 7 - */ 8 - sealed class BuildResult { 9 - /** Asset doesn't need building. */ 10 - object NoOp : BuildResult() 11 - 12 - /** Asset built successfully. */ 13 - data class Success( 14 - val artifacts: List<BuildArtifact> = emptyList() 15 - ) : BuildResult() 16 - 17 - /** Asset build failed. */ 18 - data class Failed(val error: Exception) : BuildResult() 19 - } 20 - 21 - /** 22 - * A file produced by building an asset. 23 - */ 24 - data class BuildArtifact( 25 - val path: Path, 26 - val type: ArtifactType 27 - ) 28 - 29 - enum class ArtifactType { 30 - HEADER, 31 - OBJECT, 32 - BINARY, 33 - SHAPE, 34 - COLLISION, 35 - OTHER 36 - }