From 79f17ddf670ee58629826fc5118ad513ddcf9f99 Mon Sep 17 00:00:00 2001 From: Cruor Date: Fri, 23 Oct 2020 14:19:03 +0200 Subject: [PATCH] Things you didn't know you needed. More performance improvements. Reduced memory usage of Ahorn a bit. Updated controls in the README. Please read it. It's good now. Added option to disable parallax preview by double clicking it. Added smoothening when drawing with brushes. Should feel much nicer now. Added Fade X and Fade Y to parallax in stylegrounds window. Added colour to windsnow styleground effect. Added Cutscene Node entity. Added hotkeys to zoom in and out. Added hotkeys to rotate tiles and entities. Added hotkeys to flip tiles, entities, and areas. Added hotkey to clear the selection. Inverted the rotation of brushes to be consistent with the rest. Improved behaviour when using keyboard shortcuts in selection tool. Improved selection previews in selection tool. Fixed middle clicking entities/triggers. Fixed effects having the wrong fields sometimes. Fixed weird alpha issues with tilesets. Unknown tiles are actually being rendered now. Warnings on bad XML files are now more visible. Update window should now be less confusing. Added better error messages to ahorn.bat Added launch arguments to ahorn.bat Julia detection in ahorn.bat is better now. Please download the new ahorn.bat For plugin developers: You can now sort your plugins into subfolders. Organize! You can now put plugins in a "libraries" folder. Libraries are loaded before any other plugins. Makes it easy to share code across multiple files and mods. Added Ahorn.editingOrder() and Ahorn.editingIgnored() to entities, triggers, and effects. Added Ahorn.moved() and Ahorn.deleted() to entities and triggers. Implement Ahorn.moved() instead of moved(). moved() has been deprecated. Added Ahorn.resized(), Ahorn.flipped(), and Ahorn.rotated() to entities and triggers. Entity placement and resizing now respect Ahorn.canResize() better. Improved loading of plugins in zip files. Improved error messages from plugins. Co-authored-by: Vexatos --- README.md | 109 ++++--- ahorn.bat | 175 ++++++++++-- assets/cutscene_node.png | Bin 0 -> 415 bytes lang/en_gb.lang | 19 +- src/Ahorn.jl | 1 + src/assets.jl | 1 + src/auto_tiler.jl | 137 ++++----- src/brush.jl | 2 +- src/camera.jl | 20 +- src/celeste_render.jl | 48 ++-- src/debug.jl | 25 +- src/decals.jl | 32 ++- src/drawable_room.jl | 4 +- src/effects.jl | 9 +- src/entities.jl | 34 ++- src/entities/cutscenenode.jl | 19 ++ src/entities/dashswitch.jl | 48 +++- src/entities/kevin.jl | 15 + src/entities/puffer.jl | 10 +- src/entities/spikes.jl | 89 +++++- src/entities/spring.jl | 12 + src/entities/wallbooster.jl | 10 + src/helpers/tileset_splitter.jl | 2 +- src/history.jl | 2 +- src/history_manager.jl | 16 +- src/hotkey.jl | 12 +- src/hotkeys.jl | 8 + src/init_external_modules.jl | 16 +- src/layers.jl | 1 + src/libraries.jl | 5 + src/menubar.jl | 1 + src/mods.jl | 23 +- src/module_loader.jl | 2 +- src/property_menu.jl | 125 ++++---- src/selections.jl | 32 ++- src/shapes/circle.jl | 4 +- src/shapes/ellipse.jl | 4 +- src/shapes/line.jl | 4 +- src/shapes/rectangle.jl | 4 +- src/shapes/simple_curve.jl | 2 +- src/tools/brushes.jl | 57 +++- src/tools/placements.jl | 143 ++++++---- src/tools/selection.jl | 454 +++++++++++++++++++++++++----- src/triggers.jl | 16 +- src/windows/styleground_window.jl | 79 ++++-- src/windows/update_window.jl | 48 ++-- 46 files changed, 1430 insertions(+), 449 deletions(-) create mode 100644 assets/cutscene_node.png create mode 100644 src/entities/cutscenenode.jl create mode 100644 src/libraries.jl diff --git a/README.md b/README.md index 338f32c..b3800b2 100644 --- a/README.md +++ b/README.md @@ -43,40 +43,81 @@ Hold right click to move around the map. Left click is your main way to place an In any menu, you can hover over the name of an option or field for a detailed tooltip explaining its meaning and usage. Ahorn supports a couple of keybinds and special mouse functionality, with more to come. The following list might not be comprehensive. - - Ctrl + t: New room - - Ctrl + shift + t: Configure current room - - Ctrl + n: New map - - Ctrl + m: Metadata window - - Ctrl + shift + alt + s: Open settings window - - q, e: shrink / grow width on selected - - a, d: shrink / grow height on selected - - Arrow keys: move selected - - Left mouse button over selected: dragging selected - - Holding ctrl + any of the above: use 1 as step size instead of 8 for more fine-grained placements - - Shift selecting keeps previous selection as well - - Ctrl + f: Focus search field - - Ctrl + c: Copy selection - - Ctrl + x: Cut selection - - Ctrl + v: Paste selection - - Ctrl + z: Undo changes - - Ctrl + shift + z: Redo changes - - Esc/Enter: Exit search field - - v, h: vertical / horizontal mirror of decal - - r: Rotate selected brush - - delete: delete the given node / target - - n/+: add node to target (after the targeted node / entity) - - Middle click: pick what's currently under the cursor in the selected layer - - Ctrl + number key row 0-9: shortcuts to select tools - - Alt + arrow keys: move a room - - Alt + delete: delete room - - Double click layer name in selection menu: toggle visibility - - Double click material name in tools: toggle favourite - - Right click entity / trigger with placements / selection tool: open properties menu - - Double click with selections selects all similar targets - - Holding ctrl when doing the above restricts it to targets in a more strict manner - - With Everest installed and Celeste running in debug mode, it supports some more: - - Ctrl + alt + leftclick on a room in Ahorn: teleport to that room in the game + +--- + +#### General Controls + + - Ctrl + N: New map + - Ctrl + S: Save map + - Ctrl + Shift + S: Save map as... + - Ctrl + T: New room + - Ctrl + Shift + T: Configure current room + - Alt + Arrow keys: Move room + - Alt + Delete: Delete room + - Right click & drag: Move around the map + - Ctrl + M: Metadata window + - Ctrl + Shift + Alt + S: Open settings window + - Ctrl + Z: Undo action + - Ctrl + Shift + Z: Redo action + - Ctrl + F: Focus search field + - Return in search field: Exit and clear search field + - Escape in search field: Exit search field + - Scroll wheel: Zoom + - Ctrl + ➕ (plus): Zoom in + - Ctrl + ➖ (minus): Zoom out + - Double Left click material name in tools: Toggle favourite + - Double Left click preview in stylegrounds window: Toggle preview + - Ctrl + number key row 0-9: Shortcuts to select tools + +#### Placements + + - Left click: Place object + - Holding Ctrl + Left click: Use 1 as step size instead of 8 for more fine-grained placements + - Right click: Open properties of object under cursor + - Left click & drag: Adjust size of resizeable objects while placing + - Middle click: Clone object under cursor + - Q, E: Shrink / grow width on decal + - A, D: Shrink / grow height on decal + - L, R: Rotate supported objects counter-clockwise / clockwise + - V, H: Flip supported objects vertically / horizontally + +#### Selections + + - Left click & drag: Select objects + - Shift + Left click & drag: Add to current selection + - Right click selection: Open properties of selected object(s) + - Holding Left mouse button over selection: Drag selected objects + - Double Left click: Select all similar objects + - Ctrl + Double Left click: Select all similar objects but more strict + - Arrow keys: Move selected objects + - Q, E: Shrink / grow width on selected entities/triggers + - A, D: Shrink / grow height on selected entities/triggers + - Holding Ctrl + any of the above: use 1 as step size instead of 8 for more fine-grained control + - L, R: Rotate supported objects counter-clockwise / clockwise + - V, H: Flip supported objects vertically / horizontally + - Shift + V / Shift + H: Flip selected area vertically / horizontally + - N or ➕ (plus) on entity/trigger: Add starting node to entity/trigger + - N or ➕ (plus) on node: Add node to entity/trigger after selected node + - Delete: Delete selected object(s) + - Return or Escape: Clear selection + - Ctrl + C: Copy selection + - Ctrl + X: Cut selection + - Ctrl + V: Paste selection + +#### Brushes + + - Left click: Place brush + - Left click & drag: Drag brush + - Middle click: Change material to tile under cursor + - L, R: Rotate supported brush counter-clockwise / clockwise + +#### Everest Integration + +With Everest installed and Celeste running in debug mode, it supports some more: + - Ctrl + Alt + Left click on a room in Ahorn: Teleport to that room in the game + +--- If you are serious about making maps, it is highly recommended to use [Everest](https://github.com/EverestAPI/Everest) for the F5 (force map reload) and F6 (open map editor for the current map) features. diff --git a/ahorn.bat b/ahorn.bat index b6a72ad..a96666c 100644 --- a/ahorn.bat +++ b/ahorn.bat @@ -1,20 +1,89 @@ -@echo off +@echo off setlocal EnableDelayedExpansion set minimum_version="v\"1.3.0\"" -set julia_url_64bit=https://julialang-s3.julialang.org/bin/winnt/x64/1.4/julia-1.4.1-win64.exe -set julia_url_32bit=https://julialang-s3.julialang.org/bin/winnt/x86/1.4/julia-1.4.1-win32.exe -set julia_filename=julia-1.4.1-installed.exe +set julia_url_64bit=https://julialang-s3.julialang.org/bin/winnt/x64/1.5/julia-1.5.2-win64.exe +set julia_url_32bit=https://julialang-s3.julialang.org/bin/winnt/x86/1.5/julia-1.5.2-win32.exe +set julia_filename=julia-1.5.2-installed.exe set install_url=https://raw.githubusercontent.com/CelestialCartographers/Ahorn/master/install_ahorn.jl set install_filename=install_ahorn.jl +rem Set up launch arguments +rem Not the best solution, but it works for simple arguments + +set darkMode=0 +set developerMode=0 +set onlyUpdate=0 +set updateFirst=0 +set displayHelp=0 + +for %%A in (%*) do ( + if "%%A" equ "--dark" ( + set darkMode=1 + ) + + if "%%A" equ "--developer" ( + set developerMode=1 + ) + + if "%%A" equ "--update" ( + set updateFirst=1 + ) + + if "%%A" equ "--onlyUpdate" ( + set onlyUpdate=1 + set updateFirst=1 + ) + + if "%%A" equ "--help" ( + set displayHelp=1 + ) +) + +:darkmode + +if %darkMode% equ 1 ( + SET GTK_CSD=1 + SET GTK_THEME=Adwaita:dark +) + +:displayHelp + +if %displayHelp% equ 1 ( + echo Program launch flags + + echo --help + echo Displays this page. + echo. + + echo --dark + echo Sets environmental variables to use the default GTK.jl dark theme. + echo This is not a native Windows theme, some interface elements might look different. + echo. + + echo --developer + echo Puts error messages in the terminal rather than in the error log. + echo. + + echo --update + echo Attempts to update Ahorn before starting. + echo. + + echo --onlyUpdate + echo Only attempts to update Ahorn. Does not start the program afterwards. + echo. + + goto :end +) + :start -where julia >nul 2>nul +rem Check if Julia runs at all when set with PATH +rem For some reason Windows might ship without `where` program +julia -e "exit(0)" >nul 2>nul if %ERRORLEVEL% equ 0 ( echo Using Julia from PATH environmental variable. - echo Make sure this version is meeting the requirements for Ahorn. - where julia + echo Making sure this version is meeting the requirements for Ahorn. julia -e "exit(Int(VERSION < %minimum_version%))" if !ERRORLEVEL! neq 0 ( @@ -23,13 +92,30 @@ if %ERRORLEVEL% equ 0 ( goto :autodetect ) - goto :run + goto :install ) :autodetect -rem New default directory, used for Julia 1.4 -for /F " tokens=*" %%i IN ('dir /b /ad-h /o-d "%LocalAppData%\Programs\Julia"') do ( +rem Default for 1.5 +rem Looks for both Julia- and Julia, just to be safe +for /F " tokens=*" %%i in ('dir /b /ad-h /o-d "%LocalAppData%\Programs"') do ( + set fn=%%i + if "!fn:~0,6!" == "Julia-" ( + set JULIA_PATH="%LocalAppData%\Programs\%%i\bin" + + goto :foundJulia + ) + + if "!fn:~0,6!" == "Julia " ( + set JULIA_PATH="%LocalAppData%\Programs\%%i\bin" + + goto :foundJulia + ) +) + +rem Default for 1.4 +for /F " tokens=*" %%i in ('dir /b /ad-h /o-d "%LocalAppData%\Programs\Julia"') do ( set fn=%%i if "!fn:~0,6!" == "Julia-" ( set JULIA_PATH="%LocalAppData%\Programs\Julia\%%i\bin" @@ -38,8 +124,8 @@ for /F " tokens=*" %%i IN ('dir /b /ad-h /o-d "%LocalAppData%\Programs\Julia"') ) ) -rem Old default directory, Julia 1.3 is a valid target -for /F " tokens=*" %%i IN ('dir /b /ad-h /o-d "%LocalAppData%"') do ( +rem Default for 1.3 +for /F " tokens=*" %%i in ('dir /b /ad-h /o-d "%LocalAppData%"') do ( set fn=%%i if "!fn:~0,6!" == "Julia-" ( set JULIA_PATH="%LocalAppData%\%%i\bin" @@ -48,7 +134,7 @@ for /F " tokens=*" %%i IN ('dir /b /ad-h /o-d "%LocalAppData%"') do ( ) ) -echo Julia installation not found in default install directory "%LocalAppData%\Programs\Julia\". +echo Julia installation not found in default install directory "%LocalAppData%\Programs\". echo Please install to the default directory or add Julia manually to the PATH environmental variable. goto :installPrompt @@ -78,6 +164,14 @@ if %ERRORLEVEL% neq 0 ( powershell -Command "(New-Object Net.WebClient).DownloadFile('%julia_url_32bit%', '%julia_filename%')" ) + if !ERRORLEVEL! neq 0 ( + echo Unable to download Julia installer. + echo Might be due to TLS issues or PowerShell version. + echo Please manually download and install Julia from the Julia website. + + goto end + ) + echo Running installer start /wait "" "%~dp0%julia_filename%" @@ -89,7 +183,7 @@ if %ERRORLEVEL% neq 0 ( goto :end ) -:run +:install set AHORN_PATH=%LocalAppData%\Ahorn set "AHORN_PATH=%AHORN_PATH:\=/%" @@ -100,7 +194,18 @@ if %ERRORLEVEL% equ 0 ( echo Installing Ahorn, this might take a while. echo Please make sure not to put the command line into select mode, that means do not click inside the window. - powershell -Command "(New-Object Net.WebClient).DownloadFile('%install_url%', '%install_filename%')" + if not exist "%cd%\%install_filename%" ( + powershell -Command "(New-Object Net.WebClient).DownloadFile('%install_url%', '%install_filename%')" + + if !ERRORLEVEL! neq 0 ( + echo Unable to download Ahorn install script. + echo Might be due to TLS issues or PowerShell version. + echo Please manually download install_ahorn.jl and put it in the same folder as Ahorn.bat and then rerun. + echo Optionally install Ahorn manually via cross platform instructions. + + goto :end + ) + ) julia "%~dp0%install_filename%" @@ -108,18 +213,44 @@ if %ERRORLEVEL% equ 0 ( julia -e "using Pkg; Pkg.activate(%AHORN_ENV%); exit(length(Pkg.installed()))" > NUL 2>&1 if !ERRORLEVEL! equ 0 ( - goto end + goto :end + ) + + goto :install +) + +:update + +if %updateFirst% equ 1 ( + echo Attemtping to update Ahorn. + + if !developerMode! equ 1 ( + julia -e "using Pkg; Pkg.activate(%AHORN_ENV%); Pkg.update()" + + ) else ( + julia -e "using Pkg; Pkg.activate(%AHORN_ENV%); Pkg.update()" 2> "%AHORN_PATH%/update.log" ) - goto :run + if !onlyUpdate! equ 1 ( + goto :end + ) ) -echo If this is the first time running Ahorn, this might take a while. -echo The window might stay blank for a long time, this is normal as packages are precompiling in the background. -echo Be patient, the program is still running until the terminal says "Press any key to continue" (or equivalent in your language). -echo The error log is located at "%AHORN_PATH%/error.log", should any problems occur. +:run + +if %developerMode% equ 1 ( + echo Warnings and errors will be printed to this window rather than error log. -julia -e "using Pkg; Pkg.activate(%AHORN_ENV%); using Ahorn; Ahorn.displayMainWindow()" 2> "%AHORN_PATH%/error.log" + julia -e "using Pkg; Pkg.activate(%AHORN_ENV%); using Ahorn; Ahorn.displayMainWindow()" + +) else ( + echo If this is the first time running Ahorn, this might take a while. + echo The window might stay blank for a long time, this is normal as packages are precompiling in the background. + echo Be patient, the program is still running until the terminal says "Press any key to continue" (or equivalent in your language^). + echo The error log is located at "%AHORN_PATH%/error.log", should any problems occur. + + julia -e "using Pkg; Pkg.activate(%AHORN_ENV%); using Ahorn; Ahorn.displayMainWindow()" 2> "%AHORN_PATH%/error.log" +) :end endlocal diff --git a/assets/cutscene_node.png b/assets/cutscene_node.png new file mode 100644 index 0000000000000000000000000000000000000000..21d493b727f7ecc10c677abbc1312de11986e024 GIT binary patch literal 415 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85o30K$!7fntTONP^!c=q9iy!t)x7$D3u`~F*C13&(AePq0Cs% zRL{`R{j-xf&{#fC7sn6}@3m77iXKuBa0zZ$jz*yeamb;>&Vr zb2r<)>+Ti%?qyD8VpzIYO5nWc-V1Y?R%ku2GZZ?NP+%3G!#0KMlA9!N>IPxIefE{Q zH<);@1T5HhcHzOT3&aDPuKwfRJzp!~hmKi0+jS|8zdb0v%xc!L^qP9R=oc?O z-49;NxN~My@mx(`#%(UxQTD9HKvv~i zu2l!+9iFaXcB5jq9N*`eOF?hQAxvX (result, load time) +const loadedXMLCache = Dict{String, Tuple{Any, Number}}() - hasRoot, modRoot = getModRoot(fn) - xmlPath = joinpath(modRoot, metaPath) +function displayCustomXMLWarning(title::String, message::String) + topMostInfoDialog("$title\n$message") +end - if !isempty(metaPath) && hasRoot - path = xmlPath - end - end +function displayCustomXMLWarning(title::String, exception::Exception) + message = sprint(showerror, exception) - try - global fgTilerMeta = TilerMeta(path) + displayCustomXMLWarning(title, message) +end - catch e - if !loadDefault - println(Base.stderr, "Failed to load custom ForegroundTiles XML") - println(Base.stderr, e) +function getCustomTilesXMLPath(side::Union{Side, Nothing}, filename::String, key::String) + if side !== nothing + meta = get(Dict{String, Any}, side.data, "meta") + metaPath = get(meta, key, "") - for (exc, bt) in Base.catch_stack() - showerror(Base.stderr, exc, bt) - println() - end + hasRoot, modRoot = getModRoot(filename) + xmlPath = joinpath(modRoot, metaPath) - loadFgTilerMeta(side, fn, true) + if !isempty(metaPath) && hasRoot + return xmlPath end end end -function loadBgTilerMeta(side::Union{Side, Nothing}, fn::String, loadDefault::Bool=false) - path = joinpath(storageDirectory, "XML", "BackgroundTiles.xml") +function loadCustomXML(loader::Union{Function, Type}, filename::String, errorTitle::String, force::Bool=false, quiet::Bool=false) + if !force + result, modified = get(loadedXMLCache, filename, (nothing, 0)) - if side !== nothing && !loadDefault - meta = get(Dict{String, Any}, side.data, "meta") - metaPath = get(meta, "BackgroundTiles", "") - - hasRoot, modRoot = getModRoot(fn) - xmlPath = joinpath(modRoot, metaPath) - - if !isempty(metaPath) && hasRoot - path = xmlPath + if modified >= mtime(filename) + return result end end try - global bgTilerMeta = TilerMeta(path) + result = loader(filename) + loadedXMLCache[filename] = (result, mtime(filename)) + + return result catch e - if !loadDefault - println(Base.stderr, "Failed to load custom BackgroundTiles XML") + if !quiet + displayCustomXMLWarning(errorTitle, e) + println(Base.stderr, e) for (exc, bt) in Base.catch_stack() showerror(Base.stderr, exc, bt) - println() + println(Base.stderr, "") end - - loadBgTilerMeta(side, fn, true) end + + loadedXMLCache[filename] = (nothing, mtime(filename)) end + + return nothing end -function loadAnimatedTilesMeta(side::Union{Side, Nothing}, fn::String, loadDefault::Bool=false) - path = joinpath(storageDirectory, "XML", "AnimatedTiles.xml") +# Good enough to cache and retrieve via the custom XML loader +# We don't need any error message, vanilla assets shouldn't fail +# Never need to force reloads, that is mostly just for error message sake +function loadVanillaXML(loader::Union{Function, Type}, filename::String) + return loadCustomXML(loader, filename, "", false, true) +end - if side !== nothing && !loadDefault - meta = get(Dict{String, Any}, side.data, "meta") - metaPath = get(meta, "AnimatedTiles", "") +function loadTilesMeta(side::Union{Side, Nothing}, filename::String, fg::Bool=false, force::Bool=false) + defaultPath = joinpath(storageDirectory, "XML", fg ? "ForegroundTiles.xml" : "BackgroundTiles.xml") + customPath = getCustomTilesXMLPath(side, filename, fg ? "ForegroundTiles" : "BackgroundTiles") - hasRoot, modRoot = getModRoot(fn) - xmlPath = joinpath(modRoot, metaPath) + if customPath !== nothing + xmlType = fg ? "Forground Tiles" : "Bakground Tiles" + errorTitle = "Failed to load custom $xmlType XML" + customMeta = loadCustomXML(TilerMeta, customPath, errorTitle, force) - if !isempty(metaPath) && hasRoot - path = xmlPath + if customMeta !== nothing + return customMeta end end - try - global animatedTilesMeta = loadAnimatedTilesXML(path) + return loadVanillaXML(TilerMeta, defaultPath) +end - catch e - if !loadDefault - println(Base.stderr, "Failed to load custom AnimatedTiles XML") - println(Base.stderr, e) +function loadAnimatedTilesMeta(side::Union{Side, Nothing}, filename::String, force::Bool=false) + defaultPath = joinpath(storageDirectory, "XML", "AnimatedTiles.xml") + customPath = getCustomTilesXMLPath(side, filename, "AnimatedTiles") - for (exc, bt) in Base.catch_stack() - showerror(Base.stderr, exc, bt) - println() - end + if customPath !== nothing + errorTitle = "Failed to load custom Animated Tiles XML" + customMeta = loadCustomXML(loadAnimatedTilesXML, customPath, errorTitle, force) - loadAnimatedTilesMeta(side, fn, true) + if customMeta !== nothing + return customMeta end end + + return loadVanillaXML(loadAnimatedTilesXML, defaultPath) end -function loadXMLMeta() +function loadXMLMeta(force::Bool=true) side = loadedState.side filename = loadedState.filename - loadFgTilerMeta(side, filename) - loadBgTilerMeta(side, filename) - loadAnimatedTilesMeta(side, filename) + global fgTilerMeta = loadTilesMeta(side, filename, true, force) + global bgTilerMeta = loadTilesMeta(side, filename, false, force) + global animatedTilesMeta = loadAnimatedTilesMeta(side, filename, force) end function getTile(tiles::Tiles, x::Int, y::Int) diff --git a/src/brush.jl b/src/brush.jl index ec1fdea..707e140 100644 --- a/src/brush.jl +++ b/src/brush.jl @@ -44,7 +44,7 @@ end tileNames(layer::Layer) = tileNames(layerName(layer)) function getBrushMaterialsNames(name::String, sortByDisplayName::Bool=true) - loadXMLMeta() + loadXMLMeta(false) validTileIds = validTiles(name) tileTileNames = tileNames(name) diff --git a/src/camera.jl b/src/camera.jl index 9b49117..3047a3e 100644 --- a/src/camera.jl +++ b/src/camera.jl @@ -36,13 +36,13 @@ function updateCameraZoomVariables() global defaultZoom = 2.0^round(Int, log(2, get(config, "camera_default_zoom", defaultZoom))) end -function zoomIn!(camera::Camera, event::Gtk.GdkEventScroll) +function zoomIn!(camera::Camera, x::Number, y::Number) updateCameraZoomVariables() if minimumZoom <= camera.scale * 2 <= maximumZoom camera.scale = camera.scale * 2 - camera.x = round(Int, camera.x * 2 + event.x) - camera.y = round(Int, camera.y * 2 + event.y) + camera.x = round(Int, camera.x * 2 + x) + camera.y = round(Int, camera.y * 2 + y) draw(canvas) @@ -52,13 +52,18 @@ function zoomIn!(camera::Camera, event::Gtk.GdkEventScroll) return false end -function zoomOut!(camera::Camera, event::Gtk.GdkEventScroll) +zoomIn!(camera::Camera, event::Gtk.GdkEventScroll) = zoomIn!(camera, event.x, event.y) +zoomIn!(camera::Camera=camera) = zoomIn!(camera, width(canvas) / 2, height(canvas) / 2) + +function zoomOut!(camera::Camera, x::Number, y::Number) updateCameraZoomVariables() if minimumZoom <= camera.scale / 2 <= maximumZoom camera.scale = camera.scale / 2 - camera.x = round(Int, (camera.x - event.x) / 2) - camera.y = round(Int, (camera.y - event.y) / 2) + camera.x = round(Int, (camera.x - x) / 2) + camera.y = round(Int, (camera.y - y) / 2) + + draw(canvas) return true end @@ -66,6 +71,9 @@ function zoomOut!(camera::Camera, event::Gtk.GdkEventScroll) return false end +zoomOut!(camera::Camera, event::Gtk.GdkEventScroll) = zoomOut!(camera, event.x, event.y) +zoomOut!(camera::Camera=camera) = zoomOut!(camera, width(canvas) / 2, height(canvas) / 2) + minimumZoom = 2.0^-6 maximumZoom = 2.0^6 defaultZoom = 4 \ No newline at end of file diff --git a/src/celeste_render.jl b/src/celeste_render.jl index 3a4a7ee..229f781 100644 --- a/src/celeste_render.jl +++ b/src/celeste_render.jl @@ -124,6 +124,9 @@ function drawTile(ctx::Cairo.CairoContext, x, y, tile, tiles, meta, states, exis else drawRectangle(ctx, drawX, drawY, 8, 8, colors.unknown_tile_color, (0.0, 0.0, 0.0, 0.0)) end + + else + drawRectangle(ctx, drawX, drawY, 8, 8, colors.unknown_tile_color, (0.0, 0.0, 0.0, 0.0)) end else @@ -203,11 +206,6 @@ function drawBackground(layer::Layer, dr::DrawableRoom, camera::Camera, fg::Bool backdrops = fg ? dr.map.style.foregrounds : dr.map.style.backgrounds ctx = getSurfaceContext(layer.surface) - if !fg - color = something(dr.fillColor, getRoomBackgroundColor(dr.room)) - paintSurface(ctx, color) - end - for backdrop in backdrops t = typeof(backdrop) @@ -256,7 +254,7 @@ end drawingLayers = nothing -const drawableRooms = Dict{Map, Dict{Room, DrawableRoom}}() +const drawableRooms = Dict{String, Dict{String, DrawableRoom}}() redrawingFuncs["fgDecals"] = (layer, room, camera) -> drawDecals(layer, room, true) redrawingFuncs["bgDecals"] = (layer, room, camera) -> drawDecals(layer, room, false) @@ -271,23 +269,28 @@ redrawingFuncs["entities"] = (layer, room, camera) -> drawEntities(layer, room) redrawingFuncs["triggers"] = (layer, room, camera) -> drawTriggers(layer, room) function getDrawableRooms(map::Map) - if !haskey(drawableRooms, map) - drawableRooms[map] = Dict{Room, DrawableRoom}() + package = map.package + + if !haskey(drawableRooms, package) + drawableRooms[package] = Dict{String, DrawableRoom}() end - return collect(values(drawableRooms[map])) + return collect(values(drawableRooms[package])) end function getDrawableRoom(map::Map, room::Room) - if !haskey(drawableRooms, map) - drawableRooms[map] = Dict{Room, DrawableRoom}() + package = map.package + roomName = room.name + + if !haskey(drawableRooms, package) + drawableRooms[package] = Dict{String, DrawableRoom}() end - if !haskey(drawableRooms[map], room) - drawableRooms[map][room] = DrawableRoom(map, room) + if !haskey(drawableRooms[package], roomName) + drawableRooms[package][roomName] = DrawableRoom(map, room) end - return drawableRooms[map][room] + return drawableRooms[package][roomName] end function deleteDrawableRoomCache(map::Map) @@ -296,7 +299,7 @@ function deleteDrawableRoomCache(map::Map) destroy(room) end - delete!(drawableRooms, map) + delete!(drawableRooms, map.package) end function updateDrawingLayers!(map::Map, room::Room) @@ -306,6 +309,7 @@ end function drawMap(ctx::Cairo.CairoContext, camera::Camera, map::Map; adjacentAlpha::Number=colors.adjacent_room_alpha, backgroundFill::colorTupleType=colors.background_canvas_fill) deleteNonVisibleRooms = get(config, "delete_non_visible_rooms_cache", false) width, height = floor(Int, ctx.surface.width), floor(Int, ctx.surface.height) + package = map.package paintSurface(ctx, backgroundFill) pattern_set_filter(get_source(ctx), Cairo.FILTER_NEAREST) @@ -327,10 +331,12 @@ function drawMap(ctx::Cairo.CairoContext, camera::Camera, map::Map; adjacentAlph else if deleteNonVisibleRooms - if haskey(drawableRooms, map) - if haskey(drawableRooms[map], room) - destroy(drawableRooms[map][room]) - delete!(drawableRooms[map], room) + if haskey(drawableRooms, package) + roomName = room.name + + if haskey(drawableRooms[package], roomName) + destroy(drawableRooms[package][roomName]) + delete!(drawableRooms[package], roomName) end end end @@ -360,6 +366,10 @@ function drawRoom(ctx::Cairo.CairoContext, camera::Camera, room::DrawableRoom; a scale(ctx, camera.scale, camera.scale) translate(ctx, room.room.position...) + backgroundColor = something(room.fillColor, getRoomBackgroundColor(room.room)) + width, height = room.room.size + + drawRectangle(ctx, 0, 0, width, height, backgroundColor, (0.0, 0.0, 0.0, 0.0)) applyLayer!(ctx, renderingLayer, alpha=alpha) restore(ctx) diff --git a/src/debug.jl b/src/debug.jl index ffb831c..df9d916 100644 --- a/src/debug.jl +++ b/src/debug.jl @@ -23,12 +23,16 @@ function log(s::String, key::String) end function reloadTools!() + if Ahorn.currentTool !== nothing + Ahorn.eventToModule(Ahorn.currentTool, "cleanup") + end + empty!(Ahorn.loadedTools) append!(Ahorn.loadedTools, joinpath.(Ahorn.abs"tools", readdir(Ahorn.abs"tools"))) Ahorn.initExternalTools() Ahorn.loadModule.(Ahorn.loadedTools) - Ahorn.loadExternalModules!(Ahorn.loadedModules, Ahorn.loadedTools, "tools") + Ahorn.loadExternalZipModules!(Ahorn.loadedModules, Ahorn.loadedTools, "tools") Ahorn.changeTool!(Ahorn.loadedTools[1]) Ahorn.selectRow!(Ahorn.toolList, 1) @@ -43,7 +47,7 @@ function reloadEntities!() Ahorn.initExternalEntities() Ahorn.loadModule.(Ahorn.loadedEntities) - Ahorn.loadExternalModules!(Ahorn.loadedModules, Ahorn.loadedEntities, "entities") + Ahorn.loadExternalZipModules!(Ahorn.loadedModules, Ahorn.loadedEntities, "entities") Ahorn.registerPlacements!(Ahorn.entityPlacements, Ahorn.loadedEntities) Ahorn.getLayerByName(dr.layers, "entities").redraw = true @@ -62,12 +66,25 @@ function reloadEffects!() Ahorn.initExternalEffects() Ahorn.loadModule.(Ahorn.loadedEffects) - Ahorn.loadExternalModules!(Ahorn.loadedModules, Ahorn.loadedEffects, "effects") + Ahorn.loadExternalZipModules!(Ahorn.loadedModules, Ahorn.loadedEffects, "effects") Ahorn.registerPlacements!(Ahorn.effectPlacements, Ahorn.loadedEffects) return true end +# Ahorn doesn't use libraries +# Strictly a plugin feature +function reloadLibraries!() + empty!(Ahorn.loadedLibraries) + + Ahorn.initExternalLibraries() + + Ahorn.loadModule.(Ahorn.loadedLibraries) + Ahorn.loadExternalZipModules!(Ahorn.loadedModules, Ahorn.loadedLibraries, "libraries") + + return true +end + function reloadTriggers!() dr = Ahorn.getDrawableRoom(Ahorn.loadedState.map, Ahorn.loadedState.room) @@ -76,7 +93,7 @@ function reloadTriggers!() Ahorn.initExternalTriggers() Ahorn.loadModule.(Ahorn.loadedTriggers) - Ahorn.loadExternalModules!(Ahorn.loadedModules, Ahorn.loadedTriggers, "triggers") + Ahorn.loadExternalZipModules!(Ahorn.loadedModules, Ahorn.loadedTriggers, "triggers") Ahorn.registerPlacements!(Ahorn.triggerPlacements, Ahorn.loadedTriggers) Ahorn.getLayerByName(dr.layers, "triggers").redraw = true diff --git a/src/decals.jl b/src/decals.jl index 17955be..7047553 100644 --- a/src/decals.jl +++ b/src/decals.jl @@ -5,13 +5,37 @@ function position(decal::Maple.Decal)::Tuple{Int, Int} ) end +editingOrder(decal::Maple.Decal) = String["x", "y", "scaleX", "scaleY", "texture"] +editingIgnored(decal::Maple.Decal, multiple::Bool=false) = multiple ? String["x", "y"] : String[] + +deleted(decal::Maple.Decal, node::Int) = nothing + +moved(decal::Maple.Decal) = nothing +moved(decal::Maple.Decal, x::Int, y::Int) = nothing + +resized(decal::Maple.Decal) = nothing +resized(decal::Maple.Decal, width::Int, height::Int) = nothing + +function flipped(decal::Maple.Decal, horizontal::Bool) + if horizontal + decal.scaleX *= -1 + + else + decal.scaleY *= -1 + end + + return true +end + +rotated(decal::Maple.Decal, steps::Int) = nothing + function decalSelection(decal::Maple.Decal) texture = "decals/$(decal.texture)" sprite = getTextureSprite(texture) x, y = round(Int, decal.x), round(Int, decal.y) sx, sy = round(Int, decal.scaleX), round(Int, decal.scaleY) - + width, height = sprite.width, sprite.height realWidth, realHeight = sprite.realWidth, sprite.realHeight @@ -63,7 +87,7 @@ function spritesToDecalTextures(sprites::Dict{String, SpriteHolder}) end end end - + return res end @@ -83,7 +107,7 @@ function decalTextures(animationFrames::Bool=false) return textures end -function decalConfigOptions(decal::Maple.Decal, ignores::Array{String, 1}=String[]) +function propertyOptions(decal::Maple.Decal, ignores::Array{String, 1}=String[]) res = Form.Option[] names = get(langdata, ["placements", "decals", "names"]) @@ -101,7 +125,7 @@ function decalConfigOptions(decal::Maple.Decal, ignores::Array{String, 1}=String displayName = isempty(name) ? humanizeVariableName(attr) : name tooltip = expandTooltipText(get(tooltips, Symbol(attr), "")) textures = attr == "texture" ? decalTextures() : nothing - + push!(res, Form.suggestOption(displayName, value, dataName=attr, tooltip=tooltip, choices=textures, editable=true)) end diff --git a/src/drawable_room.jl b/src/drawable_room.jl index 5d6313f..511eadb 100644 --- a/src/drawable_room.jl +++ b/src/drawable_room.jl @@ -39,12 +39,12 @@ updateTileStates!(room::Room, package::String, states::TileStates, width::Int, h function getDrawingLayers() return Layer[ - Layer("bgParallax"), + Layer("bgParallax", dummy=true), # Not currently used, kept to keep order intact Layer("bgTiles", clearOnReset=false), Layer("bgDecals"), Layer("entities"), Layer("fgTiles", clearOnReset=false), - Layer("fgParallax"), + Layer("fgParallax", dummy=true), # Not currently used, kept to keep order intact Layer("fgDecals"), Layer("triggers"), diff --git a/src/effects.jl b/src/effects.jl index f7b1056..98e21c9 100644 --- a/src/effects.jl +++ b/src/effects.jl @@ -2,9 +2,12 @@ function canFgBg(effect::Maple.Effect) return true, true end -function editingOptions(effect::Maple.Effect) - return Dict{String, Any}() -end +editingOptions(entity::Maple.Effect) = Dict{String, Any}() +editingIgnored(entity::Maple.Effect) = String[] +editingOrder(entity::Maple.Effect) = String[ + "name", "only", "exclude", "tag", + "flag", "notflag" +] function registerPlacements!(placements::Array{Type{Maple.Effect{T}} where T, 1}, loaded::Array{String, 1}) empty!(placements) diff --git a/src/entities.jl b/src/entities.jl index 92c369b..dc47633 100644 --- a/src/entities.jl +++ b/src/entities.jl @@ -133,7 +133,7 @@ function minimumSizeWrapper(target::Union{Maple.Entity, Maple.Trigger}) if get(debug.config, "IGNORE_MINIMUM_SIZE", false) return 0, 0 end - + return minimumSize(target) end @@ -152,6 +152,20 @@ resizable(entity::Maple.Entity) = false, false nodeLimits(entity::Maple.Entity) = 0, 0 editingOptions(entity::Maple.Entity) = Dict{String, Any}() +editingOrder(entity::Maple.Entity) = String["x", "y", "width", "height"] +editingIgnored(entity::Maple.Entity, multiple::Bool=false) = multiple ? String["x", "y", "width", "height", "nodes"] : String[] + +deleted(entity::Maple.Entity, node::Int) = nothing + +moved(entity::Maple.Entity) = nothing +moved(entity::Maple.Entity, x::Int, y::Int) = nothing + +resized(entity::Maple.Entity) = nothing +resized(entity::Maple.Entity, width::Int, height::Int) = nothing + +flipped(entity::Maple.Entity, horizontal::Bool) = nothing + +rotated(entity::Maple.Entity, steps::Int) = nothing function registerPlacements!(placements::PlacementDict, loaded::Array{String, 1}) empty!(placements) @@ -187,17 +201,19 @@ function updateEntityPosition!(target::Union{Entity, Trigger}, ep::EntityPlaceme elseif ep.placement == "rectangle" rect = selectionRectangle(x, y, nx, ny) - resizeW, resizeH = canResizeWrapper(target) - minW, minH = minimumSizeWrapper(target) - - w = resizeW ? max(minW, rect.w - 1) : minW - h = resizeH ? max(minH, rect.h - 1) : minH + resizeHorizontal, resizeVertical = canResizeWrapper(target) + minWidth, minHeight = minimumSizeWrapper(target) target.x = rect.x target.y = rect.y - target.width = w - target.height = h + if resizeHorizontal + target.width = max(minWidth, rect.w - 1) + end + + if resizeVertical + target.height = max(minHeight, rect.h - 1) + end elseif ep.placement == "line" target.x = x @@ -226,7 +242,7 @@ function updateCachedEntityPosition!(cache::Dict{String, T}, placements::Placeme return updateEntityPosition!(target, ep, map, room, x, y, nx, ny) end -function entityConfigOptions(entity::Union{Maple.Entity, Maple.Trigger}, ignores::Array{String, 1}=String[]) +function propertyOptions(entity::Union{Maple.Entity, Maple.Trigger}, ignores::Array{String, 1}=String[]) addedNodes = false res = Form.Option[] diff --git a/src/entities/cutscenenode.jl b/src/entities/cutscenenode.jl new file mode 100644 index 0000000..7aa7bb9 --- /dev/null +++ b/src/entities/cutscenenode.jl @@ -0,0 +1,19 @@ +module CutsceneNode + +using ..Ahorn, Maple + +const placements = Ahorn.PlacementDict( + "Cutscene Node" => Ahorn.EntityPlacement( + Maple.CutsceneNode + ) +) + +function Ahorn.selection(entity::Maple.CutsceneNode) + x, y = Ahorn.position(entity) + + return Ahorn.Rectangle[Ahorn.Rectangle(x - 12, y - 12, 24, 24)] +end + +Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::Maple.CutsceneNode, room::Maple.Room) = Ahorn.drawImage(ctx, Ahorn.Assets.cutsceneNode, -12, -12) + +end \ No newline at end of file diff --git a/src/entities/dashswitch.jl b/src/entities/dashswitch.jl index b78fc37..7188b23 100644 --- a/src/entities/dashswitch.jl +++ b/src/entities/dashswitch.jl @@ -12,6 +12,8 @@ const directions = Dict{String, Tuple{Type, String, Bool}}( "right" => (Maple.DashSwitchHorizontal, "leftSide", true), ) +const clockwiseDirections = String["up", "right", "down", "left"] + for texture in textures for (dir, data) in directions key = "Dash Switch ($(uppercasefirst(dir)), $(uppercasefirst(texture)))" @@ -35,7 +37,7 @@ function Ahorn.selection(entity::Maple.DashSwitchHorizontal) return Ahorn.Rectangle(x, y - 1, 10, 16) else - return Ahorn.Rectangle(x - 2, y, 10, 16) + return Ahorn.Rectangle(x - 2, y - 1, 10, 16) end end @@ -60,7 +62,7 @@ function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::Maple.DashSwitchHor Ahorn.drawSprite(ctx, texture, 20, 25, rot=pi) else - Ahorn.drawSprite(ctx, texture, 8, 8) + Ahorn.drawSprite(ctx, texture, 8, 7) end end @@ -77,4 +79,46 @@ function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::Maple.DashSwitchVer end end +function Ahorn.flipped(entity::Maple.DashSwitchHorizontal, horizontal::Bool) + if horizontal + entity.leftSide = !entity.leftSide + + return entity + end +end + +function Ahorn.flipped(entity::Maple.DashSwitchVertical, horizontal::Bool) + if !horizontal + entity.ceiling = !entity.ceiling + + return entity + end +end + +# TODO - Might need rotation offset +function Ahorn.rotated(entity::Maple.DashSwitchHorizontal, steps::Int) + sideIndex = entity.leftSide ? 2 : 4 + targetIndex = mod1(sideIndex + steps, 4) + + if targetIndex != sideIndex + side = clockwiseDirections[targetIndex] + func, attr, value = directions[side] + + return func(entity.x, entity.y, value) + end +end + +# TODO - Might need rotation offset +function Ahorn.rotated(entity::Maple.DashSwitchVertical, steps::Int) + sideIndex = entity.ceiling ? 3 : 1 + targetIndex = mod1(sideIndex + steps, 4) + + if targetIndex != sideIndex + side = clockwiseDirections[targetIndex] + func, attr, value = directions[side] + + return func(entity.x, entity.y, value) + end +end + end \ No newline at end of file diff --git a/src/entities/kevin.jl b/src/entities/kevin.jl index e31e682..9018699 100644 --- a/src/entities/kevin.jl +++ b/src/entities/kevin.jl @@ -81,4 +81,19 @@ function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::Maple.CrushBlock, r Ahorn.drawImage(ctx, frame, width - 8, height - 8, 24, 24, 8, 8) end +function Ahorn.rotated(entity::Maple.CrushBlock, steps::Int) + if abs(steps) % 2 == 1 + if entity.axes == "horizontal" + entity.axes = "vertical" + + return entity + + elseif entity.axes == "vertical" + entity.axes = "horizontal" + + return entity + end + end +end + end \ No newline at end of file diff --git a/src/entities/puffer.jl b/src/entities/puffer.jl index ab1303a..3410b84 100644 --- a/src/entities/puffer.jl +++ b/src/entities/puffer.jl @@ -16,7 +16,7 @@ const placements = Ahorn.PlacementDict( Dict{String, Any}( "right" => false ) - ) + ) ) sprite = "objects/puffer/idle00" @@ -34,4 +34,12 @@ function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::Maple.Puffer, room: Ahorn.drawSprite(ctx, sprite, 0, 0, sx=scaleX) end +function Ahorn.flipped(entity::Maple.Puffer, horizontal::Bool) + if horizontal + entity.right = !entity.right + + return entity + end +end + end \ No newline at end of file diff --git a/src/entities/spikes.jl b/src/entities/spikes.jl index 94a3a55..968ba50 100644 --- a/src/entities/spikes.jl +++ b/src/entities/spikes.jl @@ -251,7 +251,7 @@ function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::spikesUnion) Ahorn.drawSprite(ctx, "danger/triggertentacle/wiggle_v03", drawX + 3 * updown, drawY + 3 * !updown, rot=rotations[direction], tint=color2) end - else + else width = get(entity.data, "width", 8) height = get(entity.data, "height", 8) @@ -265,4 +265,91 @@ function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::spikesUnion) end end +for (to, from) in [(Maple.SpikesUp, Maple.SpikesDown), (Maple.TriggerSpikesUp, Maple.TriggerSpikesDown), (Maple.TriggerSpikesOriginalUp, Maple.TriggerSpikesOriginalDown)] + @eval function Ahorn.flipped(entity::$to, horizontal::Bool) + if !horizontal + return $from(entity.x, entity.y, entity.width, entity.type) + end + end + + @eval function Ahorn.flipped(entity::$from, horizontal::Bool) + if !horizontal + return $to(entity.x, entity.y, entity.width, entity.type) + end + end +end + +for (to, from) in [(Maple.SpikesLeft, Maple.SpikesRight), (Maple.TriggerSpikesLeft, Maple.TriggerSpikesRight), (Maple.TriggerSpikesOriginalLeft, Maple.TriggerSpikesOriginalRight)] + @eval function Ahorn.flipped(entity::$to, horizontal::Bool) + if horizontal + return $from(entity.x, entity.y, entity.height, entity.type) + end + end + + @eval function Ahorn.flipped(entity::$from, horizontal::Bool) + if horizontal + return $to(entity.x, entity.y, entity.height, entity.type) + end + end +end + +# TODO - Rotations might need offsets + +const spikesUp = [Maple.SpikesUp, Maple.TriggerSpikesUp, Maple.TriggerSpikesOriginalUp] +const spikesRight = [Maple.SpikesRight, Maple.TriggerSpikesRight, Maple.TriggerSpikesOriginalRight] +const spikesDown = [Maple.SpikesDown, Maple.TriggerSpikesDown, Maple.TriggerSpikesOriginalDown] +const spikesLeft = [Maple.SpikesLeft, Maple.TriggerSpikesLeft, Maple.TriggerSpikesOriginalLeft] + +for (left, normal, right) in zip(spikesLeft, spikesUp, spikesRight) + @eval function Ahorn.rotated(entity::$normal, steps::Int) + if steps > 0 + return Ahorn.rotated($right(entity.x, entity.y, entity.width, entity.type), steps - 1) + + elseif steps < 0 + return Ahorn.rotated($left(entity.x, entity.y, entity.width, entity.type), steps + 1) + end + + return entity + end +end + +for (left, normal, right) in zip(spikesUp, spikesRight, spikesDown) + @eval function Ahorn.rotated(entity::$normal, steps::Int) + if steps > 0 + return Ahorn.rotated($right(entity.x, entity.y, entity.height, entity.type), steps - 1) + + elseif steps < 0 + return Ahorn.rotated($left(entity.x, entity.y, entity.height, entity.type), steps + 1) + end + + return entity + end +end + +for (left, normal, right) in zip(spikesRight, spikesDown, spikesLeft) + @eval function Ahorn.rotated(entity::$normal, steps::Int) + if steps > 0 + return Ahorn.rotated($right(entity.x, entity.y, entity.width, entity.type), steps - 1) + + elseif steps < 0 + return Ahorn.rotated($left(entity.x, entity.y, entity.width, entity.type), steps + 1) + end + + return entity + end +end + +for (left, normal, right) in zip(spikesDown, spikesLeft, spikesUp) + @eval function Ahorn.rotated(entity::$normal, steps::Int) + if steps > 0 + return Ahorn.rotated($right(entity.x, entity.y, entity.height, entity.type), steps - 1) + + elseif steps < 0 + return Ahorn.rotated($left(entity.x, entity.y, entity.height, entity.type), steps + 1) + end + + return entity + end +end + end \ No newline at end of file diff --git a/src/entities/spring.jl b/src/entities/spring.jl index 264c843..f6e9a24 100644 --- a/src/entities/spring.jl +++ b/src/entities/spring.jl @@ -38,4 +38,16 @@ Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::Maple.Spring, room::Maple.Ro Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::Maple.SpringLeft, room::Maple.Room) = Ahorn.drawSprite(ctx, sprite, 9, -11, rot=pi / 2) Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::Maple.SpringRight, room::Maple.Room) = Ahorn.drawSprite(ctx, sprite, 3, 1, rot=-pi / 2) +function Ahorn.flipped(entity::Maple.SpringLeft, horizontal::Bool) + if horizontal + return Maple.SpringRight(entity.x, entity.y) + end +end + +function Ahorn.flipped(entity::Maple.SpringRight, horizontal::Bool) + if horizontal + return Maple.SpringLeft(entity.x, entity.y) + end +end + end \ No newline at end of file diff --git a/src/entities/wallbooster.jl b/src/entities/wallbooster.jl index fa4526a..53e8f49 100644 --- a/src/entities/wallbooster.jl +++ b/src/entities/wallbooster.jl @@ -62,4 +62,14 @@ function Ahorn.render(ctx::Ahorn.Cairo.CairoContext, entity::Maple.WallBooster, end end +# Offset X position so it flips in place +function Ahorn.flipped(entity::Maple.WallBooster, horizontal::Bool) + if horizontal + entity.left = !entity.left + entity.x += entity.left ? 8 : -8 + + return entity + end +end + end \ No newline at end of file diff --git a/src/helpers/tileset_splitter.jl b/src/helpers/tileset_splitter.jl index 0704d9b..523eba9 100644 --- a/src/helpers/tileset_splitter.jl +++ b/src/helpers/tileset_splitter.jl @@ -18,7 +18,7 @@ function generateTilesetSpriteMatrix(sprite::Ahorn.Sprite, tileWidth=8, tileHeig surface = CairoARGBSurface(tileWidth, tileHeight) ctx = Ahorn.getSurfaceContext(surface) - Ahorn.drawImage(ctx, sprite.surface, 0, 0, sprite.x + x * tileWidth, sprite.y + y * tileHeight, tileWidth, tileHeight) + Ahorn.drawImage(ctx, sprite.surface, 0, 0, sprite.x + x * tileWidth, sprite.y + y * tileHeight, tileWidth, tileHeight, alpha=1.0) res[y + 1, x + 1] = Ahorn.Sprite( 0, diff --git a/src/history.jl b/src/history.jl index b9cd3be..2642cb6 100644 --- a/src/history.jl +++ b/src/history.jl @@ -1,6 +1,6 @@ abstract type Snapshot end -mutable struct HistoryTimeline +@valueequals mutable struct HistoryTimeline snapshots::Array{Snapshot, 1} index::Int skip::Bool diff --git a/src/history_manager.jl b/src/history_manager.jl index 492cda9..5835b09 100644 --- a/src/history_manager.jl +++ b/src/history_manager.jl @@ -6,7 +6,7 @@ using ..Ahorn include("history.jl") -struct RoomSnapshot <: Snapshot +@valueequals struct RoomSnapshot <: Snapshot description::String room::Maple.Room layers::Array{String, 1} @@ -14,7 +14,7 @@ struct RoomSnapshot <: Snapshot RoomSnapshot(description::String, room::Maple.Room, layers::Array{String, 1}=String[]) = new(description, deepcopy(room), layers) end -struct SelectionSnapshot <: Snapshot +@valueequals struct SelectionSnapshot <: Snapshot description::String room::Maple.Room selections::Set{Ahorn.SelectedObject} @@ -22,7 +22,7 @@ struct SelectionSnapshot <: Snapshot SelectionSnapshot(description::String, room::Maple.Room, selections::Set{Ahorn.SelectedObject}) = new(description, room, deepcopy(selections)) end -struct MultiSnapshot <: Snapshot +@valueequals struct MultiSnapshot <: Snapshot description::String snapshots::Array{Snapshot, 1} end @@ -84,16 +84,18 @@ function getToolSelections() return Set{Ahorn.SelectedObject}() end -const historyTimelines = Dict{Maple.Map, HistoryTimeline}() +const historyTimelines = Dict{String, HistoryTimeline}() currentMap() = Ahorn.loadedState.map function getHistory(map::Maple.Map) - if !haskey(historyTimelines, map) - historyTimelines[map] = HistoryTimeline() + package = map.package + + if !haskey(historyTimelines, package) + historyTimelines[package] = HistoryTimeline() end - return historyTimelines[map] + return historyTimelines[package] end function undo!(map::Maple.Map=currentMap()) diff --git a/src/hotkey.jl b/src/hotkey.jl index 2bd4e37..7d3dc4a 100644 --- a/src/hotkey.jl +++ b/src/hotkey.jl @@ -16,6 +16,11 @@ const modifierNames = Dict{String, Function}( "meta" => modifierMeta ) +const specialNames = Dict{String, Int}( + "plus" => Int('+'), + "minus" => Int('-'), +) + struct Hotkey key::Integer callback::Function @@ -46,7 +51,10 @@ function Hotkey(s::String, callback::Function, modifiers::Array{Function, 1}=Fu if i == length(words) if length(word) == 1 key = Int(word[1]) - + + elseif haskey(specialNames, word) + key = specialNames[word] + # This is case sensitive! # Use values from Gtk.GdkKeySyms elseif isdefined(Gtk.GdkKeySyms, Symbol(word)) @@ -54,7 +62,7 @@ function Hotkey(s::String, callback::Function, modifiers::Array{Function, 1}=Fu else println(Base.stderr, "Invalid hotkey sequence") - + return nothing end end diff --git a/src/hotkeys.jl b/src/hotkeys.jl index 9e76875..5e552b3 100644 --- a/src/hotkeys.jl +++ b/src/hotkeys.jl @@ -42,5 +42,13 @@ const hotkeys = Hotkey[ Hotkey( "ctrl + f", focusFilterEntry! + ), + Hotkey( + "ctrl + plus", + zoomIn! + ), + Hotkey( + "ctrl + minus", + zoomOut! ) ] \ No newline at end of file diff --git a/src/init_external_modules.jl b/src/init_external_modules.jl index f579e39..c8620c7 100644 --- a/src/init_external_modules.jl +++ b/src/init_external_modules.jl @@ -2,7 +2,7 @@ function initExternalEntities() externalEntities = findExternalModules("entities") append!(loadedEntities, externalEntities) loadModule.(externalEntities) - loadExternalModules!(loadedModules, loadedEntities, "entities") + loadExternalZipModules!(loadedModules, loadedEntities, "entities") registerPlacements!(entityPlacements, loadedEntities) end @@ -10,7 +10,7 @@ function initExternalTriggers() externalTriggers = findExternalModules("triggers") append!(loadedTriggers, externalTriggers) loadModule.(externalTriggers) - loadExternalModules!(loadedModules, loadedTriggers, "triggers") + loadExternalZipModules!(loadedModules, loadedTriggers, "triggers") registerPlacements!(triggerPlacements, loadedTriggers) end @@ -18,18 +18,26 @@ function initExternalEffects() externalEffects = findExternalModules("effects") append!(loadedEffects, externalEffects) loadModule.(externalEffects) - loadExternalModules!(loadedModules, loadedEffects, "effects") + loadExternalZipModules!(loadedModules, loadedEffects, "effects") registerPlacements!(effectPlacements, loadedEffects) end +function initExternalLibraries() + externalLibraries = findExternalModules("libraries") + append!(loadedLibraries, externalLibraries) + loadModule.(externalLibraries) + loadExternalZipModules!(loadedModules, loadedLibraries, "libraries") +end + function initExternalTools() externalTools = findExternalModules("tools") append!(loadedTools, externalTools) loadModule.(externalTools) - loadExternalModules!(loadedModules, loadedTools, "tools") + loadExternalZipModules!(loadedModules, loadedTools, "tools") end function initExternalModules() + initExternalLibraries() initExternalEntities() initExternalTriggers() initExternalEffects() diff --git a/src/layers.jl b/src/layers.jl index 587d7c3..34a11e9 100644 --- a/src/layers.jl +++ b/src/layers.jl @@ -15,6 +15,7 @@ end include("drawable_room.jl") Base.:(==)(lhs::Layer, rhs::Layer) = lhs.name == rhs.name +Base.hash(l::Layer, h::UInt) = hash(l.name, h) const redrawingFuncs = Dict{String, Function}() diff --git a/src/libraries.jl b/src/libraries.jl new file mode 100644 index 0000000..c0ca3d7 --- /dev/null +++ b/src/libraries.jl @@ -0,0 +1,5 @@ +# For shared libraries in plugins +# This file only serves as a place to keep track of this constant +# Ahorn will not use it for anything besides loading/reloading plugin libraries + +const loadedLibraries = String[] \ No newline at end of file diff --git a/src/menubar.jl b/src/menubar.jl index cd53a29..8ab66ac 100644 --- a/src/menubar.jl +++ b/src/menubar.jl @@ -76,6 +76,7 @@ const menubarDebugChoices = Menubar.AbstractMenuItem[ Menubar.MenuChoice("Entities", (w) -> debug.reloadEntities!()), Menubar.MenuChoice("Triggers", (w) -> debug.reloadTriggers!()), Menubar.MenuChoice("Effects", (w) -> debug.reloadEffects!()), + Menubar.MenuChoice("Libraries", (w) -> debug.reloadLibraries!()), Menubar.MenuChoice("External Sprites", (w) -> loadAllExternalSprites!(force=true)), Menubar.MenuChoice("Language Files", (w) -> debug.reloadLangdata()), ] diff --git a/src/mods.jl b/src/mods.jl index e335871..9c0b23b 100644 --- a/src/mods.jl +++ b/src/mods.jl @@ -386,10 +386,13 @@ function findExternalModulesInFolder(filename::String, pluginPath::String) return res end - for file in readdir(pluginDir) - if hasExt(file, ".jl") - rawpath = joinpath(pluginDir, file) - push!(res, (rawpath, rawpath, open(f -> read(f, String), rawpath))) + for (root, dirs, files) in walkdir(pluginDir) + for file in files + if hasExt(file, ".jl") + rawpath = joinpath(root, file) + + push!(res, (rawpath, rawpath, open(f -> read(f, String), rawpath))) + end end end @@ -515,9 +518,11 @@ function findExternalModules(args::String...) for folder in targetFolders path = joinpath(folder, args...) if isdir(path) - for file in readdir(path) - if hasExt(file, ".jl") - push!(res, joinpath(path, file)) + for (root, dirs, files) in walkdir(path) + for file in files + if hasExt(file, ".jl") + push!(res, joinpath(root, file)) + end end end end @@ -526,7 +531,7 @@ function findExternalModules(args::String...) return res end -function loadExternalModules!(loadedModules::Dict{String, Module}, loadedNames::Array{String, 1}, args::String...) +function loadExternalZipModules!(loadedModules::Dict{String, Module}, loadedNames::Array{String, 1}, args::String...) # ZipFile uses unix paths path = joinpath("Ahorn", args...) path = replace(path, "\\" => "/") @@ -545,7 +550,7 @@ function loadExternalModules!(loadedModules::Dict{String, Module}, loadedNames:: try if targetMtime > get(loadedModulesTimes, fakeFn, 0) - loadedModules[fakeFn] = Base.eval(Main, Meta.parse(content)) + loadedModules[fakeFn] = Base.eval(Ahorn, Meta.parse(strip(content))) loadedModulesTimes[fakeFn] = targetMtime end diff --git a/src/module_loader.jl b/src/module_loader.jl index 7c14c78..9addf70 100644 --- a/src/module_loader.jl +++ b/src/module_loader.jl @@ -13,7 +13,7 @@ function loadModule(fn::String, force::Bool=false) fileMtime = stat(fn).mtime if fileMtime > get(loadedModulesTimes, fn, 0) || force - loadedModules[fn] = Base.eval(Ahorn, Meta.parse(open(file -> read(file, String), fn))) + loadedModules[fn] = include(fn) loadedModulesTimes[fn] = fileMtime return true diff --git a/src/property_menu.jl b/src/property_menu.jl index 292c2d7..fb7cf7e 100644 --- a/src/property_menu.jl +++ b/src/property_menu.jl @@ -1,5 +1,4 @@ -const lockedEntityEditingFields = ["x", "y", "width", "height"] -const lockedDecalEditingFields = ["x", "y", "scaleX", "scaleY", "texture"] +const entityTriggerUnion = Union{Maple.Entity, Maple.Trigger} lastPropertyWindow = nothing lastPropertyWindowDestroyed = false @@ -48,13 +47,14 @@ function getTargets(x::Number, y::Number, room::Maple.Room, targetLayer::Layer, if isempty(selections) selection = bestSelection(getSelected(room, targetLayer, rect)) + if selection !== nothing push!(targets, selection.target) end - - else + + else base = hasSelectionAt(selections, rect, room) - + if isa(base, Entity) || isa(base, Trigger) push!(targets, base) @@ -69,83 +69,90 @@ function getTargets(x::Number, y::Number, room::Maple.Room, targetLayer::Layer, elseif isa(base, Decal) push!(targets, base) - push!(targets, [ + append!(targets, [ selection.target for selection in selections if isa(selection.target, Decal) && selection.target != base - ]...) + ]) end end return targets end -function displayProperties(x::Number, y::Number, room::Maple.Room, targetLayer::Layer, toolsLayer::Layer, selections::Set{Ahorn.SelectedObject}=Set{Ahorn.SelectedObject}()) - targets = getTargets(x, y, room, targetLayer, selections) - - if !isempty(targets) - baseTarget = targets[1] - - if isa(baseTarget, Entity) || isa(baseTarget, Trigger) - callback = function(data::Dict{String, Any}) - updateTarget = true - - minWidth, minHeight = minimumSize(baseTarget) - hasWidth, hasHeight = haskey(baseTarget.data, "width"), haskey(baseTarget.data, "height") - width, height = Int(get(data, "width", minWidth)), Int(get(data, "height", minHeight)) +function displayProperties(x::Number, y::Number, room::Maple.Room, targetLayer::Layer, toolsLayer::Layer, baseTarget::entityTriggerUnion, targets::Array{Any, 1}) + callback = function(data::Dict{String, Any}) + updateTarget = true - if hasWidth && width < minWidth || hasHeight && height < minHeight - updateTarget = Ahorn.topMostAskDialog("The size specified is smaller than the recommended minimum size ($minWidth, $minHeight)\nDo you want to keep this size regardless?", lastPropertyWindow) - end + minWidth, minHeight = minimumSize(baseTarget) + hasWidth, hasHeight = haskey(baseTarget.data, "width"), haskey(baseTarget.data, "height") + width, height = Int(get(data, "width", minWidth)), Int(get(data, "height", minHeight)) - if updateTarget - History.addSnapshot!(History.RoomSnapshot("Properties", room)) + if hasWidth && width < minWidth || hasHeight && height < minHeight + updateTarget = Ahorn.topMostAskDialog("The size specified is smaller than the recommended minimum size ($minWidth, $minHeight)\nDo you want to keep this size regardless?", lastPropertyWindow) + end - for target in targets - if isa(target, Entity) || isa(target, Trigger) - merge!(target.data, deepcopy(data)) - end - end + if updateTarget + History.addSnapshot!(History.RoomSnapshot("Properties", room)) - redrawLayer!(targetLayer) - redrawLayer!(toolsLayer) + for target in targets + if isa(target, Entity) || isa(target, Trigger) + merge!(target.data, deepcopy(data)) end end - ids = join([Int(t.id) for t in targets], ", ") - ignores = length(targets) > 1 ? String["x", "y", "width", "height", "nodes"] : String[] - options = entityConfigOptions(baseTarget, ignores) + redrawLayer!(targetLayer) + redrawLayer!(toolsLayer) + end + end - if !isempty(options) - spawnPropertyWindow("$(baseTitle) - Editing '$(baseTarget.name)' - ID: $ids", options, callback, lockedEntityEditingFields) - end + ids = join([Int(t.id) for t in targets], ", ") + ignores = editingIgnored(baseTarget, length(targets) > 1) + options = propertyOptions(baseTarget, ignores) + fieldOrder = editingOrder(baseTarget) - elseif isa(baseTarget, Decal) - callback = function(data::Dict{String, Any}) - History.addSnapshot!(History.RoomSnapshot("Properties", room)) + if !isempty(options) + spawnPropertyWindow("$(baseTitle) - Editing '$(baseTarget.name)' - ID: $ids", options, callback, fieldOrder) + end +end - for target in targets - if isa(target, Decal) - texture = hasExt(data["texture"], ".png") ? data["texture"] : data["texture"] * ".png" +function displayProperties(x::Number, y::Number, room::Maple.Room, targetLayer::Layer, toolsLayer::Layer, baseTarget::Maple.Decal, targets::Array{Any, 1}) + callback = function(data::Dict{String, Any}) + History.addSnapshot!(History.RoomSnapshot("Properties", room)) - target.x = get(data, "x", target.x) - target.y = get(data, "y", target.y) + for target in targets + if isa(target, Decal) + texture = hasExt(data["texture"], ".png") ? data["texture"] : data["texture"] * ".png" - target.texture = texture + target.x = get(data, "x", target.x) + target.y = get(data, "y", target.y) - target.scaleX = data["scaleX"] - target.scaleY = data["scaleY"] - end - end + target.texture = texture - redrawLayer!(targetLayer) - redrawLayer!(toolsLayer) + target.scaleX = data["scaleX"] + target.scaleY = data["scaleY"] end + end - ignores = length(targets) > 1 ? String["x", "y"] : String[] - options = decalConfigOptions(baseTarget, ignores) + redrawLayer!(targetLayer) + redrawLayer!(toolsLayer) + end - if !isempty(options) - spawnPropertyWindow("$(baseTitle) - Editing '$(splitext(baseTarget.texture)[1])'", options, callback, lockedDecalEditingFields) - end - end + ignores = editingIgnored(baseTarget, length(targets) > 1) + options = propertyOptions(baseTarget, ignores) + fieldOrder = editingOrder(baseTarget) + + if !isempty(options) + spawnPropertyWindow("$(baseTitle) - Editing '$(splitext(baseTarget.texture)[1])'", options, callback, fieldOrder) end +end + + +function displayProperties(x::Number, y::Number, room::Maple.Room, targetLayer::Layer, toolsLayer::Layer, baseTarget::Nothing, targets::Array{Any, 1}) + # Do nothing +end + +function displayProperties(x::Number, y::Number, room::Maple.Room, targetLayer::Layer, toolsLayer::Layer, selections::Set{Ahorn.SelectedObject}=Set{Ahorn.SelectedObject}()) + targets = getTargets(x, y, room, targetLayer, selections) + baseTarget = get(targets, 1, nothing) + + displayProperties(x, y, room, targetLayer, toolsLayer, baseTarget, targets) end \ No newline at end of file diff --git a/src/selections.jl b/src/selections.jl index 9d83e1e..cf45a2b 100644 --- a/src/selections.jl +++ b/src/selections.jl @@ -1,4 +1,4 @@ -mutable struct TileSelection +@valueequals mutable struct TileSelection fg::Bool tiles::Array{Char, 2} selection::Rectangle @@ -8,7 +8,7 @@ mutable struct TileSelection offsetY::Number end -struct SelectedObject +@valueequals mutable struct SelectedObject layerName::String rectangle::Rectangle target @@ -17,6 +17,34 @@ end TileSelection(fg::Bool, tiles::Array{Char, 2}, selection::Rectangle) = TileSelection(fg, tiles, selection, selection.x, selection.y, 0, 0) +deleted(tiles::TileSelection, node::Int) = nothing + +moved(tiles::TileSelection) = nothing +moved(tiles::TileSelection, x::Int, y::Int) = nothing + +resized(tiles::TileSelection) = nothing +resized(tiles::TileSelection, width::Int, height::Int) = nothing + +function flipped(tiles::TileSelection, horizontal::Bool) + tiles.tiles = reverse(tiles.tiles, dims=horizontal ? 2 : 1) + + return tiles +end + +function rotated(tiles::TileSelection, steps::Int) + rotationFunc = steps > 0 ? rotr90 : rotl90 + + for i in 1:abs(steps) % 4 + tiles.tiles = rotationFunc(tiles.tiles) + end + + # -16 to remove air padding around the tiles + height, width = size(tiles.tiles) + tiles.selection = Rectangle(tiles.selection.x, tiles.selection.y, width * 8 - 16, height * 8 - 16) + + return tiles +end + const selectableLayers = ["fgTiles", "bgTiles", "entities", "triggers", "fgDecals", "bgDecals"] const selectionTargets = Dict{String, Function}( "entities" => room -> room.entities, diff --git a/src/shapes/circle.jl b/src/shapes/circle.jl index dbd14bc..d6a8580 100644 --- a/src/shapes/circle.jl +++ b/src/shapes/circle.jl @@ -1,4 +1,4 @@ -struct Circle +@valueequals struct Circle x::Number y::Number @@ -12,8 +12,6 @@ function circumference(circle::Circle) return 2 * pi * circle.r end -Base.:(==)(lhs::Circle, rhs::Circle) = lhs.x == rhs.x && lhs.y == rhs.y && lhs.r == rhs.r - # Efficient enough, checks might be too small for huge circles function pointsOnCircle(circle::Circle; filled::Bool=false, checks::Int=max(1, ceil(Int, circumference(circle)))) res = Set{Tuple{Number, Number}}() diff --git a/src/shapes/ellipse.jl b/src/shapes/ellipse.jl index 54c0b8c..b4683a3 100644 --- a/src/shapes/ellipse.jl +++ b/src/shapes/ellipse.jl @@ -1,4 +1,4 @@ -struct Ellipse +@valueequals struct Ellipse x::Number y::Number @@ -10,8 +10,6 @@ function circumference(ellipse::Ellipse) return pi * (3 * (ellipse.rh + ellipse.rv) - sqrt(10 * ellipse.rh * ellipse.rv + 3 * (ellipse.rh^2 + ellipse.rv^2))) end -Base.:(==)(lhs::Ellipse, rhs::Ellipse) = lhs.x == rhs.x && lhs.y == rhs.y && lhs.rv == rhs.rv && lhs.rh == rhs.rh - ellipse_t(theta, e::Ellipse) = atan(e.rh / e.rv * tan(theta)) ellipse_f(t, e::Ellipse) = sqrt(e.rh^2 * sin(t)^2 + e.rv^2 * cos(t)^2) diff --git a/src/shapes/line.jl b/src/shapes/line.jl index a730904..cb0b4e5 100644 --- a/src/shapes/line.jl +++ b/src/shapes/line.jl @@ -1,6 +1,6 @@ const tolerance = 10.0^-10 -struct Line +@valueequals struct Line x1::Number y1::Number @@ -8,8 +8,6 @@ struct Line y2::Number end -Base.:(==)(lhs::Line, rhs::Line) = lhs.x1 == rhs.x1 && lhs.y1 == rhs.y1 && lhs.x2 == rhs.x2 && lhs.y2 == rhs.y2 - # How much is missing to get to the closest new "grid square" function rayDelta(n, a) s = sign(a) diff --git a/src/shapes/rectangle.jl b/src/shapes/rectangle.jl index db3ec14..247a902 100644 --- a/src/shapes/rectangle.jl +++ b/src/shapes/rectangle.jl @@ -1,4 +1,4 @@ -struct Rectangle +@valueequals struct Rectangle x::Union{Int64, Float64} y::Union{Int64, Float64} w::Union{Int64, Float64} @@ -8,8 +8,6 @@ struct Rectangle Rectangle(x, y, w, h) = new(Float64(x), Float64(y), Float64(w), Float64(h)) end -Base.:(==)(lhs::Rectangle, rhs::Rectangle) = lhs.x == rhs.x && lhs.y == rhs.y && lhs.w == rhs.w && lhs.h == rhs.h - # AABB check function checkCollision(box1::Rectangle, box2::Rectangle) return ( diff --git a/src/shapes/simple_curve.jl b/src/shapes/simple_curve.jl index bd19d1d..f291110 100644 --- a/src/shapes/simple_curve.jl +++ b/src/shapes/simple_curve.jl @@ -1,4 +1,4 @@ -struct SimpleCurve +@valueequals struct SimpleCurve start::Tuple{Number, Number} stop::Tuple{Number, Number} control::Tuple{Number, Number} diff --git a/src/tools/brushes.jl b/src/tools/brushes.jl index dda30d2..58b049d 100644 --- a/src/tools/brushes.jl +++ b/src/tools/brushes.jl @@ -45,9 +45,11 @@ selectedBrush = brushes[1] hoveringBrush = nothing const phantomBrushes = Dict{Tuple{Integer, Integer}, Ahorn.Brush}() +lastX, lastY = nothing, nothing + function applyPhantomBrushes() if !isempty(phantomBrushes) - Ahorn.History.addSnapshot!(Ahorn.History.RoomSnapshot("Brush ($(selectedBrush.name), $material)", Ahorn.loadedState.room)) + Ahorn.History.addSnapshot!(Ahorn.History.RoomSnapshot("Brush ($(selectedBrush.name), $material)", Ahorn.loadedState.room)) end for (pos, brush) in phantomBrushes @@ -83,6 +85,30 @@ function drawBrushes(layer::Ahorn.Layer, room::Ahorn.DrawableRoom, camera::Ahorn end end +function addPhantomBrush(x::Int, y::Int, selectedBrush::Ahorn.Brush) + if !haskey(phantomBrushes, (x, y)) + phantomBrushes[(x, y)] = deepcopy(selectedBrush) + + return true + end + + return false +end + +function addPhantomBrush(points::Array{Tuple{Int, Int}, 1}, selectedBrush::Ahorn.Brush) + res = false + + for (x, y) in points + res |= addPhantomBrush(x, y, selectedBrush) + end + + return res +end + +function addBrushOffset(bx, bw, ox, box, by, bh, oy, boy) + return div(bx, bw) * bw + ox - box + 1, div(by, bh) * bh + oy - boy + 1 +end + function cleanup() global hoveringBrush = nothing empty!(phantomBrushes) @@ -138,16 +164,35 @@ function selectionMotion(x1::Number, y1::Number, x2::Number, y2::Number) bh, bw = size(pixels) ox, oy = mod(startX, bw), mod(startY, bh) - bx, by = div(x2, bw) * bw + ox - box + 1, div(y2, bh) * bh + oy - boy + 1 + bx, by = addBrushOffset(x2, bw, ox, box, y2, bw, oy, boy) - if !haskey(phantomBrushes, (bx, by)) - phantomBrushes[(bx, by)] = deepcopy(selectedBrush) + smoothWithLines = get(Ahorn.config, "tools_brushes_smoother_brushes", true) + redraw = false + if smoothWithLines && lastX !== nothing && lastY !== nothing + pointsRaw = Ahorn.pointsOnLine(Ahorn.Line(x2, y2, lastX, lastY)) + points = Tuple{Int, Int}[] + + for (x, y) in pointsRaw + push!(points, addBrushOffset(x, bw, ox, box, y, bw, oy, boy)) + end + + redraw = addPhantomBrush(points, selectedBrush) + + else + redraw = addPhantomBrush(bx, by, selectedBrush) + end + + if redraw Ahorn.redrawLayer!(toolsLayer) end + + global lastX, lastY = x2, y2 end function selectionFinish(rect::Ahorn.Rectangle) + global lastX, lastY = nothing, nothing + applyPhantomBrushes() end @@ -230,12 +275,12 @@ function keyboard(event::Ahorn.eventKey) shouldRedraw = false if event.keyval == Ahorn.keyval("l") - selectedBrush.rotation = mod(selectedBrush.rotation + 1, 4) + selectedBrush.rotation = mod(selectedBrush.rotation - 1, 4) shouldRedraw |= true elseif event.keyval == Ahorn.keyval("r") - selectedBrush.rotation = mod(selectedBrush.rotation - 1, 4) + selectedBrush.rotation = mod(selectedBrush.rotation + 1, 4) shouldRedraw |= true end diff --git a/src/tools/placements.jl b/src/tools/placements.jl index fe2a494..87ff5fd 100644 --- a/src/tools/placements.jl +++ b/src/tools/placements.jl @@ -20,10 +20,15 @@ selectionRect = nothing previewGhost = nothing clonedEntity = nothing +lastPreviewX = -1 +lastPreviewY = -1 + blacklistedCloneAttrs = ["id", "x", "y"] placementLayers = String["entities", "triggers", "fgDecals", "bgDecals"] +const EntityTriggerUnion = Union{Maple.Entity, Maple.Trigger} + function drawPlacements(layer::Ahorn.Layer, room::Ahorn.DrawableRoom, camera::Ahorn.Camera) ctx = Ahorn.getSurfaceContext(toolsLayer.surface) @@ -60,9 +65,7 @@ function generatePreview!(layer::Ahorn.Layer, material::Any, x, y; sx=1, sy=1, n return Ahorn.updateCachedEntityPosition!(placementsCache, placements, Ahorn.loadedState.map, Ahorn.loadedState.room, materialName, x, y, nx, ny) else - if clonedEntity === nothing - global clonedEntity = Ahorn.generateEntity(Ahorn.loadedState.map, Ahorn.loadedState.room, material, x, y, nx, ny) - end + global clonedEntity = Ahorn.generateEntity(Ahorn.loadedState.map, Ahorn.loadedState.room, material, x, y, nx, ny) return Ahorn.updateEntityPosition!(clonedEntity, material, Ahorn.loadedState.map, Ahorn.loadedState.room, x, y, nx, ny) end @@ -75,7 +78,7 @@ function generatePreview!(layer::Ahorn.Layer, material::Any, x, y; sx=1, sy=1, n end end -function pushPreview!(layer::Ahorn.Layer, room::Maple.Room, preview::Any) +function pushPreview!(layer::Ahorn.Layer, room::Maple.Room, preview) if !get(Ahorn.config, "allow_out_of_bounds_placement", false) width, height = room.size x, y = Ahorn.position(preview) @@ -230,29 +233,20 @@ function materialDoubleClicked(material::String) end end -function updatePreviewGhost(x::Number, y::Number) - targetX, targetY = x, y +samePosition(a::Maple.Decal, b::Maple.Decal) = a.x == b.x && a.y == b.y +samePosition(a::EntityTriggerUnion, b::EntityTriggerUnion) = a.x == b.x && a.y == b.y +samePosition(a, b) = false - if !Ahorn.modifierControl() - targetX = x * 8 - 8 - targetY = y * 8 - 8 - end +function updatePreviewGhost(x::Number, y::Number, force::Bool=false) + global lastPreviewX, lastPreviewY = x, y prevGhost = deepcopy(previewGhost) - newGhost = generatePreview!(targetLayer, material, targetX, targetY, sx=scaleX, sy=scaleY) + newGhost = generatePreview!(targetLayer, material, x, y, sx=scaleX, sy=scaleY) if newGhost != prevGhost # No need to redraw if the target is on the same tile - if isa(prevGhost, Maple.Entity) || isa(prevGhost, Maple.Trigger) - if newGhost.data["x"] == prevGhost.data["x"] && newGhost.data["y"] == prevGhost.data["y"] - return false - end - end - - if isa(prevGhost, Maple.Decal) - if newGhost.x == prevGhost.x && newGhost.y == prevGhost.y - return false - end + if !force && samePosition(newGhost, prevGhost) + return false end global previewGhost = newGhost @@ -264,7 +258,7 @@ end function mouseMotion(x::Number, y::Number) if !Ahorn.modifierControl() && selectionRect === nothing - updatePreviewGhost(x, y) + updatePreviewGhost(x * 8 - 8, y * 8 - 8) end end @@ -274,7 +268,7 @@ function mouseMotionAbs(x::Number, y::Number) end end -function placementFunc(target::Union{Maple.Entity, Maple.Trigger}) +function placementFunc(target::EntityTriggerUnion) constructor = isa(target, Maple.Entity) ? Maple.Entity : Maple.Trigger return (x::Number, y::Number) -> constructor(target.name, x=x, y=y) @@ -295,7 +289,7 @@ function generateClonedEntityPlacement(name::String, target) Dict{String, Any}((k, v) for (k, v) in deepcopy(target.data) if !(k in blacklistedCloneAttrs)), Dict{String, Any}("__x" => target.data["x"], "__y" => target.data["y"]) ), - function(entity::Union{Maple.Entity, Maple.Trigger}) + function(entity::EntityTriggerUnion) nodes = get(entity.data, "nodes", Tuple{Integer, Integer}[]) if length(nodes) > 0 x, y = entity.data["x"], entity.data["y"] @@ -352,8 +346,6 @@ end function rightClickAbs(x::Number, y::Number) Ahorn.displayProperties(x, y, Ahorn.loadedState.room, targetLayer, toolsLayer) - - Ahorn.redrawLayer!(toolsLayer) end function selectionMotionAbs(x1::Number, y1::Number, x2::Number, y2::Number) @@ -450,44 +442,97 @@ resizeModifiers = Dict{Integer, Tuple{Number, Number}}( Int('d') => (0, -1) ) -scaleMultipliers = Dict{Integer, Tuple{Number, Number}}( +# (Key, steps clockwise) +rotationSteps = Dict{Integer, Integer}( + Int('r') => 1, + Int('l') => -1, +) + +# (Key code, horizontal flip) +flipDirections = Dict{Integer, Bool}( # Vertical Flip - Int('v') => (1, -1), + Int('v') => false, # Horizontal Flip - Int('h') => (-1, 1), + Int('h') => true, ) +function handleFlipping(event::Ahorn.eventKey, ghost::Maple.Decal) + horizontal = flipDirections[event.keyval] + Ahorn.flipped(ghost, horizontal) + + global scaleX = ghost.scaleX + global scaleY = ghost.scaleY + + return true +end + +function handleFlipping(event::Ahorn.eventKey, ghost::EntityTriggerUnion) + horizontal = flipDirections[event.keyval] + flipped = Ahorn.flipped(ghost, horizontal) + + if flipped !== nothing + global materialName = nothing + global material = generateClonedEntityPlacement(targetLayer.name, flipped) + + updatePreviewGhost(lastPreviewX, lastPreviewY, true) + end + + return flipped !== nothing +end + +handleFlipping(event::Ahorn.eventKey, ghost) = false + +function handleRotation(event::Ahorn.eventKey, ghost::EntityTriggerUnion) + steps = rotationSteps[event.keyval] + rotated = Ahorn.rotated(ghost, steps) + + if rotated !== nothing + global materialName = nothing + global material = generateClonedEntityPlacement(targetLayer.name, rotated) + + updatePreviewGhost(lastPreviewX, lastPreviewY, true) + end + + return rotated !== nothing +end + +handleRotation(event::Ahorn.eventKey, ghost::Maple.Decal) = false +handleRotation(event::Ahorn.eventKey, ghost) = false + +function handleResize(event::Ahorn.eventKey, ghost::Maple.Decal) + extraW, extraH = resizeModifiers[event.keyval] + minVal, maxVal = decalScaleVals + + global scaleX = floor(Int, sign(scaleX) * clamp(abs(scaleX) * 2.0^extraW, minVal, maxVal)) + global scaleY = floor(Int, sign(scaleY) * clamp(abs(scaleY) * 2.0^extraH, minVal, maxVal)) + + ghost.scaleX = scaleX + ghost.scaleY = scaleY + + return true +end + +handleResize(event::Ahorn.eventKey, ghost::EntityTriggerUnion) = false +handleResize(event::Ahorn.eventKey, ghost) = false + function keyboard(event::Ahorn.eventKey) redraw = false - if haskey(scaleMultipliers, event.keyval) - msx, msy = scaleMultipliers[event.keyval] - - global scaleX *= msx - global scaleY *= msy + if haskey(flipDirections, event.keyval) + redraw |= handleFlipping(event, previewGhost) + end - redraw = true + if haskey(rotationSteps, event.keyval) + redraw |= handleRotation(event, previewGhost) end if haskey(resizeModifiers, event.keyval) - extraW, extraH = resizeModifiers[event.keyval] - minVal, maxVal = decalScaleVals - - global scaleX = floor(Int, sign(scaleX) * clamp(abs(scaleX) * 2.0^extraW, minVal, maxVal)) - global scaleY = floor(Int, sign(scaleY) * clamp(abs(scaleY) * 2.0^extraH, minVal, maxVal)) - - redraw = true + redraw |= handleResize(event, previewGhost) end if redraw - name = Ahorn.layerName(targetLayer) - - if name == "fgDecals" || name == "bgDecals" - global previewGhost = generatePreview!(targetLayer, material, previewGhost.x, previewGhost.y, sx=scaleX, sy=scaleY) - - Ahorn.redrawLayer!(toolsLayer) - end + Ahorn.redrawLayer!(toolsLayer) end return true diff --git a/src/tools/selection.jl b/src/tools/selection.jl index d0cfc79..4c052c9 100644 --- a/src/tools/selection.jl +++ b/src/tools/selection.jl @@ -15,6 +15,8 @@ selectionRect = Ahorn.Rectangle(0, 0, 0, 0) selectionPreviews = Set{Ahorn.SelectedObject}() selections = Set{Ahorn.SelectedObject}() +areaOperationArea = nothing + lastX, lastY = -1, -1 shouldDrag = false canDrag = false @@ -122,6 +124,45 @@ function getSelectionLeftCorner(selections::Set{Ahorn.SelectedObject}) return tlx, tly end +getUpdatedRectangle(selection::Ahorn.SelectedObject, target::Ahorn.TileSelection) = target.selection + +function getUpdatedRectangle(selection::Ahorn.SelectedObject, target::Ahorn.Decal) + Ahorn.getSelection(target, relevantRoom) +end + +function getUpdatedRectangle(selection::Ahorn.SelectedObject, target::Union{Maple.Entity, Maple.Trigger}) + node = selection.node + rectangle = Ahorn.getSelection(target, relevantRoom) + + if isa(rectangle, Ahorn.Rectangle) + return rectangle + + else + return rectangle[node + 1] + end +end + +function getSelectionArea(fitToGrid::Bool=true) + rectangles = [ + getUpdatedRectangle(selection, selection.target) + for selection in selections + ] + + baseArea = Ahorn.coverRectangles(rectangles) + + if fitToGrid + offsetX, offsetY = baseArea.x % 8, baseArea.y % 8 + width, height = baseArea.w + offsetX, baseArea.h + offsetY + width = width % 8 == 0 ? width : ceil(Int, width / 8) * 8 + height = height % 8 == 0 ? height : ceil(Int, height / 8) * 8 + + return Ahorn.Rectangle(baseArea.x - offsetX, baseArea.y - offsetY, width, height) + + else + return baseArea + end +end + function pasteSelections() if isempty(selectionsClipboard) return false @@ -237,7 +278,7 @@ function drawSelections(layer::Ahorn.Layer, room::Ahorn.DrawableRoom, camera::Ah for selection in selectionsArray layer, box, target, node = selection.layerName, selection.rectangle, selection.target, selection.node - + if isa(target, Maple.Entity) && !(target in [row[1] for row in drawnTargets]) Ahorn.renderEntitySelection(ctx, toolsLayer, target, relevantRoom) end @@ -263,27 +304,41 @@ function drawSelections(layer::Ahorn.Layer, room::Ahorn.DrawableRoom, camera::Ah end end - for preview in selectionPreviews - layer, box, target, node = preview.layerName, preview.rectangle, preview.target, preview.node + if !shouldDrag + for preview in selectionPreviews + layer, box, target, node = preview.layerName, preview.rectangle, preview.target, preview.node - if isa(target, Maple.Entity) && !(target in [row[1] for row in drawnTargets]) - Ahorn.renderEntitySelection(ctx, toolsLayer, target, relevantRoom) - end + if isa(target, Maple.Entity) && !(target in [row[1] for row in drawnTargets]) + Ahorn.renderEntitySelection(ctx, toolsLayer, target, relevantRoom) + end - if isa(target, Maple.Trigger) && !(target in [row[1] for row in drawnTargets]) - Ahorn.renderTriggerSelection(ctx, toolsLayer, target, relevantRoom) - end + if isa(target, Maple.Trigger) && !(target in [row[1] for row in drawnTargets]) + Ahorn.renderTriggerSelection(ctx, toolsLayer, target, relevantRoom) + end - if !((target, node) in drawnTargets) && !(preview in selections) - Ahorn.drawRectangle(ctx, box, Ahorn.colors.selection_preview_fc, Ahorn.colors.selection_preview_bc) - end + if !((target, node) in drawnTargets) && !(preview in selections) + Ahorn.drawRectangle(ctx, box, Ahorn.colors.selection_preview_fc, Ahorn.colors.selection_preview_bc) + end - push!(drawnTargets, (target, node)) + push!(drawnTargets, (target, node)) + end end return true end +function updateAreaOperationRectangle!(area::Ahorn.Rectangle) + if areaOperationArea === nothing + global areaOperationArea = area + end + + return areaOperationArea +end + +function clearAreaOperationRectangle!() + global areaOperationArea = nothing +end + function clearDragging!() global lastX = -1 global lastY = -1 @@ -296,6 +351,11 @@ function clearResize!() global canResize = false end +function clearSelections!() + finalizeSelections!(selections) + empty!(selections) +end + function cleanup() finalizeSelections!(selections) empty!(selections) @@ -313,7 +373,7 @@ end function toolSelected(subTools::Ahorn.ListContainer, layers::Ahorn.ListContainer, materials::Ahorn.ListContainer) global relevantRoom = Ahorn.loadedState.room - + wantedLayer = get(Ahorn.persistence, "placements_layer", "entities") Ahorn.updateLayerList!(vcat(["all"], Ahorn.selectableLayers), row -> row[1] == Ahorn.layerName(targetLayer)) @@ -340,9 +400,7 @@ function updateSelectionPreviews(x::Int, y::Int) rect = Ahorn.Rectangle(x, y, 1, 1) properlyUpdateSelections!(rect, selectionPreviews, best=true) - # TODO - Unhaunt haunted branch, this works for now - #sameSelections = previousPreviews == selectionPreviews - sameSelections = length(previousPreviews) == length(selectionPreviews) && sort(collect(previousPreviews)) == sort(collect(selectionPreviews)) + sameSelections = previousPreviews == selectionPreviews if !sameSelections Ahorn.redrawLayer!(toolsLayer) @@ -469,6 +527,8 @@ function selectionMotionAbs(x1::Number, y1::Number, x2::Number, y2::Number) toolsLayer.redraw = true Ahorn.redrawLayer!(Ahorn.getLayerByName(drawingLayers, layer)) + + clearAreaOperationRectangle!() end end end @@ -489,6 +549,8 @@ function selectionMotionAbs(x1::Number, y1::Number, x2::Number, y2::Number) toolsLayer.redraw = true redrawTargetLayer!(targetLayer, selections, String["fgTiles", "bgTiles"], onlyMark=true) Ahorn.redrawCanvas!() + + clearAreaOperationRectangle!() end end end @@ -553,8 +615,14 @@ function selectionFinishAbs(rect::Ahorn.Rectangle) properlyUpdateSelections!(rect, selections) end + # Clear previews after movement drag + if shouldDrag + empty!(selectionPreviews) + end + clearDragging!() clearResize!() + clearAreaOperationRectangle!() updateCursor() global selectionRect = Ahorn.Rectangle(0, 0, 0, 0) @@ -568,6 +636,7 @@ function leftClickAbs(x::Number, y::Number) clearDragging!() clearResize!() + clearAreaOperationRectangle!() updateCursor() Ahorn.redrawLayer!(toolsLayer) @@ -605,7 +674,7 @@ end function applyTileSelecitonBrush!(target::Ahorn.TileSelection, clear::Bool=false) Maple.updateTileSize!(relevantRoom, '0', '0') - + roomTiles = target.fg ? relevantRoom.fgTiles : relevantRoom.bgTiles tiles = clear ? fill('0', size(target.tiles)) : target.tiles @@ -752,6 +821,9 @@ function applyGridMovement!(target::Ahorn.TileSelection, gridSize, directionX, d applyMovement!(target, ox - target.offsetX, oy - target.offsetY) end + +# Deprecated, now uses Ahorn.moved +# Getting removed in the future function notifyMovement!(entity::Maple.Entity) Ahorn.eventToModules(Ahorn.loadedEntities, "moved", entity) Ahorn.eventToModules(Ahorn.loadedEntities, "moved", entity, relevantRoom) @@ -762,12 +834,17 @@ function notifyMovement!(trigger::Maple.Trigger) Ahorn.eventToModules(Ahorn.loadedTriggers, "moved", trigger, relevantRoom) end -function notifyMovement!(decal::Maple.Decal) - # Decals doesn't care -end +# Decals and Tiles don't care +notifyMovement!(decal::Maple.Decal) = nothing +notifyMovement!(target::Ahorn.TileSelection) = nothing -function notifyMovement!(target::Ahorn.TileSelection) - # Tiles doesn't care +mutable struct KeyboardHandleResults + redraw::Bool + clearDrag::Bool + clearResize::Bool + clearAreaOperation::Bool + + KeyboardHandleResults() = new(false, false, false, false) end resizeModifiers = Dict{Integer, Tuple{Number, Number}}( @@ -781,20 +858,42 @@ resizeModifiers = Dict{Integer, Tuple{Number, Number}}( Int('d') => (0, -1) ) +# (Key, steps clockwise) +rotationSteps = Dict{Integer, Integer}( + Int('r') => 1, + Int('l') => -1, +) + +# (Key, steps clockwise) +rotationAreaSteps = Dict{Integer, Integer}( + Int('R') => 1, + Int('L') => -1, +) + addNodeKeys = [Int('n'), Int('+')] -# Turns out having scales besides -1 and 1 on decals causes weird behaviour? -scaleMultipliers = Dict{Integer, Tuple{Number, Number}}( +# (Key code, horizontal flip) +flipDirections = Dict{Integer, Bool}( # Vertical Flip - Int('v') => (1, -1), + Int('v') => false, # Horizontal Flip - Int('h') => (-1, 1), + Int('h') => true, +) + +# (Key code, horizontal flip) +flipAreaDirections = Dict{Integer, Bool}( + # Vertical Area Flip + Int('V') => false, + + # Horizontal Area Flip + Int('H') => true, ) # Consider exposing the grid snap value -function handleMovement(event::Ahorn.eventKey) - redraw = false +function handleMovement(results::KeyboardHandleResults, event::Ahorn.eventKey) + handled = false + step = Ahorn.modifierControl() ? 1 : 8 snapMode = get(Ahorn.config, "use_grid_snapping", true) @@ -806,7 +905,10 @@ function handleMovement(event::Ahorn.eventKey) if applyGridMovement!(target, step, dirX, dirY, node) notifyMovement!(target) - redraw = true + Ahorn.moved(target) + Ahorn.moved(target, step * dirX, step * dirY) + + handled = true end else @@ -814,15 +916,25 @@ function handleMovement(event::Ahorn.eventKey) if redraw notifyMovement!(target) + + Ahorn.moved(target) + Ahorn.moved(target, step * dirX, step * dirY) + + handled = true end end end - return redraw + if handled + results.redraw = true + results.clearDrag = true + results.clearResize = true + results.clearAreaOperation = true + end end -function handleResize(event::Ahorn.eventKey) - redraw = false +function handleResize(results::KeyboardHandleResults, event::Ahorn.eventKey) + handled = false step = Ahorn.modifierControl() ? 1 : 8 processed = Set{Union{Maple.Entity, Maple.Trigger}}() @@ -836,57 +948,212 @@ function handleResize(event::Ahorn.eventKey) horizontal, vertical = Ahorn.canResizeWrapper(target) minWidth, minHeight = Ahorn.minimumSizeWrapper(target) - baseWidth = get(target.data, "width", minWidth) - baseHeight = get(target.data, "height", minHeight) + baseWidth = get(target, "width", minWidth) + baseHeight = get(target, "height", minHeight) - if snapMode - target.data["width"] = horizontal ? max(gridSnapped(baseWidth, step, dirW), minWidth) : baseWidth - target.data["height"] = vertical ? max(gridSnapped(baseHeight, step, dirH), minHeight) : baseHeight + newWidth = snapMode ? max(gridSnapped(baseWidth, step, dirW), minWidth) : max(baseWidth + dirW * step, minWidth) + newHeight = snapMode ? max(gridSnapped(baseHeight, step, dirH), minHeight) : max(baseHeight + dirH * step, minHeight) - else - target.data["width"] = horizontal ? max(baseWidth + dirW * step, minWidth) : baseWidth - target.data["height"] = vertical ? max(baseHeight + dirH * step, minHeight) : baseHeight + if horizontal + target.width = newWidth + end + + if vertical + target.height = newHeight end - redraw = true + Ahorn.resized(target) + + handled = true push!(processed, target) elseif name == "fgDecals" || name == "bgDecals" extraW, extraH = resizeModifiers[event.keyval] minVal, maxVal = decalScaleVals - + target.scaleX = sign(target.scaleX) * clamp(abs(target.scaleX) * 2.0^extraW, minVal, maxVal) target.scaleY = sign(target.scaleY) * clamp(abs(target.scaleY) * 2.0^extraH, minVal, maxVal) - redraw = true + handled = true + end + end + + if handled + results.redraw = true + results.clearAreaOperation = true + results.clearResize = true + end +end + +function handleFlipping!(selected::Ahorn.SelectedObject, target::Ahorn.Decal, horizontal::Bool) + Ahorn.flipped(target, horizontal) + + return true +end + +function handleFlipping!(selected::Ahorn.SelectedObject, target::Ahorn.TileSelection, horizontal::Bool) + Ahorn.flipped(target, horizontal) + + return true +end + +function handleFlipping!(selected::Ahorn.SelectedObject, target::Union{Ahorn.Entity, Ahorn.Trigger}, horizontal::Bool) + if selected.node == 0 + flipped = Ahorn.flipped(target, horizontal) + + if flipped !== nothing + selected.rectangle = Ahorn.getSelection(flipped, relevantRoom, 0) + selected.target = flipped + + targets = selected.layerName == "entities" ? relevantRoom.entities : relevantRoom.triggers + index = findfirst(==(target), targets) + + # Some entities can just change their attributes + if index !== nothing + deleteat!(targets, index) + insert!(targets, index, flipped) + end + + return true + end + end + + return false +end + +function handleAreaFlipping!(selected::Ahorn.SelectedObject, target::Ahorn.Decal, horizontal::Bool, area::Ahorn.Rectangle) + Ahorn.flipped(target, horizontal) + + if horizontal + target.x = 2 * area.x + area.w - target.x + + else + target.y = 2 * area.y + area.h - target.y + end + + return true +end + +function handleAreaFlipping!(selected::Ahorn.SelectedObject, target::Ahorn.TileSelection, horizontal::Bool, area::Ahorn.Rectangle) + Ahorn.flipped(target, horizontal) + + if horizontal + target.offsetX = 2 * area.x + area.w - target.selection.x - target.selection.w - target.startX + + else + target.offsetY = 2 * area.y + area.h - target.selection.y - target.selection.h - target.startY + end + + target.selection = Ahorn.Rectangle(target.startX + div(target.offsetX, 8) * 8, target.startY + div(target.offsetY, 8) * 8, target.selection.w, target.selection.h) + + return true +end + +function handleAreaFlipping!(selected::Ahorn.SelectedObject, target::Union{Ahorn.Entity, Ahorn.Trigger}, horizontal::Bool, area::Ahorn.Rectangle) + # handleFlipping updates the target in selected to the correct target + # The passed target arugment is only for multiple dispatch + + handleFlipping!(selected, target, horizontal) + + width = get(target, "width", 0) + height = get(target, "height", 0) + + if selected.node == 0 + if horizontal + selected.target.x = 2 * area.x + area.w - width - selected.target.x + + else + selected.target.y = 2 * area.y + area.h - height - selected.target.y + end + + else + x, y = selected.target.nodes[selected.node] + + if horizontal + x = 2 * area.x + area.w - width - x + + else + y = 2 * area.y + area.h - height - y end + + selected.target.nodes[selected.node] = (x, y) end - return redraw + return true end -# Requires a runtime dispatch regardless, using isa for less boilerplate -function handleScaling(event::Ahorn.eventKey) - redraw = false +function handleFlipping(results::KeyboardHandleResults, event::Ahorn.eventKey) + horizontal = flipDirections[event.keyval] + + for selection in selections + results.redraw |= handleFlipping!(selection, selection.target, horizontal) + end + + results.clearAreaOperation |= results.redraw +end + +function handleAreaFlipping(results::KeyboardHandleResults, event::Ahorn.eventKey) + selectionRectangle = getSelectionArea() + area = updateAreaOperationRectangle!(selectionRectangle) + horizontal = flipAreaDirections[event.keyval] + for selection in selections - msx, msy = scaleMultipliers[event.keyval] - target = selection.target + results.redraw |= handleAreaFlipping!(selection, selection.target, horizontal, area) + end +end + +function handleRotation!(selected::Ahorn.SelectedObject, target::Ahorn.TileSelection, steps::Int) + selected.target = Ahorn.rotated(target, steps) + selected.rectangle = target.selection + + return true +end - if isa(target, Maple.Decal) - target.scaleX *= msx - target.scaleY *= msy +handleRotation!(selected::Ahorn.SelectedObject, target::Ahorn.Decal, steps::Int) = false + +function handleRotation!(selected::Ahorn.SelectedObject, target::Union{Ahorn.Entity, Ahorn.Trigger}, steps::Int) + if selected.node == 0 + rotated = Ahorn.rotated(target, steps) + + if rotated !== nothing + selected.rectangle = Ahorn.getSelection(rotated, relevantRoom, 0) + selected.target = rotated + + targets = selected.layerName == "entities" ? relevantRoom.entities : relevantRoom.triggers + index = findfirst(==(target), targets) + + # Some entities can just change their attributes + if index !== nothing + deleteat!(targets, index) + insert!(targets, index, rotated) + end - redraw = true + return true end end - return redraw + return false end -function handleAddNodes(event::Ahorn.eventKey) - redraw = false +function handleRotation(results::KeyboardHandleResults, event::Ahorn.eventKey) + steps = rotationSteps[event.keyval] + for selection in selections + results.redraw |= handleRotation!(selection, selection.target, steps) + end + + results.clearAreaOperation |= results.redraw +end + +function handleAreaRotation(results::KeyboardHandleResults, event::Ahorn.eventKey) + # TODO - Implement + # Seems super niche compared to area flipping + + return false +end + +function handleAddNodes(results::KeyboardHandleResults, event::Ahorn.eventKey) for selection in selections name, box, target, node = selection.layerName, selection.rectangle, selection.target, selection.node @@ -902,18 +1169,20 @@ function handleAddNodes(event::Ahorn.eventKey) end insert!(nodes, node + 1, (x + 16, y)) - redraw = true + results.redraw = true target.data["nodes"] = nodes end end end - - return redraw end -function handleDeletion(selections::Set{Ahorn.SelectedObject}) - res = !isempty(selections) +handleDeletion(selections::Set{Ahorn.SelectedObject}) = handleDeletion(KeyboardHandleResults(), selections) + +function handleDeletion(results::KeyboardHandleResults, selections::Set{Ahorn.SelectedObject}) + results.redraw |= !isempty(selections) + results.clearAreaOperation |= results.redraw + selectionsArray = collect(selections) # Split into different arrays @@ -947,11 +1216,13 @@ function handleDeletion(selections::Set{Ahorn.SelectedObject}) deleteat!(targetList, index) end end + + Ahorn.deleted(target, node) end end # Deletion for decals - for selection in decalSelections + for selection in decalSelections targetList = Ahorn.selectionTargets[selection.layerName](relevantRoom) index = findfirst(isequal(selection.target), targetList) @@ -965,51 +1236,84 @@ function handleDeletion(selections::Set{Ahorn.SelectedObject}) if !isempty(selections) empty!(selections) end +end + +function handleClearSelections(results::KeyboardHandleResults) + clearSelections!() - return res + results.redraw = true end # Refactor and prettify code once we know how to handle tiles here, # this also includes the handle functions function keyboard(event::Ahorn.eventKey) - needsRedraw = false + results = KeyboardHandleResults() + layersSelected = getLayersSelected(selections) snapshot = Ahorn.History.MultiSnapshot("Selections", Ahorn.History.Snapshot[ Ahorn.History.RoomSnapshot("Selections", Ahorn.loadedState.room), Ahorn.History.SelectionSnapshot("Selections", relevantRoom, selections) ]) - - needsRedraw |= Ahorn.callbackFirstActive(hotkeys, event) + + results.redraw |= Ahorn.callbackFirstActive(hotkeys, event) if haskey(Ahorn.moveDirections, event.keyval) - needsRedraw |= handleMovement(event) + handleMovement(results, event) end if haskey(resizeModifiers, event.keyval) - needsRedraw |= handleResize(event) + handleResize(results, event) + end + + if haskey(flipDirections, event.keyval) && !Ahorn.modifierControl() + handleFlipping(results, event) + end + + if haskey(flipAreaDirections, event.keyval) && !Ahorn.modifierControl() + handleAreaFlipping(results, event) + end + + if haskey(rotationSteps, event.keyval) && !Ahorn.modifierControl() + handleRotation(results, event) end - if haskey(scaleMultipliers, event.keyval) && !Ahorn.modifierControl() - needsRedraw |= handleScaling(event) + if haskey(rotationAreaSteps, event.keyval) && !Ahorn.modifierControl() + handleAreaRotation(results, event) end if event.keyval in addNodeKeys && !Ahorn.modifierControl() - needsRedraw |= handleAddNodes(event) + handleAddNodes(results, event) end if event.keyval == Ahorn.Gtk.GdkKeySyms.Delete && !Ahorn.modifierControl() - needsRedraw |= handleDeletion(selections) + handleDeletion(results, selections) + end + + if event.keyval == Ahorn.Gtk.GdkKeySyms.Return || event.keyval == Ahorn.Gtk.GdkKeySyms.Escape + handleClearSelections(results) end - if needsRedraw + # Actions based on results from events + if results.clearDrag clearDragging!() + end + + if results.clearResize clearResize!() - mouseMotionAbs(lastX, lastY) + end + + if results.clearAreaOperation + clearAreaOperationRectangle!() + end + if results.redraw toolsLayer.redraw = true redrawTargetLayer!(targetLayer, layersSelected) Ahorn.History.addSnapshot!(snapshot) + + # Send a fake event to clear up visuals + mouseMotionAbs(lastX, lastY) end return true diff --git a/src/triggers.jl b/src/triggers.jl index ce1fb9b..41b726f 100644 --- a/src/triggers.jl +++ b/src/triggers.jl @@ -25,7 +25,7 @@ function renderTriggerSelection(ctx::Cairo.CairoContext, layer::Layer, trigger:: width, height = Int(trigger.data["width"]), Int(trigger.data["height"]) nodes = get(trigger.data, "nodes", Tuple{Int, Int}[]) offsetCenterX, offsetCenterY = floor(Int, width / 2), floor(Int, height / 2) - + text = humanizeVariableName(trigger.name) for node in nodes @@ -39,10 +39,24 @@ end nodeLimits(trigger::Maple.Trigger) = 0, 0 editingOptions(trigger::Maple.Trigger) = Dict{String, Any}() +editingOrder(trigger::Maple.Trigger) = String["x", "y", "width", "height"] +editingIgnored(trigger::Maple.Trigger, multiple::Bool=false) = multiple ? String["x", "y", "width", "height", "nodes"] : String[] minimumSize(trigger::Maple.Trigger) = 8, 8 resizable(trigger::Maple.Trigger) = true, true +deleted(trigger::Maple.Trigger, node::Int) = nothing + +moved(trigger::Maple.Trigger) = nothing +moved(trigger::Maple.Trigger, x::Int, y::Int) = nothing + +resized(trigger::Maple.Trigger) = nothing +resized(trigger::Maple.Trigger, width::Int, height::Int) = nothing + +flipped(trigger::Maple.Trigger, horizontal::Bool) = nothing + +rotated(trigger::Maple.Trigger, steps::Int) = nothing + function triggerSelection(trigger::Maple.Trigger, room::Maple.Room, node::Int=0) x, y = Int(trigger.x), Int(trigger.y) width, height = Int(trigger.width), Int(trigger.height) diff --git a/src/windows/styleground_window.jl b/src/windows/styleground_window.jl index 2287c29..aab0765 100644 --- a/src/windows/styleground_window.jl +++ b/src/windows/styleground_window.jl @@ -7,19 +7,15 @@ stylegroundWindow = nothing effectSectionGrid = nothing effectOptions = nothing -const effectFieldOrder = String[ - "name", "only", "exclude", "tag", - "flag", "notflag" -] - const parallaxFieldOrder = String[ "texture", "only", "exclude", "tag", "flag", "notflag", "blendmode", "color", "x", "y", "scrollx", "scrolly", - "speedx", "speedy", "alpha" + "speedx", "speedy", "fadex", "fadey", + "alpha" ] -function drawPreviewFailed(canvas::Gtk.GtkCanvas, reason::String, width::Int=320, height::Int=180, scale::Number=3) +function drawPreviewText(canvas::Gtk.GtkCanvas, reason::String, width::Int=320, height::Int=180, scale::Number=3) ctx = Gtk.getgc(canvas) set_gtk_property!(canvas, :width_request, width) @@ -34,28 +30,34 @@ function drawPreview(canvas::Gtk.GtkCanvas, textureOption::Ahorn.Form.Option, co texture = Ahorn.Form.getValue(textureOption) sprite = Ahorn.getSprite(texture, "Gameplay") rawColor = Ahorn.Form.getValue(colorOption) - width, height = sprite.width, sprite.height - if width > 0 && height > 0 && !(sprite.surface == Ahorn.Assets.missingImage) - try - @assert length(rawColor) == 6 - color = Ahorn.argb32ToRGBATuple(parse(Int, "0x" * rawColor) + 255 << 24) ./ 255.0 + hidePreview = get(Ahorn.config, "hide_styleground_parallax_preview", false) - offsetX, offsetY = sprite.offsetX, sprite.offsetY + if hidePreview + drawPreviewText(canvas, "Preview has been disabled.\nDouble click to reenable.") - set_gtk_property!(canvas, :width_request, width) - set_gtk_property!(canvas, :height_request, height) + else + if width > 0 && height > 0 && !(sprite.surface == Ahorn.Assets.missingImage) + try + @assert length(rawColor) == 6 + color = Ahorn.argb32ToRGBATuple(parse(Int, "0x" * rawColor) + 255 << 24) ./ 255.0 - Ahorn.clearSurface(ctx) - Ahorn.drawImage(ctx, sprite, -offsetX, -offsetY, tint=color) + offsetX, offsetY = sprite.offsetX, sprite.offsetY - catch e - drawPreviewFailed(canvas, "Color is invalid.\nExpected hex triplet without #.") - end + set_gtk_property!(canvas, :width_request, width) + set_gtk_property!(canvas, :height_request, height) - else - drawPreviewFailed(canvas, "Unable to preview backdrop, image not found.\nAhorn can only preview from the Gameplay atlas.") + Ahorn.clearSurface(ctx) + Ahorn.drawImage(ctx, sprite, -offsetX, -offsetY, tint=color) + + catch e + drawPreviewText(canvas, "Color is invalid.\nExpected hex triplet without #.") + end + + else + drawPreviewText(canvas, "Unable to preview backdrop, image not found.\nAhorn can only preview from the Gameplay atlas.") + end end end @@ -112,6 +114,9 @@ const parallaxFields = Dict{String, Any}( "instantOut" => false, "fadeIn" => false, + "fadex" => "", + "fadey" => "", + "tag" => "" ) @@ -251,7 +256,7 @@ function getParallaxOptions(fields::Dict{String, Any}, langdata::Ahorn.LangData) return options end -function getEffectOptions(effect::Maple.Effect, langdata::Ahorn.LangData, fg::Bool=true) +function getEffectOptions(effect::Maple.Effect, langdata::Ahorn.LangData, fg::Bool=true, ignored::Array{String, 1}=String[]) updateEffectTemplates() options = Ahorn.Form.Option[] @@ -278,6 +283,10 @@ function getEffectOptions(effect::Maple.Effect, langdata::Ahorn.LangData, fg::Bo end for (dataName, value) in merge(effectFields, data) + if dataName in ignored + continue + end + symbolDataName = Symbol(dataName) keyOptions = get(dropdownOptions, dataName, nothing) displayName = haskey(names, symbolDataName) ? names[symbolDataName] : Ahorn.humanizeVariableName(dataName) @@ -300,8 +309,6 @@ function updateEffect(effect::Maple.Effect, data::Dict{String, Any}, fg::Bool=tr res.data[attr] = get(effect.data, attr, value) end - res.data = merge(get(effectTemplates, newName, Dict{String, Any}()), effect.data) - else res.data = data end @@ -356,7 +363,10 @@ function updateEffectSectionGrid(grid::Gtk.GtkGrid, backdrop::Maple.Effect, fg:: :tooltips => merge(get(langdataEffect, :tooltips), get(langdataEffects, :tooltips)) )) - global effectOptions = getEffectOptions(backdrop, langdataCombined, fg) + effectFieldOrder = Ahorn.editingOrder(backdrop) + ignoredFields = Ahorn.editingIgnored(backdrop) + + global effectOptions = getEffectOptions(backdrop, langdataCombined, fg, ignoredFields) section = Ahorn.Form.Section("Effect", effectOptions, fieldOrder=effectFieldOrder) global effectSectionGrid = Ahorn.Form.generateSectionGrid(section, columns=8) @@ -537,12 +547,27 @@ function getParallaxGrid(map::Maple.Map) Ahorn.updateTreeView!(parallaxList, getParallaxListRows(map.style), select, updateByReplacement=true) end end - + Ahorn.connectChanged(parallaxRowHandler, parallaxList) signal_connect(widget -> draw(preview), textureOption.combobox, "changed") signal_connect(widget -> draw(preview), colorOption.entry, "changed") + add_events(preview, + GConstants.GdkEventMask.BUTTON_PRESS | + GConstants.GdkEventType.DOUBLE_BUTTON_PRESS + ) + + signal_connect(preview, "button-press-event") do widget, event + # Double click + if event.event_type == 5 + hidePreview = get(Ahorn.config, "hide_styleground_parallax_preview", false) + Ahorn.config["hide_styleground_parallax_preview"] = !hidePreview + + draw(preview) + end + end + @guarded draw(preview) do widget drawPreview(preview, textureOption, colorOption) end diff --git a/src/windows/update_window.jl b/src/windows/update_window.jl index 8176908..406a231 100644 --- a/src/windows/update_window.jl +++ b/src/windows/update_window.jl @@ -3,17 +3,6 @@ module UpdateWindow using Gtk, Gtk.ShortNames, Pkg using ..Ahorn -function do_ask_dialog(text::String; no::String="Cancel", yes::String="Update") - if ask_dialog(text, no, yes, Ahorn.window) - info_dialog("Ahorn will now be updated.\nThis will freeze the window during the process.\nDo not close Ahorn, you will get notified when the update completes.", Ahorn.window) - Pkg.update(["Maple", "Ahorn"]) - if ask_dialog("Ahorn updated!\nBut the old version of Ahorn still lingers around.\nWe can run gc for you to clean up installed package versions,\nbut that is going to affect all your julia environments.\nIf you do not have any other Julia packages installed,\nthis should be safe to do and save you some disk space.", Ahorn.window) - Pkg.gc() - end - exit() - end -end - # Modified from stream.jl function take_stdout(f::Function) local oldout = Base.stdout @@ -38,30 +27,51 @@ function pkghash() end function updateAhorn(widget::Union{Ahorn.MenuItemsTypes, Nothing}=nothing) - ask_dialog("""Would you like to check for updates? + ask_dialog("""Would you like to try updating Ahorn? This will download files required for the update if there is one. The window might also freeze for a bit. Make sure not to close Ahorn during this!""", - "Cancel", "Check for Updates", Ahorn.window) || return + "Cancel", "Update", Ahorn.window) || return try + info_dialog("""Ahorn will now be updated. + This will freeze the window during the process. + Do not close Ahorn, you will get notified when the update completes.""", + Ahorn.window) sim = take_stdout() do - # Simulate an update. This is still going to download the repo. - Pkg.update(["Maple", "Ahorn"], preview=true) + Pkg.update(["Maple", "Ahorn"]) end - + + println(Base.stdout, sim) + if occursin(r"(Ahorn|Maple)[^\n]+⇒", sim) || occursin(r"~ (Ahorn|Maple)", sim) - do_ask_dialog("A new version is available!\nDo you wish to update Ahorn?\nThis will close the program afterwards and you will have to rerun it.") + if ask_dialog("""Ahorn updated! + But the old version of Ahorn still lingers around. + We can run gc for you to clean up installed package versions, + but that is going to affect all your julia environments. + If you do not have any other Julia packages installed, + this should be safe to do and save you some disk space.""", + "Skip", "Run gc", Ahorn.window) + Pkg.gc() + end + info_dialog("""Ahorn will now close. + Start it again to use the updated version.""", + Ahorn.window) + exit() else h = pkghash() - do_ask_dialog("Ahorn seems to be up-to-date$(h !== nothing ? " at hash $h" : "").\nDo you wish to try updating anyway?\nThis will close the program afterwards and you will have to rerun it.", yes="Try Updating Anyway") + info_dialog("""Ahorn seems to be up-to-date$(h !== nothing ? " at hash $h" : ""). + No new update was found.""", + Ahorn.window) end catch e - do_ask_dialog("The update check failed for some reason.\nDo you wish to update Ahorn anyway?\nThis will close the program afterwards and you will have to rerun it.") println(Base.stderr, "Update check failed") for (exc, bt) in Base.catch_stack() showerror(Base.stderr, exc, bt) println(Base.stderr, "") end + info_dialog("""Something went wrong during the update. + Please check your error.log file in the Ahorn config directory.""", + Ahorn.window) end end