Undo crashes Aseprite when using my script

Hello, I’m really new to the api but I’ve got a script working somewhat, the only thing is that undo crashes Aseprite. I’ve tried to make something like Viza74’s C64 Multicolor Check but that it instead replaces and edits colors live. So if someone could take a look at my script that would be awesome! Sorry that there aren’t any comments yet.

Make a new 160x200 sprite, change the color mode to indexed and the background to black, run the script below, draw a pixel and try undoing.

local function checkPrerequesities()
    local sprite = app.activeSprite
    if (not sprite) then
        app.alert("There is no active sprite!")

        return false
    end

    local spriteCellWidth = sprite.width / 4
    local spriteCellHeight = sprite.height / 8
    if spriteCellWidth - math.floor(spriteCellWidth) ~= 0 or spriteCellHeight - math.floor(spriteCellHeight) ~= 0 then
        app.alert("Invalid sprite dimensions, consider changing dimensions to: " ..
            math.floor(spriteCellWidth) * 4 ..
            "x" ..
            math.floor(spriteCellHeight) * 8 ..
            " or " .. math.ceil(spriteCellWidth) * 4 .. "x" .. math.ceil(spriteCellHeight) * 8 .. "!")

        return false
    end


    if (sprite.colorMode ~= ColorMode.INDEXED) then
        app.alert("The sprite should use indexed color mode!")

        return false
    end

    return true
end

local function tableCount(table)
    local count = 0
    for i in pairs(table) do
        count = count + 1
    end

    return count
end

local function getCells(image)
    local cells = {}
    local cellIndex = 0
    for cellY = 0, image.height / 8 - 1 do
        for cellX = 0, image.width / 4 - 1 do
            local cellPixels = {}
            local pixelIndex = 0
            for pixelY = 0, 7 do
                for pixelX = 0, 3 do
                    local color = image:getPixel(cellX * 4 + pixelX, cellY * 8 + pixelY)
                    cellPixels[pixelIndex] = {
                        position = {
                            x = pixelX,
                            y = pixelY,
                        },
                        color = color,
                    }
                    pixelIndex = pixelIndex + 1
                end
            end
            cells[cellIndex] = {
                position = {
                    x = cellX,
                    y = cellY,
                },
                pixels = cellPixels,
            }
            cellIndex = cellIndex + 1
        end
    end

    return cells
end

local function getDifferences(prevCells, newCells)
    local differences = {}
    local differenceIndex = 0
    for prevCellIndex, prevCell in pairs(prevCells) do
        for prevPixelIndex, prevPixel in pairs(prevCell.pixels) do
            if (newCells[prevCellIndex].pixels[prevPixelIndex].color ~= prevPixel.color) then
                differences[differenceIndex] = {
                    prevCell = prevCell,
                    newCell = newCells[prevCellIndex],

                    prevPixel = prevPixel,
                    newPixel = newCells[prevCellIndex].pixels[prevPixelIndex],
                }
                differenceIndex = differenceIndex + 1
            end
        end
    end

    return differences
end

local function computeDifferences(differences, image, backgroundColor)
    app.transaction(function()
        for differenceIndex, difference in pairs(differences) do
            local colors = {}
            for pixelIndex, pixel in pairs(difference.newCell.pixels) do
                if (not colors[pixel.color]) then
                    colors[pixel.color] = pixel.color
                end
            end

            if (tableCount(colors) > 3 and not colors[backgroundColor] or tableCount(colors) > 4 and colors[backgroundColor]) then
                if (difference.prevPixel.color ~= backgroundColor) then
                    for pixelIndex, pixel in pairs(difference.newCell.pixels) do
                        if (pixel.color == difference.prevPixel.color) then
                            image:putPixel(difference.newCell.position.x * 4 + pixel.position.x,
                                difference.newCell.position.y * 8 + pixel.position.y, difference.newPixel.color)
                        end
                    end
                else
                    image:putPixel(difference.newCell.position.x * 4 + difference.newPixel.position.x,
                        difference.newCell.position.y * 8 + difference.newPixel.position.y, backgroundColor)
                end
            end
        end

        app.activeLayer:cel(app.activeFrame).image = image
    end)
end

local function replaceBackgroundColor(colorToReplace, colorToReplaceWith)
    local image = Image(app.activeSprite.spec)
    image:drawSprite(app.activeSprite, app.activeFrame)
    for pixelY = 0, image.height - 1 do
        for pixelX = 0, image.width - 1 do
            local pixelColor = image:getPixel(pixelX, pixelY)
            if (pixelColor == colorToReplace) then
                image:drawPixel(pixelX, pixelY, colorToReplaceWith)
            end
        end
    end

    app.activeLayer:cel(app.activeFrame).image = image
    app.refresh()
end

local canRun = checkPrerequesities()
if (canRun) then
    local sprite = app.activeSprite

    local prevImage = Image(sprite.spec)
    prevImage:drawSprite(sprite, app.activeFrame)
    local prevCells = getCells(prevImage)

    local prevBackgroundColor = 0

    local dialog = Dialog("C64 Multicolor Live")
    dialog
        :color({ id = "backgroundColor", label = "Background Color", color = prevBackgroundColor, onchange = function() replaceBackgroundColor(prevBackgroundColor, dialog.data.backgroundColor.index) prevBackgroundColor = dialog.data.backgroundColor.index end})
        :show({ wait = false })

    sprite.events:on("change", function()
        local newImage = Image(sprite.spec)
        newImage:drawSprite(sprite, app.activeFrame)
        local newCells = getCells(newImage)

        local differences = getDifferences(prevCells, newCells)

        computeDifferences(differences, newImage, dialog.data.backgroundColor.index)
        local newCells = getCells(newImage)
        prevCells = newCells

        app.refresh()
    end)
end

Also when using Undo History instead of Ctrl+Z or Edit>Undo Aseprite never crashes for me.

I can add to this. I tried this script and its great for us C64 pixel artists! Live color clash checking. But unfortunately it crashes Aseprite without any error message. The app just disappears.
Two things makes it not crash.

  • Using the UNDO HISTORY instead. However it makes the undo history fill up in a weird way.
  • Checking ALLOW NON LINEAR HISTORY in the UNDO/SETTINGS. But that only makes it possible to go back one step. But CTRL+Z now works without the app crashing.

Feels like there is some conflict between this script and how the undo system works here.

Thanks @iFrexsim, I’ll take a look to this. I was able to reproduce the crash.

1 Like

I’ve identified that the problem here is changing the sprite in the same “change” event. This event is called when undoing/redoing something. A minimal script to reproduce the crash is this:

local spr = app.activeSprite
if not spr then return end

spr.events:on("change", function()
 -- The problem here is that the change event can be received from a
 -- "undo" command, so we are modifying the sprite while we're undoing
 -- something
 spr:newLayer()
 app.refresh()
end)

I’ve created this issue: Crash undoing actions from a script · Issue #3539 · aseprite/aseprite · GitHub

2 Likes

This crash will be fixed for the next version and a new argument for the “change” event was added to detect if changes occur from a undo/redo or from direct user interaction. This is an example for future possible code in Aseprite v1.2.41, when the user edits the sprite, a new layer is added (but it’s not done when an undo/redo is executed):

local spr = app.activeSprite
spr.events:on("change", function(ev)
 if ev and not ev.fromUndo then
   spr:newLayer()
 end
end)
2 Likes