From 834f07929f1fed9cc63b1a33d21a914b66401df5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Feb 2026 23:15:15 +0000 Subject: [PATCH 001/172] Improve CLI consistency: simplify update/resources display, hide docs command - DocsCommand: Add hidden=true to Clikt constructor so it no longer appears in help output (still callable for dev use) - UpdateCommand: Replace interactive table UI with clean emoji-prefixed display matching palette style. Editable params (zoom, cursor) are now set via CLI options (--zoom, --hide-cursor, --show-cursor) instead of arrow-key navigation - ResourcesCommand: Replace interactive table UI with clean categorized display. Resource deletion now uses --delete option by filename instead of keyboard navigation https://claude.ai/code/session_01SjyS1GFQhfDWqL4YgpmZyw --- .../tiny/cli/command/ResourcesCommand.kt | 205 ++++++------------ .../minigdx/tiny/cli/command/UpdateCommand.kt | 195 +++++------------ 2 files changed, 117 insertions(+), 283 deletions(-) diff --git a/tiny-cli/src/main/kotlin/com/github/minigdx/tiny/cli/command/ResourcesCommand.kt b/tiny-cli/src/main/kotlin/com/github/minigdx/tiny/cli/command/ResourcesCommand.kt index 2afa59fe..41b70f1b 100644 --- a/tiny-cli/src/main/kotlin/com/github/minigdx/tiny/cli/command/ResourcesCommand.kt +++ b/tiny-cli/src/main/kotlin/com/github/minigdx/tiny/cli/command/ResourcesCommand.kt @@ -1,15 +1,12 @@ package com.github.minigdx.tiny.cli.command +import com.github.ajalt.clikt.core.Abort import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.Context -import com.github.ajalt.clikt.core.terminal import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.multiple import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.types.file -import com.github.ajalt.mordant.input.InputReceiver -import com.github.ajalt.mordant.input.receiveKeyEvents -import com.github.ajalt.mordant.rendering.TextStyles -import com.github.ajalt.mordant.table.table import com.github.minigdx.tiny.cli.config.GameParameters import com.github.minigdx.tiny.cli.config.GameParametersV1 import com.github.minigdx.tiny.cli.exception.MissingTinyConfigurationException @@ -20,166 +17,86 @@ class ResourcesCommand : CliktCommand(name = "resources") { .file(mustExist = true, canBeDir = true, canBeFile = false) .default(File(".")) - private val categoryFilter by option("--category", help = "Filter categories by name or pattern") + private val delete by option("--delete", help = "Remove a resource from the game by filename.") + .multiple() - private var selectedIndex = 0 - - override fun help(context: Context) = "Inspect and manage categorized resources in the game." + override fun help(context: Context) = "Inspect and manage game resources." override fun run() { - val tiny = gameDirectory.resolve("_tiny.json") - if (!tiny.exists()) { - throw MissingTinyConfigurationException(tiny) + val configFile = gameDirectory.resolve("_tiny.json") + if (!configFile.exists()) { + throw MissingTinyConfigurationException(configFile) } - try { - var gameParameters = GameParameters.read(tiny) as GameParametersV1 - - showResourceManager(gameParameters) - currentContext.terminal.receiveKeyEvents { event -> - val next = when (event.key) { - "ArrowUp" -> { - selectedIndex = (selectedIndex - 1).coerceAtLeast(0) - true - } - - "ArrowDown" -> { - val maximumValue = getResourceCategories(gameParameters as GameParametersV1).map { it.value.size }.sum() - 1 - selectedIndex = (selectedIndex + 1).coerceAtMost(maximumValue) - true - } - - "d" -> { - gameParameters = deleteSelected(selectedIndex, gameParameters as GameParametersV1) - selectedIndex = (selectedIndex - 1).coerceAtLeast(0) - true - } - - "q" -> { - save(gameParameters) - false - } - - else -> true - } - if (next) { - showResourceManager(gameParameters) - InputReceiver.Status.Continue - } else { - InputReceiver.Status.Finished - } - } + val gameParameters = try { + GameParameters.read(configFile) } catch (e: Exception) { echo("❌ Error reading _tiny.json: ${e.message}") - } - } - - private fun save(configuration: GameParameters) { - val configurationFile = gameDirectory.resolve("_tiny.json") - configuration.write(configurationFile) - } - - private fun deleteSelected( - selectedIndex: Int, - parameters: GameParametersV1, - ): GameParametersV1 { - val categories = getResourceCategories(parameters) - - // Flatten all resources while keeping track of their category and index within that category - val flattenedResources = mutableListOf>() // (categoryName, resourcePath, indexInCategory) - categories.forEach { (categoryName, resources) -> - resources.forEachIndexed { indexInCategory, resourcePath -> - flattenedResources.add(Triple(categoryName, resourcePath, indexInCategory)) - } + throw Abort() } - // Validate selectedIndex - if (selectedIndex < 0 || selectedIndex >= flattenedResources.size) { - throw IndexOutOfBoundsException("Invalid selectedIndex: $selectedIndex. Valid range: 0-${flattenedResources.size - 1}") + if (gameParameters !is GameParametersV1) { + echo("❌ Only V1 game configuration is supported.") + throw Abort() } - // Get the resource to delete - val (categoryName, resourceToDelete, indexInCategory) = flattenedResources[selectedIndex] - - // Create a copy of parameters with the resource removed from the appropriate category - return when (categoryName) { - "\uD83D\uDCDD scripts" -> { - val updatedScripts = parameters.scripts.toMutableList() - updatedScripts.removeAt(indexInCategory) - parameters.copy(scripts = updatedScripts) - } - "\uD83D\uDDBC\uFE0F spritesheets" -> { - val updatedSpritesheets = parameters.spritesheets.toMutableList() - updatedSpritesheets.removeAt(indexInCategory) - parameters.copy(spritesheets = updatedSpritesheets) - } - "\uD83D\uDDFA\uFE0F levels" -> { - val updatedLevels = parameters.levels.toMutableList() - updatedLevels.removeAt(indexInCategory) - parameters.copy(levels = updatedLevels) - } - "\uD83D\uDD08 sounds" -> { - val updatedSounds = listOfNotNull(parameters.sound).toMutableList() - updatedSounds.removeAt(indexInCategory) - parameters.copy(sound = updatedSounds.firstOrNull()) - } - else -> throw IllegalStateException("Unknown category: $categoryName") + if (delete.isNotEmpty()) { + deleteResources(gameParameters, configFile) + } else { + displayResources(gameParameters) } } - private fun showResourceManager(gameParameters: GameParametersV1) { - val categories = getResourceCategories(gameParameters) + private fun deleteResources(params: GameParametersV1, configFile: File) { + var updated = params - if (categories.isEmpty()) { - echo("No categories found${if (categoryFilter != null) " matching filter '$categoryFilter'" else ""}") - return - } + for (resource in delete) { + val found = resource in updated.scripts || + resource in updated.spritesheets || + resource in updated.levels || + resource == updated.sound - val table = table { - header { - row("Category", "Resource") - } - body { - var index = 0 - categories.forEach { (categorie, resources) -> - resources.forEachIndexed { rindex, resource -> - row { - val isSelected = selectedIndex == index - cell(if (isSelected) TextStyles.bold(categorie) else categorie) - val lineContent = "$rindex $resource" - val content = if (isSelected) { - TextStyles.bold(lineContent) - } else { - lineContent - } - cell(content) { - columnSpan = 4 - } - - index++ - } - } - } + if (!found) { + echo("❌ Resource not found: $resource") + continue } + + updated = updated.copy( + scripts = updated.scripts.filter { it != resource }, + spritesheets = updated.spritesheets.filter { it != resource }, + levels = updated.levels.filter { it != resource }, + sound = if (updated.sound == resource) null else updated.sound, + ) + echo("✅ Removed: $resource") } - currentContext.terminal.cursor.move { - clearScreen() + try { + updated.write(configFile) + } catch (e: Exception) { + echo("❌ Error saving _tiny.json: ${e.message}") + throw Abort() } - echo(table) - echo("\nCommands:") - echo("• Enter category number to manage resources") - echo("• 'q' to quit") - echo("• 'd' to delete from the game") + + echo() + displayResources(updated) + } + + private fun displayResources(params: GameParametersV1) { + echo("📦 Game resources") + echo() + + displayCategory("📝 Scripts", params.scripts) + displayCategory("🖼️ Spritesheets", params.spritesheets) + displayCategory("🗺️ Levels", params.levels) + displayCategory("🔊 Sounds", listOfNotNull(params.sound)) } - private fun getResourceCategories(gameParameters: GameParametersV1): Map> { - return listOf( - "\uD83D\uDCDD scripts" to gameParameters.scripts, - "\uD83D\uDDBC\uFE0F spritesheets" to gameParameters.spritesheets, - "\uD83D\uDDFA\uFE0F levels" to gameParameters.levels, - "\uD83D\uDD08 sound" to listOfNotNull(gameParameters.sound), - ).filter { it.second.isNotEmpty() } - .toMap() + private fun displayCategory(label: String, resources: List) { + if (resources.isEmpty()) return + echo("$label:") + resources.forEachIndexed { index, resource -> + echo(" ${index + 1}. $resource") + } + echo() } } diff --git a/tiny-cli/src/main/kotlin/com/github/minigdx/tiny/cli/command/UpdateCommand.kt b/tiny-cli/src/main/kotlin/com/github/minigdx/tiny/cli/command/UpdateCommand.kt index b6e63314..f2108eef 100644 --- a/tiny-cli/src/main/kotlin/com/github/minigdx/tiny/cli/command/UpdateCommand.kt +++ b/tiny-cli/src/main/kotlin/com/github/minigdx/tiny/cli/command/UpdateCommand.kt @@ -3,184 +3,101 @@ package com.github.minigdx.tiny.cli.command import com.github.ajalt.clikt.core.Abort import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.Context -import com.github.ajalt.clikt.core.terminal import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.types.file -import com.github.ajalt.mordant.input.InputReceiver -import com.github.ajalt.mordant.input.receiveKeyEvents -import com.github.ajalt.mordant.rendering.TextStyles -import com.github.ajalt.mordant.table.table +import com.github.ajalt.clikt.parameters.types.int import com.github.minigdx.tiny.cli.command.utils.ColorUtils import com.github.minigdx.tiny.cli.config.GameParameters import com.github.minigdx.tiny.cli.config.GameParametersV1 import java.io.File class UpdateCommand : CliktCommand(name = "update") { - val gameDirectory by option("-d", "--directory", help = "The directory containing your game to be updated.") + private val gameDirectory by option("-d", "--directory", help = "The directory containing your game to be updated.") .file(mustExist = true, canBeDir = true, canBeFile = false) .default(File(".")) - override fun help(context: Context) = "Interactively view and update game parameters." + private val newZoom by option("--zoom", help = "Set the game zoom level (1-8).") + .int() - private var currentParameters: GameParametersV1? = null - private var selectedIndex = 0 - private val editableParameters = mutableListOf() + private val hideMouseCursor by option("--hide-cursor", help = "Hide the system mouse cursor.") + .flag() + + private val showMouseCursor by option("--show-cursor", help = "Show the system mouse cursor.") + .flag() + + override fun help(context: Context) = "View and update game parameters." override fun run() { val configFile = gameDirectory.resolve("_tiny.json") if (!configFile.exists()) { - echo("❌ No _tiny.json found in ${gameDirectory.absolutePath}! Can't update parameters without it.") + echo("❌ No _tiny.json found in ${gameDirectory.absolutePath}") throw Abort() } - try { - val gameParameters = GameParameters.read(configFile) - if (gameParameters !is GameParametersV1) { - echo("❌ Only GameParametersV1 is supported for updates.") - throw Abort() - } - - currentParameters = gameParameters - setupEditableParameters() - runInteractiveLoop(configFile) + val gameParameters = try { + GameParameters.read(configFile) } catch (e: Exception) { echo("❌ Error reading _tiny.json: ${e.message}") throw Abort() } - } - - private fun setupEditableParameters() { - val params = currentParameters ?: return - editableParameters.clear() - - // Add basic parameters - editableParameters.add(EditableParameter("name", params.name, false)) - editableParameters.add( - EditableParameter( - "resolution", - "${params.resolution.width}x${params.resolution.height}", - false, - ), - ) - editableParameters.add(EditableParameter("sprites", "${params.sprites.width}x${params.sprites.height}", false)) - editableParameters.add(EditableParameter("zoom", params.zoom.toString(), true)) - editableParameters.add(EditableParameter("palette", ColorUtils.formatCurrentPaletteDisplay(params.colors, maxColors = 16), false)) - editableParameters.add(EditableParameter("scripts", params.scripts.joinToString(", "), false)) - editableParameters.add(EditableParameter("spritesheets", params.spritesheets.joinToString(", "), false)) - editableParameters.add(EditableParameter("levels", params.levels.joinToString(", "), false)) - editableParameters.add(EditableParameter("sounds", listOfNotNull(params.sound).joinToString(", "), false)) - editableParameters.add(EditableParameter("hideMouseCursor", if (params.hideMouseCursor) "Yes" else "No", true)) - } - - private fun runInteractiveLoop(configFile: File) { - echo("🎮 Interactive Game Parameter Editor") - echo("Use ↑/↓ arrow keys to navigate, Enter to toggle values, 'q' to quit and save") - echo() - - displayParameters() - currentContext.terminal.receiveKeyEvents { event -> - val next = when (event.key) { - "ArrowUp" -> { - selectedIndex = (selectedIndex - 1).coerceAtLeast(0) - true - } - - "ArrowDown" -> { - selectedIndex = (selectedIndex + 1).coerceAtMost(editableParameters.lastIndex) - true - } - - "Enter" -> { - toggleParameter() - true - } - - "q" -> { - saveAndExit(configFile) - false - } - else -> true - } - if (next) { - displayParameters() - InputReceiver.Status.Continue - } else { - InputReceiver.Status.Finished - } - } - } - - private fun displayParameters() { - currentContext.terminal.cursor.move { - clearScreen() - } - - echo("🎮 Game Parameters for: ${currentParameters?.name}") - echo() - val table = table { - header { - row("Parameter", "Value", "Editable") - } - body { - editableParameters.forEachIndexed { index, param -> - val isSelected = index == selectedIndex - val paramName = if (isSelected) TextStyles.bold(param.name) else param.name - val paramValue = if (isSelected) TextStyles.bold(param.value) else param.value - val editable = if (param.isEditable) "✓" else "✗" - - row(paramName, paramValue, editable) - } - } + if (gameParameters !is GameParametersV1) { + echo("❌ Only V1 game configuration is supported.") + throw Abort() } - echo(table) - echo() - echo("Selected: ${editableParameters[selectedIndex].name}") - if (editableParameters[selectedIndex].isEditable) { - echo("Press Enter to toggle this value") + val hasUpdates = newZoom != null || hideMouseCursor || showMouseCursor + if (hasUpdates) { + applyUpdates(gameParameters, configFile) + } else { + displayParameters(gameParameters) } - echo("Press 'q' to quit and save changes") } - private fun toggleParameter() { - val param = editableParameters[selectedIndex] - if (!param.isEditable) { - echo("⚠️ This parameter is not editable") - return - } + private fun applyUpdates(params: GameParametersV1, configFile: File) { + var updated = params - when (param.name) { - "hideMouseCursor" -> { - val currentParams = currentParameters ?: return - val newValue = !currentParams.hideMouseCursor - currentParameters = currentParams.copy(hideMouseCursor = newValue) - param.value = if (newValue) "Yes" else "No" - echo("✅ ${param.name} toggled to: ${param.value}") + newZoom?.let { zoom -> + if (zoom !in 1..8) { + echo("❌ Zoom must be between 1 and 8.") + throw Abort() } + updated = updated.copy(zoom = zoom) + echo("✅ Zoom updated to $zoom") + } - "zoom" -> { - val currentParams = currentParameters ?: return - val newValue = ((currentParams.zoom + 1) % 9).coerceIn(1, 8) - param.value = newValue.toString() - currentParameters = currentParams.copy(zoom = newValue) - } + if (hideMouseCursor) { + updated = updated.copy(hideMouseCursor = true) + echo("✅ Mouse cursor hidden") + } else if (showMouseCursor) { + updated = updated.copy(hideMouseCursor = false) + echo("✅ Mouse cursor visible") } - } - private fun saveAndExit(configFile: File) { try { - currentParameters?.write(configFile) - echo("✅ Parameters saved successfully!") + updated.write(configFile) } catch (e: Exception) { echo("❌ Error saving parameters: ${e.message}") + throw Abort() } + + echo() + displayParameters(updated) } - private data class EditableParameter( - val name: String, - var value: String, - val isEditable: Boolean, - ) + private fun displayParameters(params: GameParametersV1) { + echo("🎮 ${params.name}") + echo() + echo("🖥 Resolution: ${params.resolution.width}x${params.resolution.height}") + echo("📐 Sprites: ${params.sprites.width}x${params.sprites.height}") + echo("🔍 Zoom: ${params.zoom}") + echo("🎨 Palette: ${ColorUtils.formatCurrentPaletteDisplay(params.colors, maxColors = 16)}") + echo("📝 Scripts: ${params.scripts.joinToString(", ").ifEmpty { "none" }}") + echo("🖼️ Spritesheets: ${params.spritesheets.joinToString(", ").ifEmpty { "none" }}") + echo("🗺️ Levels: ${params.levels.joinToString(", ").ifEmpty { "none" }}") + echo("🔊 Sounds: ${listOfNotNull(params.sound).joinToString(", ").ifEmpty { "none" }}") + echo("🖱️ Hide mouse cursor: ${if (params.hideMouseCursor) "yes" else "no"}") + } } From 48858b915a2db2916d1cc32365e403deac2825db Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Mon, 10 Nov 2025 09:38:51 +0100 Subject: [PATCH 002/172] Fix issue with possible invalid color index --- .../tiny/graphic/FrameBufferParameters.kt | 34 +++---------------- .../com/github/minigdx/tiny/lua/ShapeLib.kt | 10 +++--- .../com/github/minigdx/tiny/lua/StdLib.kt | 5 +-- .../tiny/render/DefaultVirtualFrameBuffer.kt | 12 +++---- 4 files changed, 20 insertions(+), 41 deletions(-) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/FrameBufferParameters.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/FrameBufferParameters.kt index 28b42d4d..a845dabd 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/FrameBufferParameters.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/FrameBufferParameters.kt @@ -10,11 +10,6 @@ class Blender(internal val gamePalette: ColorPalette) { internal var dithering: Int = 0xFFFF - internal val hasDithering: Boolean - get() { - return dithering != 0xFFFF - } - fun palette(): Array { fun cache(): Array { val copyOf = switch.copyOf() @@ -24,6 +19,11 @@ class Blender(internal val gamePalette: ColorPalette) { return cachedPaletteReference ?: cache() } + fun color(color: ColorIndex): ColorIndex { + val currentPalette = palette() + return currentPalette[color % currentPalette.size] + } + fun dither(pattern: Int): Int { val prec = dithering dithering = pattern and 0xFFFF @@ -42,30 +42,6 @@ class Blender(internal val gamePalette: ColorPalette) { cachedPaletteReference = null switch[gamePalette.check(source)] = gamePalette.check(target) } - - fun mix( - colors: ByteArray, - x: Pixel, - y: Pixel, - transparency: Array?, - ): ByteArray? { - fun dither(pattern: Int): Boolean { - val a = x % 4 - val b = (y % 4) * 4 - - return (pattern shr (15 - (a + b))) and 0x01 == 0x01 - } - - val color = gamePalette.check(colors[0].toInt()) - colors[0] = switch[color].toByte() - // Return null if transparent - if (transparency == null && gamePalette.isTransparent(colors[0].toInt())) return null - return if (!dither(dithering)) { - null - } else { - colors - } - } } class Camera() { diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/ShapeLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/ShapeLib.kt index aade6249..37503705 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/ShapeLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/ShapeLib.kt @@ -59,10 +59,11 @@ private class Shape(private val gameOptions: GameOptions) { } private fun LuaValue.checkColorIndex(): Int { + val colors = gameOptions.colors() return if (this.isnumber()) { - this.checkint() + colors.check(this.checkint()) } else { - gameOptions.colors().getColorIndex(this.checkjstring()!!) + colors.getColorIndex(this.checkjstring()!!) } } } @@ -167,10 +168,11 @@ class ShapeLib( } private fun LuaValue.checkColorIndex(): Int { + val colors = gameOptions.colors() return if (this.isnumber()) { - this.checkint() + colors.check(this.checkint()) } else { - gameOptions.colors().getColorIndex(this.checkjstring()!!) + colors.getColorIndex(this.checkjstring()!!) } } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/StdLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/StdLib.kt index 4cdc41a6..78421e5b 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/StdLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/StdLib.kt @@ -304,10 +304,11 @@ class StdLib( } private fun LuaValue.checkColorIndex(): Int { + val colors = gameOptions.colors() return if (this.isnumber()) { - this.checkint() + colors.check(this.checkint()) } else { - gameOptions.colors().getColorIndex(this.checkjstring()!!) + colors.getColorIndex(this.checkjstring()!!) } } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/DefaultVirtualFrameBuffer.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/DefaultVirtualFrameBuffer.kt index 7c8ef024..f39628fb 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/DefaultVirtualFrameBuffer.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/DefaultVirtualFrameBuffer.kt @@ -182,7 +182,7 @@ class DefaultVirtualFrameBuffer( invalidateCachedReadFrame() updateDepthIndex(source) - val palColor = parameters.blender.palette()[color] + val palColor = parameters.blender.color(color) val key = spriteBatchManager.createKey() key.set( @@ -258,7 +258,7 @@ class DefaultVirtualFrameBuffer( width, height, filled = filled, - color = parameters.blender.palette()[color], + color = parameters.blender.color(color), dither = parameters.blender.dithering, depth = currentDepth, ) @@ -285,7 +285,7 @@ class DefaultVirtualFrameBuffer( y1 - parameters.camera.y, x2 - parameters.camera.x, y2 - parameters.camera.y, - color = parameters.blender.palette()[color], + color = parameters.blender.color(color), dither = parameters.blender.dithering, depth = currentDepth, ) @@ -307,7 +307,7 @@ class DefaultVirtualFrameBuffer( centerY - parameters.camera.y, radius, filled = filled, - color = parameters.blender.palette()[color], + color = parameters.blender.color(color), dither = parameters.blender.dithering, depth = currentDepth, ) @@ -325,7 +325,7 @@ class DefaultVirtualFrameBuffer( val instance = primitiveBatchManager.createInstance().setPoint( x - parameters.camera.x, y - parameters.camera.y, - color = parameters.blender.palette()[color], + color = parameters.blender.color(color), dither = parameters.blender.dithering, depth = currentDepth, ) @@ -362,7 +362,7 @@ class DefaultVirtualFrameBuffer( y2 - parameters.camera.y, x3 - parameters.camera.x, y3 - parameters.camera.y, - parameters.blender.palette()[color], + parameters.blender.color(color), parameters.blender.dithering, filled, depth = currentDepth, From d3e263158f614f72083c39dd53ee0306a527f8f6 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Mon, 10 Nov 2025 23:49:10 +0100 Subject: [PATCH 003/172] Fix the screenshot feature --- .../github/minigdx/tiny/engine/GameEngine.kt | 6 +++ .../github/minigdx/tiny/platform/Platform.kt | 6 +++ .../tiny/platform/glfw/GlfwPlatform.kt | 41 ++++++++++--------- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt index bbeef9d2..753a886e 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt @@ -31,6 +31,7 @@ class GameEngine( private var accumulator: Seconds = 0f private var currentFrame: Long = 0L + private var previousFrame: Long = 0L private var currentMetrics: PerformanceMetrics? = null lateinit var inputHandler: InputHandler @@ -139,6 +140,11 @@ class GameEngine( // Complete frame monitoring and get metrics currentMetrics = performanceMonitor.frameEnd() + + if(currentFrame != previousFrame) { + platform.newFrameRendered(virtualFrameBuffer) + previousFrame = currentFrame + } } private suspend fun advanceEngineScript() { diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/platform/Platform.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/platform/Platform.kt index edc829a6..43a52fd8 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/platform/Platform.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/platform/Platform.kt @@ -7,6 +7,7 @@ import com.github.minigdx.tiny.file.SourceStream import com.github.minigdx.tiny.input.InputHandler import com.github.minigdx.tiny.input.InputManager import com.github.minigdx.tiny.platform.performance.PerformanceMonitor +import com.github.minigdx.tiny.render.VirtualFrameBuffer import com.github.minigdx.tiny.sound.SoundManager import kotlinx.coroutines.CoroutineDispatcher @@ -121,4 +122,9 @@ interface Platform { ) fun saveWave(sound: FloatArray) + + /** + * Notify that a new frame has been rendered + */ + fun newFrameRendered(virtualFrameBuffer: VirtualFrameBuffer) = Unit } diff --git a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt index 936df01b..5ca5b4a3 100644 --- a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt +++ b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt @@ -10,6 +10,7 @@ import com.github.minigdx.tiny.file.InputStreamStream import com.github.minigdx.tiny.file.SoundDataSourceStream import com.github.minigdx.tiny.file.SourceStream import com.github.minigdx.tiny.file.VirtualFileSystem +import com.github.minigdx.tiny.graphic.PixelArray import com.github.minigdx.tiny.graphic.PixelFormat.RGBA import com.github.minigdx.tiny.input.InputHandler import com.github.minigdx.tiny.input.InputManager @@ -19,6 +20,7 @@ import com.github.minigdx.tiny.platform.Platform import com.github.minigdx.tiny.platform.SoundData import com.github.minigdx.tiny.platform.WindowManager import com.github.minigdx.tiny.platform.performance.PerformanceMonitor +import com.github.minigdx.tiny.render.VirtualFrameBuffer import com.github.minigdx.tiny.sound.BITS_PER_SAMPLE import com.github.minigdx.tiny.sound.CHANNELS import com.github.minigdx.tiny.sound.IS_BIG_ENDIAN @@ -70,7 +72,7 @@ class GlfwPlatform( // Keep 30 seconds at 60 frames per seconds private val gifFrameCache: MutableFixedSizeList = MutableFixedSizeList(gameOptions.record.toInt() * FPS) - private var lastDraw: ByteArray? = null + private var lastDraw: PixelArray = PixelArray(gameOptions.width, gameOptions.height) private val lwjglInputHandler = LwjglInput(gameOptions) @@ -208,30 +210,31 @@ class GlfwPlatform( override fun endGameLoop() = Unit + override fun newFrameRendered(virtualFrameBuffer: VirtualFrameBuffer) { + virtualFrameBuffer.readFrameBuffer().copyInto(lastDraw) + } + override fun record() { val origin = newFile("video", "gif") logger.info("GLWF") { "Starting to generate GIF in '${origin.absolutePath}' (Wait for it...)" } - val buffer = - mutableListOf().apply { - addAll(gifFrameCache) - } + val buffer = mutableListOf().apply { + addAll(gifFrameCache) + } recordScope.launch { val now = System.currentTimeMillis() - val options = - ImageOptions().apply { - this.setDelay(20, TimeUnit.MILLISECONDS) - } + val options = ImageOptions().apply { + this.setDelay(20, TimeUnit.MILLISECONDS) + } ByteArrayOutputStream().use { out -> - val encoder = - FastGifEncoder( - out, - gameOptions.width, - gameOptions.height, - 0, - gameOptions.colors(), - ) + val encoder = FastGifEncoder( + out, + gameOptions.width, + gameOptions.height, + 0, + gameOptions.colors(), + ) buffer.forEach { img -> encoder.addImage(img, gameOptions.width, options) @@ -254,7 +257,7 @@ class GlfwPlatform( if (index >= 999) { throw IllegalStateException( "Too many file '${prefixName}_xxx.$extension' generated! " + - "You might need to delete some", + "You might need to delete some", ) } origin = gameDirectory.resolve("${prefixName}_${index.toString().padStart(3, '0')}.$extension") @@ -263,7 +266,7 @@ class GlfwPlatform( } override fun screenshot() { - val buffer = lastDraw ?: return + val buffer = lastDraw.pixels.copyOf() recordScope.launch { writeImage(buffer) From b24f98cb906694b3a91a34b3d012f605ef8d54cc Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Tue, 11 Nov 2025 15:17:17 +0100 Subject: [PATCH 004/172] Bug fix in the "new" lua method with nested table as default arguments. --- .../github/minigdx/tiny/engine/GameOptions.kt | 6 +- .../com/github/minigdx/tiny/lua/StdLib.kt | 41 ++++--- .../com/github/minigdx/tiny/lua/GfxLibTest.kt | 109 ------------------ .../com/github/minigdx/tiny/lua/StdLibTest.kt | 74 ++++++++++++ 4 files changed, 99 insertions(+), 131 deletions(-) delete mode 100644 tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/GfxLibTest.kt create mode 100644 tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/StdLibTest.kt diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameOptions.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameOptions.kt index ef6dd2c3..21f5bf70 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameOptions.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameOptions.kt @@ -9,9 +9,9 @@ import com.github.minigdx.tiny.input.Vector2 data class GameOptions( val width: Pixel, val height: Pixel, - val palette: List, - val gameScripts: List, - val spriteSheets: List, + val palette: List = emptyList(), + val gameScripts: List = emptyList(), + val spriteSheets: List = emptyList(), val gameLevels: List = emptyList(), val sound: String? = null, val zoom: Int = 2, diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/StdLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/StdLib.kt index 78421e5b..46826467 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/StdLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/StdLib.kt @@ -38,7 +38,7 @@ class StdLib( @TinyFunction( "Create new instance of a class by creating a new table and setting the metatable. " + - "It allow to create kind of Object Oriented Programming.\n\n ", + "It allow to create kind of Object Oriented Programming.\n\n ", example = STD_NEW_EXAMPLE, ) inner class new : TwoArgFunction() { @@ -54,12 +54,11 @@ class StdLib( @TinyArg("class", type = LuaType.TABLE) arg1: LuaValue, @TinyArg("default", type = LuaType.TABLE) arg2: LuaValue, ): LuaValue { - val default = - if (arg2.istable()) { - arg2.checktable()!!.deepCopy() - } else { - LuaTable() - } + val default = if (arg2.istable()) { + arg2.checktable()!!.deepCopy() + } else { + LuaTable() + } val reference = arg1.checktable()!!.deepCopy() default.setmetatable(reference) reference.rawset("__index", reference) @@ -70,14 +69,18 @@ class StdLib( val result = LuaTable() this.keys().forEach { key -> var value = this[key] - value = - if (value.istable()) { - value.checktable()!!.deepCopy() - } else { - value - } + value = if (value.istable()) { + value.checktable()!!.deepCopy() + } else { + value + } result[key] = value } + // Preserve the metatable during deep copy + val metatable = this.getmetatable() + if (metatable != null) { + result.setmetatable(metatable) + } return result } } @@ -135,9 +138,9 @@ class StdLib( @TinyFunction( "Iterate over values of a table.\n\n" + - "- If you want to iterate over keys, use `pairs(table)`.\n " + - "- If you want to iterate over index, use `ipairs(table)`.\n " + - "- If you want to iterate in reverse, use `rpairs(table)`.\n", + "- If you want to iterate over keys, use `pairs(table)`.\n " + + "- If you want to iterate over index, use `ipairs(table)`.\n " + + "- If you want to iterate in reverse, use `rpairs(table)`.\n", ) internal inner class all : VarArgFunction() { @TinyCall("Iterate over the values of the table") @@ -172,9 +175,9 @@ class StdLib( @TinyFunction( "Iterate over values of a table in reverse order. " + - "The iterator return an index and the value. " + - "The method is useful to remove elements from a table while " + - "iterating on it.", + "The iterator return an index and the value. " + + "The method is useful to remove elements from a table while " + + "iterating on it.", example = STD_RPAIRS_EXAMPLE, ) internal inner class rpairs : VarArgFunction() { diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/GfxLibTest.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/GfxLibTest.kt deleted file mode 100644 index ad507ef1..00000000 --- a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/GfxLibTest.kt +++ /dev/null @@ -1,109 +0,0 @@ -package com.github.minigdx.tiny.lua - -class GfxLibTest { - // FIXME: - - /* - private val frameBuffer = FrameBuffer(10, 10, ColorPalette(listOf("#FFFFFF"))) - - private val gameResourceAccess = - mock { - every { frameBuffer } returns this@GfxLibTest.frameBuffer - every { addOp(any()) } returns Unit - } - - private val gameOptions = GameOptions(10, 10, listOf("#FFFFFF"), listOf("game.lua"), emptyList()) - - @Test - fun it_sets_the_clip() { - val clip = GfxLib(gameResourceAccess, gameOptions).clip() - clip.call(valueOf(1), valueOf(2), valueOf(3), valueOf(4)) - - assertEquals(1, frameBuffer.clipper.left) - assertEquals(2, frameBuffer.clipper.top) - assertEquals(1 + 3, frameBuffer.clipper.right) - assertEquals(2 + 4, frameBuffer.clipper.bottom) - } - - @Test - fun it_sets_the_dither() { - val dither = GfxLib(gameResourceAccess, gameOptions).dither() - - dither.call(valueOf(0xA5A5)) - - val a = frameBuffer.blender.mix(byteArrayOf(1), 0, 0, null)?.get(0) ?: 0 - val b = frameBuffer.blender.mix(byteArrayOf(1), 1, 0, null)?.get(0) ?: 0 - val c = frameBuffer.blender.mix(byteArrayOf(1), 0, 1, null)?.get(0) ?: 0 - val d = frameBuffer.blender.mix(byteArrayOf(1), 1, 1, null)?.get(0) ?: 0 - - assertEquals(1, a) - assertEquals(0, b) - assertEquals(0, c) - assertEquals(1, d) - } - - @Test - fun it_sets_the_dither_full_pattern() { - val dither = GfxLib(gameResourceAccess, gameOptions).dither() - - /** - * 1000 -> 8 - * 1100 -> 8 + 4 = 12 = C - * 0010 -> 2 - * 0001 -> 1 - * - * --> 8C21 - */ - dither.call(valueOf(0x8C21)) - - val result = Array(4 * 4) { 0x01 } - for (x in 0 until 4) { - for (y in 0 until 4) { - val index = x + y * 4 - val r = frameBuffer.blender.mix(byteArrayOf(result[index]), x, y, null)?.get(0) ?: 0 - result[index] = r - } - } - - val expected = Array(4 * 4) { i -> - if (i == 0 || i == 4 || i == 5 || i == 10 || i == 15) { - 0x01 - } else { - 0x00 - } - } - result.forEachIndexed { index, value -> - assertEquals(value, expected[index]) - } - } - - @Test - fun it_sets_the_dither_pattern_no_effect() { - val dither = GfxLib(gameResourceAccess, gameOptions).dither() - - dither.call(valueOf(0xFFFF)) - - val a = frameBuffer.blender.mix(byteArrayOf(1), 0, 0, null)?.get(0) ?: 0 - val b = frameBuffer.blender.mix(byteArrayOf(1), 1, 0, null)?.get(0) ?: 0 - val c = frameBuffer.blender.mix(byteArrayOf(1), 0, 1, null)?.get(0) ?: 0 - val d = frameBuffer.blender.mix(byteArrayOf(1), 1, 1, null)?.get(0) ?: 0 - - assertEquals(1, a) - assertEquals(1, b) - assertEquals(1, c) - assertEquals(1, d) - } - - @Test - fun it_reset_the_clip() { - val clip = GfxLib(gameResourceAccess, gameOptions).clip() - clip.call() - - assertEquals(0, frameBuffer.clipper.left) - assertEquals(0, frameBuffer.clipper.top) - assertEquals(10, frameBuffer.clipper.right) - assertEquals(10, frameBuffer.clipper.bottom) - } - - */ -} diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/StdLibTest.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/StdLibTest.kt new file mode 100644 index 00000000..e9667fc0 --- /dev/null +++ b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/StdLibTest.kt @@ -0,0 +1,74 @@ +package com.github.minigdx.tiny.lua + +import com.github.minigdx.tiny.engine.GameOptions +import com.github.minigdx.tiny.engine.GameResourceAccess +import com.github.minigdx.tiny.render.VirtualFrameBuffer +import dev.mokkery.mock +import org.luaj.vm2.LuaTable +import org.luaj.vm2.LuaValue.Companion.valueOf +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class StdLibTest { + + val gameOptions = GameOptions(100, 100) + val gameResourceAccess = mock { } + val virtualFrameBuffer = mock { } + val lib = StdLib(gameOptions, gameResourceAccess, virtualFrameBuffer) + + @Test + fun `new should create instance with access to class fields`() { + val newFunc = lib.new() + + // Create a class table with active = true + val classTable = LuaTable() + classTable.set("active", valueOf(true)) + + // Create instance with one argument + val instance = newFunc.call(classTable) + + // Instance should have access to 'active' field via metatable lookup + val activeValue = instance.get("active") + assertTrue(activeValue.toboolean(), "Instance should have access to 'active' field from class") + } + + @Test + fun `new with two arguments should override class fields`() { + val newFunc = lib.new() + + // Create a class table with active = true + val classTable = LuaTable() + classTable.set("active", valueOf(true)) + + // Create instance with one argument + val default = LuaTable() + default.set("active", valueOf("test")) + val instance = newFunc.call(classTable, default) + + // Instance should have access to 'active' field via metatable lookup + val activeValue = instance.get("active") + assertEquals("test", activeValue.tojstring()) + } + + @Test + fun `new should keep instance neested instance`() { + val newFunc = lib.new() + + // Create a class table with active = true + val classTable = LuaTable() + classTable.set("active", valueOf(true)) + + // Create instance with one argument + val childInstance = newFunc.call(classTable) + + val classPlayerTable = LuaTable() + val defaultPlayer = LuaTable() + defaultPlayer.set("child", childInstance) + + val instance = newFunc.call(classPlayerTable, defaultPlayer) + // Instance should have access to 'active' field via metatable lookup + val activeValue = instance.get("child").get("active") + assertTrue(activeValue.toboolean(), "Nested instance should preserve metatable and access 'active' field") + } +} \ No newline at end of file From db3946c48d239372578a632a58b404cf38398a05 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Fri, 14 Nov 2025 22:39:21 +0100 Subject: [PATCH 005/172] Fix shutdown when stopping the game --- .../github/minigdx/tiny/cli/command/RunCommand.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tiny-cli/src/main/kotlin/com/github/minigdx/tiny/cli/command/RunCommand.kt b/tiny-cli/src/main/kotlin/com/github/minigdx/tiny/cli/command/RunCommand.kt index 7c1440fc..10a8db08 100644 --- a/tiny-cli/src/main/kotlin/com/github/minigdx/tiny/cli/command/RunCommand.kt +++ b/tiny-cli/src/main/kotlin/com/github/minigdx/tiny/cli/command/RunCommand.kt @@ -39,6 +39,7 @@ import kotlinx.coroutines.launch import kotlinx.serialization.json.Json import org.luaj.vm2.LuaError import java.io.File +import kotlin.system.exitProcess import kotlin.time.ExperimentalTime class RunCommand : CliktCommand(name = "run") { @@ -101,7 +102,7 @@ class RunCommand : CliktCommand(name = "run") { val debugCommandReceiver = Channel() val engineCommandSender = Channel() - embeddedServer( + val server = embeddedServer( factory = Netty, port = debug, ) { @@ -187,6 +188,12 @@ class RunCommand : CliktCommand(name = "run") { ) gameEngine.main() + + // Clean shutdown: stop the debug server and exit + echo("\uD83D\uDEE1️ Shutting down debug server...") + server.stop(1000, 2000) + echo("✅ Debug server stopped. Exiting...") + exitProcess(0) } catch (ex: Exception) { echo( "\uD83E\uDDE8 An unexpected exception occurred. " + @@ -205,6 +212,11 @@ class RunCommand : CliktCommand(name = "run") { } echo() ex.printStackTrace() + + // Clean shutdown even on exception + echo("\uD83D\uDEE1️ Shutting down debug server...") + server.stop(1000, 2000) + exitProcess(1) } } } From d76bd570b26f056ee0761b293560b0a156d1d904 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Fri, 14 Nov 2025 22:40:42 +0100 Subject: [PATCH 006/172] lintfix --- .../com/github/minigdx/tiny/engine/GameEngine.kt | 2 +- .../kotlin/com/github/minigdx/tiny/lua/StdLib.kt | 14 +++++++------- .../minigdx/tiny/platform/glfw/GlfwPlatform.kt | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt index 753a886e..29b21807 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt @@ -141,7 +141,7 @@ class GameEngine( // Complete frame monitoring and get metrics currentMetrics = performanceMonitor.frameEnd() - if(currentFrame != previousFrame) { + if (currentFrame != previousFrame) { platform.newFrameRendered(virtualFrameBuffer) previousFrame = currentFrame } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/StdLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/StdLib.kt index 46826467..97580059 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/StdLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/StdLib.kt @@ -38,7 +38,7 @@ class StdLib( @TinyFunction( "Create new instance of a class by creating a new table and setting the metatable. " + - "It allow to create kind of Object Oriented Programming.\n\n ", + "It allow to create kind of Object Oriented Programming.\n\n ", example = STD_NEW_EXAMPLE, ) inner class new : TwoArgFunction() { @@ -138,9 +138,9 @@ class StdLib( @TinyFunction( "Iterate over values of a table.\n\n" + - "- If you want to iterate over keys, use `pairs(table)`.\n " + - "- If you want to iterate over index, use `ipairs(table)`.\n " + - "- If you want to iterate in reverse, use `rpairs(table)`.\n", + "- If you want to iterate over keys, use `pairs(table)`.\n " + + "- If you want to iterate over index, use `ipairs(table)`.\n " + + "- If you want to iterate in reverse, use `rpairs(table)`.\n", ) internal inner class all : VarArgFunction() { @TinyCall("Iterate over the values of the table") @@ -175,9 +175,9 @@ class StdLib( @TinyFunction( "Iterate over values of a table in reverse order. " + - "The iterator return an index and the value. " + - "The method is useful to remove elements from a table while " + - "iterating on it.", + "The iterator return an index and the value. " + + "The method is useful to remove elements from a table while " + + "iterating on it.", example = STD_RPAIRS_EXAMPLE, ) internal inner class rpairs : VarArgFunction() { diff --git a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt index 5ca5b4a3..ce314dc1 100644 --- a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt +++ b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt @@ -257,7 +257,7 @@ class GlfwPlatform( if (index >= 999) { throw IllegalStateException( "Too many file '${prefixName}_xxx.$extension' generated! " + - "You might need to delete some", + "You might need to delete some", ) } origin = gameDirectory.resolve("${prefixName}_${index.toString().padStart(3, '0')}.$extension") From 921696f25d4f2cf365d94f684cef8839fc23e35d Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Wed, 19 Nov 2025 21:41:39 +0100 Subject: [PATCH 007/172] Bug fix the dithering pattern. --- .../github/minigdx/tiny/render/gl/PrimitiveBatchStage.kt | 8 ++++---- .../com/github/minigdx/tiny/render/gl/SpriteBatchStage.kt | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/gl/PrimitiveBatchStage.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/gl/PrimitiveBatchStage.kt index 3701c3bc..45e39dd1 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/gl/PrimitiveBatchStage.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/gl/PrimitiveBatchStage.kt @@ -351,10 +351,10 @@ class PrimitiveBatchStage( int a = imod(x, 4); int b = imod(y, 4) * 4; int bitPosition = a + b; - - float powerOfTwo = pow(2.0, float(bitPosition)); - int bit = int(floor(mod(float(pattern) / powerOfTwo, 2.0))); - + + // Use bitwise shift to extract bit at position + int bit = (pattern >> bitPosition) & 1; + return bit > 0; } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/gl/SpriteBatchStage.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/gl/SpriteBatchStage.kt index bf242302..7d28f23d 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/gl/SpriteBatchStage.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/gl/SpriteBatchStage.kt @@ -186,9 +186,9 @@ class SpriteBatchStage( int b = imod(y, 4) * 4; int bitPosition = a + b; - float powerOfTwo = pow(2.0, float(bitPosition)); - int bit = int(floor(mod(float(pattern) / powerOfTwo, 2.0))); - + // Use bitwise shift to extract bit at position + int bit = (pattern >> bitPosition) & 1; + return bit > 0; } From 720a18f25d2b0dd5e9b0f098ddf69f6c903567a2 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Wed, 19 Nov 2025 21:53:53 +0100 Subject: [PATCH 008/172] lintfix --- .../kotlin/com/github/minigdx/tiny/lua/StdLibTest.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/StdLibTest.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/StdLibTest.kt index e9667fc0..7cb77293 100644 --- a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/StdLibTest.kt +++ b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/StdLibTest.kt @@ -11,7 +11,6 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue class StdLibTest { - val gameOptions = GameOptions(100, 100) val gameResourceAccess = mock { } val virtualFrameBuffer = mock { } @@ -71,4 +70,4 @@ class StdLibTest { val activeValue = instance.get("child").get("active") assertTrue(activeValue.toboolean(), "Nested instance should preserve metatable and access 'active' field") } -} \ No newline at end of file +} From 36b3553be7575c32082e644eaf47f007515318c1 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Sun, 7 Dec 2025 11:02:09 +0100 Subject: [PATCH 009/172] Increase max BPM and fix max beat in the SFX Editor --- tiny-cli/mockup-sfx2.aseprite | Bin 12455 -> 26659 bytes tiny-cli/sfx-spritesheet.aseprite | Bin 3352 -> 3375 bytes .../src/main/resources/sfx/sfx-editor.ldtk | 144 +++++++----------- .../src/main/resources/sfx/sfx-editor.lua | 7 +- .../main/resources/sfx/sfx-spritesheet.png | Bin 4140 -> 4175 bytes 5 files changed, 57 insertions(+), 94 deletions(-) diff --git a/tiny-cli/mockup-sfx2.aseprite b/tiny-cli/mockup-sfx2.aseprite index 913749a35cb2958e59c708903821c345e69aefc3..9b27b986f0f5fa1eb7477d26c638b93d758f0c5d 100644 GIT binary patch delta 8101 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 TNO0VfCOALs++-$wCBptM<(W1P delta 2265 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 From e19d0eadd5607482f61148cded05710dd7784701 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Sun, 1 Feb 2026 00:07:13 +0100 Subject: [PATCH 010/172] Start to add music editor --- .../tiny_doc_annotations_js_DEV_SNAPSHOT.xml | 4 +- .../tiny_doc_annotations_jvm_DEV_SNAPSHOT.xml | 4 +- .../artifacts/tiny_engine_js_DEV_SNAPSHOT.xml | 4 +- .../tiny_engine_jvm_DEV_SNAPSHOT.xml | 4 +- .../tiny_web_editor_js_DEV_SNAPSHOT.xml | 4 +- .../tiny_web_editor_jvm_DEV_SNAPSHOT.xml | 4 +- .idea/gradle.xml | 1 - tiny-cli/mockup-sfx2.aseprite | Bin 26659 -> 27291 bytes tiny-cli/sfx-spritesheet.aseprite | Bin 3375 -> 3694 bytes .../src/main/resources/sfx/music-editor.lua | 303 +- .../src/main/resources/sfx/sfx-editor.ldtk | 2852 ++++++++++++++++- .../main/resources/sfx/sfx-spritesheet.png | Bin 4175 -> 4397 bytes .../main/resources/sfx/widgets/ModeSwitch.lua | 2 +- 13 files changed, 2870 insertions(+), 312 deletions(-) diff --git a/.idea/artifacts/tiny_doc_annotations_js_DEV_SNAPSHOT.xml b/.idea/artifacts/tiny_doc_annotations_js_DEV_SNAPSHOT.xml index 7e7d67e4..296595ef 100644 --- a/.idea/artifacts/tiny_doc_annotations_js_DEV_SNAPSHOT.xml +++ b/.idea/artifacts/tiny_doc_annotations_js_DEV_SNAPSHOT.xml @@ -1,8 +1,6 @@ $PROJECT_DIR$/tiny-doc-annotations/build/libs - - - + \ No newline at end of file diff --git a/.idea/artifacts/tiny_doc_annotations_jvm_DEV_SNAPSHOT.xml b/.idea/artifacts/tiny_doc_annotations_jvm_DEV_SNAPSHOT.xml index 6ef44cdc..cb421545 100644 --- a/.idea/artifacts/tiny_doc_annotations_jvm_DEV_SNAPSHOT.xml +++ b/.idea/artifacts/tiny_doc_annotations_jvm_DEV_SNAPSHOT.xml @@ -1,8 +1,6 @@ $PROJECT_DIR$/tiny-doc-annotations/build/libs - - - + \ No newline at end of file diff --git a/.idea/artifacts/tiny_engine_js_DEV_SNAPSHOT.xml b/.idea/artifacts/tiny_engine_js_DEV_SNAPSHOT.xml index ae061d86..95757cd2 100644 --- a/.idea/artifacts/tiny_engine_js_DEV_SNAPSHOT.xml +++ b/.idea/artifacts/tiny_engine_js_DEV_SNAPSHOT.xml @@ -1,8 +1,6 @@ $PROJECT_DIR$/tiny-engine/build/libs - - - + \ No newline at end of file diff --git a/.idea/artifacts/tiny_engine_jvm_DEV_SNAPSHOT.xml b/.idea/artifacts/tiny_engine_jvm_DEV_SNAPSHOT.xml index 81bd4b18..594823a2 100644 --- a/.idea/artifacts/tiny_engine_jvm_DEV_SNAPSHOT.xml +++ b/.idea/artifacts/tiny_engine_jvm_DEV_SNAPSHOT.xml @@ -1,8 +1,6 @@ $PROJECT_DIR$/tiny-engine/build/libs - - - + \ No newline at end of file diff --git a/.idea/artifacts/tiny_web_editor_js_DEV_SNAPSHOT.xml b/.idea/artifacts/tiny_web_editor_js_DEV_SNAPSHOT.xml index ab61da67..bd1a1072 100644 --- a/.idea/artifacts/tiny_web_editor_js_DEV_SNAPSHOT.xml +++ b/.idea/artifacts/tiny_web_editor_js_DEV_SNAPSHOT.xml @@ -1,8 +1,6 @@ $PROJECT_DIR$/tiny-web-editor/build/libs - - - + \ No newline at end of file diff --git a/.idea/artifacts/tiny_web_editor_jvm_DEV_SNAPSHOT.xml b/.idea/artifacts/tiny_web_editor_jvm_DEV_SNAPSHOT.xml index f40eb118..e6a035d4 100644 --- a/.idea/artifacts/tiny_web_editor_jvm_DEV_SNAPSHOT.xml +++ b/.idea/artifacts/tiny_web_editor_jvm_DEV_SNAPSHOT.xml @@ -1,8 +1,6 @@ $PROJECT_DIR$/tiny-web-editor/build/libs - - - + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 6238726f..b2960349 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -12,7 +12,6 @@